https://rpadovani.com/ Riccardo Padovani 2026-02-07T10:35:54+00:00 https://rpadovani.com/media/i8eX1hEmyGGs1W1Td6x0PjjqOOozWXObI434M2dN.ico Hyvor Blogs https://rpadovani.com/k8s-doesnt-care-about-uptime Kubernetes doesn't care about your uptime 2026-02-07T10:35:54+00:00 It's 3:00 AM. A node in your production cluster has gone dark. You wait for the Kubernetes scheduler to do its magic: to recognize the loss and spin up your database elsewhere. But nothing happens. Why? <p>Your stateful pod sits in a &#039;Terminating&#039; state, blocked by the same system that you chose to keep it alive. This is when you realize that Kubernetes doesn&#039;t actually care about your uptime; it cares about data integrity, and it will sacrifice your availability to protect it.</p><p>We often treat Kubernetes like a magic High Availability agent, and assume that it will always move workloads to healthy hardware. However, <strong>Kubernetes prioritizes data integrity over availability.</strong> When in doubt, K8s leaves your app down to keep your data safe.</p><p>This is a foundational part of how a <code>StatefulSet</code> works: it guarantees “At most one” semantic. No more than one pod to prevent data corruption.</p><figure><img src="https://rpadovani.com/media/196.png" loading="lazy" alt="The K8s availability vs consistency standoff" srcset="https://rpadovani.com/media/196.png 1024w, https://rpadovani.com/media/196.png/500w 500w, https://rpadovani.com/media/196.png/750w 750w, https://rpadovani.com/media/196.png/1000w 1000w"><figcaption><em>Image generated by </em><a href="https://gemini.google/overview/image-generation/" target="_blank" rel="noopener noreferrer"><em>Nano Banana Pro</em></a></figcaption></figure><h1>Why the scheduler ignores your pod</h1><h2>The split brain problem</h2><p>K8s doesn&#039;t know, and cannot know if a node is dead for real (for example, due to a hardware failure) or if the node is working but is not able to communicate back. This can lead to a <a href="https://en.wikipedia.org/wiki/Split-brain_(computing)" target="_blank" rel="noopener noreferrer">split brain</a>. Split brain happens when the network fails, and the cluster is split in half. Both sides think that they are the surviving healthy side. Why is it dangerous?</p><h2>The danger of multi-writer</h2><p>If Kubernetes would blindly reschedule the pod, two pods might attach to the same <code>ReadWriteOnce</code> volume: the existing pod, that Kubernetes thinks dead, and the new one just scheduled.</p><p>In this case, your data will be corrupted, due to the concurrent writing. To avoid that, Kubernetes adopts a very conservative default behavior.</p><p>Most databases are <strong>ACID</strong> (Atomicity, Consistency, Isolation, Durability): they would rather be offline than wrong. We tend to consider Kubernetes a <strong>BASE</strong> system (Basically Available, Soft-state, Eventual consistency). When a node goes dark, Kubernetes chooses the C (Consistency) in the <a href="https://en.wikipedia.org/wiki/CAP_theorem" target="_blank" rel="noopener noreferrer">CAP Theorem</a>. It freezes your pod because it cannot guarantee that &quot;Isolation&quot; isn&#039;t being violated. The goal is for your data stays durable and uncorrupted.</p><h2>The default behavior</h2><p>As soon as the node is seen as unhealthy by Kubernetes, one of two standards taints are applied:</p><ul><li><p><code>node.kubernetes.io/unreachable</code>: this taint is added when Kubernetes cannot communicate with the node;</p></li><li><p><code>node.kubernetes.io/not-ready</code>: this taint is added when the node is actually reachable, but it reports that is not ready;</p></li></ul><p>By default, every pod has a toleration of 300 seconds (5 minutes), to continue existing on a tainted node. You can reduce this time using tolerations, but it doesn&#039;t resolve the problem for stateful pods: because the node is unreachable, K8s has no way to confirm that a pod is actually dead. Pod might still be writing data to the disk.</p><p>Additionally, reducing the toleration is highly risky also for stateless pods: reducing the toleration too much leads to flapping in unstable network conditions, which is often worse than 5 minutes of downtime.</p><p>So, Kubernetes will move the pod to <code>Terminating</code> or <code>Unknown</code>, and it will stay like that forever. And trying to force a new pod in a new node wouldn&#039;t help either: the <code>VolumeAttachment</code> would remain in use by the dead node, so your new pod stays in <code>ContainerCreating</code> or <code>Pending</code> with an error in your events saying something like <code>Multiattach error for volume &lt;X&gt;. Volume is already used by pod…</code></p><h1>If K8s doesn&#039;t help, who will?</h1><h2>Application level replication</h2><p>Databases, but also event queues, as Postgres, MongoDB, or RabbitMQ, should handle their own failovers. In a robust stateful setup, you don&#039;t just run one pod. You run a cluster, with a leader (Read/Write) and one or more followers (Read-only). Kubernetes task is only to run them, but it’s the application itself that manages data synchronization, and especially, the election of a new leader. How does it work?</p><ol><li><p>The leader pod becomes unreachable</p></li><li><p>The remaining follower pods, on healthy node, fail to observe a heartbeat. They perform a vote, and promote a new leader.</p></li><li><p>The old “dead” pod is still stuck in <code>Terminating</code>, but it doesn&#039;t matter, because it is now ignored by the application</p></li></ol><p>For this to work, your application should <strong>share nothing: </strong>every pod has its own volume. In this way, K8s is not the data gatekeeper anymore, but the app itself is.</p><p>For example, Postgres doesn&#039;t know how to handle K8s failover. You can use <a href="https://github.com/patroni/patroni" target="_blank" rel="noopener noreferrer">Patroni</a>, which is a “wrapper” around Postgres, that uses a Distributed Configuration Store (like <a href="https://etcd.io/" target="_blank" rel="noopener noreferrer">etcd</a> or <a href="https://www.hashicorp.com/en/products/consul" target="_blank" rel="noopener noreferrer">Consul</a>) to keep track of who is the leader. If the leader node&#039;s network cuts, the leader “lease” in the DCS expires. A standby Patroni instance sees that the lease has expired, and grabs it, promoting itself to primary.</p><p>MongoDB, on the other hand, is natively built for this. All MongoDB instances talk to each other constantly (with so-called heartbeats). If the primary goes down, the secondaries hold an election. Each MongoDB pod has its own persistent volume, so they don&#039;t need to steal the old disk from the dead node.</p><h2>Node power off</h2><p>You can also, manually or via automation, manually power off the “failed” machine. It can be a Virtual Machine or a bare-metal server: if it is off, it cannot write to the volume. At that point, you can force detach the volume, and you are sure that there won&#039;t be data corruption.</p><p>This is called fencing, or more colloquially, <strong>STONITH: </strong>shoot the other node in the head. Make sure that the problematic node is isolated and dead, so the integrity of the system is guaranteed.</p><p>While you can do this manually, the best course of action would be having automatic systems performing it for you.</p><h2>CSI Drivers</h2><p>Modern <a href="https://github.com/container-storage-interface/spec" target="_blank" rel="noopener noreferrer">Container Storage Interfaces</a> are getting smarter. <a href="https://archive.fosdem.org/2024/events/attachments/fosdem-2024-1959-boosting-cephfs-security-in-kubernetes-rook-s-intelligent-network-fencing-for-uninterrupted-data-flow-and-workload-harmony-/slides/22131/Network_Fencing_Z0pkpQA.pdf" target="_blank" rel="noopener noreferrer">Some of them can handle “fencing”</a> a node by automatically invalidating its access to the disk. However, this is still far from being a universal “out of the box” feature.</p><h1>Design for distrust</h1><p>Start thinking about failure scenarios where a pod dies for good: how would your app react? While it is true that, for stateless workloads, scheduling is not a problem anymore, when there is data involved, you want to be prescriptive on how you want to manage data.</p><p><strong>Kubernetes is a world-class orchestrator, but it is not a DBA. If your data is important, your database should be smarter than your orchestrator.</strong></p><p>And, what are some of your firefighting stories about Kubernetes in the middle of the night?</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/teachability On the importance of teachability 2026-01-05T09:36:41+00:00 Internet is full of posts, video, and instructions on how to give great feedback. Way less discuss about how to receive feedback in the right way, and get the most value out of it. <p>I would argue that “being teachable&quot; is even a more precious skill than being able to give great feedback: we can spend our whole life without giving any feedback, but for sure we can&#039;t avoid receiving it.</p><p>And given that feedback is one of the most valuable tool for your own growth, career-wise: you definitely want to optimize how you use it, and you want to encourage people to give you more.</p><figure><img src="https://rpadovani.com/media/undraw-feedback-ebmx.png" loading="lazy" alt="Image of getting feedback" srcset="https://rpadovani.com/media/undraw-feedback-ebmx.png 1600w, https://rpadovani.com/media/undraw-feedback-ebmx.png/500w 500w, https://rpadovani.com/media/undraw-feedback-ebmx.png/750w 750w, https://rpadovani.com/media/undraw-feedback-ebmx.png/1000w 1000w, https://rpadovani.com/media/undraw-feedback-ebmx.png/1500w 1500w"><figcaption>Illustration by <a href="https://undraw.co" target="_blank" rel="noopener noreferrer">undraw.co</a></figcaption></figure><p>As &quot;<a href="https://www.goodreads.com/book/show/20487821-thanks-for-the-feedback" target="_blank" rel="noopener noreferrer">Thanks for the Feedback</a>&quot; by Douglas Stone and Sheila Heen teaches us, there are three types of feedback we could receive: Appreciation, Coaching, and Evaluation.</p><p>A first challenge arises when we are expecting one kind of feedback, but we receive a different one. Our own expectations set us up to not listening to what we hear, because we are not ready for it.</p><p>Other times, we receive low-quality feedback, so we react in a defensive way. However, feedback is always a gift: maybe a ugly gift, that we will throw away as soon as we are home, but still a gift.</p><h2 id="being-teachable"><a href="#being-teachable">What being “teachable” means</a></h2><p>Most people think “being teachable” means “doing what you are told”. This is definitely not it. </p><p>Anyone giving feedback has only a partial view of the receiver’s work, so teachability is actually the ability to sustain the discomfort of the conversation, metabolize it quickly, and use the result of that metabolization to grow.</p><p>In the same fashion, having low teachability doesn’t mean rejecting feedback. I have seen many people that say they want feedback, they crave feedback, but that still have a very low teachability, <strong>because they make the feedback too expensive to give.</strong></p><p>Feedback conversation is not a debate: there is no objective truth to find, nor logic to defend. During feedback conversation, we must be in <em>“collection mode”</em>: collecting information and data points on how our work is perceived.</p><h2 id="reaction"><a href="#reaction">The reaction to feedback</a></h2><p>Giving feedback is a lot of work. It requires the giver to think about words, to prepare to a potentially uncomfortable conversation, and to spend time thinking about the impact of the talk before, during, and after.</p><p>If we react defensively to the feedback, we are punishing the giver. If we punish the giver, they will stop giving us any more data.</p><p>Often we get defensive because we wanted a pat on the back (<em>Appreciation</em>), or we were expecting some guidance (<em>Coaching</em>), but we got only raw data (<em>Evaluation). </em>The mismatch causes us pain, but we shouldn’t let it take over.</p><p><strong>The goal should be to lower the “social cost” of telling us the truth: giving us feedback should be a positive, painless, effective effort.</strong></p><h2 id="discern"><a href="#discern">Discerning the valuable parts</a></h2><p>Not all feedback is high quality: some of it is driven by the other person’s insecurity, lack of context, bad mood, or simply having a hard time communicating the point they want to make.</p><p>But discussing the feedback doesn’t help: as mentioned above, it increases the social cost to get feedback. And word gets out: you don’t want to label yourself as the one that is unable to receive feedback.</p><p>Even when the feedback is bad, somewhere there is a 10% of truth. This might just be how actions appear (more on that below in “<a href="#you-misunderstood" rel="noopener noreferrer">But … you misunderstood!</a>”), and that in itself is already valuable. </p><p>So, the goal is to take the feedback, say “Thank you”, and find that 10% of value, discarding the rest. </p><h2 id="getting-more"><a href="#getting-more">Getting more out of feedback</a></h2><p>What does all of this mean in practice? How can you get more out of the feedback you get?</p><p>I have personally seen the best results when, after having metabolized the feedback, and acted upon it, I went back to the giver <strong>explicitly telling them how I used their last piece of feedback.</strong></p><p>This has a huge value for different reasons:</p><ul><li><p>I show that I did my homework: I took the time to understand the feedback, integrating it in my daily activities, and seeing results;</p></li><li><p>The giver can comment on the results, explaining if I metabolized correctly what they shared, or if more adjustments are needed;</p></li><li><p>It transforms what it could have been a transactional comment in the beginning of deeper relation: with the excuse of discussing the feedback, you can start a relationship with someone that has already shown you the willingness to provide feedback. They could even become mentors!</p></li></ul><h3 id="feedback-budget"><a href="#feedback-budget">The feedback budget</a></h3><p>Mentors have a limited budget of energy: every minute they spend arguing with you about the feedback they shared, is a minute they are not spending in helping you grow.</p><p>If you waste their energy in senseless discussions because you cannot accept feedback, they will stop giving you any feedback at all. It won’t change a thing in their career trajectory, it will be a huge loss for you.</p><p>If you are difficult to work with, people will simply stop coming to you. They will route around you.</p><p><strong>Being teachable means optimizing how you receive feedback for low friction. You want to make it effortless for people to dump information into your brain.</strong></p><h3 id="you-misunderstood"><a href="#you-misunderstood">But… you misunderstood!</a></h3><p>This is the most common fallacy I have seen in smart people: they love to say “<em>Ah, but you have misunderstood what I have done!”. </em>They have a reasoning on why they acted in a certain way, and they really want to explain that to you.</p><p>However, especially growing into more senior positions, <strong>perception is reality</strong>. If stakeholders perceive you as difficult, you <em>are</em> difficult. Arguing that you &quot;aren&#039;t difficult, you&#039;re just precise&quot; is ironic and proves the point.</p><p>When receiving feedback, your only job is to understand <em>the perspective</em> of the giver. You don’t necessarily have to agree, or act upon it. But you must understand it!</p><p>Explaining away why what you have done was the right thing to do, is wasted time. <strong>If I misunderstood you, go and fix how you communicate, don’t fix </strong><em><strong>me.</strong></em></p><h3 id="wait-24hours"><a href="#wait-24hours">Wait 24 hours</a></h3><p>A very simple rule of thumb to help you be teachable is simply <strong>waiting at least 24 hours before discussing the feedback.</strong></p><p>When you receive the feedback, you can ask clarifying questions if needed, but you can&#039;t argue, you can&#039;t say that it is wrong, you can&#039;t discuss it.</p><p>Simply say “Thank you, I will need some time to reflect on this.&quot;. Then, you really reflect on it. You try and find the valuable part of the message. You empathize with the giver. You spend some idle time not thinking about it.</p><p>Only after all that, and 24 hours, if you still feel the urge to discuss the feedback, you go back and discuss it.</p><h3 id="another-mentor"><a href="#another-mentor">Talk with a different mentor</a></h3><p>Sometimes, also after having let some time pass by, we still find the feedback we received quite cryptic. We see there is value in it, but we are not really sure how to decode it, or how to get something actionable out of it.</p><p>If there is someone we trust, it is a great opportunity to share the feedback with them as well, and ask them if they have noticed the same pattern in us, or if they can rephrase the feedback in a way that is maybe more comprehensible for us.</p><h2 id="conclusion"><a href="#conclusion">Conclusion</a></h2><p>Feedback is always a precious gift, and as that, we should treat it carefully: thanks for having received it, grateful, and making a good use of it.</p><p>This can make all the difference in the world in our career: having a tight, quick feedback loop allows to grow exponentially, and integrate in ourselves small changes that make us better.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/k8s-algorithm-pick-pod-scale-in How Kubernetes picks which pods to delete during scale-in 2025-01-26T21:31:43+00:00 Have you ever wondered how K8s choose which pods to delete when a deployment is scaled down? Given it is not documented, I dived in the source code to learn. <p>It doesn’t matter if the number of pods is changed manually in a <code>Deployment</code> or through a horizontal pod autoscaler (HPA): Kubernetes (K8s) must pick which pods to delete, and it doesn’t choose randomly.</p><p>Users can help K8s choose, setting a value for the annotation <code>controller.kubernetes.io/pod-deletion-cost</code>, which we will cover in the article, but it is just a part of the algorithm. Given the behavior is not properly documented, this article will explain it, based on the source code.</p><p>The article will dive deep into the details of the implementation: if you are not interested in the low-level working, at the end of the page there is a <a href="https://rpadovani.com/k8s-algorithm-pick-pod-scale-in#summary" rel="noopener noreferrer">summary</a>. If you want to customize the scale-in behavior, please jump to the section about <a href="https://rpadovani.com/k8s-algorithm-pick-pod-scale-in#pod-deletion-cost" rel="noopener noreferrer">Pod deletion cost</a>.</p><aside style="background-color:#f1f1ef;color:#000000"><span>💡</span><div>Of course, the algorithm could change between versions (although has been more or less the same for some years now), so I&#039;ll focus on version <code>v1.30.0-alpha.0</code>: all the links to the source code, and the snippets included in the article, will refer to that.</div></aside><h2>Scaling-in</h2><figure><img src="https://rpadovani.com/media/u04zb2jzVHi8CkC4G2DEnGh6joa9Q92qC7v5tT6o.png" alt="checking boxes" srcset="https://rpadovani.com/media/u04zb2jzVHi8CkC4G2DEnGh6joa9Q92qC7v5tT6o.png 1161w, https://rpadovani.com/media/u04zb2jzVHi8CkC4G2DEnGh6joa9Q92qC7v5tT6o.png/500w 500w, https://rpadovani.com/media/u04zb2jzVHi8CkC4G2DEnGh6joa9Q92qC7v5tT6o.png/750w 750w, https://rpadovani.com/media/u04zb2jzVHi8CkC4G2DEnGh6joa9Q92qC7v5tT6o.png/1000w 1000w"><figcaption>Illustration by <a href="https://undraw.co/" target="_blank" rel="noopener noreferrer">unDraw</a></figcaption></figure><p>Scaling-in means reducing the number of pods available in a deployment. There could have been a spike in traffic in the application, so more pods were necessary to serve all the clients, and after the spike is gone, we can save resources by reducing the number of pods we request K8s to run.</p><p>This can be done manually, patching a <code>Deployment</code> with <code>kubectl</code>, or could be managed automatically by an autoscaler which will monitor some metrics to choose how many pods are necessary.</p><aside style="background-color:#f1f1ef;color:#000000"><span>📚</span><div>Understanding how scaling works is out of scope for this article, so if you are interested in learning more, I suggest reading the <a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/scale/scale-intro/" target="_blank" rel="noopener noreferrer">documentation</a>.</div></aside><h2 id="replicaset"><a href="#replicaset">ReplicaSet</a></h2><p>The logic for the scale-in of pods, behind the curtain, is managed by the <code>ReplicaSet</code> controller. When the number of expected pods decreases (but it doesn’t become zero), it does two things:</p><ul><li><p>It creates a rank among the current list of pods managed by itself: this ranking will be one of the metrics used for sorting the pods, but not the only one nor the most important one;</p></li><li><p>It sorts the pods using 8 different rules (including the ranking above);</p></li></ul><h3>Ranking calculation</h3><p>When the <code>ReplicaSet</code> sees that it must decrement the number of pods it manages, it <a href="https://github.com/kubernetes/kubernetes/blob/ec5096fa869b801d6eb1bf019819287ca61edc4d/pkg/controller/replicaset/replica_set.go#L627" target="_blank" rel="noopener noreferrer">invokes</a> the function <code>getPodsToDelete(filteredPods, relatedPods, diff)</code></p><p>The three arguments are:</p><ul><li><p><strong>filteredPods:</strong> the active pods managed by this <code>ReplicaSet</code>: inactive pods are filtered out because they won’t be deleted;</p></li><li><p><strong>relatedPods:</strong> all the pods owned by any <code>ReplicaSet</code> which has the same owner of this same <code>ReplicaSet</code>, including its own.</p></li><li><p><strong>diff:</strong> the number of pods to delete;</p></li></ul><h4>RelatedPods</h4><p>The <code>relatedPods</code> informs the ranking, so let’s deep dive to understand better what they represent. They are calculated in a <a href="https://github.com/kubernetes/kubernetes/blob/ec5096fa869b801d6eb1bf019819287ca61edc4d/pkg/controller/replicaset/replica_set.go#L796" target="_blank" rel="noopener noreferrer">dedicated function</a> that returns all pods that are owned by any ReplicaSet that is owned by the given ReplicaSet&#039;s owner.</p><p>This means that <code>relatedPods</code> is a <strong>superset</strong> of <code>filteredPods</code>: it will contain all the pods that are also in <code>filteredPods</code>, plus other if there is any other <code>ReplicaSet</code> with the same owner.</p><p>In this way, we have a list of all the pods that are somehow related to the <code>ReplicaSet</code> which is scaling-in. As an example, if a Helm Chart is managing two deployments, an application and a database, and the application is being scaled-in, the database pods are part of the <code>relatedPods</code>.</p><hr><p>The <code>getPodsToDelete</code> function first <a href="https://github.com/kubernetes/kubernetes/blob/ec5096fa869b801d6eb1bf019819287ca61edc4d/pkg/controller/replicaset/replica_set.go#L826C20-L826C56" target="_blank" rel="noopener noreferrer">invokes</a> <code>getPodsRankedByRelatedPodsOnSameNode</code>, and then it sorts the pods.</p><p>The function <code>getPodsRankedByRelatedPodsOnSameNode</code> calculates a rank for each pod, based on how many <code>relatedPods</code>are running on the same node. The rank is the number of active pods.</p><p>Two pods on the same node always have the same ranking.</p><p>If there is only one <code>ReplicaSet</code> with a given owner, then the ranking is simply the number of pods on each node: this means that if multiple pods are colocated on the same node, they will have a higher ranking of a pod running alone on a node.</p><p>Things become muddier when you have multiple <code>ReplicaSet</code> with the same owner. The ranking will be based on all the pods across the multiple <code>ReplicaSet</code>. Sticking with the example above, let’s say we have two nodes, and two <code>ReplicaSet</code>: a <code>app</code> and <code>db</code>, and we are scaling in the <code>app</code>.</p><p>If the pods are deployed in this way:</p><ul><li><p><strong>Node 1</strong>: <code>app</code>, <code>db</code>, <code>db</code></p></li><li><p><strong>Node 2</strong>: <code>app</code>, <code>app</code></p></li></ul><p>The <code>app</code> pod on the first node will have a ranking of 3, while the two <code>app</code> pods on the second node will have a ranking of 2.</p><h3>Sorting</h3><figure><img src="https://rpadovani.com/media/H4ucJE5PhT9xpMkikpzytSLEmQDH0FkPIHdOmZX7.png" alt="sorting" srcset="https://rpadovani.com/media/H4ucJE5PhT9xpMkikpzytSLEmQDH0FkPIHdOmZX7.png 802w, https://rpadovani.com/media/H4ucJE5PhT9xpMkikpzytSLEmQDH0FkPIHdOmZX7.png/500w 500w, https://rpadovani.com/media/H4ucJE5PhT9xpMkikpzytSLEmQDH0FkPIHdOmZX7.png/750w 750w"><figcaption>Illustration by <a href="https://undraw.co/" target="_blank" rel="noopener noreferrer">unDraw</a></figcaption></figure><p>Now that the <code>ReplicaSet</code> has assigned a rank to each pod, it delegates the sorting to the <code>ActivePodsWithRanks</code> <a href="https://github.com/kubernetes/kubernetes/blob/55f2bc10435160619d1ece8de49a1c0f8fcdf276/pkg/controller/controller_utils.go#L802" target="_blank" rel="noopener noreferrer">structure</a>, that implements the <code><a href="https://pkg.go.dev/sort" target="_blank" rel="noopener noreferrer">sort.Sort()</a></code> interface.</p><p>The logic of the sorting, that is what we are interested in, is all contained in the <code>Less()</code> <a href="https://github.com/kubernetes/kubernetes/blob/55f2bc10435160619d1ece8de49a1c0f8fcdf276/pkg/controller/controller_utils.go#L827" target="_blank" rel="noopener noreferrer">implementation</a>. The pods that will be sorted in front of the list will be the first ones to be deleted. </p><p>There are 8 different rules: when comparing two pods, each of them is applied in turn until one matches.</p><ol><li><p>The first thing that is compared is if a pod is assigned to a node: the ones that are not assigned are deleted first;</p></li><li><p>Then, the phase of the pods is the next criteria. A pod in <code>Pending</code> state will be deleted before a pod in <code>Unknown</code> state, and the ones in <code>Ready</code> phase will be deleted last;</p></li><li><p>Then, the <code>Ready</code> status is compared: pods not <code>Ready</code> will be deleted before pods marked as <code>Ready</code>;</p></li><li><p>If the feature <code>pod-deletion-cost</code> is enabled, (we will speak about it later, as it is the only way to shape the choice of which pod to delete), the pod with a lower <code>controller.kubernetes.io/pod-deletion-cost</code> (if any), will be deleted first;</p></li><li><p>Then, Kubernetes uses the rank of the pod: we explained above, is the number of related pods running on the same node. The one with a higher rank will be deleted first;</p></li><li><p>Then, if both pods are <code>Ready</code>, the pod that has been ready for a shorter amount of time will be deleted before the pod that has been ready for longer;</p></li><li><p>Then, everything else equal, the pods that have restarted the most will be deleted first;</p></li><li><p>If nothing else matches, the pod that has been created most recently, according to the <code>CreationTimestamp</code> field, will be deleted first.</p></li></ol><p>If all these 8 criteria are the same, so there is no clear indication of which pod should be deleted first, they are sorted by UUID to provide a pseudorandom order. The one that comes before in alphabetical order will be deleted first.</p><h2 id="pod-deletion-cost"><a href="#pod-deletion-cost">Pod deletion cost</a></h2><p><a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#pod-deletion-cost" target="_blank" rel="noopener noreferrer">The pod deletion cost is</a> a feature introduced in Kubernetes v1.22, currently in the <code>Beta</code> state and enabled by default.</p><p>It allows users to set an annotation on a pod, <code>controller.kubernetes.io/pod-deletion-cost</code>, which represents the cost of deleting a pod. The cost can be any value between -2147483648 and 2147483647, and the pods with a lower value will be deleted first.</p><p>As we have seen, this is on a best-effort basis: it is not the first criteria that Kubernetes will use to pick a pod to delete.</p><p>You shouldn’t update this value too often, to not put too much pressure on the <code>api-server</code>, and you shouldn’t update this value manually: if you want to use it, I suggest writing some controller that implements the logic you’d like to see applied.</p><h2 id="summary"><a href="#summary">Summary</a></h2><p>Long story short, the algorithm compares all the pods and orders them following these criteria:</p><ol><li><p>Unassigned &lt; assigned;</p></li><li><p>PodPending &lt; PodUnknown &lt; PodRunning;</p></li><li><p>Not ready &lt; ready;</p></li><li><p>Lower pod-deletion-cost &lt; higher pod-deletion cost;</p></li><li><p>Doubled up &lt; not doubled up;</p></li><li><p>Been ready for empty time &lt; less time &lt; more time;</p></li><li><p>Pods with containers with higher restart counts &lt; lower restart counts;</p></li><li><p>Empty creation time pods &lt; newer pods &lt; older pods;</p></li></ol><p>The first pod in the list will be the first to be deleted;</p><p>I hope you found this article somehow useful, please let me know in the comments if you have any feedback or suggestions, or any other questions!</p><p>Ciao,</p><p>R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/hyvor-blogs Moving to HyvorBlogs 2025-01-26T21:31:31+00:00 A refresh of the blog was long due, and I finally moved to a proper blogging platform. Let's take a look at the how and why <p>It’s almost 10 years since I wrote <a href="https://rpadovani.com/im-here-too" rel="noopener noreferrer">my first blog post</a> on this domain. Back at the time, it was a WordPress blog.</p><p>After WordPress, I moved to <a href="https://jekyllrb.com/" target="_blank" rel="noopener noreferrer">Jekyll</a>, and I hosted the website in many different places: GitHub Pages, my own VPS, and lately on GitLab Pages.</p><p>However, I wasn’t satisfied with the setup: Jekyll is a powerful tool, and quite a simple one, but it still requires maintenance. Moreover, it doesn’t have some interesting features, such as a search feature, and I am not good at customizing themes.</p><figure><img src="https://rpadovani.com/media/Zs30HviL4LiHC1k7oRazKiPkcPjjkzJVuAJ0QumR.png" alt="publishing a blog" srcset="https://rpadovani.com/media/Zs30HviL4LiHC1k7oRazKiPkcPjjkzJVuAJ0QumR.png 869w, https://rpadovani.com/media/Zs30HviL4LiHC1k7oRazKiPkcPjjkzJVuAJ0QumR.png/500w 500w, https://rpadovani.com/media/Zs30HviL4LiHC1k7oRazKiPkcPjjkzJVuAJ0QumR.png/750w 750w"><figcaption>Image courtesy of unDraw.co</figcaption></figure><p>At the same time, while managing a blog through Git sounds like fun, at the end of the day, it has some limitations, and it proves itself as a barrier to creating more content.</p><p>Meaning, given I was spending too much time tweaking Jekyll, and I had some friction in writing content, I basically stopped writing anything.</p><p>And this is a shame, at least for me: maybe the Internet doesn’t need yet another blog, but I like writing: it helps me to understand better the topic I am writing about, it forces me doing proper research, and all in all, I like communicating.</p><aside style="background-color:#f1f1ef;color:#000000"><span>✍️</span><div>My colleagues know I will put anything we discuss in a document: it’s my way to stay on top of stuff</div></aside><p>So, when I had the opportunity to change the platform to something I liked more, I took it. I spent the last month preparing the migration, and now this blog is hosted on <a href="https://blogs.hyvor.com/" target="_blank" rel="noopener noreferrer">Hyvor Blogs</a>!</p><p>But why Hyvor Blogs among all the platforms?</p><h2>Hyvor Talk</h2><p>Two years ago, almost to the date, <a href="https://rpadovani.com/add-comments" rel="noopener noreferrer">I introduced comments</a> on this blog. </p><p>I am thrilled with it: for a small fee, I have a nice product, without any tracked or shady business going on. So, when they launched Hyvor Blogs I was truly interested in them, given how happy I am with the comments system</p><h2>No maintenance</h2><p>Once again, for just a small fee I have a platform that just works: I don’t have to worry about software versions, or infrastructure, or anything else. Growing old, I learned to focus on the core of stuff, and delegate everything else. My blog is here for me to write and communicate, not for maintaining software. So, it makes sense to have somebody expert taking care of it.</p><h2>Pretty theme</h2><p>I like having a pretty theme for my blog. I customized one of the default ones to have a style similar to the previous one. Moreover, I also gained a dark theme, and a search function. Neither things I would have ever implemented on my own.</p><h2>Amazing support</h2><p>Hyvor is still a small company, not a faceless behemoth: they have outstanding support channels, and they helped me so much in setting everything up. It is nice to have somebody to talk to when I encounter bugs, or I am not confident how to do something. The product has room for improvement (which product doesn’t?), but any bug I reported has been fixed within a few days, if not hours.</p><h2>Writing more</h2><p>Now I can focus on just writing, and I can also do from my smartphone (I never installed git on it, so it was almost impossible with Jekyll). Maybe I will write more, or perhaps I will continue writing just a couple of posts every year. I definitely aim for the former, let’s see. </p><h2>Feedback</h2><p>I hope the look and feel of the blog remembers the old one. If you notice anything broken, if the change introduced any bug, or just if you want to let me know what you think of this change, please leave a comment below :-)</p><aside style="background-color:#f1f1ef;color:#000000"><span>🗯️</span><div>Any kind of feedback is always welcome! And you don’t want to leave a comment, you can always drop me an email at [email protected]</div></aside><p>So, thanks Hyvor for this pretty blog and the wonderful commenting system, and see you all in the next blog post!</p><p>Ciao,<br>R.</p><p></p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/no-heroes No heroes needed 2025-01-26T21:31:29+00:00 Being a hero is nice, isn't it? You work hard, single-handedly save the day, and your teammates are eternally grateful to you. However, such behavior is, in fact, highly problematic. Let's see why, and what to do instead. <p>In Google, we have an internal document, quite often quoted, that says to let things fail if the workload to keep them alive is too high. From Google, many good practices have been exported and adopted in the IT world, and I believe this could be very useful to many teams.</p><figure><img src="https://rpadovani.com/media/rb1ZvGMrrZ52kIxd.png" alt="Illustration: be the hero" srcset="https://rpadovani.com/media/rb1ZvGMrrZ52kIxd.png 1307w, https://rpadovani.com/media/rb1ZvGMrrZ52kIxd.png/500w 500w, https://rpadovani.com/media/rb1ZvGMrrZ52kIxd.png/750w 750w, https://rpadovani.com/media/rb1ZvGMrrZ52kIxd.png/1000w 1000w"><figcaption>Illustration by <a href="https://undraw.co/" target="_blank" rel="noopener noreferrer">unDraw</a>.</figcaption></figure><p>The story of how “heroism is bad” is not new. The internal Google document has actually been a presentation at SRE TechCon 2015. And, other companies have a similar take. But let’s start from the principle.</p><h2 id="#what-is-the-significance-of-heroism"><a href="##what-is-the-significance-of-heroism">What is the significance of heroism?</a></h2><p>When we talk about a hero for the team, we mean a single person taking on a systemic problem to compensate for it. <strong>No matter the cost</strong>, how many hours are needed, or the consequences. Of course, thanks to the hero, the crisis due to the systemic issue is momentarily avoided.</p><p>And, heroes get immediate reward! They have completed a gigantic task, they get praised from teammates, and they feel crucial: after all, without them, <strong>everything would have failed</strong>.</p><h2 id="why-being-a-hero-is-bad"><a href="#why-being-a-hero-is-bad">Why being a hero is bad</a></h2><p>Heroic behavior, although quite encouraged in many realities, at the end of the day, brings more damage than benefits. <strong>It is bad for the individual</strong>: takes a toll on them, brings to burnout, creates unsatisfiable expectations (you did once, why don’t you do it again?), and puts a lot of pressure on them.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Of course, we are talking about behavior in the working environment, not of heroic acts to <em>actually</em> save lives.</div></aside><p><strong>It is also bad for the team</strong>: it creates incorrect expectations from the team members, and it leads to inaction because the heroes will pick up anything left behind. Not only that, but it damages the morals because it makes it feel like everything has been solved by the hero, and everybody else has no purpose.</p><p>And <strong>it is bad for systems and processes</strong>: they don’t improve because teams don’t realize they are broken, given that at the end of the day, the heroes make everything work. No systemic fixes are carried on, ‘cause such problems are hidden due to the heroes efforts.</p><p>We work in teams for a reason: the idea that one person has the solution to everything is ridiculous.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div> The idea that one person has the solution to everything is ridiculous.</div></aside><h2 id="what-to-do-instead"><a href="#what-to-do-instead">What to do instead?</a></h2><p>On YouTube, there is an astonishing TED talk by Lorna Davis, <a href="https://www.youtube.com/watch?v=9zC2Bc22QfA" target="_blank" rel="noopener noreferrer">“A guide to collaborative leadership”</a>. If you have 14 minutes of spare time today, you should really use them to watch it.</p><p>There is a key concept in the video: <strong>radical interdependence</strong>.</p><h3 id="radical-interdependence"><a href="#radical-interdependence">What’s radical interdependence?</a></h3><p>Radical interdependence is just a fancy way to say that we need each other. In a complex, ever evolving, interconnected world, a single individual cannot change the world on its own.</p><p>And a team can delivery things that could be impossible to deliver by an individual alone.</p><p>However, quoting from the video:</p><blockquote><p>Interdependence is harder than being a hero. It requires us to be open, transparent, and vulnerable. And that’s not what traditional leaders have been trained to do.</p></blockquote><h3 id="let-things-fail"><a href="#let-things-fail">Let things fail</a></h3><p>It’s hard, but nowadays, every time I feel the urge to step in to save the day, I stop a minute, and ask myself: “<em>Can’t I sit down with the team and work this out?</em>”. And every so often, this requires things to fail: but <strong>that’s okay</strong>.</p><p>When things fail, we sit down all together, and we find out what went wrong, and what we can do better the next time. This allows improvements to the system, to the process, and at the end things are remarkably better than what they would have been if they hadn’t failed.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?️</span><div>It is imperative that a good process for postmortems is in place, and that such <a href="https://sre.google/sre-book/postmortem-culture/" target="_blank" rel="noopener noreferrer">process is blameless</a>.</div></aside><p>It’s a complex, difficult approach, but it provides significant improvements. Have you ever tried it, or plan to do so? Can you think of an occasion where it would have been useful? Let me know in the comments!</p><p>If you have any question, other than here in the comments, you can always reach me by email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/cka-exam How to prepare for the Certified Kubernetes Administrator (CKA) exam 2025-01-26T21:31:29+00:00 The Certified Kubernetes Administrator (CKA) exam tests your ability to operate a Kubernetes (K8s) cluster and your knowledge of how to run jobs over a cluster. I am sharing here some tips&tricks on how to pass it. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/01h3S6xK6oxC4Z20.png" alt="Illustration: road to knowledge" srcset="https://rpadovani.hyvorblogs.io/media/01h3S6xK6oxC4Z20.png 1152w, https://rpadovani.hyvorblogs.io/media/01h3S6xK6oxC4Z20.png/500w 500w, https://rpadovani.hyvorblogs.io/media/01h3S6xK6oxC4Z20.png/750w 750w, https://rpadovani.hyvorblogs.io/media/01h3S6xK6oxC4Z20.png/1000w 1000w"><figcaption>Illustration by <a href="https://undraw.co/" target="_blank" rel="noopener noreferrer">unDraw</a>.</figcaption></figure><h1 id="preparation"><a href="#preparation">Preparation</a></h1><p>Before taking the exam, I already had extensive experience using K8s, however always through managed services. I had to learn everything about how a K8s cluster is actually run.</p><p>I used the course hosted over <a href="https://acloudguru.com/course/certified-kubernetes-administrator-cka" target="_blank" rel="noopener noreferrer">A Cloud Guru</a>. It is up-to-date, and covers everything needed for the CKA. The course is very hands-on, so if you sign up for it, I recommend doing the laboratories (they provide the environment) because the exam is a long series of tasks similar to what you would be asked to do during the lab sections of this course.</p><p>Furthermore, you should learn how to navigate the <a href="https://kubernetes.io/docs/home/" target="_blank" rel="noopener noreferrer">K8s doc</a>. You can use it during the exam, so there is no point in learning every single field of every single resource. Do you need to create a pod? Go to the documentation for it, copy the basis example, and customize it.</p><h2 id="simulation"><a href="#simulation">Simulation</a></h2><p>When you register for the CKA certification exam on the Linux Foundation site, you will receive access to a simulated exam hosted by killer.sh. The simulation is significantly more difficult and longer than the actual exam: <em>I attempted a simulation two days before the exam, and I achieved 55%. On the actual exam, 91%</em>.</p><p><strong>I highly recommend taking the simulation</strong>, not only to see how prepared you are, but to familiarize yourself with the exam environment.</p><h1 id="the-exam"><a href="#the-exam">The exam</a></h1><p>The exam is 2 hours long, with a variable number of tasks. Points are also assigned based on how many tasks of a question you answered. You need a score of 66% to clear the exam.</p><p>During the exam <strong>you can access K8s documentation</strong>, use this power to your advantage. The environment in which the exam will run is based on XFCE. You will have a browser, a note pad, and a terminal you can use.</p><p>Being familiar with <code>vi</code> can help you work more quickly, especially if you spend a lot of time in there, and you just use the browser to read the documentation.</p><p>You can use the note pad to keep track of which questions you skipped, or you want to revise later.</p><p>During your exam, <strong>you will work on multiple K8s clusters</strong>. Each question is about one cluster, and at the beginning of the question, it says which context you should use. Don’t jump to answering the question without having switched context!</p><h2 id="requirements"><a href="#requirements">Requirements</a></h2><p>To attend the exam, you will have to download a dedicated browser, called PSI Safe Browser. You cannot download this in advance, it will be unlocked for you only 30 minutes before the beginning of the exam.</p><p>Theoretically, it is compatible with Windows, macOS, and Linux: however, many users have problems with Linux. Be aware that <em>there could be issues</em>, and have a macOS or Windows machine available, if possible, as a backup plan.</p><p>This browser will make sure that you are not running any background application, and that you don’t have multiple displays (only one monitor, sorry!).</p><p>After you installed the browser, you will have to identify yourself with some official document. You will have to show that you are alone in the room, that your desk is clean, that there is nothing under your desk, and so on. You will have to show your smartphone, and then show that you placed it in some unreachable place. The whole check-in procedure takes time, and it is error-prone (I had to show the whole room twice, then the browser crashed, and I had to start from scratch again).</p><p><strong>The onboarding procedure is the most stressful part of the exam</strong>: read all the requirements on the Linux Foundation Website, try to clear your desk from any unrelated things, and start the procedure as soon as possible. In my case, it took almost 50 minutes.</p><p>Their browser is not a perfect piece of software: in my case, the virtual environment inside this browser had a resolution of 800x600, making it impossible to have two windows on a side-by-side. However, you spend the huge majority of your time in the terminal, and sometimes on the browser to copy-paste snippets from the browser.</p><h1 id="tips"><a href="#tips">Tips</a></h1><ul><li><p>Keep a Windows or macOS machine nearby, if Linux doesn’t work for you;</p></li><li><p>Answering a question partially is better than not answering at all;</p></li><li><p><strong>Always double-check the K8s context</strong>! The first thing you must do for each question, it is switching the context of your kubectl according to the instructions;</p></li><li><p>Create files for each resource you create, so you can go back and adjust stuff. Moreover, if you have time at the end of the exam, makes way easier to check what you have done;</p></li><li><p>Remember to have a valid ID document with you for the exam check-in;</p></li></ul><h1 id="conclusion"><a href="#conclusion">Conclusion</a></h1><p><strong>Time constraints and the unfamiliar environment will be your greatest challenges</strong>: however, if you have used K8s in production before, you should be able to clear the exam without any major difficulty. Spending sometime training before is strongly suggested, no matter your level of expertise, just to understand the format of the exam.</p><p>Good luck, and reach out if you have any question, here in the comments or by email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/kubernetes-deployments Why K8s deployments need `matchLabels` keyword 2025-01-26T21:31:29+00:00 Kubernetes deployment want you to specify the `matchLabel` field. But why? It should be able to infer it on its own. Let's deep dive and understand how it works. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/VVCMq8Z45jMe7fIS.png" alt="A deep dive in K8s deployment matchLabels field" srcset="https://rpadovani.hyvorblogs.io/media/VVCMq8Z45jMe7fIS.png 1600w, https://rpadovani.hyvorblogs.io/media/VVCMq8Z45jMe7fIS.png/500w 500w, https://rpadovani.hyvorblogs.io/media/VVCMq8Z45jMe7fIS.png/750w 750w, https://rpadovani.hyvorblogs.io/media/VVCMq8Z45jMe7fIS.png/1000w 1000w, https://rpadovani.hyvorblogs.io/media/VVCMq8Z45jMe7fIS.png/1500w 1500w"><figcaption>Illustration by <a href="https://plus.undraw.co/" target="_blank" rel="noopener noreferrer">unDraw+</a>.</figcaption></figure><p>A Kubernetes (K8s) <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" target="_blank" rel="noopener noreferrer">Deployment</a> provides a way to define how many replicas of a <a href="https://kubernetes.io/docs/concepts/workloads/pods/" target="_blank" rel="noopener noreferrer">Pod</a> K8s should aim to keep alive. I’m especially bothered by the Deployment spec’s requirement that we must specify a label selector for pods, and that label selector must match the same labels we have defined in the template. Why can’t we just define them once? Why can’t K8s infer them on its own? As I will explain, there is actually a good reason. However, to understand it, you would have to go down a rabbit hole to figure it out.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Did you know? K8s is short for Kubernetes because there are 8 letters between K and S.</div></aside><h2 id="a-deployment-specification"><a href="#a-deployment-specification">A deployment specification</a></h2><p>Firstly, let’s take a look at a simple deployment specification for K8s:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-yaml has-highlight has-line-numbers" data-language="yaml" data-annotations="h=9" data-name="deployment-example.yaml" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #8FBCBB">apiVersion</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">apps/v1</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #8FBCBB">kind</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Deployment</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #8FBCBB">metadata</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx-deployment</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #8FBCBB">spec</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">replicas</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">3</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">selector</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">matchLabels</span><span style="color: #ECEFF4">:</span></span></div><div class="line highlight" style="background-color:#3b4252"><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">app</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># Why can&#039;t K8s figure it out on its own?</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">template</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">metadata</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">labels</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">app</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">spec</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">containers</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">image</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx:latest</span></span></div></code></pre><p>This is a basic deployment, taken from the <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#creating-a-deployment" target="_blank" rel="noopener noreferrer">official documentation</a>, and here we can already see that we need to fill the <code>matchLabels</code> field.</p><p>What happens if we drop the <em>“selector”</em> field completely?</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-sh has-line-numbers" data-language="sh" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #88C0D0">➜</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">kubectl</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">apply</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-f</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx-deployment.yaml</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #88C0D0">The</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Deployment</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">nginx-deployment</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">is</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">invalid:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">3</span><span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> spec.selector: Required value</span></span></div></code></pre><p>Okay, so we need to specify a selector. Can it be different from the <em>“label”</em> field in the <em>“template”</em>? I will try with:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-yaml has-line-numbers" data-language="yaml" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #8FBCBB">matchLabels</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">app</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx-different</span></span></div></code></pre><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-sh has-line-numbers" data-language="sh" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #88C0D0">➜</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">kubectl</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">apply</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-f</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">nginx-deployment.yaml</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #88C0D0">The</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Deployment</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">nginx-deployment</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">is</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">invalid:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">spec.template.metadata.labels:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Invalid</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">value:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">map[string]string{</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">app</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">:</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">nginx</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">}:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">`</span><span style="color: #88C0D0">selector</span><span style="color: #ECEFF4">`</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">does</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">not</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">match</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">template</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">`</span><span style="color: #88C0D0">labels</span><span style="color: #ECEFF4">`</span></span></div></code></pre><p>As expected, K8s doesn’t like it, it must match the template. So, we must fill a field with a well-defined value. It really seems that a computer could do that for us, why do we have to specify it manually? It drives me crazy having to do something a computer could do without any problem. <strong>Or could it</strong>?</p><aside style="background-color:#f1f1ef;color:#000000"><span>❗</span><div>There are usually good reasons behind what it seems, on a first sight, a not well-thought implementation, and this is true here as well, as we’ll see.</div></aside><h2 id="behind-a-deployment"><a href="#behind-a-deployment">Behind a deployment</a></h2><p>How does a deployment work? Behind the curtains, when you create a new deployment, K8s creates two different objects: a <a href="https://kubernetes.io/docs/concepts/workloads/pods/" target="_blank" rel="noopener noreferrer">Pod</a> definition, using as its specification what is available in the <em>“template”</em> field of the <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" target="_blank" rel="noopener noreferrer">Deployment</a>, and a <a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/" target="_blank" rel="noopener noreferrer">ReplicaSet</a>. You can easily verify this using <code>kubectl</code> to retrieve pods and replica sets <em>after</em> you have created a deployment.</p><blockquote><p>A ReplicaSet’s purpose is to maintain a stable set of replica Pods running at any given time. As such, it is often used to guarantee the availability of a specified number of identical Pods.</p></blockquote><p>A ReplicaSet needs a <em>selector</em> that specifies how to identify Pods it can acquire and manage: however, this doesn’t explain why we must specify it, and why K8s cannot do it on its own. In the end, a Deployment is a high-level construct that should hide ReplicaSet quirks: such details shouldn’t concern us, and the Deployment should take care of them on its own.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Probably, you may never need to manipulate ReplicaSet objects: use a <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" target="_blank" rel="noopener noreferrer">Deployment</a> instead, and define your application in the spec section.</div></aside><h2 id="digging-deeper"><a href="#digging-deeper">Digging deeper</a></h2><p>Understanding how a Deployment works doesn’t help us find the reason for this particular behavior. Given that Googling doesn’t seem to bring any interesting results on this particular topic, it’s time to go to the source (literally): luckily, K8s is open source, so we can check its history on <a href="https://github.com/kubernetes/kubernetes" target="_blank" rel="noopener noreferrer">GitHub</a>.</p><p>Going back in time, we find out that, actually, K8s used to infer it the <code>matchLabels</code> field! The behavior was removed with <code>apps/v1beta2</code> (released with Kubernetes 1.8), through Pull Request <a href="https://github.com/kubernetes/kubernetes/pull/50164" target="_blank" rel="noopener noreferrer">#50164</a>. Such pull request links to issue <a href="https://github.com/kubernetes/kubernetes/issues/50339" target="_blank" rel="noopener noreferrer">#50339</a>, that, however, has a very brief description, and lacks the reasoning behind such a choice.</p><p>Luckily, other issues provide way more context, as <a href="https://github.com/kubernetes/kubernetes/issues/26202" target="_blank" rel="noopener noreferrer">#26202</a>: it turns out, the main problem with defaulting is when in subsequent updates to the resource, labels are mutated: the patch operation is somehow fickle, and <code>apply</code> breaks when you update a label that was used as default.</p><p>Many other concerns have been described by <a href="https://github.com/bgrant0607" target="_blank" rel="noopener noreferrer">Brian Grant</a> in deep in the issue <a href="https://github.com/kubernetes/kubernetes/issues/15894#issuecomment-222194015" target="_blank" rel="noopener noreferrer">#15894</a>.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>The linked issues are rich of technical details, and they have many comments. If you want to understand exactly how <code>kubectl apply</code> works, take a look!</div></aside><p>Basically, assuming a default value, creates many questions and concerns: what’s the difference between explicitly setting a label as null, and leaving it empty? How to manage all the cases where users left the default, and now they intend to update the resource to manage themselves the label (or the other way around)?</p><h2 id="conclusion"><a href="#conclusion">Conclusion</a></h2><p>Given that in K8s everything is intended to be <strong>declarative</strong>, developers have chosen that explicit is better than implicit, especially for corner cases: specifying things explicitly allows a more robust validation on creation and update time, and removes some possible bugs that existed due to uncertainties caused by lack of clarity.</p><p>Shortly after dropping the defaulting behavior, developers also <a href="https://github.com/kubernetes/kubernetes/issues/50808" target="_blank" rel="noopener noreferrer">made the labels immutable</a>, to make sure behaviors were well-defined. Maybe in the future labels will be mutable again, but to have that working, somebody needs to design a well-though document explaining how to manage all the possible edge cases that could happen when a controller is updated.</p><p>I hope you found this deep dive into the question interesting. I spent some time on it, since I was very curious, and I hope that the next person with the same question can find this article and get an answer quicker than what it took me.</p><p>If you have any question, or feedback, please leave a comment below, or write me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/terraform-helm-crds-manager Managing Helm CRDs with Terraform 2025-01-26T21:31:29+00:00 Introducing a Terraform module to manage Helm Custom Resource Definitions (CRDs) through code, to being able to manage Kubernetes deployments completely through GitOps. <p>Helm is a remarkable piece of technology to manage your Kubernetes deployments, and used with Terraform is perfect for deploying following the GitOps strategy.</p><figure><img src="https://rpadovani.hyvorblogs.io/media/TLKa1M5VValXoxjq.png" alt="Terraform Helm CRDs manager" srcset="https://rpadovani.hyvorblogs.io/media/TLKa1M5VValXoxjq.png 1600w, https://rpadovani.hyvorblogs.io/media/TLKa1M5VValXoxjq.png/500w 500w, https://rpadovani.hyvorblogs.io/media/TLKa1M5VValXoxjq.png/750w 750w, https://rpadovani.hyvorblogs.io/media/TLKa1M5VValXoxjq.png/1000w 1000w, https://rpadovani.hyvorblogs.io/media/TLKa1M5VValXoxjq.png/1500w 1500w"><figcaption>Illustration by <a href="https://plus.undraw.co/" target="_blank" rel="noopener noreferrer">unDraw+</a>.</figcaption></figure><aside style="background-color:#f1f1ef;color:#000000"><span>❔</span><div>What’s GitOps? Great question! As <a href="https://about.gitlab.com/topics/gitops/" target="_blank" rel="noopener noreferrer"> this helpful, introductory article summarizes</a>, it is Infrastructure as Code, plus Merge Requests, plus Continuous Integration. Follow the link to explore further the concept.</div></aside><p>However, Helm has a limitation: <a href="https://helm.sh/docs/chart_best_practices/custom_resource_definitions/" target="_blank" rel="noopener noreferrer">it doesn’t manage the lifecycle of Custom Resource Definitions</a> (CRDs), meaning it will only install the CRD during the first installation of a chart. Subsequent chart upgrades will not add or remove CRDs, even if the CRDs have changed.</p><p>This can be a huge problem for a GitOps approach: having to update CRDs manually isn’t a great strategy, and makes it very hard to keep in sync with deployments and rollbacks.</p><p>For this very reason, I created <a href="https://registry.terraform.io/modules/rpadovani/helm-crds/kubectl/latest" target="_blank" rel="noopener noreferrer">a small Terraform module</a> that will read from some online manifests of CRDs, and apply them. When parametrizing the version of the chart, it is simple to keep Helm Charts and CRDs in sync, without having to do anything manually.</p><h2 id="example"><a href="#example">Example</a></h2><p>Let’s use <a href="https://github.com/aws/karpenter" target="_blank" rel="noopener noreferrer">Karpenter</a> as an example of how to use the module. We want to deploy the chart with the <a href="https://registry.terraform.io/providers/hashicorp/helm/latest/docs" target="_blank" rel="noopener noreferrer">Helm provider</a>, and we use this new Terraform module to manage its CRDs as well:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-terraform has-line-numbers" data-language="terraform" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9FF">resource &quot;helm_release&quot; &quot;karpenter&quot; {</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> name = &quot;karpenter&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> namespace = &quot;karpenter&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> repository = &quot;https://charts.karpenter.sh&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> chart = &quot;karpenter&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> version = var.chart_version</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> // ... All the other parameters necessary, skipping them here ...</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF">}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF">module &quot;karpenter-crds&quot; {</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> source = &quot;rpadovani/helm-crds/kubectl&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> version = &quot;0.1.0&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> crds_urls = [</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> &quot;https://raw.githubusercontent.com/aws/karpenter/v${var.chart_version}/charts/karpenter/crds/karpenter.sh_provisioners.yaml&quot;,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> &quot;https://raw.githubusercontent.com/aws/karpenter/v${var.chart_version}/charts/karpenter/crds/karpenter.k8s.aws_awsnodetemplates.yaml&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> ]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF">}</span></span></div></code></pre><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Karpenter is an incredible open-source Kubernetes node provisioner built by AWS. If you haven’t tried it yet, take some minutes to read about it.</div></aside><p>As you can see, we parametrize the version of the chart, so we can be sure to have the same version for CRDs as the Helm chart. Behind the curtains, Terraform will download the raw file, and apply it with <code>kubectl</code>. Of course, the operator running Terraform needs to have enough permissions to launch such scripts, so you need to <a href="https://registry.terraform.io/providers/gavinbunney/kubectl/latest/docs#configuration" target="_blank" rel="noopener noreferrer">configure</a> the kubectl provider.</p><p>The URLs must point to just the Kubernetes manifests, and this is why we use the raw version of the GitHub URL.</p><p>The source code of the module is <a href="https://github.com/rpadovani/terraform-kubectl-helm-crds" target="_blank" rel="noopener noreferrer">available on GitHub</a>, so you are welcome to chime in and open any issue: I will do my best to address problems and implement suggestions.</p><h2 id="conclusion"><a href="#conclusion">Conclusion</a></h2><p>I use this module in production, and I am very satisfied with it: it brings under GitOps the last part I missed: the CRDs. Now, my only task when I install a new chart is finding all the CRDs, and build a URL that contains the chart version. Terraform will take care of the rest.</p><p>I hope this module can be useful to you as it is to me. If you have any question, or feedback, or if you would like some help, please leave a comment below, or write me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/contribute-to-gitlab Why you should contribute to GitLab 2025-01-26T21:31:29+00:00 Contributing to any open-source project is a great way to spend a few hours each month. I started more than 10 years ago, and it has ultimately shaped my career in ways I couldn’t have imagined! <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/BDoNEGKQ8sr1nP4y.svg" alt="GitLab logo as cover"><figcaption>The new GitLab logo, <a href="https://about.gitlab.com/blog/2022/04/27/devops-is-at-the-center-of-gitlab/" target="_blank" rel="noopener noreferrer">just announced</a> on the 27th April 2022.</figcaption></figure><p>Nowadays, my contributions focus mostly on <a href="https://about.gitlab.com/" target="_blank" rel="noopener noreferrer">GitLab</a>, so you will see many references to it in this blog post, but the content is quite generalizable; I would like to share my experience to highlight why you should consider contributing to an open-source project.</p><p>And contributing doesn’t mean only coding: there are countless ways to help an open-source project: <strong>translating</strong> it to different languages, <strong>reporting</strong> issues, <strong>designing</strong> new features, <strong>writing</strong> the documentation, offering <strong>support</strong> on forums and <a href="https://stackoverflow.com/collectives/gitlab" target="_blank" rel="noopener noreferrer">StackOverflow</a>, and so on.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Writing blog posts, tweeting, and helping foster the community are nice ways to contribute to a project ;-)</div></aside><p>Before deep diving into this wall of text, be aware there are mainly three parts to this blog post, after this introduction: a <em>context</em> section, where I describe my personal experience with open source, and what does it mean to me. Then, a nice <em>list of reasons</em> to contribute to any project. In closing, there are some <em>tips</em> on how to start contributing, both in general, and something specific to GitLab.</p><h1 id="context"><a href="#context">Context</a></h1><p>Ten years ago, I was fresh out of high school, without (almost) any knowledge about IT: however, I found out that I had a massive passion for it, so I enrolled in a Computer Engineering university (boring!), and I started contributing to <a href="https://ubuntu.com/" target="_blank" rel="noopener noreferrer">Ubuntu</a> (cool!). I began with the Italian Local Community, and I soon moved to <a href="https://en.wikipedia.org/wiki/Ubuntu_Touch" target="_blank" rel="noopener noreferrer">Ubuntu Touch</a>.</p><p>We all know how it ended, but it still has been a fantastic ride, with plenty of great moments: just take a look at the <a href="https://rpadovani.com/blog" target="_blank" rel="noopener noreferrer">archive</a> of this blog, and you can see the passion and the enthusiasm I had. I was so enthusiast, that I wrote a <a href="https://rpadovani.com/why-you-should-contribute-to-ubuntu-touch" target="_blank" rel="noopener noreferrer">similar blog post</a> to this one! I think it highlights really well some considerable differences 10 years make.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>I often considered rewriting that old article: however, I have a strange attachment to it as it is, with all that English mistakes. One of the first blog posts I have written, and it was really well received!</div></aside><p>Back then, I wasn’t working, just studying, so I had a lot of spare time. My English was <em>way</em> worse. I was at the beginning of my journey in the computer world, and Ubuntu has ultimately shaped a big part of it. My knowledge was very limited, and I never worked before. Contributing to Ubuntu gave me a glimpse of the real world, I met outstanding engineers who taught me a lot, and boosted my CV, helping me to land <a href="https://rpadovani.com/my-first-job" target="_blank" rel="noopener noreferrer">my first job</a>.</p><p>Since then, I completed a master’s degree in C.S., worked in different companies in three different countries, and became a professional. Nowadays, my contributions to open source are more sporadic (adulthood, yay), but given how much it meant to me, I am still a big fan, and I try to contribute when I can, and how I can.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Advocacy, as in this blog post, is a great way to contribute! You spread awareness, and this helps to find new contributors, and maybe inspire some young students to try out!</div></aside><h1 id="why-contributing"><a href="#why-contributing">Why contributing</a></h1><h2 id="friends"><a href="#friends">Friends</a></h2><p>During my years contributing to open-source software, I’ve met countless incredible people, with some of whom I’ve become friends. In the old blog post I mentioned <a href="https://www.linkedin.com/in/davidplanella/" target="_blank" rel="noopener noreferrer">David</a>: in the last 9 years we stayed in touch, met on different occasions in different cities: the last time was as recent as last summer. Back at the time, he was a Manager in the Ubuntu Community Team at Canonical, and then, he became Director of Community Relations at GitLab. Small world!</p><figure><img src="https://rpadovani.hyvorblogs.io/media/JrSFuEPc51rB3O7h.jpg" alt="The Ubuntu Touch Community Team in Malta, in 2014" srcset="https://rpadovani.hyvorblogs.io/media/JrSFuEPc51rB3O7h.jpg 1317w, https://rpadovani.hyvorblogs.io/media/JrSFuEPc51rB3O7h.jpg/500w 500w, https://rpadovani.hyvorblogs.io/media/JrSFuEPc51rB3O7h.jpg/750w 750w, https://rpadovani.hyvorblogs.io/media/JrSFuEPc51rB3O7h.jpg/1000w 1000w"><figcaption>The Ubuntu Touch Community Team in <a href="https://rpadovani.com/canonical-sprint-in-malta" target="_blank" rel="noopener noreferrer">Malta</a>, in 2014. It has been an incredible week, sponsored by Canonical!</figcaption></figure><p>One interesting thing is people contribute to open-source projects from their homes, all around the world: when I travel, I usually know somebody living in my destination city, so I’ve always at least one night booked for a beer with somebody I’ve met only online; it’s a pleasure to speak with people from different backgrounds, and having a glimpse in their life, all united by one common passion.</p><h2 id="fun"><a href="#fun">Fun</a></h2><p>Having fun is important! You cannot spend your leisure time getting bored or annoyed: contributing to open source is fun ‘cause you pick the problems you would like to work on, and you don’t need all that <em>bureaucracy</em> and <em>meetings</em> that is often needed in your daily job. You can be challenged, and feel useful, and improving a product, without any manager on your shoulder, and at your pace.</p><h2 id="being-up-to-date-on-how-things-evolve"><a href="#being-up-to-date-on-how-things-evolve">Being up-to-date on how things evolve</a></h2><p>Contributing to a project typically gives you an idea of how the teams behind it work, which technologies they use, and which methodologies. Many open-source projects use bleeding-edge technologies, or draw a path. Being in contact with new ideas is a great way to know where the industry is headed, and what is the latest news: it is especially true if you hang out in the channels where the community meets, being them Discord, forums, or IRC (well, IRC is not really bleeding-edge, but it is <em>fun</em>).</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>For example, the <a href="https://about.gitlab.com/handbook/" target="_blank" rel="noopener noreferrer">GitLab Handbook</a> is a precious collection of resources, ideas, and methodologies on how to run a 1000 people company in a transparent, full remote, way. It’s a great reading, with a lot of wisdom.</div></aside><h2 id="learning"><a href="#learning">Learning</a></h2><p>When contributing in an area that doesn’t match your expertise, you always learn something new: reviews are usually precise and on point, and projects of a remarkable size commonly have a coaching team that help you to start contributing, and guide you on how to land your first patches.</p><p>In GitLab, if you need a help with merging your code, there are the <a href="https://about.gitlab.com/job-families/expert/merge-request-coach/" target="_blank" rel="noopener noreferrer">Merge Request Coaches</a>! And for any type of help, you can always join <a href="https://gitter.im/gitlabhq/contributors" target="_blank" rel="noopener noreferrer">Gitter</a>, or ask on the <a href="https://forum.gitlab.com" target="_blank" rel="noopener noreferrer">forum</a>, or write to the <a href="mailto:[email protected]" rel="noopener noreferrer">dedicated email address</a>.</p><p>Feel also free to <a href="mailto:[email protected]" rel="noopener noreferrer">ping me</a> directly if you want some general guidance!</p><h2 id="giving-back"><a href="#giving-back">Giving back</a></h2><p>I work as a Platform Engineer. My job is built on an incredible amount of open-source libraries, amazing FOSS services, and I basically have just to glue together different pieces. When I find some rough edge that could be improved, I try to do so.</p><p>Nowadays, I find crucial to having well-maintained documentation, so after I have achieved something complex, I usually go back and try to improve the documentation, where lacking. It is my tiny way to say thanks, and giving back to a world that really has shaped my career.</p><p>This is also what mostly of my blogs posts are about: after having completed something on which I spent fatigue on, I find it nice to be able to share such information. Every so often, I find myself years later following my guide, and I really also appreciate when other people find the content useful.</p><h2 id="swag"><a href="#swag">Swag</a></h2><p>Who doesn’t like <a href="https://shop.gitlab.com/shop/sale/" target="_blank" rel="noopener noreferrer">swag</a>? :-) Numerous projects have delightful swags, starting from stickers, that they like to share with the whole community. Of course, it shouldn’t be your main driver, ‘cause you will soon notice that it is ultimately not worth the amount of time you spend contributing, but it is charming to have GitLab socks!</p><figure><img src="https://rpadovani.hyvorblogs.io/media/txwBdw6AqWGQckDR.jpg" alt="A GitLab branded mechanical keyboard" srcset="https://rpadovani.hyvorblogs.io/media/txwBdw6AqWGQckDR.jpg 720w, https://rpadovani.hyvorblogs.io/media/txwBdw6AqWGQckDR.jpg/500w 500w"><figcaption>A GitLab branded mechanical keyboard, courtesy of the GitLab&#039;s security team! This very article has been typed with it!</figcaption></figure><h1 id="tips"><a href="#tips">Tips</a></h1><p>I hope I inspired you to contribute to some open-source project (maybe GitLab!). Now, let’s discuss some small tricks on how to begin easily.</p><h2 id="find-something-you-are-passionate-about"><a href="#find-something-you-are-passionate-about">Find something you are passionate about</a></h2><p>You must find a project you are passionate about, and that you use frequently. Looking forward to a release, knowing that your contributions will be included, it is a <strong>wonderful satisfaction</strong>, and can really push you to do more.</p><p>Moreover, if you already know the project you want to contribute to, you probably know already the biggest pain points, and where the project needs some contributions.</p><h2 id="start-small-and-easy"><a href="#start-small-and-easy">Start small and easy</a></h2><p>You don’t need to do gigantic contributions to begin. Find something tiny, so you can get familiar with the project workflows, and how contributions are received.</p><p>My journey with Ubuntu started <a href="https://code.launchpad.net/~rpadovani/phablet-tools/fix-for-1139999/+merge/153419" target="_blank" rel="noopener noreferrer">correcting a typo</a> in a README, and here I am, years later, having contributed to dozens of projects, and having a career in the C.S. field. Back then, I really had no idea of what my future would hold.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Launchpad and bazaar instead of GitLab and git — down the memory lane! </div></aside><p>For GitLab, you can take a look at the issues marked as “<a href="https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&amp;state=opened&amp;label_name%5B%5D=good+for+new+contributors" target="_blank" rel="noopener noreferrer">good for new contributors</a>”. They are designed to be addressed quickly, and onboard new people in the community. In this way, you don’t have to focus on the difficulties of the task at hand, but you can easily explore how the community works.</p><h2 id="writing-issues-is-a-good-start"><a href="#writing-issues-is-a-good-start">Writing issues is a good start</a></h2><p>Writing high-quality issues is a great way to start contributing: maintainers of a project are not always aware of how the software is used, and cannot be aware of all the issues. If you know that something could be improved, write it down: spend some time explicating what happens, what you expect, how to reproduce the problem, and maybe suggest some solutions as well! Perhaps, the first issue you write down could be the very first issue you resolve.</p><h2 id="not-much-time-required"><a href="#not-much-time-required">Not much time required!</a></h2><p>Contributing to a project doesn’t require necessarily a lot of time. When I was younger, I definitely dedicated way more time to open-source projects, implementing gigantic features. Nowadays, I don’t do that anymore (life is much more than computers), but I like to think that my contributions are still useful. Still, I don’t spend more than a couple of hours a month, based on my schedule, and how much is raining (yep, in winter I definitely contribute more than in summer).</p><h2 id="gitlab-is-super-easy"><a href="#gitlab-is-super-easy">GitLab is super easy</a></h2><p>Do you use GitLab? Then you should undoubtedly try to contribute to it. It is easy, it is fun, and there are many ways. Take a look at <a href="https://about.gitlab.com/community/contribute/" target="_blank" rel="noopener noreferrer">this guide</a>, hang out on <a href="https://gitter.im/gitlabhq/contributors" target="_blank" rel="noopener noreferrer">Gitter</a>, and see you around. ;-)</p><p>Next week (9th-13th May 2022) there is also a <a href="https://about.gitlab.com/community/hackathon/" target="_blank" rel="noopener noreferrer">GitLab Hackathon</a>! It is a real fun and easy way to start contributing: many people are available to help you, there are video sessions talking about contributing, and just doing a small contribution you will receive a pretty <a href="https://about.gitlab.com/community/hackathon/#prize" target="_blank" rel="noopener noreferrer">prize</a>.</p><p>And in time, if you are consistent in your contributions, you can become a <a href="https://about.gitlab.com/community/heroes/" target="_blank" rel="noopener noreferrer">GitLab Hero</a>! How cool is that?</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>And if I was able to do it with my few contributions, you can as well!</div></aside><p>I really hope this wall of text made you consider contributing to an open-source project. If you have any question, or feedback, or if you would like some help, please leave a comment below, or write me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/private-rust-crates Managing Rust crates in private Git repositories 2025-01-26T21:31:29+00:00 Rust is all hot these days, and it is indeed a nice language to work with. Let's take a look at a small challenge: how to host private crates in the form of Git repositories, making them easily available both to developers and CI/CD systems. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/dU9DCt6CSk9n3M23.png" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/dU9DCt6CSk9n3M23.png 1200w, https://rpadovani.hyvorblogs.io/media/dU9DCt6CSk9n3M23.png/500w 500w, https://rpadovani.hyvorblogs.io/media/dU9DCt6CSk9n3M23.png/750w 750w, https://rpadovani.hyvorblogs.io/media/dU9DCt6CSk9n3M23.png/1000w 1000w"><figcaption>Ferris the crab, unofficial mascot for Rust.</figcaption></figure><p>A Rust crate can be hosted in different places: on a public registry on <a href="https://crates.io/" target="_blank" rel="noopener noreferrer">crates.io</a>, but also in a private Git repo hosted somewhere. In this latter case, there are some challenges on how to make the crate easily accessible to both engineers and CI/CD systems.</p><p>Developers usually authenticate through <a href="https://docs.gitlab.com/ee/ssh/" target="_blank" rel="noopener noreferrer">SSH keys</a>: given humans are terrible at remembering long passwords, using SSH keys allows us to do not memorize credentials, and having authentication methods that just work. The security is quite high as well: each device has a unique key, and if a device gets compromised, deactivating the related key solves the problem.</p><p>On the other hand, it is better to avoid using SSH keys for CI/CD systems: such systems are highly volatile, with dozens, if not hundreds, instances that get created and destroyed hourly. For them, a short-living token is a way better choice. Creating and revoking SSH keys on the fly can be tedious and error-prone.</p><p>This arises a challenge with Rust crates: if hosted on a private repository, the dependency can be reachable through SSH, such as</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-toml has-line-numbers" data-language="toml" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">dependencies</span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9">my-secret-crate</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">git</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssh://[email protected]/rpadovani/my-secret-crate.git</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">branch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">main</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span></div></code></pre><p>or through HTTPS, such as</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-toml has-line-numbers" data-language="toml" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">dependencies</span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9">my-secret-crate</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">git</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">https://gitlab.com/rpadovani/my-secret-crate</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">branch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">main</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span></div></code></pre><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>In the future, I hope we don’t need to host private crates on Git repositories, <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/33060" target="_blank" rel="noopener noreferrer">GitLab should add</a> a native implementation of a private registry for crates.</div></aside><p>The former is really useful and simple for engineers: authentication is the same as always, so no need to worry about it. However, it is awful for CI/CD: now there is a need to manage the lifecycle of SSH keys for automatic systems.</p><p>The latter is awful for engineers: they need a new additional authentication method, slowing them down, and of course, there will be authentication problems. On the other hand, it is great for automatic systems.</p><h2 id="howto"><a href="#howto">How to conciliate the two worlds?</a></h2><p>Well, let’s use them both! In the <code>Cargo.toml</code> file, use the SSH protocol, so developers can simply clone the main repo, and they will be able to clone the dependencies without further hassle.</p><p>Then, configure the CI/CD system to clone every dependency through HTTPS, thanks to a neat feature of Git itself: <code>insteadOf</code>.</p><p>From the <a href="https://git-scm.com/docs/git-config" target="_blank" rel="noopener noreferrer">Git-SCM website</a>:</p><blockquote><p><code>url.&lt;base&gt;.insteadOf</code>:<br> Any URL that starts with this value will be rewritten to start, instead, with . In cases where some site serves a large number of repositories, and serves them with multiple access methods, and some users need to use different access methods, this feature allows people to specify any of the equivalent URLs and have Git automatically rewrite the URL to the best alternative for the particular user, even for a never-before-seen repository on the site. When more than one insteadOf strings match a given URL, the longest match is used.</p></blockquote><p>Basically, it allows rewriting part of the URL automatically. In this way, it is easy to change the protocol used: developers will be happy, and the security team won’t have to scream about long-lived credentials on CI/CD systems.</p><p>An implementation example using GitLab, but it can be done on any CI/CD system:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-yaml has-line-numbers" data-language="yaml" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #8FBCBB">my-job</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">before_script</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">git config --global credential.helper store</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">echo &quot;https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com&quot; &gt; ~/.git-credentials</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">git config --global url.&quot;https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com&quot;.insteadOf ssh://[email protected]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">script</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">cargo build</span></span></div></code></pre><p>The <code>CI_JOB_TOKEN</code> is a <a href="https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html" target="_blank" rel="noopener noreferrer">unique token</a> valid only for the duration of the GitLab pipeline. In this way, also if a machine got compromised, or logs leaked, the code is still sound and safe.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Do you need an introduction to GitLab CI/CD? I’ve <a href="https://rpadovani.com/introduction-gitlab-ci" target="_blank" rel="noopener noreferrer">written something about it</a>!</div></aside><p>What do you think about Rust? If you use it, have you integrated it with your CI/CD systems? Share your thoughts in the comments below, or drop me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br>R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/eks-iam-permissions The inconsistencies of AWS EKS IAM permissions 2025-01-26T21:31:29+00:00 AWS EKS is a remarkable product: it manages Kubernetes for you, letting you focussing on creating and deploying applications. However, if you want to manage permissions accordingly to the shared responsibility model, you are in for some wild rides. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/cME02PkSvLfjxRpz.png" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/cME02PkSvLfjxRpz.png 1263w, https://rpadovani.hyvorblogs.io/media/cME02PkSvLfjxRpz.png/500w 500w, https://rpadovani.hyvorblogs.io/media/cME02PkSvLfjxRpz.png/750w 750w, https://rpadovani.hyvorblogs.io/media/cME02PkSvLfjxRpz.png/1000w 1000w"><figcaption>Image courtesy of <a href="https://undraw.co" target="_blank" rel="noopener noreferrer">unDraw.</a></figcaption></figure><h2 id="the-shared-responsibility-model"><a href="#the-shared-responsibility-model">The shared responsibility model</a></h2><p>First, what’s the shared responsibility model? Well, to design a <a href="https://aws.amazon.com/architecture/well-architected/" target="_blank" rel="noopener noreferrer">well-architected application</a>, AWS suggests following six pillars. Among these six pillars, one is <a href="https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html" target="_blank" rel="noopener noreferrer"><strong>security</strong></a>. Security includes sharing responsibility between AWS and the customer. In particular, and I <a href="https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/shared-responsibility.html" target="_blank" rel="noopener noreferrer">quote</a>,</p><blockquote><p>Customers are responsible for managing their data (including encryption options), classifying their assets, and using IAM tools to apply the appropriate permissions.</p></blockquote><p>Beautiful, isn’t it? AWS gives us a powerful tool, IAM, to manage permissions; we have to configure things in the best way, and AWS gives us the way to do so. Or does it? Let’s take a look together.</p><h2 id="our-goal"><a href="#our-goal">Our goal</a></h2><aside style="background-color:#f1f1ef;color:#000000"><span>⚡</span><div>I would say the goal is simple, but since we are talking about Kubernetes, things cannot be <em>just</em> simple.</div></aside><p>Our goal is quite straightforward: setting up a Kubernetes cluster for our developers. Given that AWS offers AWS EKS, a managed Kubernetes service, we only need to configure it properly, and we are done. Of course, we will follow best practices to do so.</p><h2 id="a-proper-setup"><a href="#a-proper-setup">A proper setup</a></h2><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Infrastructure as code is out of scope for this post, but if you have never heard about it before, I strongly suggest taking a look into it.</div></aside><p>Of course, we don’t use the AWS console to manually configure stuff, but <a href="https://docs.aws.amazon.com/whitepapers/latest/introduction-devops-aws/infrastructure-as-code.html" target="_blank" rel="noopener noreferrer">Infrastructure as Code</a>: basically, we will write some code that will call the AWS APIs on our behalf to set up AWS EKS and everything correlated. In this way, we can have a reproducible setup that we could deploy in multiple environments, and countless other advantages.</p><p>Moreover, we want to avoid launching scripts that interact with our infrastructure from our PC: we prefer not to have permissions to destroy important stuff! <a href="https://en.wikipedia.org/wiki/Separation_of_concerns" target="_blank" rel="noopener noreferrer">Separation of concerns</a> is a fundamental, and we wish to write code without worrying about having consequences on the real world. All our code should be vetted from somebody else through a merge request, and after being approved and merged to our <em>main</em> branch, a runner will pick it up and apply the changes.</p><p>We are at the core of the problem: our runner should follow the <a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege" target="_blank" rel="noopener noreferrer">principle of least privilege</a>: it should be able to do only what it needs to do, and nothing more. This is why we will create a IAM role only for it, with only the permissions to manage our EKS cluster and everything in it, but nothing more.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>A runner is any CI/CD that will execute the code on your behalf. In my case, it is a GitLab runner, but it could be any continuous integration system.</div></aside><p>I would have a massive rant about how bad is the AWS documentation for IAM in general, not only for EKS, but I will leave it to some other day.</p><p>The first part of creating a role with minimum privileges is, well, understanding what <em>minimum</em> means in our case. A starting point is the AWS documentation: unfortunately, it is always a <strong>bad</strong> starting point concerning IAM permissions because it is always too generous in allowing permissions.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>I would have a massive rant about how bad is the AWS documentation for IAM in general, not only for EKS, but I will leave it to some other day.</div></aside><h2 id="the-minimum-permission-accordingly-to-aws"><a href="#the-minimum-permission-accordingly-to-aws">The “minimum” permission accordingly to AWS</a></h2><p>Okay, hardening this will be fun, but hey, do not let bad documentation get in the way of a proper security posture.</p><p>You know what will get in the way? Bugs! A ton of bugs, with absolutely useless error messages.</p><p>I started limiting access to only the namespace of the EKS cluster I wanted to create. I ingenuously thought that we could simply limit access to the resources belonging to the cluster. But, oh boy, I was mistaken!</p><p>Looking at the documentation for IAM resources and actions, I created this policy:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-json has-line-numbers" data-language="json" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Version</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">2012-10-17</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Statement</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Effect</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Allow</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Action</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">eks:ListClusters</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">eks:DescribeAddonVersions</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">eks:CreateCluster</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">],</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Resource</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">*</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">},</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Effect</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Allow</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Action</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">eks:*</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Resource</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">arn:aws:eks:eu-central-1:123412341234:addon/my-cluster/*/*</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">arn:aws:eks:eu-central-1:123412341234:fargateprofile/my-cluster/*/*</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">arn:aws:eks:eu-central-1:123412341234:identityproviderconfig/my-cluster/*/*/*</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">arn:aws:eks:eu-central-1:123412341234:nodegroup/my-cluster/*/*</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">21</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">arn:aws:eks:eu-central-1:123412341234:cluster/my-cluster</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">22</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">23</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">24</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">25</span><span><span style="color: #ECEFF4">}</span></span></div></code></pre><p>Unfortunately, if a role with these permissions try to create a cluster, this error message appears:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #D8DEE9FF">Error: error creating EKS Add-On (my-cluster:kube-proxy): AccessDeniedException: User: arn:aws:sts::123412341234:assumed-role/</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9">role</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">/</span><span style="color: #81A1C1">&lt;iam-user&gt;</span><span style="color: #D8DEE9FF"> is not authorized to perform: eks:TagResource on resource: arn:aws:eks:eu-central-1:123412341234:/createaddon</span></span></div></code></pre><p>I have to say that at least the error message gives you a hint: the <code>/createddon</code> action is not scoped to the cluster.</p><p>After fighting with different polices for a while, I asked DuckDuckGo for a help, and indeed somebody reported this problem to AWS before, in <a href="https://github.com/aws/containers-roadmap/issues/1172" target="_blank" rel="noopener noreferrer">this GitHub issue</a>.</p><p>What the issue basically says is that if we want to give an IAM role permission to manage an add-on inside a cluster, we must give it permissions over all the EKS add-ons in our AWS account.</p><p>This of course breaks the AWS shared responsibility principle, ‘cause they don’t give us the tools to upheld our part of the deal. This is why it is a real and urgent issue, as they also mention in the ticket:</p><blockquote><p>Can’t share a timeline in this forum, but it’s a high priority item.</p></blockquote><p>And indeed it is so high priority, that it has been reported the <strong>3rd December 2020</strong>, and today, more than one year later, the issue is still there.</p><p>To add insult to the injury, you have to write the right policy manually because if you use the IAM interface to select “Any resource” for the add-ons as in the screenshot below, it will generate the wrong policy! If you check carefully, the generated resource name is <code>arn:aws:eks:eu-central-1:123412341234:addon/*/*/*</code>, which of course doesn’t match the ARN expected by AWS EKS. Basically, also if you are far too permissive, and you use the tools that AWS provides you, you still will have some broken policy.</p><figure><img src="https://rpadovani.hyvorblogs.io/media/8x4Op4WUAPwYySi6.png" alt="generate-addons-policy" srcset="https://rpadovani.hyvorblogs.io/media/8x4Op4WUAPwYySi6.png 827w, https://rpadovani.hyvorblogs.io/media/8x4Op4WUAPwYySi6.png/500w 500w, https://rpadovani.hyvorblogs.io/media/8x4Op4WUAPwYySi6.png/750w 750w"></figure><p>Do you have some horror story about IAM yourself? I have a lot of them, and I am thinking about a more general post. What do you think? Share your thoughts in the comments below, or drop me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br>R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/terraform-cloudinit How to make Terraform waiting for cloud-init to finish on EC2 without SSH 2025-01-26T21:31:29+00:00 Terraform is a powerful tool, but it doesn't have a way to wait for EC2 instances to be ready, instead of just created. We will see how to use AWS SSM to do just that. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/IZrKKFdV8UMEvM41.png" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/IZrKKFdV8UMEvM41.png 2992w, https://rpadovani.hyvorblogs.io/media/IZrKKFdV8UMEvM41.png/500w 500w, https://rpadovani.hyvorblogs.io/media/IZrKKFdV8UMEvM41.png/750w 750w, https://rpadovani.hyvorblogs.io/media/IZrKKFdV8UMEvM41.png/1000w 1000w, https://rpadovani.hyvorblogs.io/media/IZrKKFdV8UMEvM41.png/1500w 1500w"><figcaption>Terraform logo, courtesy of HashiCorp.</figcaption></figure><p>I find using SSH in <a href="https://www.terraform.io/" target="_blank" rel="noopener noreferrer">Terraform</a> quite problematic: you need to distribute a private SSH key to anybody that will launch the Terraform script, including your CI/CD system. This is a no-go for me: it adds the complexity to manage SSH keys, including their rotation. There is a huge <a href="https://github.com/hashicorp/terraform/issues/4668" target="_blank" rel="noopener noreferrer">issue</a> on the Terraform repo on GitHub about this functionality, and the most voted solution is indeed connecting via SSH to run a check:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-terraform has-line-numbers" data-language="terraform" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #D8DEE9FF">provisioner &quot;remote-exec&quot; {</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF"> inline = [</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">3</span><span><span style="color: #D8DEE9FF"> &quot;cloud-init status --wait&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">4</span><span><span style="color: #D8DEE9FF"> ]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">5</span><span><span style="color: #D8DEE9FF">}</span></span></div></code></pre><h1 id="aws-systems-manager-run-command"><a href="#aws-systems-manager-run-command">AWS Systems Manager Run Command</a></h1><p>The idea of using <code>cloud-init status --wait</code> is indeed quite good. The only problem is <strong>how</strong> do we ask Terraform to run such command. Luckily for us, AWS has a service, <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/execute-remote-commands.html" target="_blank" rel="noopener noreferrer">AWS SSM Run Command</a> that allow us to run commands on an EC2 instance through AWS APIs! In this way, our CI/CD system needs only an appropriate <a href="https://aws.amazon.com/iam/" target="_blank" rel="noopener noreferrer">IAM role</a>, and a way to invoke AWS APIs. I use the <a href="https://aws.amazon.com/cli/" target="_blank" rel="noopener noreferrer">AWS CLI</a> in the examples below, but you can adapt them to any language you prefer.</p><h2 id="prerequisites"><a href="#prerequisites">Prerequisites</a></h2><p>There are some prerequisites to use AWS SSM Run Command: we need to have AWS SSM Agent installed on our instance. It is preinstalled on <em>Amazon Linux 2</em> and <em>Ubuntu 16.04</em>, <em>18.04</em>, and <em>20.04</em>. For any other OS, we need to install it manually: it is supported on <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-install-ssm-agent.html" target="_blank" rel="noopener noreferrer">Linux</a>, <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/install-ssm-agent-macos.html" target="_blank" rel="noopener noreferrer">macOS</a>, and <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-install-ssm-win.html" target="_blank" rel="noopener noreferrer">Windows</a>.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>If you don’t know AWS SSM yet, go and take a look to their <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html" target="_blank" rel="noopener noreferrer">introductory guide.</a> </div></aside><p>The user or the role that executes the Terraform code needs to be able to create, update, and read AWS SSM Documents, and run SSM commands. A possible policy could be look like this:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-json has-line-numbers" data-language="json" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Version</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">2012-10-17</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Statement</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Sid</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Stmt1629387563127</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Action</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:CreateDocument</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:DeleteDocument</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:DescribeDocument</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:DescribeDocumentParameters</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:DescribeDocumentPermission</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:GetDocument</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:ListDocuments</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:SendCommand</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:UpdateDocument</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:UpdateDocumentDefaultVersion</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ssm:UpdateDocumentMetadata</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">],</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Effect</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Allow</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Resource</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">*</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">21</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">22</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">23</span><span><span style="color: #ECEFF4">}</span></span></div></code></pre><p>If we already know the name of the documents, or the instances where we want to run the commands, it is better to lock down the policy specifying the resources, accordingly to the <a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege" target="_blank" rel="noopener noreferrer">principle of least privilege</a>.</p><p>Last but not least, we need to have the AWS CLI installed on the system that will execute Terraform.</p><h2 id="the-terraform-code"><a href="#the-terraform-code">The Terraform code</a></h2><p>After having set up the prerequisites as above, we need two different Terraform resources. The first will create the AWS SSM Document with the command we want to execute on the instance. The second one will execute such command while provisioning the EC2 instance.</p><p>The AWS SSM Document code will look like this:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-terraform has-line-numbers" data-language="terraform" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9FF">resource &quot;aws_ssm_document&quot; &quot;cloud_init_wait&quot; {</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> name = &quot;cloud-init-wait&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> document_type = &quot;Command&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> document_format = &quot;YAML&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> content = </span><span style="color: #D8DEE9">&lt;&lt;</span><span style="color: #D8DEE9FF">-DOC</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> schemaVersion: &#039;2.2&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> description: Wait for cloud init to finish</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> mainSteps:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> - action: aws:runShellScript</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> name: StopOnLinux</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> precondition:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> StringEquals:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> - platformType</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> - Linux</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> inputs:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> runCommand:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> - cloud-init status --wait</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> DOC</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF">}</span></span></div></code></pre><p>We can refer such document from within our EC2 instance resource, with a local provisioner:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-terraform has-line-numbers" data-language="terraform" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9FF">resource &quot;aws_instance&quot; &quot;example&quot; {</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> ami = &quot;my-ami&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> instance_type = &quot;t3.micro&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> provisioner &quot;local-exec&quot; {</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> interpreter = [&quot;/bin/bash&quot;, &quot;-c&quot;]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> command = </span><span style="color: #D8DEE9">&lt;&lt;</span><span style="color: #D8DEE9FF">-EOF</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> set -Ee -o pipefail</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> export AWS_DEFAULT_REGION=${data.aws_region.current.name}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> command_id=$(aws ssm send-command --document-name ${aws_ssm_document.cloud_init_wait.arn} --instance-ids ${self.id} --output text --query &quot;Command.CommandId&quot;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> if ! aws ssm wait command-executed --command-id $command_id --instance-id ${self.id}; then</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> echo &quot;Failed to start services on instance ${self.id}!&quot;;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> echo &quot;stdout:&quot;;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> aws ssm get-command-invocation --command-id $command_id --instance-id ${self.id} --query StandardOutputContent;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> echo &quot;stderr:&quot;;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> aws ssm get-command-invocation --command-id $command_id --instance-id ${self.id} --query StandardErrorContent;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF"> exit 1;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><span style="color: #D8DEE9FF"> fi;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">21</span><span><span style="color: #D8DEE9FF"> echo &quot;Services started successfully on the new instance with id ${self.id}!&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">22</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">23</span><span><span style="color: #D8DEE9FF"> EOF</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">24</span><span><span style="color: #D8DEE9FF"> }</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">25</span><span><span style="color: #D8DEE9FF">}</span></span></div></code></pre><p>From now on, Terraform will wait for cloud-init to complete before marking the instance ready.</p><h1 id="conclusion"><a href="#conclusion">Conclusion</a></h1><p>AWS Session Manager, AWS Run Commands, and the others tools in the AWS Systems Manager family are quite powerful, and in my experience they are not widely use. I find them extremely useful: for example, they also allows connecting via <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-enable-ssh-connections.html" target="_blank" rel="noopener noreferrer">SSH to the instances</a> without having any port open, included the 22! Basically, they allow managing and running commands inside instances only through AWS APIs, with a lot of benefits, as <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html" target="_blank" rel="noopener noreferrer">they explain</a>:</p><blockquote><p>Session Manager provides secure and auditable instance management without the need to open inbound ports, maintain bastion hosts, or manage SSH keys. Session Manager also allows you to comply with corporate policies that require controlled access to instances, strict security practices, and fully auditable logs with instance access details, while still providing end users with simple one-click cross-platform access to your managed instances.</p></blockquote><p>Do you have any questions, feedback, critics, request for support? Leave a comment below, or drop me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br>R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/add-comments Adding comments to the blog 2025-01-26T21:31:29+00:00 After years of blogging, I’ve finally chosen to add a comment system, including reactions, to this blog. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/g2WO1VS8YL9xOFJX.jpg" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/g2WO1VS8YL9xOFJX.jpg 720w, https://rpadovani.hyvorblogs.io/media/g2WO1VS8YL9xOFJX.jpg/500w 500w"><figcaption>A picture I took at the CCC 2015 - and the moderation policy for comments!</figcaption></figure><p>I’ve done so to make it easier engaging with the four readers of my blabbering: of course, it took some time to choose the right comment provider, but finally, here we are!</p><p>I’ve chosen, long ago, to do not put any client-side analytics on this website: while the nerd in me loves graph and numbers, I don’t think they are worthy of the loss of privacy and performance for the readers. However, I am curious to have feedback on the content I write, if some of them are useful, and how can I improve. In all these years, I’ve received different emails about some posts, and they are heart-warming. With comments, I hope to reduce the friction in communicating with me and having some meaningful interaction with you.</p><h1 id="enter-hyvorcom"><a href="#enter-hyvorcom">Enter hyvor.com</a></h1><p>Looking for a comment system, I had three requirements in mind: being <strong>privacy-friendly</strong>, being <strong>performant</strong>, and being managed by somebody else.</p><p>A lot of comments system are <em>“free”</em>, because they live on advertisements, or tracking user activities, and more often a combination of both. However, since <em>I</em> want comments on <em>my</em> website, I find dishonest that <em>you</em>, the user, has to pay the price for it. So, I was looking for something that didn’t track users, and bases its business on dear old money. My website, my wish, my wallet.</p><p>I find <strong>performances</strong> important, and unfortunately, quite undervalued on the bloated Internet. I like having just some HTML, some CSS, and the minimum necessary of JavaScript, so whoever stumbles on these pages don’t waste CPU and time waiting for some rendering. Being a static website, there isn’t a server side, so I cannot put comments there. I had to find a really light JavaScript based comment system.</p><p>Given these two prerequisites, you would say the answer is obvious: find a comment system that you can <em>self-host</em>! And you would be perfectly right - however, since I spend already 8 hours a day keeping stuff online, I really don’t want to have to care about performance and uptime in my free time - I definitely prefer going drinking a beer with a friend.</p><p>After some shopping around, I’ve chosen to go with <a href="https://talk.hyvor.com" target="_blank" rel="noopener noreferrer">Hyvor Talk</a>, since it checks all three requirements above. I’ve read nice things about it, so let’s see how it goes! And if you don’t see comments at the end of the page, probably a privacy plugin for your browser is blocking it - up to you if you want to whitelist my website, or communicate with me in other ways ;-)</p><p>A nice plus of Hyvor is they also support reactions, so if you are in an hurry but want still to leave a quick feedback on the post, you can simply click a button. Fancy, isn’t it?</p><h1 id="moderation"><a href="#moderation">Moderation</a></h1><p>Internet can be ugly sometime, and this is why I will keep a strict eye on the comments, and I will probably adjust moderation settings in the future, based on how things evolve - maybe no-one will comment, then no need for any strict moderation! The only rule I ask you to abide, and I’ve put as moderation policy, is: “<strong>Be excellent to each other</strong>”. I’ve read it at the <a href="https://events.ccc.de/camp/2015/wiki/Main_Page" target="_blank" rel="noopener noreferrer">CCC Camp 2015</a>, and it sticks with me: as every short sentence, cannot capture all the nuances of human interaction, but I think it is a very solid starting point. If you have any concern or feedback you prefer to do not express in public, feel free to reach me through email. Otherwise, I hope to see you all in the comment section ;-)</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Be excellent to each other!</div></aside><p>Questions, comments, feedback, critics, suggestions on how to improve my English? Leave a comment below, or drop me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>. Or, from today, <strong>leave a comment below</strong>!</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/read-env-tauri Reading env variables from a Tauri App 2025-01-26T21:31:29+00:00 “Build smaller, faster, and more secure desktop applications with a web frontend” is the promise made by Tauri. And indeed, it is a great Electron replacement. <p>But being in its first days (the beta has just been released!) a bit of documentation is still missing, and on the internet there aren’t many examples on how to write code.</p><figure><img src="https://rpadovani.hyvorblogs.io/media/7jvHPE7nJnLcLXGV.png" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/7jvHPE7nJnLcLXGV.png 1266w, https://rpadovani.hyvorblogs.io/media/7jvHPE7nJnLcLXGV.png/500w 500w, https://rpadovani.hyvorblogs.io/media/7jvHPE7nJnLcLXGV.png/750w 750w, https://rpadovani.hyvorblogs.io/media/7jvHPE7nJnLcLXGV.png/1000w 1000w"><figcaption>Tauri is very light, as highlighted by <a href="https://tauri.studio/en/benchmarks" target="_blank" rel="noopener noreferrer">these benchmarks</a>.</figcaption></figure><p>One thing that took me a bit of time to figure out, is how to read environment variables from the frontend of the application (JavaScript / Typescript or whichever framework you are using).</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Haven’t you started with Tauri yet? Go and read the <a href="https://tauri.studio/en/docs/getting-started/intro" target="_blank" rel="noopener noreferrer">intro doc</a>! </div></aside><h1 id="the-command"><a href="#the-command">The Command</a></h1><p>As usual, when you know <em>the solution</em> the problem seems easy. In this case, we just need an old trick: <strong>delegating</strong>!</p><p>In particular, we will use the <code><a href="https://tauri.studio/en/docs/api/js/classes/shell.command" target="_blank" rel="noopener noreferrer">Command</a></code><a href="https://tauri.studio/en/docs/api/js/classes/shell.command" target="_blank" rel="noopener noreferrer"> API</a> to spawn a sub-process (<code>printenv</code>) and reading the returned value. The code is quite straightforward, and it is synchronous:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-rust has-line-numbers" data-language="rust" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Command</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">from</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">@tauri-apps/api/shell</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #81A1C1">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">readEnvVariable</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">variableName</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Promise</span><span style="color: #ECEFF4">&lt;</span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> commandResult </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Command</span><span style="color: #ECEFF4">(</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">printenv</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> variableName</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">.</span><span style="color: #88C0D0">execute</span><span style="color: #ECEFF4">();</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">commandResult</span><span style="color: #81A1C1">.</span><span style="color: #D8DEE9FF">code </span><span style="color: #81A1C1">!==</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">throw</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Error</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">commandResult</span><span style="color: #81A1C1">.</span><span style="color: #D8DEE9FF">stderr</span><span style="color: #ECEFF4">);</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> commandResult</span><span style="color: #81A1C1">.</span><span style="color: #D8DEE9FF">stdout</span><span style="color: #ECEFF4">;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #ECEFF4">}</span></span></div></code></pre><p>The example is in Typescript, but can be easily transformed in standard JavaScript.</p><p>The <code>Command</code> API is quite powerful, so you can read its <a href="https://tauri.studio/en/docs/api/js/classes/shell.command" target="_blank" rel="noopener noreferrer">documentation</a> to adapt the code to your needs.</p><h1 id="requirements"><a href="#requirements">Requirements</a></h1><p>There are another couple of things you should consider: first, you need to have installed in your frontend environment the <code><a href="https://www.npmjs.com/package/@tauri-apps/api" target="_blank" rel="noopener noreferrer">@tauri-apps/api</a></code> package. You also need to enable your app to execute commands. Since Tauri puts a strong emphasis on the <a href="https://tauri.studio/en/docs/about/security" target="_blank" rel="noopener noreferrer">security</a> (and rightly so!), your app is by default sandboxed. To being able to use the <code>Command</code> API, you need to enable the <code><a href="https://tauri.studio/en/docs/api/config#tauri.allowlist.shell.execute" target="_blank" rel="noopener noreferrer">shell.execute</a></code> API.</p><p>It should be as easy as setting <code>tauri.allowlist.shell.execute</code> to <code>true</code> in your <code>tauri.json</code> config file.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Tauri puts a strong emphasis on the security of the application.</div></aside><p>Tauri is very nice, and I really hope it will conquer the desktops and replace Electron, since it is lighter, faster, and safer!</p><p>Questions, comments, feedback, critics, suggestions on how to improve my English? Leave a comment below, or drop me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/gitlab-jetbrains-qodana Integrating JetBrains Qodana with GitLab pipelines 2025-01-26T21:31:29+00:00 JetBrains Qodana is a new product, still in early access, that brings the “Smarts” of JetBrains IDEs into your CI pipeline, and it can be easily integrated in GitLab. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/KcYKOlAS9Pl1Cfmq.png" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/KcYKOlAS9Pl1Cfmq.png 1760w, https://rpadovani.hyvorblogs.io/media/KcYKOlAS9Pl1Cfmq.png/500w 500w, https://rpadovani.hyvorblogs.io/media/KcYKOlAS9Pl1Cfmq.png/750w 750w, https://rpadovani.hyvorblogs.io/media/KcYKOlAS9Pl1Cfmq.png/1000w 1000w, https://rpadovani.hyvorblogs.io/media/KcYKOlAS9Pl1Cfmq.png/1500w 1500w"><figcaption>Qodana on Gitlab Pages</figcaption></figure><p>In this blog post we will see how to integrate this new tool by JetBrains in our GitLab pipeline, including having a dedicated website to see the cool report it produces.</p><p>If you don’t have a proper GitLab pipeline to lint your code, run your test, and manage all that other annoying small tasks, you should definitely create one! I’ve written an <a href="https://rpadovani.com/introduction-gitlab-ci" target="_blank" rel="noopener noreferrer">introductory guide to GitLab CI</a>, and many more are available on the <a href="https://docs.gitlab.com/ee/ci/README.html" target="_blank" rel="noopener noreferrer">documentation website</a>.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Aren’t you convinced yet? Read <a href="https://about.gitlab.com/blog/2019/06/27/positive-outcomes-ci-cd/" target="_blank" rel="noopener noreferrer">4 Benefits of CI/CD</a>! </div></aside><h1 id="jetbrains-qodana"><a href="#jetbrains-qodana">JetBrains Qodana </a></h1><blockquote><p>Qodana comprises two main parts: a nicely packaged GUI-less IntelliJ IDEA engine tailored for use in a CI pipeline as a typical “linter” tool, and an interactive web-based reporting UI. It makes it easy to set up workflows to get an overview of the project quality, set quality targets, and track progress on them. You can quickly adjust the list of checks applied for the project and include or remove directories from the analysis. </p></blockquote><p> <a href="https://blog.jetbrains.com/idea/2021/02/early-access-program-for-qodana-a-new-product-that-brings-the-smarts-of-jetbrains-ides-into-your-ci-pipeline/" target="_blank" rel="noopener noreferrer">Qodana launching post.</a> </p><p>I’m a huge fun of JetBrains products, and I happily pay their license every year: my productivity using their IDEs is through the roof. This very blog post has been written using WebStorm :-)</p><p>Therefore, when they announced a new product to bring the smartness of their IDE on CI pipelines, I was super enthusiastic! In <a href="https://github.com/JetBrains/Qodana/blob/main/Docker/README.md" target="_blank" rel="noopener noreferrer">their documentation</a> there is also a small paragraph about GitLab, and we’ll improve the example to have a nice way to browse the output.</p><p>At the moment, Qodana has support for <strong>Java</strong>, <strong>Kotlin</strong>, and <strong>PHP</strong>. With time, Qodana will support all languages and technologies covered by JetBrains IDEs.</p><p>Remember: <strong>Qodana is in an early access version</strong>. Using it, you expressly acknowledge that the product may not be reliable, work as not intended, and may contain errors. Any use of the EAP product is at your own risk.</p><p>I suggest you to also taking a look to the <a href="https://github.com/JetBrains/Qodana" target="_blank" rel="noopener noreferrer">official GitHub page</a> of the project, to see licenses and issues.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Qodana has support for Java, Kotlin, and PHP. With time, Qodana will support all languages and technologies covered by JetBrains IDEs.</div></aside><h1 id="integrating-qodana-in-gitlab"><a href="#integrating-qodana-in-gitlab">Integrating Qodana in GitLab</a></h1><p>The basic example provided by JetBrains is the following:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-yaml has-line-numbers" data-language="yaml" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #8FBCBB">qodana</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">image</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">jetbrains/qodana</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">entrypoint</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #A3BE8C">sh</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-c</span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">script</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">/opt/idea/bin/entrypoint --results-dir=$CI_PROJECT_DIR/qodana --save-report --report-dir=$CI_PROJECT_DIR/qodana/report</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">artifacts</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">8</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">paths</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">qodana</span></span></div></code></pre><p>While this works, it doesn’t provide a way to explore the report without firstly downloading it on your PC. If we have <a href="https://docs.gitlab.com/ee/user/project/pages/" target="_blank" rel="noopener noreferrer">GitLab Pages</a> enabled, we can publish the report and explore it online, thanks to the <code><a href="https://docs.gitlab.com/ee/ci/yaml/#artifactsexpose_as" target="_blank" rel="noopener noreferrer">artifacts:expose_as</a></code> keyword.</p><p>We also need GitLab to upload the right directory to Pages, so we change the artifact path as well:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-yaml has-line-numbers" data-language="yaml" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #8FBCBB">qodana</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">image</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">jetbrains/qodana</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">entrypoint</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #A3BE8C">sh</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-c</span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">script</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">/opt/idea/bin/entrypoint --results-dir=$CI_PROJECT_DIR/qodana --save-report --report-dir=$CI_PROJECT_DIR/qodana/report</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">artifacts</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">paths</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">qodana/report/</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">expose_as</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Qodana report</span><span style="color: #ECEFF4">&#039;</span></span></div></code></pre><p>Now in our merge requests page we have a new button, as soon as the Qodana job finishes, to explore the report! You can see such a merge request <a href="https://gitlab.com/rpadovani/qodana-test/-/merge_requests/1" target="_blank" rel="noopener noreferrer">here</a>, with the button <strong>“View exposed artifact”</strong>, while <a href="https://gitlab.com/rpadovani/qodana-test/-/jobs/1028883758/artifacts/file/qodana/report/index.html" target="_blank" rel="noopener noreferrer">here</a> you can find an interactive online report, published on GitLab Pages!</p><h1 id="configuring-qodana"><a href="#configuring-qodana">Configuring Qodana</a></h1><p>Full reference for the Qodana config file can be found on <a href="https://github.com/JetBrains/Qodana/blob/main/General/qodana-yaml.md" target="_blank" rel="noopener noreferrer">GitHub</a>. Qodana can be easily customized: we only need to create a file called <code>qodana.yaml</code>, and enter our preferences!</p><p>There are two options I find extremely useful: one is <code>exclude</code>, that we can use to skip some checks, or to skip some directories, so we can focus on what is important and save some time. The other is <code>failThreshold</code>. When this number of problems is reached, the container would exit with error 255. In this way, we can fail the pipeline, and enforce a high quality of the code!</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Qodana shows already a lot of potential, also if it only at the first version.</div></aside><p>Qodana shows already a lot of potential, also if it only at the first version! I am really looking forward to support for other languages, and to improvements that JetBrains will do in the upcoming releases!</p><p>Questions, comments, feedback, critics, suggestions on how to improve my English? Leave a comment below, or drop me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/gitlab-code-coverage Fail a Gitlab pipeline when code coverage decreases 2025-01-26T21:31:29+00:00 Automatic and continuous testing is a fundamental part of today’s development cycle. Given a Gitlab pipeline that runs for each commit, we should enforce not only all tests are passing, but also that a sufficient number of them are present. <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/D27sm9KlwQT61kSh.jpg" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/D27sm9KlwQT61kSh.jpg 5599w, https://rpadovani.hyvorblogs.io/media/D27sm9KlwQT61kSh.jpg/500w 500w, https://rpadovani.hyvorblogs.io/media/D27sm9KlwQT61kSh.jpg/750w 750w, https://rpadovani.hyvorblogs.io/media/D27sm9KlwQT61kSh.jpg/1000w 1000w, https://rpadovani.hyvorblogs.io/media/D27sm9KlwQT61kSh.jpg/1500w 1500w"><figcaption>Photo by <a href="https://unsplash.com/@pankajpatel?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer">Pankaj Patel</a> on <a href="https://unsplash.com/s/photos/gitlab?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer">Unsplash</a></figcaption></figure><p>If you don’t have a proper Gitlab pipeline to lint your code, run your test, and manage all that other annoying small tasks, you should definitely create one! I’ve written an <a href="https://rpadovani.com/introduction-gitlab-ci" target="_blank" rel="noopener noreferrer">introductory guide to Gitlab CI</a>, and many more are available on the <a href="https://docs.gitlab.com/ee/ci/README.html" target="_blank" rel="noopener noreferrer">documentation website</a>.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Aren’t you convinced yet? Read <a href="https://about.gitlab.com/blog/2019/06/27/positive-outcomes-ci-cd/" target="_blank" rel="noopener noreferrer">4 Benefits of CI/CD</a>! </div></aside><p>While there isn’t (unfortunately!) a magic wand to highlight if the code is covered by enough tests, and, in particular, if these tests are of a sufficient good quality, we can nonetheless find some valuable KPI we can act on. Today, we will check <strong>code coverage</strong>, what indicates, what does not, and how it can be helpful.</p><h1 id="code-coverage"><a href="#code-coverage">Code coverage </a></h1><blockquote><p>In computer science, test coverage is a measure used to describe the degree to which the source code of a program is executed when a particular test suite runs. A program with high test coverage, measured as a percentage, has had more of its source code executed during testing, which suggests it has a lower chance of containing undetected software bugs compared to a program with low test coverage. </p></blockquote><p> Wikipedia </p><p>Basically, code coverage indicates how much of your code has been executed while your tests were running. Personally, I don’t find a high code coverage a significant measure: if tests are fallacious, or they run only on the happy path, the code coverage percentage will be high, but <strong>the tests will not actually guarantee a high quality of the code</strong>.</p><p>On the other hand, a <strong>low code coverage is definitely worrisome</strong>, because it means some parts of the code aren’t tested at all. Thus, code coverage has to be taken, as every other KPI based only exclusively on lines of code, with a grain of salt.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>High code coverage doesn&#039;t guarantee a high quality test suite, but a low code coverage definitely highlights a problem in the testing process.</div></aside><h1 id="code-coverage-and-gitlab"><a href="#code-coverage-and-gitlab">Code coverage and Gitlab</a></h1><p>Gitlab allows collecting code coverage from test suites directly from pipelines. Major information on the setup can be found in the <a href="https://docs.gitlab.com/ee/ci/pipelines/settings.html#test-coverage-parsing" target="_blank" rel="noopener noreferrer">pipelines guide</a> and in the <a href="https://docs.gitlab.com/ee/ci/yaml/#coverage" target="_blank" rel="noopener noreferrer">Gitlab CI reference guide</a>. Since there are lots of different test suites out there, I cannot include how to configure them here. However, if you need any help, feel free to reach out to me at the contacts reported below.</p><p>Gitlab will also report code coverage statistic for pipelines over time in nice graphs under <em>Project Analytics</em> &gt; <em>Repository</em>. Data can also be exported as <em>csv</em>! We will use such data to check if, in the commit, the code coverage decreased comparing to the main branch.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>With Gitlab 13.5 there is also a Test Coverage Visualization tool, <a href="https://docs.gitlab.com/ee/user/project/merge_requests/test_coverage_visualization.html" target="_blank" rel="noopener noreferrer">check it out</a>! </div></aside><p>This means that every new code written has to be tested at least as much as the rest of the code is tested. Of course, this strategy can be easily changed. The check is only one line of bash, and can be easily be replaced with a fixed number, or any other logic.</p><h1 id="the-gitlab-pipeline-job"><a href="#the-gitlab-pipeline-job">The Gitlab Pipeline Job</a></h1><p>The job that checks the coverage runs in a stage after the testing stage. It uses alpine as base, and curl and jq to query the APIs and read the code coverage.</p><p>On self hosted instances, or on Gitlab.com Bronze or above, you should use a <a href="https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html" target="_blank" rel="noopener noreferrer">project access token</a> to give access to the APIs. On Gitlab.com Free, use a <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html" target="_blank" rel="noopener noreferrer">personal access token</a>. If the project is public, the API are accessible without any token. It needs three variables: the name of the job which generates the code coverage percentage (<code>JOB_NAME</code>), the target branch to compare the coverage with (<code>TARGET_BRANCH</code>), and a private token to read the APIs (<code>PRIVATE_TOKEN</code>). The job will not run when the pipeline is running on the target branch, since it would be comparing the code coverage with itself, wasting minutes of runners for nothing.</p><p>The last line is the one providing the logic to compare the coverages.</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-yaml has-line-numbers" data-language="yaml" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #8FBCBB">checkCoverage</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">image</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">alpine:latest</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">stage</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">postTest</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">variables</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">JOB_NAME</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">testCoverage</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TARGET_BRANCH</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">main</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">before_script</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">apk add --update --no-cache curl jq</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">rules</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">if</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">$CI_COMMIT_BRANCH != $TARGET_BRANCH</span><span style="color: #ECEFF4">&#039;</span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">script</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">TARGET_PIPELINE_ID=`curl -s &quot;${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines?ref=${TARGET_BRANCH}&amp;status=success&amp;private_token=${PRIVATE_TOKEN}&quot; | jq &quot;.[0].id&quot;`</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">TARGET_COVERAGE=`curl -s &quot;${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines/${TARGET_PIPELINE_ID}/jobs?private_token=${PRIVATE_TOKEN}&quot; | jq --arg JOB_NAME &quot;$JOB_NAME&quot; &#039;.[] | select(.name==$JOB_NAME) | .coverage&#039;`</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">CURRENT_COVERAGE=`curl -s &quot;${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?private_token=${PRIVATE_TOKEN}&quot; | jq --arg JOB_NAME &quot;$JOB_NAME&quot; &#039;.[] | select(.name==$JOB_NAME) | .coverage&#039;`</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">if [ &quot;$CURRENT_COVERAGE&quot; -lt &quot;$TARGET_COVERAGE&quot; ]; then echo &quot;Coverage decreased from ${TARGET_COVERAGE} to ${CURRENT_COVERAGE}&quot; &amp;&amp; exit 1; fi;</span></span></div></code></pre><p>This simple job works both on Gitlab.com and on private Gitlab instances, for it doesn’t hard-code any URL.</p><p>Gitlab will now block merging merge requests without enough tests! Again, code coverage is not the magic bullet, and you shouldn’t strive to have 100% of code coverage: better fewer tests, but with high quality, than more just for increasing the code coverage. In the end, a human is always the best reviewer. However, a small memo to write just one more test is, in my opinion, quite useful ;-)</p><p>Questions, comments, feedback, critics, suggestions on how to improve my English? Leave a comment below, or drop me an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/pytorch-docker-image Create a PyTorch Docker image ready for production 2025-01-26T21:31:29+00:00 Given a PyTorch model, how should we put it in a Docker image, with all the related dependencies, ready to be deployed? <p></p><figure><img src="https://rpadovani.hyvorblogs.io/media/mJklDR7jdkOO022J.jpg" alt="cover" srcset="https://rpadovani.hyvorblogs.io/media/mJklDR7jdkOO022J.jpg 4200w, https://rpadovani.hyvorblogs.io/media/mJklDR7jdkOO022J.jpg/500w 500w, https://rpadovani.hyvorblogs.io/media/mJklDR7jdkOO022J.jpg/750w 750w, https://rpadovani.hyvorblogs.io/media/mJklDR7jdkOO022J.jpg/1000w 1000w, https://rpadovani.hyvorblogs.io/media/mJklDR7jdkOO022J.jpg/1500w 1500w"><figcaption>Photo by <a href="https://unsplash.com/@lazycreekimages?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer">Michael Dziedzic</a> on <a href="https://unsplash.com/s/photos/data-science?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer">Unsplash</a></figcaption></figure><p>You know the drill: your Data Science team has created an amazing PyTorch model, and now they want you to put it in production. They give you a <code>.pt</code> file and some preprocessing script. What now?</p><p>Luckily, AWS and Facebook have <a href="https://aws.amazon.com/blogs/aws/announcing-torchserve-an-open-source-model-server-for-pytorch/" target="_blank" rel="noopener noreferrer">created</a> a project, called <a href="https://pytorch.org/serve/" target="_blank" rel="noopener noreferrer">Torch Serve</a>, to put PyTorch images in production, similarly to <a href="https://www.tensorflow.org/tfx/guide/serving" target="_blank" rel="noopener noreferrer">Tensorflow Serving</a>. It is a well-crafted Docker image, where you can upload your models. In this tutorial, we will see how to customize the Docker image to include your model, how to install other dependencies inside it, and which configuration options are available.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>TorchServe is a flexible and easy to use tool for serving PyTorch models.</div></aside><p>We include the PyTorch model directly inside the Docker image, instead of loading it at runtime; while loading it at runtime as some advantages and makes sense in some scenario (as in testing labs where you want to try a lot of different models), I don’t think it is suitable for production. Including the model directly in the Docker image has different advantages: </p><ul><li><p>if you use <a href="https://en.wikipedia.org/wiki/Continuous_integration" target="_blank" rel="noopener noreferrer">CI</a>/CD you can achieve <a href="https://reproducible-builds.org/" target="_blank" rel="noopener noreferrer">reproducible builds</a>;</p></li><li><p>to spawn a new instance serving your model, you need to have available only your Docker registry, and not also a storage solutions to store the model;</p></li><li><p>you need to authenticate only to your Docker registry, and not to the storage solution;</p></li><li><p>it makes easier keeping track of what has been deployed, ‘cause you have to check only the Docker image version, and not the model version. This is especially important if you have a cluster of instances serving your model;</p></li></ul><p>Let’s now get our hands dirty and dive in what is necessary to have the Docker image running!</p><h1 id="building-the-model-archive"><a href="#building-the-model-archive">Building the model archive</a></h1><p>The Torch Serve Docker image needs a <em>model archive</em> to work: it’s a file with inside a model, and some configurations file. To create it, first <a href="https://github.com/pytorch/serve#install-torchserve" target="_blank" rel="noopener noreferrer">install Torch Serve</a>, and have a PyTorch model available somewhere on the PC.</p><p>To create this model archive, we need only one command:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-bash has-line-numbers" data-language="bash" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #88C0D0">torch-model-archiver</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--model-name</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;</span><span style="color: #A3BE8C">MODEL_NAM</span><span style="color: #D8DEE9FF">E</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--version</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;</span><span style="color: #A3BE8C">MODEL_VERSIO</span><span style="color: #D8DEE9FF">N</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--serialized-file</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;</span><span style="color: #A3BE8C">MODE</span><span style="color: #D8DEE9FF">L</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--export-path</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;</span><span style="color: #A3BE8C">WHERE_TO_SAVE_THE_MODEL_ARCHIV</span><span style="color: #D8DEE9FF">E</span><span style="color: #81A1C1">&gt;</span></span></div></code></pre><p>There are four options we need to specify in this command:</p><ul><li><p><code>MODEL_NAME</code> is an identifier to recognize the model, we can use whatever we want here: it’s useful when we include multiple models inside the same Docker image, a nice feature of Torch Serve that we won’t cover for now;</p></li><li><p><code>MODEL_VERSION</code> is used to identify, as the name implies, the version of the model;</p></li><li><p><code>MODEL</code> is the path, on the local PC, with the <code>.pt</code> file acting as model;</p></li><li><p><code>WHERE_TO_SAVE_THE_MODEL_ARCHIVE</code> is a local directory where Torch Serve will put the model archive it generates;</p></li></ul><p>Putting all together, the command should be something similar to:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-bash has-line-numbers" data-language="bash" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #88C0D0">torch-model-archiver</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--model-name</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">predict_the_future</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--version</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1.0</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--serialized-file</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">~/models/predict_the_future</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--export-path</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">model-store/</span></span></div></code></pre><p>After having run it, we now have a file with <code>.mar</code> extension, the first step to put in production our PyTorch model! </p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div><code>.mar</code> files are actually just <code>.zip</code> files with a different extension, so feel free to open it and analyze it to see how it works behind the scenes.</div></aside><p>Probably some pre-processing before invoking the model is necessary. If this is the case, we can create a file where we can put all the necessary instructions. This file can have external dependencies, so we can code an entire application in front of our model.</p><p>To include the handler file in the model archive, we need only to add the <code>--handler</code> flag to the command above, like this:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-bash has-line-numbers" data-language="bash" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #88C0D0">torch-model-archiver</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--model-name</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">predict_the_future</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--version</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1.0</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--serialized-file</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">~/models/predict_the_future</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--export-path</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">model-store/</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">--handler</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">handler.py</span></span></div></code></pre><h1 id="create-the-docker-image"><a href="#create-the-docker-image">Create the Docker image</a></h1><p>Now we have the model archive, and we include it in the PyTorch Docker Image. Other than the model archive, we need to create a configuration file as well, to say to PyTorch which model to automatically load at the startup.</p><p>We need a <code>config.properties</code> file similar to the following. Later in this tutorial we will see what these lines mean, and what other options are available.</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #D8DEE9FF">inference_address=http://0.0.0.0:8080</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF">management_address=http://0.0.0.0:8081</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">3</span><span><span style="color: #D8DEE9FF">number_of_netty_threads=32</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">4</span><span><span style="color: #D8DEE9FF">job_queue_size=1000</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">5</span><span><span style="color: #D8DEE9FF">model_store=/home/model-server/model-store</span></span></div></code></pre><h2 id="docker-image-with-just-the-model"><a href="#docker-image-with-just-the-model">Docker image with just the model</a></h2><p>If we need to include just the model archive, and the config file, the Dockerfile is quite straightforward, since we just have to copy the files, all the other things will be managed by TorchServe itself. Our Dockerfile will thus be:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-docker has-line-numbers" data-language="docker" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #81A1C1">FROM</span><span style="color: #D8DEE9FF"> pytorch/torchserve </span><span style="color: #81A1C1">as</span><span style="color: #D8DEE9FF"> production</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">3</span><span><span style="color: #81A1C1">COPY</span><span style="color: #D8DEE9FF"> config.properties /home/model-server/config.properties</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">4</span><span><span style="color: #81A1C1">COPY</span><span style="color: #D8DEE9FF"> predict_the_future.mar /home/model-server/model-store</span></span></div></code></pre><p>TorchServe already includes <code>torch</code>, <code>torchvision</code>, <code>torchtext</code>, and <code>torchaudio</code>, so there is no need to add them. To see the current version of these libraries, please go see the <a href="https://github.com/pytorch/serve/blob/master/requirements/cpu.txt" target="_blank" rel="noopener noreferrer">requirements file of TorchServe on GitHub</a>.</p><h2 id="docker-image-with-the-model-and-external-dependencies"><a href="#docker-image-with-the-model-and-external-dependencies">Docker image with the model and external dependencies</a></h2><p>What if we need different Python Dependencies for our Python handler?</p><p>In this case, we want to use a two-step Docker image: in the first step we build our dependencies, and then we copy them over to the final image. We list our dependencies in a file called <code>requirements.txt</code>, and we use pip to install them. Pip is the <a href="https://packaging.python.org/guides/tool-recommendations/" target="_blank" rel="noopener noreferrer">package installer</a> for Python. Their <a href="https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format" target="_blank" rel="noopener noreferrer">documentation about the format of the requirements file</a> is very complete.</p><p>The Dockerfile is now something like this:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-docker has-line-numbers" data-language="docker" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #81A1C1">ARG</span><span style="color: #D8DEE9FF"> BASE_IMAGE=ubuntu:18.04</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #616E88"># Compile image loosely based on pytorch compile image</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #81A1C1">FROM</span><span style="color: #D8DEE9FF"> ${BASE_IMAGE} </span><span style="color: #81A1C1">AS</span><span style="color: #D8DEE9FF"> compile-image</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #81A1C1">ENV</span><span style="color: #D8DEE9FF"> PYTHONUNBUFFERED TRUE</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #616E88"># Install Python and pip, and build-essentials if some requirements need to be compiled</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> apt-get update &amp;&amp; \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> python3-dev \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> python3-distutils \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> python3-venv \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> curl \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> build-essential \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> &amp;&amp; rm -rf /var/lib/apt/lists/* \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> &amp;&amp; cd /tmp \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> &amp;&amp; curl -O https://bootstrap.pypa.io/get-pip.py \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> &amp;&amp; python3 get-pip.py</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> python3 -m venv /home/venv</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">21</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">22</span><span><span style="color: #81A1C1">ENV</span><span style="color: #D8DEE9FF"> PATH=</span><span style="color: #A3BE8C">&quot;/home/venv/bin:$PATH&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">23</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">24</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> update-alternatives --install /usr/bin/python python /usr/bin/python3 1</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">25</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> update-alternatives --install /usr/local/bin/pip pip /usr/local/bin/pip3 1</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">26</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">27</span><span><span style="color: #616E88"># The part above is cached by Docker for future builds</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">28</span><span><span style="color: #616E88"># We can now copy the requirements file from the local system</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">29</span><span><span style="color: #616E88"># and install the dependencies</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">30</span><span><span style="color: #81A1C1">COPY</span><span style="color: #D8DEE9FF"> requirements.txt .</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">31</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">32</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> pip install --no-cache-dir -r requirements.txt</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">33</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">34</span><span><span style="color: #81A1C1">FROM</span><span style="color: #D8DEE9FF"> pytorch/torchserve </span><span style="color: #81A1C1">as</span><span style="color: #D8DEE9FF"> production</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">35</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">36</span><span><span style="color: #616E88"># Copy dependencies after having built them</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">37</span><span><span style="color: #81A1C1">COPY</span><span style="color: #D8DEE9FF"> --from=compile-image /home/venv /home/venv</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">38</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">39</span><span><span style="color: #616E88"># We use curl for health checks on AWS Fargate</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">40</span><span><span style="color: #81A1C1">USER</span><span style="color: #D8DEE9FF"> root</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">41</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> apt-get update &amp;&amp; \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">42</span><span><span style="color: #D8DEE9FF"> DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">43</span><span><span style="color: #D8DEE9FF"> curl \</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">44</span><span><span style="color: #D8DEE9FF"> &amp;&amp; rm -rf /var/lib/apt/lists/*</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">45</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">46</span><span><span style="color: #81A1C1">USER</span><span style="color: #D8DEE9FF"> model-server</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">47</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">48</span><span><span style="color: #81A1C1">COPY</span><span style="color: #D8DEE9FF"> config.properties /home/model-server/config.properties</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">49</span><span><span style="color: #81A1C1">COPY</span><span style="color: #D8DEE9FF"> predict_the_future.mar /home/model-server/model-store</span></span></div></code></pre><p>If PyTorch is among the dependencies, we should change the line to install the requirements from</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-docker has-line-numbers" data-language="docker" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> pip install --no-cache-dir -r requirements.txt</span></span></div></code></pre><p>to</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-docker has-line-numbers" data-language="docker" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> pip install --no-cache-dir -r requirements.txt -f https://download.pytorch.org/whl/torch_stable.html</span></span></div></code></pre><p>In this way, we will use the pre-build Python packages for PyTorch instead of installing them from scratch: it will be faster, and it requires less resources, making it suitable also for small CI/CD systems.</p><h2 id="configuring-the-docker-image"><a href="#configuring-the-docker-image">Configuring the Docker image</a></h2><p>We created a configuration file above, but what does it? Of course, going through all the possible configurations would be impossible, so I leave here the <a href="https://github.com/pytorch/serve/blob/master/docs/configuration.md" target="_blank" rel="noopener noreferrer">link to the documentation</a>. Among the other things explained there, there is a way to configure Cross-Origin Resource Sharing (necessary to use the model as APIs over the web), a guide on how to enable SSL, and much more.</p><p>There is a set of configurations parameters in particular I’d like to focus on: the ones related to logging. First, for production environment, I suggest setting <code>async_logging</code> to <code>true</code>: it could delay a bit the output, but allows a higher throughput. Then, it’s important to notice that <a href="https://github.com/pytorch/serve/blob/master/docs/logging.md" target="_blank" rel="noopener noreferrer">by default</a> Torch Serve captures every message, including the ones with severity <code>DEBUG</code>. In production, we probably don’t want this, especially because it can become quite verbose.</p><p>To override the default behavior, we need to create a new file, called <code>log4j.properties</code>. For more information on every possible options I suggest familiarizing with the <a href="https://logging.apache.org/log4j/2.x/manual/configuration.html" target="_blank" rel="noopener noreferrer">official guide</a>. To start, copy <a href="https://github.com/pytorch/serve/blob/master/frontend/server/src/main/resources/log4j.properties" target="_blank" rel="noopener noreferrer">the default Torch Serve</a> configuration, and increase the severity of the printed messages. In particular, change</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #D8DEE9FF">log4j.logger.org.pytorch.serve = DEBUG, ts_log</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF">log4j.logger.ACCESS_LOG = INFO, access_log</span></span></div></code></pre><p>to</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #D8DEE9FF">log4j.logger.org.pytorch.serve = WARNING, ts_log</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF">log4j.logger.ACCESS_LOG = WARNING, access_log</span></span></div></code></pre><p>We need also to copy this new file to the Docker Image, so copy the logging config just after the config file:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #D8DEE9FF">COPY config.properties /home/model-server/config.properties</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF">COPY config.properties /home/model-server/log4j.properties</span></span></div></code></pre><p>We need to inform Torch Serve about this new config file, and we do so adding a line to <code>config.properties</code>:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #D8DEE9FF">vmargs=-Dlog4j.configuration=file:///home/model-server/log4j.properties</span></span></div></code></pre><p>We now have a full functional Torch Serve Docker image, with our custom model, ready to be deployed!</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>Our PyTorch model is ready to meet the real world and serve traffic.</div></aside><p>For any question, comment, feedback, critic, suggestion on how to improve my English, leave a comment below, or drop an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/introducing-daintree Introducing Daintree.app: an opensource alternative implementation of the AWS console. 2025-01-26T21:31:29+00:00 Daintree is a website to manage some of your AWS resources: since this is an early preview, at the moment, it supports a subset of Networking, EC2, SQS, and SNS. Daintree does not aim to replace the original console, but would like to improve the user experience <p></p><p>The AWS Console is an amazing piece of software: it has hundreds of thousands of features, it is reliable, and it is the front end of an incredible world. However, as any software, it is not perfect: sometimes is a bit slow, so many features can be confusing, and it is clear it has evolved over time, so there are a lot of different styles, and if it would be made from scratch today, some choices would probably be different.</p><p><a href="https://daintree.app" target="_blank" rel="noopener noreferrer">Daintree</a> has born wanting to fix one particular problem of the AWS Console: the impossibility to see resources from multiple regions in the same view.</p><p>I’ve starting working on it last month, and now I’m ready to publish a first version: it’s still quite young and immature, but I’m starting using it to check some resources on AWS accounts I have access to. A lot of features are still missing, of course, and if you like, <a href="https://www.daintree.app/#/contribute" target="_blank" rel="noopener noreferrer">you can contribute to its development</a>.</p><h1 id="multiple-region-support"><a href="#multiple-region-support">Multiple region support</a></h1><p>The main reason Daintree exists is to display resources from multiple regions in the same screen: why limiting to one, when you can have 25?</p><p>Also, changing enabled regions doesn’t require a full page reload, but just a click on a flag: Daintree will smartly require resources from the freshly enabled regions.</p><figure><img src="https://www.daintree.app/assets/features/multiple-regions.gif" alt="multiple-regions"></figure><h1 id="fast-role-switching"><a href="#fast-role-switching">Fast role switching</a></h1><p>If you belong to an AWS organization, and you have multiple accounts, you probably switch often role: such operation on the original AWS console requires a full page reload, and it always brings you to the homepage.</p><p>On Daintree, changing roles will only reload the resources in the page you are currently in, without having to wait for a full page reload!</p><figure><img src="https://www.daintree.app/assets/features/switch-role.gif" alt="fast-role"></figure><h1 id="coherent-interface"><a href="#coherent-interface">Coherent interface</a></h1><p>Beauty is in the eye of the beholder, so claiming Daintree is more beautiful than the original console would be silly: however, Daintree has been built to be coherent: all the styling is made thanks to the <a href="https://gitlab.com/gitlab-org/gitlab-ui" target="_blank" rel="noopener noreferrer">Gitlab UI project</a>, and the illustrations are made by Katerina Limpitsouni from <a href="https://undraw.co/" target="_blank" rel="noopener noreferrer">unDraw</a>.</p><p>This guarantees a coherent and polished experience all over the application.</p><h1 id="free-software"><a href="#free-software">Free software</a></h1><p>Daintree is licensed under AGPL-3.0, meaning is truly free software: this way, everyone can contribute improving the experience of using it. The full source code is available over <a href="https://gitlab.com/rpadovani/daintree" target="_blank" rel="noopener noreferrer">Gitlab</a>.</p><p>The project doesn’t have any commercial goal, and as explained in <a href="https://www.daintree.app/#/security" target="_blank" rel="noopener noreferrer">the page about security</a>, no trackers are present on the website. To help Daintree, you <a href="https://www.daintree.app/#/contribute" target="_blank" rel="noopener noreferrer">can contribute</a> and spread the word!</p><h1 id="fast-navigation"><a href="#fast-navigation">Fast navigation</a></h1><p>Daintree heavily uses Javascript (with Vue.js) to do not have to reload the page: also, it tries to perform as few operations as possible to keep always updated on your resources. In this way, you don’t waste your precious time waiting for the page to load.</p><h1 id="and-much-more"><a href="#and-much-more">And much more!</a></h1><p>While implementing Daintree, new features have been introduced to make life easier to whoever uses it. As an example, you can create Internet Gateway and attach them to a VPC in the same screen, without having to wait for the gateway to be created. Or, while visualizing a security groups, you can click on the linked security groups, without having to look for them or remember complicated IDs. If you have any idea on how to improve workflows, please <a href="https://www.daintree.app/#/contribute" target="_blank" rel="noopener noreferrer">share it with developers</a>!</p><h1 id="supported-components"><a href="#supported-components">Supported components</a></h1><p>Daintree is still at early stages of development, so the number of supported resources is quite limited. <a href="https://www.daintree.app/#/contribute" target="_blank" rel="noopener noreferrer">You can report which feature you’d like to see to the developers, or you can implement them!</a></p><p>Daintree allows to view VPCs, Subnets, Internet Gateways, Nat Gateways, Route Tables, Elastic IPs, Security Groups, Instances, SNS, and SQS. You can also create, delete, and edit some of these resources. Development is ongoing, so remember to check the <a href="https://www.daintree.app/#/changelog" target="_blank" rel="noopener noreferrer">changelog</a> from time to time.</p><p>What are you waiting? Go to <a href="https://daintree.app" target="_blank" rel="noopener noreferrer">https://daintree.app</a> and enjoy your AWS resources in a way you haven’t before!</p><p>Needless to say, Daintree website is not affiliated to Amazon, or AWS, or any of their subsidiaries. ;-)</p><p>For any comment, feedback, critic, suggestion on how to improve my English, leave me a comment below, or drop an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/aws-lambda-access-key-notifications Leveraging AWS Lambda to notify users about their old access keys 2025-01-26T21:31:29+00:00 I love to spend time trying to automatize out boring part of my job. One of these boring side is remembering people to rotate AWS Access Keys, as suggested also by AWS in their best practices. <p></p><p>The AWS IAM console helps to highlight which keys are old, but if you have dozens of users, or multiple AWS accounts, it is still boring doing it manually. So, I wrote some code to doing it automatically leveraging AWS Lambda - since it has a generous free-tier, this check is free (however, your mileage may vary).</p><figure><img src="https://img.rpadovani.com/posts/automation.png" alt="Comic on automation"><figcaption>Enter a caption...</figcaption></figure><p> Image by Randall Munroe, <a href="https://xkcd.com/1319/" target="_blank" rel="noopener noreferrer">xkcd.com</a></p><h1 id="setting-up-the-permissions"><a href="#setting-up-the-permissions">Setting up the permissions</a></h1><p>Of course, we want to follow the <a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege" target="_blank" rel="noopener noreferrer">principle of least privilege</a>: the Lambda function will have access only to the minimum data necessary to perform its task. Thus, we need to create a dedicated role over the IAM Console. <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html" target="_blank" rel="noopener noreferrer">AWS Guide</a> to create roles for AWS services</p><p>Our custom role needs to have the managed policy <code>AWSLambdaBasicExecutionRole</code>, needed to execute a Lambda function. Other than this, we create a custom inline policy with this permissions:</p><ul><li><p><code>iam:ListUsers</code>, to know which users have access to the account. If you want to check only a subset of users, like filtering by department, you can use the <code>Resource</code> field to limit the access.</p></li><li><p><code>iam:ListAccessKeys</code>, to read access keys of the users. Of course, you can limit here as well which users the Lambda has access to.</p></li><li><p><code>ses:SendEmail</code>, to send the notification emails. Once again, you can (and should!) restrict the ARN to which it has access to.</p></li></ul><p>And that are all the permissions we need!</p><p>The generated policy should look like this, more or less:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-json has-line-numbers" data-language="json" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Version</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">2012-10-17</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Statement</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Sid</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">VisualEditor0</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Effect</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Allow</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Action</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">ses:SendEmail</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">iam:ListAccessKeys</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">],</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Resource</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">arn:aws:iam::&lt;ACCOUNT_ID&gt;:user/*</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">arn:aws:ses:eu-central-1:&lt;ACCOUNT_ID&gt;:identity/*</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">},</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Sid</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">VisualEditor1</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Effect</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Allow</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Action</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">iam:ListUsers</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">Resource</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">*</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">21</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">22</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">23</span><span><span style="color: #ECEFF4">}</span></span></div></code></pre><h1 id="setting-up-ses"><a href="#setting-up-ses">Setting up SES</a></h1><p>To send the notification email, we use AWS Simple Email Service.</p><p>Before using it, you need to move out of the <a href="https://docs.aws.amazon.com/ses/latest/DeveloperGuide/request-production-access.html" target="_blank" rel="noopener noreferrer">sandbox mode</a>, or to <a href="https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-domains.html" target="_blank" rel="noopener noreferrer">verify domains</a> you want to send emails to.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>If all your users have emails from the same domain, and you have access to the DNS, probably is faster to just verify your domain, especially if the AWS account is quite new.</div></aside><p>After that, you don’t have to do anything else, SES will be used by the Lambda code.</p><h1 id="setting-up-lambda"><a href="#setting-up-lambda">Setting up Lambda</a></h1><p>You can now create an AWS Lambda function. I’ve written the code that you find below in Python, since I find it is the fastest way to put in production such a simple script. However, you can use any of the supported languages.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div>If you have never used AWS Lambda before, you can start <a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started-create-function.html" target="_blank" rel="noopener noreferrer">from here</a></div></aside><p>You need to assign the role we created before as an execution role. As memory, 128 MB is more than enough. About the timeout, it’s up to how big your company is. More or less, it can check 5/10 users every second. You should test it and see if it goes in timeout.</p><h1 id="lambda-code"><a href="#lambda-code">Lambda Code</a></h1><p>Following there is the code to perform the task. To read it better, you can find it also on this <a href="https://gitlab.com/snippets/1946017" target="_blank" rel="noopener noreferrer">Gitlab’s snippet</a>.</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language-python has-line-numbers" data-language="python" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #81A1C1">from</span><span style="color: #D8DEE9FF"> collections </span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> defaultdict</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #81A1C1">from</span><span style="color: #D8DEE9FF"> datetime </span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> datetime</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> timezone</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> logging</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> boto3</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #81A1C1">from</span><span style="color: #D8DEE9FF"> botocore</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">exceptions </span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> ClientError</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #616E88"># How many days before sending alerts about the key age?</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 10</span><span><span style="color: #D8DEE9FF">ALERT_AFTER_N_DAYS </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">100</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 11</span><span><span style="color: #616E88"># How ofter we have set the cron to run the Lambda?</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 12</span><span><span style="color: #D8DEE9FF">SEND_EVERY_N_DAYS </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">3</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 13</span><span><span style="color: #616E88"># Who send the email?</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 14</span><span><span style="color: #D8DEE9FF">SES_SENDER_EMAIL_ADDRESS </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">[email protected]</span><span style="color: #ECEFF4">&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 15</span><span><span style="color: #616E88"># Where did we setup SES?</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 16</span><span><span style="color: #D8DEE9FF">SES_REGION_NAME </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">eu-west-1</span><span style="color: #ECEFF4">&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 17</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 18</span><span><span style="color: #D8DEE9FF">iam_client </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> boto3</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">client</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">iam</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 19</span><span><span style="color: #D8DEE9FF">ses_client </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> boto3</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">client</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">ses</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">region_name</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">SES_REGION_NAME</span><span style="color: #ECEFF4">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 20</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 21</span><span><span style="color: #616E88"># Helper function to choose if a key owner should be notified today</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 22</span><span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">is_key_interesting</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">key</span><span style="color: #ECEFF4">):</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 23</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># If the key is inactive, it is not interesting</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 24</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> key</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Status</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">!=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Active</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 25</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">False</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 26</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 27</span><span><span style="color: #D8DEE9FF"> elapsed_days </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">datetime</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">now</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">timezone</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">utc</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> key</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">CreateDate</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]).</span><span style="color: #D8DEE9FF">days</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 28</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 29</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># If the key is newer than ALERT_AFTER_N_DAYS, we don&#039;t need to notify the</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 30</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># owner</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 31</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> elapsed_days </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF"> ALERT_AFTER_N_DAYS</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 32</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">False</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 33</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 34</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">True</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 35</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 36</span><span><span style="color: #616E88"># Helper to send the notification to the user. We need the receiver email, </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 37</span><span><span style="color: #616E88"># the keys we want to notify the user about, and on which account we are</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 38</span><span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">send_notification</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">email</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">keys</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">account_id</span><span style="color: #ECEFF4">):</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 39</span><span><span style="color: #D8DEE9FF"> email_text </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">&#039;&#039;&#039;Dear </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">keys</span><span style="color: #ECEFF4">[</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">][</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">UserName</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 40</span><span><span style="color: #A3BE8C">this is an automatic reminder to rotate your AWS Access Keys at least every </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">ALERT_AFTER_N_DAYS</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> days.</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 41</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 42</span><span><span style="color: #A3BE8C">At the moment, you have </span><span style="color: #EBCB8B">{</span><span style="color: #88C0D0">len</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">keys</span><span style="color: #ECEFF4">)</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> key(s) on the account </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">account_id</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> that have been created more than </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">ALERT_AFTER_N_DAYS</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> days ago:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 43</span><span><span style="color: #A3BE8C">&#039;&#039;&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 44</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> key </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> keys</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 45</span><span><span style="color: #D8DEE9FF"> email_text </span><span style="color: #81A1C1">+=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">&quot;- </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">key</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">AccessKeyId</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> was created on </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">key</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">CreateDate</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> (</span><span style="color: #EBCB8B">{</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">datetime</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">now</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">timezone</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">utc</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> key</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">CreateDate</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]).</span><span style="color: #D8DEE9FF">days</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> days ago)</span><span style="color: #EBCB8B">\n</span><span style="color: #A3BE8C">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 46</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 47</span><span><span style="color: #D8DEE9FF"> email_text </span><span style="color: #81A1C1">+=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">&quot;&quot;&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 48</span><span><span style="color: #A3BE8C">To learn how to rotate your AWS Access Key, please read the official guide at https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_RotateAccessKey</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 49</span><span><span style="color: #A3BE8C">If you have any question, please don&#039;t hesitate to contact the Support Team at [email protected].</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 50</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 51</span><span><span style="color: #A3BE8C">This automatic reminder will be sent again in </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">SEND_EVERY_N_DAYS</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> days, if the key(s) will not be rotated.</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 52</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 53</span><span><span style="color: #A3BE8C">Regards,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 54</span><span><span style="color: #A3BE8C">Your lovely Support Team</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 55</span><span><span style="color: #A3BE8C">&quot;&quot;&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 56</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 57</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">try</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 58</span><span><span style="color: #D8DEE9FF"> ses_response </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> ses_client</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">send_email</span><span style="color: #ECEFF4">(</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 59</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Destination</span><span style="color: #81A1C1">=</span><span style="color: #ECEFF4">{</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">ToAddresses</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">email</span><span style="color: #ECEFF4">]},</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 60</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Message</span><span style="color: #81A1C1">=</span><span style="color: #ECEFF4">{</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 61</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Body</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Html</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Charset</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">UTF-8</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Data</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> email_text</span><span style="color: #ECEFF4">}},</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 62</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Subject</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Charset</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">UTF-8</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 63</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Data</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">&#039;Remember to rotate your AWS Keys on account </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">account_id</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">!&#039;</span><span style="color: #ECEFF4">}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 64</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">},</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 65</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Source</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">SES_SENDER_EMAIL_ADDRESS</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 66</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 67</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">except</span><span style="color: #D8DEE9FF"> ClientError </span><span style="color: #81A1C1">as</span><span style="color: #D8DEE9FF"> e</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 68</span><span><span style="color: #D8DEE9FF"> logging</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">error</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">e</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">response</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Error</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">][</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Message</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">])</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 69</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 70</span><span><span style="color: #D8DEE9FF"> logging</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">info</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">&#039;Notification email sent successfully to </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">email</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">! Message ID: </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">ses_response</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">MessageId</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">]</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">&#039;</span><span style="color: #ECEFF4">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 71</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 72</span><span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">lambda_handler</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">event</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">):</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 73</span><span><span style="color: #D8DEE9FF"> users </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 74</span><span><span style="color: #D8DEE9FF"> is_truncated </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">True</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 75</span><span><span style="color: #D8DEE9FF"> marker </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">None</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 76</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 77</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># We retrieve all users associated to the AWS Account. </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 78</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># Results are paginated, so we go on until we have them all</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 79</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">while</span><span style="color: #D8DEE9FF"> is_truncated</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 80</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># This strange syntax is here because `list_users` doesn&#039;t accept an </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 81</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># invalid Marker argument, so we specify it only if it is not None</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 82</span><span><span style="color: #D8DEE9FF"> response </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> iam_client</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">list_users</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">**</span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF">k</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> v </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> k</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> v </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">dict</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">Marker</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">marker</span><span style="color: #ECEFF4">)).</span><span style="color: #88C0D0">items</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> v </span><span style="color: #81A1C1">is</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">not</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">None</span><span style="color: #ECEFF4">})</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 83</span><span><span style="color: #D8DEE9FF"> users</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">extend</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">response</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Users</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">])</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 84</span><span><span style="color: #D8DEE9FF"> is_truncated </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> response</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">IsTruncated</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 85</span><span><span style="color: #D8DEE9FF"> marker </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> response</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">Marker</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">None</span><span style="color: #ECEFF4">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 86</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 87</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># Probably in this list you have bots, or users you want to filter out</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 88</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># You can filter them by associated tags, or as I do here, just filter out </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 89</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># all the accounts that haven&#039;t logged in the web console at least once</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 90</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># (probably they aren&#039;t users)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 91</span><span><span style="color: #D8DEE9FF"> filtered_users </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">list</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">filter</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">lambda</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">u</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> u</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">PasswordLastUsed</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> users</span><span style="color: #ECEFF4">))</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 92</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 93</span><span><span style="color: #D8DEE9FF"> interesting_keys </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 94</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 95</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># For every user, we want to retrieve the related access keys</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 96</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> user </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> filtered_users</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 97</span><span><span style="color: #D8DEE9FF"> response </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> iam_client</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">list_access_keys</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">UserName</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">user</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">UserName</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">])</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 98</span><span><span style="color: #D8DEE9FF"> access_keys </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> response</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">AccessKeyMetadata</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 99</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">100</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># We are interested only in Active keys, older than</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">101</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># ALERT_AFTER_N_DAYS days</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">102</span><span><span style="color: #D8DEE9FF"> interesting_keys</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">extend</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">list</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">filter</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">lambda</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">k</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">is_key_interesting</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">k</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> access_keys</span><span style="color: #ECEFF4">)))</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">103</span><span><span style="color: #D8DEE9FF"> </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">104</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># We group the keys by owner, so we send no more than one notification for every user</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">105</span><span><span style="color: #D8DEE9FF"> interesting_keys_grouped_by_user </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">defaultdict</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">list</span><span style="color: #ECEFF4">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">106</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> key </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> interesting_keys</span><span style="color: #ECEFF4">:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">107</span><span><span style="color: #D8DEE9FF"> interesting_keys_grouped_by_user</span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">key</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">UserName</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">]].</span><span style="color: #88C0D0">append</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">key</span><span style="color: #ECEFF4">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">108</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">109</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> user </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> interesting_keys_grouped_by_user</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">values</span><span style="color: #ECEFF4">():</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">110</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># In our AWS account the username is always a valid email. </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">111</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># However, you can recover the email from IAM tags, if you have them</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">112</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># or from other lookups</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">113</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># We also get the account id from the Lambda context, but you can </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">114</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># also specify any id you want here, it&#039;s only used in the email </span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">115</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># sent to the users to let them know on which account they should</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">116</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># check</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">117</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">send_notification</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">user</span><span style="color: #ECEFF4">[</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">][</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">UserName</span><span style="color: #ECEFF4">&#039;</span><span style="color: #ECEFF4">],</span><span style="color: #D8DEE9FF"> user</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">invoked_function_arn</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">split</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">:</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)[</span><span style="color: #B48EAD">4</span><span style="color: #ECEFF4">])</span></span></div></code></pre><h1 id="schedule-your-lambda"><a href="#schedule-your-lambda">Schedule your Lambda</a></h1><p>You can schedule your Lambda to run thanks to CloudWatch Events. You can use a schedule expression such <code>rate(3 days)</code> to run the email every 3 days. Lambda will add necessary permissions to the role we created before to invoke the Lambda. If you need any help, AWS covers you with a <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/RunLambdaSchedule.html" target="_blank" rel="noopener noreferrer">dedicated tutorial</a>!</p><h1 id="conclusions"><a href="#conclusions">Conclusions</a></h1><p>This is just an idea on how to create a little script, leveraging AWS Lambda and AWS SES, to keep your AWS account safe. There are, of course, plenty of possible improvements! And remember to check the logs, sometimes ;-)</p><p>If you have hundreds or thousands of users, the function will go in timeout: there are different solutions you can implement, as using tags on users to know when you have lasted checked them, or checking a different group of users every hour, leveraging the <code>PathPrefix</code> argument of <code>list_users</code>.</p><p>Also, in my example it’s simple to know to whom send the notification email - but what if your users don’t have their email as username? You can use tags, and set their contact email there. Or, you maybe have to implement a lookup somewhere else.</p><p>We could also send a daily report to admins: since users usually ignore automatic emails, admins can intervene if too many reports have been ignored. Or, we can forcibly delete keys after some time - although this could break production code, so I wouldn’t <strong>really</strong> do it - or maybe yes, it’s time developers learn to have a good secrets’ hygiene.</p><p>And you? How do you check your users rotate their access keys?</p><p>For any comment, feedback, critic, suggestion on how to improve my English, leave a comment below, or drop an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/2019-hackerone My year on HackerOne 2025-01-26T21:31:29+00:00 This year I spent some of my free time doing bug bounties over HackerOne. Here a summary of what I did, how did it go, and what I want to do in the future. <p>Last year, totally by chance, I found a <a href="https://rpadovani.com/facebook-responsible-disclosure" target="_blank" rel="noopener noreferrer">security issue over Facebook</a> - I reported it, and it was fixed quite fast. In 2018, I also found a <a href="https://rpadovani.com/gitlab-responsible-disclosure" target="_blank" rel="noopener noreferrer">security issue over Gitlab</a>, so I signed up to HackerOne, and reported it as well. That first experience with Gitlab was far from ideal, but after that first report I’ve started reporting more, and Gitlab has improved its program a lot.</p><h1 id="2019"><a href="#2019">2019</a></h1><p>Since June 2019, when I opened <a href="https://hackerone.com/reports/614355" target="_blank" rel="noopener noreferrer">my first report of the year</a>, I reported 27 security vulnerabilities: 4 has been marked as duplicated, 3 as informative, 2 as not applicable, 9 have been resolved, and 9 are currently confirmed and the fix is ongoing. All these 27 vulnerabilities were reported to Gitlab.</p><p>Especially in October and November I had a lot of fun testing the implementation of ElasticSearch over Gitlab. Two of the issues I have found on this topic have already been disclosed:</p><ul><li><p><a href="https://hackerone.com/reports/692252" target="_blank" rel="noopener noreferrer">Group search leaks private MRs, code, commits</a></p></li><li><p><a href="https://hackerone.com/reports/708820" target="_blank" rel="noopener noreferrer">Group search with Elastic search enable leaks unrelated data</a></p></li></ul><h1 id="why-just-gitlab"><a href="#why-just-gitlab">Why just Gitlab?</a></h1><p>I have an amazing daily job as Solutions Architect at <a href="https://www.nextbit.it/" target="_blank" rel="noopener noreferrer">Nextbit</a> that I love. I am not interested in becoming a full-time security researcher, but I am having fun dedicating some hours every month in looking for securities vulnerabilities.</p><p>However, since I don’t want it to be a job, I focus on a product I know very well, also because sometimes I contribute to it and I use it daily.</p><p>I also tried to target some program I didn’t know anything about, but I get bored quite fast: to find some interesting vulnerability you need to spend quite some time to learn how the system works, and how to exploit it.</p><p>Last but not least, Gitlab nowadays manages its HackerOne program in a very cool way: they are very responsive, kind, and I like they are very transparent! You can read a lot about how their security team works in their <a href="https://about.gitlab.com/handbook/engineering/security/" target="_blank" rel="noopener noreferrer">handbook</a>.</p><h1 id="can-you-teach-me"><a href="#can-you-teach-me">Can you teach me?</a></h1><p>Since I have shared a lot of the disclosed reports on Twitter, some people came and asked me to teach them how to start in the bug bounties world. Unfortunately, I don’t have any useful suggestion: I haven’t studied on any specific resource, and all the issues I reported this year come from a deep knowledge of Gitlab, and from what I know thanks to my daily job. There are definitely more interesting people to follow on Twitter, just check over some common hashtags, such as <a href="https://twitter.com/hashtag/togetherwehitharder?src=hashtag_click" target="_blank" rel="noopener noreferrer">TogetherWeHitHarder</a>.</p><h1 id="gitlabs-contest"><a href="#gitlabs-contest">Gitlab’s Contest</a></h1><p>I am writing this blog post from my new keyboard: a custom-made WASD VP3, generously donated by Gitlab after I won a <a href="https://about.gitlab.com/blog/2019/12/12/bugs-bounties-and-cherry-browns/" target="_blank" rel="noopener noreferrer">contest</a> for their first year of public program on HackerOne. I won the best written report category, and it was a complete surprise; I am not a native English speaker, 5 years ago my English was a monstrosity (if you want to have some fun, just go reading my old blog posts), and still to this day I think is quite poor, as you can read here.</p><p>Indeed, if you have any suggestion on how to improve this text, please write me!</p><figure><img src="https://rpadovani.hyvorblogs.io/media/DYVqayCQviKnQGVt.jpg" alt="custom keyboard" srcset="https://rpadovani.hyvorblogs.io/media/DYVqayCQviKnQGVt.jpg 720w, https://rpadovani.hyvorblogs.io/media/DYVqayCQviKnQGVt.jpg/500w 500w"></figure><p>Congratulations to Gitlab for their first year on HackerOne, and keep up the good work! Your program rocks, and in the last months you improved a lot!</p><h1 id="hackeone-clear"><a href="#hackeone-clear">HackeOne Clear</a></h1><p>HackerOne started a new program, called <a href="https://www.hackerone.com/product/clear" target="_blank" rel="noopener noreferrer">HackerOne Clear</a>, only on invitation, where they vet all researchers. I was invited and I thought about accepting the invitation. However, the scope of the data that has to be shared to be vetted is definitely too wide, and to be honest I am surprised so many people accepted the invitation. HackerOne doesn’t perform the check, but delegates to a 3rd party. This 3rd party company asks a lot of things.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div> T&amp;Cs for joining HackerOne Clear ask to hand over a lot of personal data. I totally don&#039;t feel comfortable in doing so, and I wonder why so many people, that should be very aware of the importance of privacy, accepted. </div></aside><p>I totally understand the need of background checks, and I’d be more than happy to provide my criminal record. It wouldn’t be the first time I am vetted, and I am quite sure it wouldn’t be the last.</p><p>More than the criminal record, I am a puzzled about these requirements:</p><ul><li><p>Financial history, including credit history, bankruptcy and financial judgments;</p></li><li><p>Employment or volunteering history, including fiduciary or directorship responsibilities;</p></li><li><p>Gap activities, including travel;</p></li><li><p>Health information, including drug tests;</p></li><li><p>Identity, including identifying numbers and identity documents;</p></li></ul><p>Not only the scope is definitely too wide, but also all these data will be stored and processed outside EU! Personal information will be stored in the United States, Canada and Ireland. Personal information will be processed in the United States, Canada, the United Kingdom, India and the Philippines.</p><p>As European citizen who wants to protect his privacy, I cannot accept such conditions. I’ve written to HackerOne asking why such a wide scope of data, and they replied that since it’s their partner that actually collects the information, there is nothing they can do. I really hope HackerOne will require fewer data in the future, preserving privacy of their researchers.</p><h1 id="2020"><a href="#2020">2020</a></h1><p>In these days I’ve though a lot about what I want to do in my future about bug bounties, and for the 2020 I will continue as I’ve done in the last months: assessing Gitlab, dedicating not more than a few hours a month. I don’t feel ready to step up my game at the moment. I have a lot of other interests I want to pursue in 2020 (travelling, learning German, improve my cooking skills), so I will not prioritize bug bounties for the time being.</p><p>That’s all for today, and also for the 2019! It has been a lot of fun, and I wish to you all a great 2020! For any comment, feedback, critic, leave a comment below, or drop an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>Ciao,<br> R.</p><h1 id="updates"><a href="#updates">Updates</a></h1><ul><li><p>29th December 2019: added paragraph about having asked to HackerOne more information on why they need such wide scope of personal data.</p></li></ul> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/gitlab-visual-reviews Exploring Gitlab Visual Reviews 2025-01-26T21:31:29+00:00 With Visual Reviews, you can provide a feedback form to your Review Apps so that reviewers can post comments directly from the app back to the merge request that spawned the Review App. <p></p><p>If you already have Continuous Integration and Continuous Delivery enabled for your websites, adding this feature is blazing fast, and will make life of your reviewers easier! If you want to start with CI/CD in Gitlab, <a href="https://rpadovani.com/aws-s3-gitlab" target="_blank" rel="noopener noreferrer">I’ve written about it</a> in the past.</p><h1 id="the-feature"><a href="#the-feature">The feature</a></h1><p>While the <a href="https://docs.gitlab.com/ee/ci/review_apps/#visual-reviews-starter" target="_blank" rel="noopener noreferrer">official documentation</a> has a good overview of the feature, we can take a deeper look with some screenshots:</p><p> We can comment directly from the staging environment! And additional metadata will be collected and published as well, making easier to reproduce a bug.</p><p> Our comment (plus the metadata) appears in the merge request, becoming actionable.</p><aside style="background-color:#f1f1ef;color:#000000"><span>?</span><div> With Visual Reviews, you can provide a feedback form to your Review Apps so that reviewers can post comments directly from the app back to the merge request that spawned the Review App. </div></aside><h1 id="implementing-the-system"><a href="#implementing-the-system">Implementing the system</a></h1><p>Adding the snippet isn’t complicate, you only need some information about the MR. Basically, this is what you should add to the head of your website for every merge request:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">1</span><span><span style="color: #81A1C1">&lt;script</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">2</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-project-id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CI_PROJECT_ID</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">3</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-merge-request-id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CI_MERGE_REQUEST_IID</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">4</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-mr-url</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">https://gitlab.example.com</span><span style="color: #ECEFF4">&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">5</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-project-path</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CI_PROJECT_PATH</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">6</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">review-app-toolbar-script</span><span style="color: #ECEFF4">&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">7</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">src</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">https://gitlab.example.com/assets/webpack/visual_review_toolbar.js</span><span style="color: #ECEFF4">&#039;</span><span style="color: #81A1C1">&gt;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">8</span><span><span style="color: #81A1C1">&lt;/script&gt;</span></span></div></code></pre><p>Of course, asking your team to add the HTML snippet, and filling it with the right information isn’t feasible. We will instead take advantage of <a href="https://about.gitlab.com/product/continuous-integration/" target="_blank" rel="noopener noreferrer">Gitlab CI/CD</a> to inject the snippet and autocomplete it with the right information for every merge request.</p><p>First we need the definition of a Gitlab CI job to build our client:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9FF">buildClient:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF"> image: node:12</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF"> stage: build</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> script:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> - ./scripts/inject-review-app-index.sh</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF"> - npm ci</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><span style="color: #D8DEE9FF"> - npm run build</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF"> artifacts:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> paths:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> - build</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> only:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> - merge_requests</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> cache:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> paths:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> - .npm</span></span></div></code></pre><p>The important bit of information here is <code>only: merge_requests</code>. When used, Gitlab injects in the job a environment variable, called <code>CI_MERGE_REQUEST_IID</code>, with the unique ID of the merge request: we will fill it in the HTML snippet. The <a href="https://docs.gitlab.com/ee/ci/yaml/README.html" target="_blank" rel="noopener noreferrer">official documentation</a> of Gitlab CI explains in detail all the other keywords of the YAML.</p><h1 id="the-script"><a href="#the-script">The script</a></h1><p>The other important bit is the script that actually injects the code: it’s a simple bash script, which looks for the <code>&lt;/title&gt;</code> tag in the HTML, and append the needed snippet:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9FF">#!/bin/sh</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF">repl() {</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF"> PATTERN=$1 REPL=$2 awk &#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF"> {gsub(ENVIRON[&quot;PATTERN&quot;], ENVIRON[&quot;REPL&quot;]); print}&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF">}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF">TEXT_TO_INJECT=$(cat </span><span style="color: #D8DEE9">&lt;&lt;</span><span style="color: #D8DEE9FF">-HTML</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #81A1C1">&lt;/title&gt;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #81A1C1">&lt;script</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-project-id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${CI_PROJECT_ID}</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-merge-request-id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${CI_MERGE_REQUEST_IID}</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-mr-url</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">${CI_SERVER_URL}</span><span style="color: #ECEFF4">&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">data-project-path</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${CI_PROJECT_PATH}</span><span style="color: #ECEFF4">&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">review-app-toolbar-script</span><span style="color: #ECEFF4">&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">src</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&#039;</span><span style="color: #A3BE8C">${CI_SERVER_URL}/assets/webpack/visual_review_toolbar.js</span><span style="color: #ECEFF4">&#039;</span><span style="color: #81A1C1">&gt;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #81A1C1">&lt;/script&gt;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF">HTML</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF">)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">21</span><span><span style="color: #D8DEE9FF">repl &quot;</span><span style="color: #81A1C1">&lt;/title&gt;</span><span style="color: #D8DEE9FF">&quot; &quot;${TEXT_TO_INJECT}&quot; </span><span style="color: #D8DEE9">&lt;</span><span style="color: #D8DEE9FF"> public/index.html &gt; tmpfile &amp;&amp; mv tmpfile public/index.html</span></span></div></code></pre><p>Thanks to the Gitlab CI environment variables, the snippet has already all the information it needs to work. Of course you should customize the script with the right path for your <code>index.html</code> (or any other page you have).</p><p>Now everything is ready! Your team needs only to generate <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html" target="_blank" rel="noopener noreferrer">personal access tokens</a> to login, and they are ready to go! You should store your personal access token in your password manager, so you don’t need to generate it each time.</p><h1 id="future-features"><a href="#future-features">Future features</a></h1><p>One of the coolest things in Gitlab is that everything is always a work in progress, and each feature has some new goodies in every release. This is true for the Visual Reviews App as well. There is an <a href="https://gitlab.com/groups/gitlab-org/-/epics/960" target="_blank" rel="noopener noreferrer">epic</a> that collects all the improvements they want to do, including <a href="https://gitlab.com/gitlab-org/gitlab/issues/29067" target="_blank" rel="noopener noreferrer">removing the need for an access token</a>, and <a href="https://gitlab.com/gitlab-org/gitlab/issues/10765" target="_blank" rel="noopener noreferrer">adding ability to take screenshots</a> that will be inserted in the MR comments as well.</p><p>That’s all for today, I hope you found this article useful! For any comment, feedback, critic, leave a comment below, or drop an email at <a href="mailto:[email protected]" rel="noopener noreferrer">[email protected]</a>.</p><p>I have also changed the blog theme to a custom version of <a href="https://nextbitlabs.github.io/Rapido/" target="_blank" rel="noopener noreferrer">Rapido.css</a>. I think it increases the readability, but let me know what you think!</p><p>Ciao,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/aws-textract Using AWS Textract in an automatic fashion with AWS Lambda 2025-01-26T21:31:29+00:00 AWS Textract does OCR reading of data: let's see how to automatize its usage with AWS Lambda, S3, and Amazon SNS <p></p><p><strong>Updated on January 4th, 2023</strong>: removed references to SQS, that is not actually used in the proposed implementation.</p><p>In that case, we need to invoke some asynchronous APIs, poll an endpoint to check when it has finished working, and then read the result, which is paginated, so we need multiple APIs call. Wouldn’t be super cool to just drop files in an S3 bucket, and after some minutes, having their content in another S3 bucket?</p><p>Let’s see how to use AWS Lambda and SNS to automatize all the process!</p><h2 id="overview-of-the-process"><a href="#overview-of-the-process">Overview of the process</a></h2><p>This is the process we are aiming to build:</p><ol><li><p>Drop files to an S3 bucket;</p></li><li><p>A trigger will invoke an AWS Lambda function, which will inform AWS Textract of the presence of a new document to analyze;</p></li><li><p>AWS Textract will do its magic, and push the status of the job to an SNS topic;</p></li><li><p>The SNS topic will invoke another Lambda function, which will read the status of the job, and if the analysis was successful, it downloads the extracted text and save to another S3 bucket (but we could replace this with a write over DynamoDB or others database systems);</p></li><li><p>The Lambda function will also publish the state over Cloudwatch, so we can trigger alarms when a read was unsuccessful.</p></li></ol><p>Since a picture is worth a thousand words, let me show a graph of this process.</p><figure><img src="https://rpadovani.hyvorblogs.io/media/5t6zmTRZS5pFIHQe.png" alt="Textract structure" srcset="https://rpadovani.hyvorblogs.io/media/5t6zmTRZS5pFIHQe.png 726w, https://rpadovani.hyvorblogs.io/media/5t6zmTRZS5pFIHQe.png/500w 500w"></figure><p>While I am writing this, Textract is available only in 4 regions: US East (Northern Virginia), US East (Ohio), US West (Oregon), and EU (Ireland). I strongly suggest therefore to create all the resources in just one region, for the sake of simplicity. In this tutorial, I will use <code>eu-west-1</code>.</p><h2 id="s3-buckets"><a href="#s3-buckets">S3 buckets</a></h2><p>First of all, we need to create two buckets: one for our raw file, and one for the JSON file with the extracted test. We could also use the same bucket, theoretically, but with two buckets we can have better access control.</p><p>Since I love <a href="https://mcfunley.com/choose-boring-technology" target="_blank" rel="noopener noreferrer">boring solutions</a>, for this tutorial I will call the two buckets <code>textract_raw_files</code> and <code>textract_json_files</code>. <a href="https://docs.aws.amazon.com/AmazonS3/latest/gsg/CreatingABucket.html" target="_blank" rel="noopener noreferrer">Official documentation explains how to create S3 buckets</a>. </p><h2 id="invoke-textract"><a href="#invoke-textract">Invoke Textract</a></h2><p>The first part of the architecture is informing Textract of every new file we upload to S3. We can leverage the <a href="https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html" target="_blank" rel="noopener noreferrer">S3 integration with Lambda</a>: each time a new file is uploaded, our Lambda function is triggered, and it will invoke Textract.</p><p>The body of the function is quite straightforward:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9FF">from urllib.parse import unquote_plus</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><span style="color: #D8DEE9FF">import boto3</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF">s3_client = boto3.client(&#039;s3&#039;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><span style="color: #D8DEE9FF">textract_client = boto3.client(&#039;textract&#039;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF">SNS_TOPIC_ARN = &#039;arn:aws:sns:eu-west-1:123456789012:AmazonTextract&#039; # We need to create this</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF">ROLE_ARN = &#039;arn:aws:iam::123456789012:role/TextractRole&#039; # This role is managed by AWS</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF">def handler(event, _):</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> for record in event[&#039;Records&#039;]:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> bucket = record[&#039;s3&#039;][&#039;bucket&#039;][&#039;name&#039;]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> key = unquote_plus(record[&#039;s3&#039;][&#039;object&#039;][&#039;key&#039;])</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> print(f&#039;Document detection for {bucket}/{key}&#039;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> textract_client.start_document_text_detection(</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><span style="color: #D8DEE9FF"> DocumentLocation={&#039;S3Object&#039;: {&#039;Bucket&#039;: bucket, &#039;Name&#039;: key}},</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><span style="color: #D8DEE9FF"> NotificationChannel={&#039;RoleArn&#039;: ROLE_ARN, &#039;SNSTopicArn&#039;: SNS_TOPIC_ARN})</span></span></div></code></pre><p>You can find a copy of this code <a href="https://gitlab.com/snippets/1869011" target="_blank" rel="noopener noreferrer">hosted over Gitlab</a>.</p><p>As you can see, we receive a list of freshly uploaded files, and for each one of them, we ask Textract to do its magic. We also ask it to notify us, when it has finished its work, sending a message over SNS. We need therefore to create an SNS topic. It is well explained how to do so in the <a href="https://docs.aws.amazon.com/sns/latest/dg/sns-tutorial-create-topic.html" target="_blank" rel="noopener noreferrer">official documentation</a>.</p><p>When we have finished, we should have something like this:</p><figure><img src="https://rpadovani.hyvorblogs.io/media/9laI5JQN8QuGbhEp.png" alt="SNS topic" srcset="https://rpadovani.hyvorblogs.io/media/9laI5JQN8QuGbhEp.png 1920w, https://rpadovani.hyvorblogs.io/media/9laI5JQN8QuGbhEp.png/500w 500w, https://rpadovani.hyvorblogs.io/media/9laI5JQN8QuGbhEp.png/750w 750w, https://rpadovani.hyvorblogs.io/media/9laI5JQN8QuGbhEp.png/1000w 1000w, https://rpadovani.hyvorblogs.io/media/9laI5JQN8QuGbhEp.png/1500w 1500w"></figure><p>We copy the ARN of our freshly created topic and insert it in the script above in the variable <code>SNS_TOPIC_ARN</code>.</p><p>Now we need to actually create our Lambda function: once again the <a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started-create-function.html" target="_blank" rel="noopener noreferrer">official documentation</a> is our friend if we have never worked with AWS Lambda before.</p><p>Since the only requirement of the script is <code>boto3</code>, and it is included by default in Lambda, we don’t need to create a custom package.</p><p>At least, this is usually the case :-) Unfortunately, while I am writing this post, <code>boto3</code> on Lambda is at version <code>boto3-1.9.42</code>, while support for Textract landed only in <code>boto3-1.9.138</code>. We can check which version is currently on Lambda from <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html" target="_blank" rel="noopener noreferrer">this page</a>, under <code>Python Runtimes</code>: if <code>boto3</code> has been updated to a version <code>&gt;= 1.9.138</code>, we don’t have to do anything more than simply create the Lambda function. Otherwise, we have to include a newer version of <code>boto3</code> in our Lambda function. But fear not! The official documentation explains <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html#python-package-dependencies" target="_blank" rel="noopener noreferrer">how to create a deployment package</a>. <strong>Update Oct ‘19</strong>: this is no longer the case, AWS has updated the boto3 package included in the Lambda runtime.</p><p>We need also to link an IAM role to our Lambda function, which requires some additional permission:</p><ul><li><p><strong>AmazonTextractFullAccess</strong>: this gives access also to SNS, other than Textract</p></li><li><p><strong>AmazonS3ReadOnlyAccess</strong>: if we want to be a bit more conservative, we can give access to just the <code>textract_raw_files</code> bucket.</p></li></ul><p>Of course, other than that, the function requires the standard permissions to be executed and to write on Cloudwatch: AWS manages that for us.</p><p>We are almost there, we need only to create the trigger: we can do that from the Lambda designer! From the designer we select S3 as the trigger, we set our <code>textract_raw_files</code> bucket, and we select <code>All object create events</code> as Event type.</p><p>If we implemented everything correctly, we can now upload a PDF file to the <code>textract_raw_files</code>, and over Cloudwatch we should be able to see the log of the Lambda function, which should say something similar to <code>Document detection for textract_raw_files/my_first_file.pdf</code>.</p><p>Now we only need to read the extracted text, all the hard work has been done by AWS :-)</p><h2 id="read-data-from-textract"><a href="#read-data-from-textract">Read data from Textract</a></h2><p>AWS Textract is so kind to notify us when it has finished extracting data from PDFs we provided: we create a Lambda function to intercept such notification, invoke AWS Textract and save the result in S3.</p><p>The Lambda function needs also to support pagination in the results, so the code is a bit longer:</p><pre style="background-color:#2e3440ff" onmouseenter="" onmouseleave="" class="language- has-line-numbers" data-language="" data-annotations="" data-name="" ><code><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 1</span><span><span style="color: #D8DEE9FF">import json</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 2</span><span><span style="color: #D8DEE9FF">import boto3</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 3</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 4</span><span><span style="color: #D8DEE9FF">textract_client = boto3.client(&#039;textract&#039;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 5</span><span><span style="color: #D8DEE9FF">s3_bucket = boto3.resource(&#039;s3&#039;).Bucket(&#039;textract_json_files&#039;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 6</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 7</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 8</span><span><span style="color: #D8DEE9FF">def get_detected_text(job_id: str, keep_newlines: bool = False) -&gt; str:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right"> 9</span><span><span style="color: #D8DEE9FF"> &quot;&quot;&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">10</span><span><span style="color: #D8DEE9FF"> Giving job_id, return plain text extracted from input document.</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">11</span><span><span style="color: #D8DEE9FF"> :param job_id: Textract DetectDocumentText job Id</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">12</span><span><span style="color: #D8DEE9FF"> :param keep_newlines: if True, output will have same lines structure as the input document</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">13</span><span><span style="color: #D8DEE9FF"> :return: plain text as extracted by Textract</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">14</span><span><span style="color: #D8DEE9FF"> &quot;&quot;&quot;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">15</span><span><span style="color: #D8DEE9FF"> max_results = 1000</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">16</span><span><span style="color: #D8DEE9FF"> pagination_token = None</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">17</span><span><span style="color: #D8DEE9FF"> finished = False</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">18</span><span><span style="color: #D8DEE9FF"> text = &#039;&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">19</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">20</span><span><span style="color: #D8DEE9FF"> while not finished:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">21</span><span><span style="color: #D8DEE9FF"> if pagination_token is None:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">22</span><span><span style="color: #D8DEE9FF"> response = textract_client.get_document_text_detection(JobId=job_id,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">23</span><span><span style="color: #D8DEE9FF"> MaxResults=max_results)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">24</span><span><span style="color: #D8DEE9FF"> else:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">25</span><span><span style="color: #D8DEE9FF"> response = textract_client.get_document_text_detection(JobId=job_id,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">26</span><span><span style="color: #D8DEE9FF"> MaxResults=max_results,</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">27</span><span><span style="color: #D8DEE9FF"> NextToken=pagination_token)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">28</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">29</span><span><span style="color: #D8DEE9FF"> sep = &#039; &#039; if not keep_newlines else &#039;\n&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">30</span><span><span style="color: #D8DEE9FF"> text += sep.join([x[&#039;Text&#039;] for x in response[&#039;Blocks&#039;] if x[&#039;BlockType&#039;] == &#039;LINE&#039;])</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">31</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">32</span><span><span style="color: #D8DEE9FF"> if &#039;NextToken&#039; in response:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">33</span><span><span style="color: #D8DEE9FF"> pagination_token = response[&#039;NextToken&#039;]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">34</span><span><span style="color: #D8DEE9FF"> else:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">35</span><span><span style="color: #D8DEE9FF"> finished = True</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">36</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">37</span><span><span style="color: #D8DEE9FF"> return text</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">38</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">39</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">40</span><span><span style="color: #D8DEE9FF">def handler(event, _):</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">41</span><span><span style="color: #D8DEE9FF"> for record in event[&#039;Records&#039;]:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">42</span><span><span style="color: #D8DEE9FF"> message = json.loads(record[&#039;Sns&#039;][&#039;Message&#039;])</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">43</span><span><span style="color: #D8DEE9FF"> job_id = message[&#039;JobId&#039;]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">44</span><span><span style="color: #D8DEE9FF"> status = message[&#039;Status&#039;]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">45</span><span><span style="color: #D8DEE9FF"> filename = message[&#039;DocumentLocation&#039;][&#039;S3ObjectName&#039;]</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">46</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">47</span><span><span style="color: #D8DEE9FF"> print(f&#039;JobId {job_id} has finished with status {status} for file {filename}&#039;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">48</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">49</span><span><span style="color: #D8DEE9FF"> if status != &#039;SUCCEEDED&#039;:</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">50</span><span><span style="color: #D8DEE9FF"> return</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">51</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">52</span><span><span style="color: #D8DEE9FF"> text = get_detected_text(job_id)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">53</span><span><span style="color: #D8DEE9FF"> to_json = {&#039;Document&#039;: filename, &#039;ExtractedText&#039;: text, &#039;TextractJobId&#039;: job_id}</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">54</span><span><span style="color: #D8DEE9FF"> json_content = json.dumps(to_json).encode(&#039;UTF-8&#039;)</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">55</span><span><span style="color: #D8DEE9FF"> output_file_name = filename.split(&#039;/&#039;)[-1].rsplit(&#039;.&#039;, 1)[0] + &#039;.json&#039;</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">56</span><span><span style="color: #D8DEE9FF"> s3_bucket.Object(f&#039;{output_file_name}&#039;).put(Body=bytes(json_content))</span></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">57</span><span><wbr /></span></div><div class="line" style=""><span class="line-number" style="-webkit-user-select:none;user-select:none;color:#4c566a;text-align:right">58</span><span><span style="color: #D8DEE9FF"> return message</span></span></div></code></pre><p>You can find a copy of this code <a href="https://gitlab.com/snippets/1869009" target="_blank" rel="noopener noreferrer">hosted over Gitlab</a>.</p><p>Again, this code has to be published as a Lambda function. As before, it shouldn’t need any special configuration, but since it requires <code>boto3 &gt;= 1.9.138</code> we have to create a deployment package, as long as AWS doesn’t update the Lambda runtime.</p><p>After we have uploaded the Lambda function, from the control panel we set as trigger <code>SNS</code>, specifying as <code>ARN</code> the <code>ARN</code> of the <code>SNS</code> topic we created before - in our case, <code>arn:aws:sns:eu-west-1:123456789012:AmazonTextract</code>.</p><p>We need also to give the IAM role which executes the Lambda function new permissions, in addition to the ones it already has. In particular, we need:</p><ul><li><p><strong>AmazonTextractFullAccess</strong></p></li><li><p><strong>AmazonS3FullAccess</strong>: in production, we should give access to just the <code>textract_json_files</code> bucket.</p></li></ul><p>This should be the final result:</p><figure><img src="https://rpadovani.hyvorblogs.io/media/npzzar4rYdNcaV4a.png" alt="Lambda Configuration" srcset="https://rpadovani.hyvorblogs.io/media/npzzar4rYdNcaV4a.png 1920w, https://rpadovani.hyvorblogs.io/media/npzzar4rYdNcaV4a.png/500w 500w, https://rpadovani.hyvorblogs.io/media/npzzar4rYdNcaV4a.png/750w 750w, https://rpadovani.hyvorblogs.io/media/npzzar4rYdNcaV4a.png/1000w 1000w, https://rpadovani.hyvorblogs.io/media/npzzar4rYdNcaV4a.png/1500w 1500w"></figure><p>And that’s all! Now we can simply drop any document in a supported format to the <code>textract_raw_files</code> bucket, and after some minutes we will find its content in the <code>textract_json_files</code> bucket! And the quality of the extraction is quite good.</p><h2 id="known-limitations"><a href="#known-limitations">Known limitations</a></h2><p>Other than being available in just 4 locations, at least for the moment, AWS Textract has other <a href="https://docs.aws.amazon.com/textract/latest/dg/limits.html" target="_blank" rel="noopener noreferrer">known hard limitations</a>:</p><ul><li><p>The maximum document image (JPEG/PNG) size is 5 MB.</p></li><li><p>The maximum PDF file size is 500 MB.</p></li><li><p>The maximum number of pages in a PDF file is 3000.</p></li><li><p>The maximum PDF media size for the height and width dimensions is 40 inches or 2880 points.</p></li><li><p>The minimum height for text to be detected is 15 pixels. At 150 DPI, this would be equivalent to 8-pt font.</p></li><li><p>Documents can be rotated a maximum of +/- 10% from the vertical axis. Text can be text aligned horizontally within the document.</p></li><li><p>Amazon Textract doesn’t support the detection of handwriting.</p></li></ul><p>It has also some <a href="https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html#limits_textract" target="_blank" rel="noopener noreferrer">soft limitations</a> that make it unsuitable for mass ingestion:</p><ul><li><p>Transactions per second per account for all Start (asynchronous) operations: <strong>0.25</strong></p></li><li><p>Maximum number of asynchronous jobs per account that can simultaneously exist: <strong>2</strong></p></li></ul><p>So, if you need it for anything but testing, you should open a ticket to ask for higher limits, and maybe poking your point of contact in AWS to speed up the process.</p><p>That’s all for today, I hope you found this article useful! For any comment, feedback, critic, leave a comment below, or drop an email at <code>[email protected]</code>.</p><p>Regards,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/gitlab-responsible-disclosure Responsible disclosure: improper access control in Gitlab private project. 2025-01-26T21:31:29+00:00 As I said back in September regarding a responsible disclosure about Facebook, data access control isn’t easy. While it can sound elementary, it is very difficult, both on a theoretical side and on a practical side. <p></p><p>This issue was firstly reported on <a href="https://hackerone.com/reports/310185" target="_blank" rel="noopener noreferrer">HackerOne</a> and was managed on the <a href="https://gitlab.com/gitlab-org/gitlab-ce/issues/42726" target="_blank" rel="noopener noreferrer">Gitlab issues’ tracker</a>. Both links are now publicly accessible.</p><h2 id="summary-of-the-issue"><a href="#summary-of-the-issue">Summary of the issue</a></h2><ul><li><p>Rogue user is added to a private group with dozens of projects</p></li><li><p>The user’s role in some projects changes</p></li><li><p>Rogue is fired, and removed from the group: they still have access to projects where their role was changed</p></li></ul><p>The second step could happen for a lot of different reasons:</p><ul><li><p><em>rogue</em> is added as <code>master</code> - knowing this vulnerability, they decrease their privileges to stay in some projects (this is the only <strong>malicious</strong> one)</p></li><li><p><em>rogue</em> is added as <code>developer</code>, but they become responsible for some projects, and are promoted to <code>master</code> role</p></li><li><p><em>rogue</em> is added as <code>reporter</code>, and then they are promoted for a project, and so on.</p></li></ul><p>When an admin removes a user from a private group, there is no indication that the user still has access to private projects, if their role was changed.</p><h2 id="impact"><a href="#impact">Impact</a></h2><p>User can still see all resources of a project of a secret group after they have been removed from the parent’s group.</p><h2 id="timeline"><a href="#timeline">Timeline</a></h2><ul><li><p><strong>29 January 2018</strong>: First disclosure to Gitlab</p></li><li><p><strong>9 February 2018</strong>: Gitlab confirmed the issue and triaged it, assigning a <strong>medium</strong> priority</p></li><li><p><strong>25 February 2018</strong>: I ask for a timeline</p></li><li><p><strong>27 February 2018</strong>: They inform me they will update me with a timeline</p></li><li><p><strong>16 March 2018</strong>: Almost two months are passed, I ask again for a timeline or suggest to go public since administrators of groups can easily check and avoid this vulnerability</p></li><li><p><strong>17 March 2018</strong>: They inform me they will update me with a timeline, and ask to do not go public</p></li><li><p><strong>Somewhere around December 2018</strong>: the team think the issue has been fixed, and close the internal issue - without communicating with me</p></li><li><p><strong>17 January 2019</strong>: I ask for an update - they will never reply to this message</p></li><li><p><strong>25 January 2019</strong>: the security team sees this is still an issue</p></li><li><p><strong>31 January 2019</strong>: the fix is deployed in production and <a href="https://about.gitlab.com/2019/01/31/security-release-gitlab-11-dot-7-dot-3-released/" target="_blank" rel="noopener noreferrer">publicly disclosed</a>, without informing me</p></li><li><p><strong>5 March 2019</strong>: I ask again for another update</p></li><li><p><strong>12 March 2019</strong>: Gitlab says the issue has been fixed and awards me a bounty</p></li></ul><h2 id="bounty"><a href="#bounty">Bounty</a></h2><p>Gitlab awarded me a $2000 bounty award for the disclosure.</p><p>If you follow my blog, you know I deeply love Gitlab: I contribute to it, I write blog posts, and I advocate for it any time I can. Still, I think this experience was <em>awful</em>, to say the least. There was a total lack of communication by their side, they thought they fixed the issue the first time, but actually, it wasn’t fixed. If they had communicated with me, I would have double checked their work. After that, they deployed the fix and went public, without telling me. I was not interested in the bounty (for which I am grateful), I reported the issue because I care about Gitlab. Nonetheless, my love for Gitlab is still the same! I just hope they will improve this part of communication / contributing to Gitlab: in the last couple of years the <a href="https://about.gitlab.com/2019/04/17/contributor-program-update/" target="_blank" rel="noopener noreferrer">community around the project grew a lot</a>, and they are doing amazing with it, maybe the Community team should step in and help also the security community?</p><p>For any comment, feedback, critic, leave a comment below, or drop an email at <code>[email protected]</code>.</p><p>Regards,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/glasnost Glasnost: yet another Gitlab's client. 2025-01-26T21:31:29+00:00 Glasnost is an opensource Gitlab's client for Android and iOs. <p></p><p>I travel often, and be able to work on issues and pipelines on the go is something essential for me. Unfortunately, Gitlab’s UX on small screens is far from ideal (while it has improved over the years).</p><h2 id="enter-glasnost"><a href="#enter-glasnost">Enter Glasnost</a></h2><p>My good friend <a href="https://puskin.it/" target="_blank" rel="noopener noreferrer">Giovanni</a> has developed a new opensource mobile client for Gitlab, with a lot of cool features: <strong>Glasnost</strong>!</p><figure><img src="https://rpadovani.hyvorblogs.io/media/QmplgLR8Nq8QcscS.png" alt="glasnost logo" srcset="https://rpadovani.hyvorblogs.io/media/QmplgLR8Nq8QcscS.png 512w, https://rpadovani.hyvorblogs.io/media/QmplgLR8Nq8QcscS.png/500w 500w"></figure><p><a href="https://puskin.it/glasnost/" target="_blank" rel="noopener noreferrer">In his words</a>:</p><blockquote><p>Glasnost is a free, currently maintained, platform independent and opensource mobile application that is aiming to visualize and edit the most important entities handled by Gitlab.</p></blockquote><p>Among the others features, I’d like to highlight <strong>support for multiple Gitlab hosts</strong> (so you can work both on your company’s Gitlab and on Gitlab.com at the same time), <strong>two different themes</strong> (a light one and a dark one), <strong>a lite version</strong> for when your data connection is stuck on <em>edge</em>, and support for <strong>fingerprint authentication</strong>.</p><p>The application is still in an early phase of development, but it already has enough features to be used daily. I am sure Giovanni would love some feedback and suggestions, so please go on the <a href="https://gitlab.com/puskin/Glasnost/issues" target="_blank" rel="noopener noreferrer">Glasnost’s issues tracker</a> or leave a feedback on the PlayStore.</p><p>If you feel a bit more adventurous, you can contribute to the application itself: it is written in React+Redux with Expo: <a href="https://gitlab.com/puskin/Glasnost" target="_blank" rel="noopener noreferrer">the code</a> is hosted on Gitlab (of course).</p><p>Enjoy yet another client for Gitlab, and let Giovanni knows what you think!</p><p></p><p>For any comment, feedback, critic, leave a comment below or drop an email at <code>[email protected]</code>.</p><p>Regards,<br> R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani https://rpadovani.com/facebook-responsible-disclosure Responsible disclosure: retrieving a user's private Facebook friends. 2025-01-26T21:31:29+00:00 Data access control isn’t easy. While it can sound quite simple, it is very difficult, both on a theoretical side and on a practical side. <p></p><p>On the pratical side, how we will see, disclose of private data is often a unwanted side effect of an useful feature.</p><h2 id="facebook-and-instagram"><a href="#facebook-and-instagram">Facebook and Instagram</a></h2><p>Facebook bought Instagram back in 2012. Since then, a lot of integrations have been implemented between them: among the others, when you suscribe to Instagram, it will suggest you who to follow based on your Facebook friends.</p><p>Your Instagram and Facebook accounts are then somehow linked: it happens both if you sign up to Instagram using your Facebook account (doh!), but also if you sign up to Instagram creating a new account but using the same email you use in your Facebook account (there are also other way Instagram links your new account with an existing Facebook account, but they are not of our interest here).</p><p>So if you want to create a <em>secret</em> Instagram account, create a new mail for it ;-)</p><p>Back in topic: Instagram used to enable all its feature to new users, <strong>before</strong> they have confirmed their email address. This was to do not “interrupt” usage of the website / app, they would have been time to confirm the email later in their usage.</p><p>Email address confirmation is useful to confirm you are signing up using your own email address, and not one of someone else.</p><h2 id="data-leak"><a href="#data-leak">Data leak</a></h2><p>One of the features available <strong>before</strong> confirming the email address, was the suggestion of who to follow based on the Facebook friends of the account Instagram automatically linked.</p><p>This made super easy to retrieve the Facebook’s friend list of anyone who doesn’t have an Instagram account, and since there are more than 2 billions Facebook accounts but just 800 millions Instagram accounts, it means that at least 1 billion and half accounts were vulnerable.</p><p>The method was simple: knowing the email address of the target (and an email address is all but secret), the attacker had just to sign up to Instagram with that email, and then go to the suggestions of people to follow to see victim’s friends.</p><figure><img src="https://rpadovani.hyvorblogs.io/media/k4nkFhEvnnALNEKn.png" alt="List of victim&#039;s friends" srcset="https://rpadovani.hyvorblogs.io/media/k4nkFhEvnnALNEKn.png 872w, https://rpadovani.hyvorblogs.io/media/k4nkFhEvnnALNEKn.png/500w 500w, https://rpadovani.hyvorblogs.io/media/k4nkFhEvnnALNEKn.png/750w 750w"></figure><h2 id="conclusion"><a href="#conclusion">Conclusion</a></h2><p>The combination of two useful features (suggestion of people to follow based on a linked Facebook account, being able to use the new Instagram account immediately) made this data leak possible.</p><p>It wasn’t important if the attacker was a Facebook’s friend with the victim, or the privacy settings of the victim’s account on Facebook. Heck, the attacker didn’t need a Facebook account at all!</p><h2 id="timeline"><a href="#timeline">Timeline</a></h2><ul><li><p><strong>20 August 2018</strong>: first disclosure to Facebook</p></li><li><p><strong>20 August 2018</strong>: request of other information from Facebook</p></li><li><p><strong>20 August 2018</strong>: more information provided to Facebook</p></li><li><p><strong>21 August 2018</strong>: Facebook closed the issue saying wasn’t a security issue</p></li><li><p><strong>21 August 2018</strong>: I submitted a new demo with more information</p></li><li><p><strong>23 August 2018</strong>: Facebook confirmed the issue</p></li><li><p><strong>30 August 2018</strong>: Facebook deployed a fix and asked for a test</p></li><li><p><strong>12 September 2018</strong>: Facebook awarded me a bounty</p></li></ul><h2 id="bounty"><a href="#bounty">Bounty</a></h2><p>Facebook awarded me a $3000 bounty award for the disclosure. This was the first time I was awarded for a <a href="https://www.facebook.com/whitehat" target="_blank" rel="noopener noreferrer">security disclosure for Facebook</a>, I am quite happy with the result and I applaude Facebook for making all the process really straightforward.</p><p>For any comment, feedback, critic, leave me a comment below or drop an email at <code>[email protected]</code>.</p><p>Regards, R.</p> Riccardo Padovani https://rpadovani.com/author/riccardo-padovani