<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jakob Beckmann</title>
    <description>The latest articles on DEV Community by Jakob Beckmann (@f4z3r).</description>
    <link>https://dev.to/f4z3r</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2319323%2Ffcd09f88-ecfa-41b5-a18d-5ea0f067d475.jpeg</url>
      <title>DEV Community: Jakob Beckmann</title>
      <link>https://dev.to/f4z3r</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/f4z3r"/>
    <language>en</language>
    <item>
      <title>Beyond the Pod: Why wasmCloud and WebAssembly Might Be the Next Evolution of the Platform</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Tue, 21 Oct 2025 05:08:44 +0000</pubDate>
      <link>https://dev.to/ipt/beyond-the-pod-why-wasmcloud-and-webassembly-might-be-the-next-evolution-of-the-platform-1i3e</link>
      <guid>https://dev.to/ipt/beyond-the-pod-why-wasmcloud-and-webassembly-might-be-the-next-evolution-of-the-platform-1i3e</guid>
      <description>&lt;p&gt;Over the past few months I have invested some time to contribute to an open source project I find fascinating: &lt;a href="https://wasmcloud.com/" rel="noopener noreferrer"&gt;wasmCloud&lt;/a&gt;. As a platform engineer and architect, I am very familiar with how software platforms are typically built in practice. However, with the ubiquity of Kubernetes, you run the risk to being stuck in the "doing it the Kubernetes way" line of thinking. But then again, are there any better ways? This is where wasmCloud caught my attention. A modern platform building on proven concepts from Kubernetes, but with some significant differences. In this article I want to introduce wasmCloud, how it compares to Kubernetes, what its internal architecture looks like, and what ideas are, in my humble opinion, a step up from "the Kubernetes way of things".&lt;/p&gt;

&lt;p&gt;Before getting started, I need to get some things out of the way. This article will make quite a few comparisons to Kubernetes and bytecode interpreters like the JVM. If you are unfamiliar with these technologies, it might make sense to have a short look at what these are. Considering you clicked on this article, I am however guessing that you are familiar with them and have some experience in platform engineering practices, either as a poweruser of a platform, or as a designer and developer of one.&lt;/p&gt;

&lt;p&gt;Moreover, I want to thank the company I work for, &lt;a href="https://ipt.ch/en/" rel="noopener noreferrer"&gt;ipt&lt;/a&gt;, for allowing me to invest time to learn about new technologies such as wasmCloud. Not only is contributing to open source a great way to pay back a community powering the modern world, it is also a huge passion of mine. Being able to help the development of such projects during paid worktime enables me to learn so much on emerging technologies, and maybe help build the revolutionary tools of tomorrow.&lt;/p&gt;

&lt;p&gt;So... wasmCloud!? I have been interested in WebAssembly ever since it promised to replace JavaScript, a language I personally consider as extremely poorly designed (someone once told me it was designed in three days, so no wonder there). While WebAssembly is very far from doing anything close to replacing JavaScript in the browser, it has evolved into something else: an application runtime and a potential replacement for containers.&lt;/p&gt;

&lt;h1&gt;
  
  
  WebAssembly as a Platform Foundation
&lt;/h1&gt;

&lt;p&gt;Modern platforms nearly all build on top of containers as their foundational element to run executable code. This is a logical evolution from Docker's meteoric growth, and the ecosystem that grew around its open standards (such as the &lt;a href="https://opencontainers.org/" rel="noopener noreferrer"&gt;OCI - Open Container Initiative&lt;/a&gt;). While containers provide a huge step in terms of ease of use, standardization, and security compared to shipping raw artefacts to virtual machines, as was the case before them, they do have some shortcomings.&lt;/p&gt;

&lt;p&gt;First and foremost, containers are not composable. In part due to their flexibility, they do not offer standard ways of expressing how the world should interact with them at runtime, or what they rely on to perform their functionality. This means that containers are typically deployed as REST-based microservices, where containers communicate with one another over a network using APIs agreed upon outside of the container standards. This lack of standardization makes building reusable components more challenging than it has to be. Moreover, each container essentially needs a server, authentication, authorization, and more to run. This results in quite some waste in the compute density of the platform, with lots of compute wasted on boilerplate.&lt;/p&gt;

&lt;p&gt;Moreover, while containers are a huge step in the right direction in terms of security, they are not quite as secure as most people are led to believe. Containers are "allow by default" constructs, which take quite some work to properly harden.&lt;/p&gt;

&lt;p&gt;Finally, due to how containers are typically built, their startup times are not that great. It is not abnormal to see container start times in the dozens of seconds. This does not bother people very much because containers are mostly used to run long running processes (since we need these REST APIs everywhere). However, a large part of containers are mostly idle, waiting for some API request to come in. If one considers that workloads could be called (and thus the process started) only when needed, startup times over 100ms is considered slow.&lt;/p&gt;

&lt;p&gt;This is where WebAssembly comes it. WebAssembly addresses these challenges. Composability is addressed by the component model.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly: The Component Model
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://component-model.bytecodealliance.org/" rel="noopener noreferrer"&gt;component model&lt;/a&gt; is a way that WebAssembly modules can be built with metadata attached to them which describe their imports and exports based on a rich type system. Moreover, they are composable such that a new component can be built from existing components as long as the imports of one are satisfied by the exports of another. This means that components can interact with one another via direct method/function calls, whose specification is fully standardized. This interface specification is declared in a language known as the WebAssembly Interface Types (WIT) language. An example of a WIT specification of a component relying on a system clock can be seen below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package wasi-example:clocks;

world mycomponent {
    import wall-clock;
}

interface wall-clock {
    record datetime {
        seconds: u64,
        nanoseconds: u32,
    }

    now: func() -&amp;gt; datetime;

    resolution: func() -&amp;gt; datetime;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;WIT can be compared to the &lt;a href="https://en.wikipedia.org/wiki/Interface_description_language" rel="noopener noreferrer"&gt;Interface Definition Language (IDL)&lt;/a&gt; from gRPC but for wasm components.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This declaration says that the component relies on an interface &lt;code&gt;wall-clock&lt;/code&gt; (it &lt;code&gt;import&lt;/code&gt;s the interface) which defines two functions: &lt;code&gt;now&lt;/code&gt; and &lt;code&gt;resolution&lt;/code&gt;. Both take no arguments and return a &lt;code&gt;datetime&lt;/code&gt; object consisting of a &lt;code&gt;seconds&lt;/code&gt; and &lt;code&gt;nanoseconds&lt;/code&gt; field. This component could then be composed with any other component which exports this &lt;code&gt;wall-clock&lt;/code&gt; interface.&lt;/p&gt;

&lt;p&gt;If this were a container which would rely on accessing some API, we would need to read a non-standardized documentation of the container image, and then read up on other containers to ensure they provide APIs that match the ones called by the first container.&lt;/p&gt;

&lt;p&gt;The WebAssembly component model can essentially be seen as a form of contract-based programming to formalize interfaces between WebAssembly core modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly: Secure by Default
&lt;/h2&gt;

&lt;p&gt;Whereas containers provide some form of security by namespacing processes and filesystems, WebAssembly actually sandboxes modules such that they cannot affect one another, or the host they run on. By default a WebAssembly module cannot perform any privileged action and needs to be granted explicit permission. I will not dive deeper into the details of this or I might loose myself in a rant on how software security in the modern day and age is abysmal.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly: Performance
&lt;/h2&gt;

&lt;p&gt;WebAssembly's main goal is performance. This means that WebAssembly modules run fast, but also that loading modules and starting them is much faster than containers. This has proven to be very useful already, for instance in use cases such as serverless computing, where hyperscalers heavily rely on WebAssembly as a runtime to reduce cold start times, and reduce the delay in function calls.&lt;/p&gt;

&lt;p&gt;Considering the idea to avoid having long running servers providing REST APIs and move to raw function calls on short running modules, having extremely short start times is imperative.&lt;/p&gt;

&lt;p&gt;Alright, so we can see that WebAssembly can be a great choice for the foundation runtime of a platform. So where are platforms leveraging this? Well, actually, quite some "platforms" leverage this idea already. For instance, &lt;a href="https://www.spinkube.dev/" rel="noopener noreferrer"&gt;SpinKube&lt;/a&gt; does exactly this, enabling to run WebAssembly functions on Kubernetes. However, you still interact with these functions via a REST call. Another example is &lt;a href="https://www.kubewarden.io/" rel="noopener noreferrer"&gt;Kubewarden&lt;/a&gt;, leveraging WebAssembly modules to evaluate policies. While some might argue that this is not a platform, Kubewarden provides a runtime for arbitrary programs, including their scheduling and deployment. Sounds like a platform to me.&lt;/p&gt;

&lt;p&gt;Finally: wasmCloud! wasmCloud is probably what people would consider the closest to a full blown platform to run WebAssembly modules. In other words, what Kubernetes is to containers, wasmCloud is to WebAssembly components. It provides a way to deploy, schedule, link, and lifecycle WebAssembly components on a distributed platform.&lt;/p&gt;

&lt;h1&gt;
  
  
  wasmCloud Architecture
&lt;/h1&gt;

&lt;p&gt;Let us look at the wasmCloud architecture a little.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This section will contain quite a few comparisons to Kubernetes concepts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Generally, the wasmCloud architecture can be seen as quite similar to the Kubernetes architecture, with the difference being that wasmCloud does not provide as much flexibility in swapping out building blocks as Kubernetes does. This makes sense as it is a more nascent technology and is currently more opinionated.&lt;/p&gt;

&lt;p&gt;As a reference, here is the diagram wasmCloud uses to provide an overview of the platform:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn21migl5ljd0uj9zvg5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn21migl5ljd0uj9zvg5z.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As one can see, the architecture is essentially a set of hosts connected via a so called "lattice". Thus, the architecture distributes the runtime over a set of compute instances in order to achieve resilience against hardware/compute failures. The principle is identical to the one from Kubernetes, providing a cluster in order to be able to quickly shift payloads on the platform to different nodes in case of node failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosts
&lt;/h2&gt;

&lt;p&gt;wasmCloud hosts are the foundation of the compute platform that is provided. They are the equivalent of Kubernetes nodes and provide a WebAssembly runtime for components to run on. Just as with Kubernetes nodes, application developers will rarely need to worry about the hosts other than for deployment affinities and the like.&lt;/p&gt;

&lt;p&gt;In practice, hosts can be anything from a virtual machine, an IoT device, or even a pod running on Kubernetes. In fact, hosting wasmCloud on Kubernetes is a relatively straight forward way to get started with the technology, providing wasmCloud as an application runtime, while providing services via Kubernetes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lattice
&lt;/h2&gt;

&lt;p&gt;The wasmCloud lattice is its networking layer. This can seem a bit strange when considering that this a &lt;a href="https://nats.io/" rel="noopener noreferrer"&gt;NATS&lt;/a&gt; instance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For those unfamiliar with NATS: it is an event streaming component similar to Kafka, but provides additional features such as a key values store, an object store, and publish-subscribe capabilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Having a NATS instance as the "networking layer" confused me quite a lot at first. However, one has to remember that thanks to the component model, we no longer require HTTP/TCP network calls for our components to interact with one another. Thus we don't necessarily need an IP to address a component we want to reach. Of course NATS itself will require a physical network to run on in order to distribute events to its different instances, but wasmCloud then only needs to use NATS.&lt;/p&gt;

&lt;p&gt;Essentially, every component exposing a function becomes a subscriber to a queue for this function on NATS. Other components can then call this function via wRPC (gRPC for WebAssembly) by publishing a call to some subject. This is quite different from Kubernetes networking, where calls need to know the location of the callee in the network. Using a subject-based addressing model simplifies deployment and improves scaling and resilience.&lt;/p&gt;

&lt;p&gt;As a user of wasmCloud, you do not need to worry about this though. How function calls are preformed under the hood is abstracted away from the user.&lt;/p&gt;

&lt;p&gt;This distributed networking aspect is one of the superpowers of wasmCloud, as one does not need to worry about how to address a component on the platform. However, it can also introduce strange behaviour in some cases. For instance, on Kubernetes, it's common sense that a HTTP call to a different pod running on the cluster can fail. On wasmCloud however, if the interface we are calling from a different component returns some type, we use the component like a raw function call in our components code. What if that call fails, not because of the called component but due to a networking issue? In the current implementation of wasmCloud this will lead to a panic in the caller. As this is typically not the desired outcome, efforts are underway to design an adapted way how the interfaces need to be designed to handle failures in the transport layer. On top of that, function calls might change such that might avoid using NATS as a transport layer if the component being called in on the same host and the caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capabilities
&lt;/h2&gt;

&lt;p&gt;This is where Kubernetes and wasmCloud start differing in their philosophy. Thanks to the standardized way interfaces can be declared in the component model, one can describe an abstract interface which provides some functionality, without providing an implementation. This is what capabilities are. They are abstract interfaces that describe some useful functionality, such as reading and writing to a key value store, or retrieving some sensitive information from a secured environment. These capabilities are published on wasmCloud for applications to use.&lt;/p&gt;

&lt;p&gt;An application developer can then write a component that makes use of that interface if he/she needs that functionality. He/she does not need to worry about how this capability is implemented. He relies on the "contract" provided by the capability.&lt;/p&gt;

&lt;p&gt;In my opinion, while this is quite challenging to grasp initially, this is what makes wasmCloud so promising. Having worked on many platforms in the past, the main challenge is always how additional services can be provided on top of raw platforms such as Kubernetes in a way that makes then highly standardized while easily consumable. In the current state of platform engineering, this quickly becomes a question of good product management. Unfortunately, doing this correctly is surprisingly difficult. Capabilities provide a technical solution to this, with the only limitation being complete incompatibility with existing software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Providers
&lt;/h2&gt;

&lt;p&gt;A provider is a specific implementation of a capability. For instance, taking the example of the capability enabling the reading and writing to a key value store, a provider might implement this by having a &lt;a href="https://valkey.io/" rel="noopener noreferrer"&gt;ValKey&lt;/a&gt; instance backing the capability. Another provider might implement the very same capability using NATS, Redis, or even an in-memory key-value store.&lt;/p&gt;

&lt;p&gt;Abstracting the provider away from the consumer via a capability enables the platform to swap providers based on needs. Of course performing such a swap might be quite complex, for instance involving a data migration from NATS to ValKey. However, the beauty is that the applications do not require any changes as would be the case in traditional platforms.&lt;/p&gt;

&lt;p&gt;It should be noted that the provider might run completely outside of wasmCloud itself. However, wasmCloud also provides internal providers that are backed into the hosts themselves, providing functionality such as logging or randomness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components
&lt;/h2&gt;

&lt;p&gt;Components refer to the WebAssembly payload that contain your business logic. In the traditional sense, this is your application. However, in wasmCloud lingo, an application is a set of interlinked components including all information about what capabilities they require.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applications
&lt;/h2&gt;

&lt;p&gt;Applications are an abstraction enabling to declaratively define a combination of components, capabilities, and providers together into a deployable unit. Applications are based on the &lt;a href="https://oam.dev/" rel="noopener noreferrer"&gt;open application model (OAM)&lt;/a&gt; and should thus look quite familiar to people working with Kubernetes. In terms of definition, they are similar to a Kubernetes Deployment, describing not only the deployment unit (component or pod in the Kubernetes context), but also its replication, affinities, links to capabilities, etc.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It should be noted that in wasmCloud v2, applications are re-worked to be much more closely modelled after Kubernetes Deployments and ReplicaSets. Version 2 drops the idea of Applications alltogether and uses &lt;code&gt;Workload&lt;/code&gt;, &lt;code&gt;WorkloadReplicaSets&lt;/code&gt;, and &lt;code&gt;WorkloadDeployments&lt;/code&gt; objects. These are also no longer linked to the OAM. In all likelihood we will write another blog post showcasing the capabilities of composition provided by version 2 in the future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  wadm
&lt;/h2&gt;

&lt;p&gt;The wasmCloud Application Deployment Manager (wadm) manages Applications. It can be seen as the deployment controller from Kubernetes for wasmCloud Applications. It essentially orchestrates the deployment of components, capabilities, their links, etc. on the platform. This construct will also be dropped with wasmCloud version 2.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verdict
&lt;/h1&gt;

&lt;p&gt;With a decent understanding of the architecture we can now get an idea of the uses of wasmCloud in the real world. While I have not yet run anything productive on wasmCloud, I have played with the platform a lot over the past few months, and have come to really appreciate some of its innovative ideas.&lt;/p&gt;

&lt;p&gt;Thus, to summarise my experience: wasmCloud is a relatively new platform and provides interesting new approaches to how inter-component communication can be modeled. On top of that, it does it while building on open standards such as WebAssembly and the component model, such that the business logic of your application remains portable. While these new concepts are very promising, wasmCloud still suffers from a couple drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For people unfamiliar with WebAssembly, it has a quite steep learning curve. This is highly accentuated for people unfamiliar with existing platforms such as Kubernetes.&lt;/li&gt;
&lt;li&gt;The set of supported providers and capabilities is extremely small to date. This will of course grow as adoption increases, but currently early adopters will have to write their own providers most of the time and will not be able to rely on third-party components.&lt;/li&gt;
&lt;li&gt;As wasmCloud shifts more responsibility to the platform level, it will require a strong platform team to operate this with low developer friction. This can be an issue as finding highly skilled platform engineers is quite difficult at the moment. However, the team behind wasmCloud is focused on making application delivery as frictionless as possible.&lt;/li&gt;
&lt;li&gt;Finally, I am not sure I currently understand the security model wasmCloud uses to authenticate and authorize calls between components. While I am not sure this is a drawback, it does not yet feel as intuitive as Kubernetes simple yet relatively powerful RBAC. I will have to dive deeper into this to have a final opinion on it though (another blog post might follow on this).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devex</category>
      <category>webassembly</category>
      <category>cloudnative</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Dissecting Kubewarden: Internals, How It's Built, and Its Place Among Policy Engines</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Mon, 07 Jul 2025 05:31:30 +0000</pubDate>
      <link>https://dev.to/ipt/dissecting-kubewarden-internals-how-its-built-and-its-place-among-policy-engines-57gf</link>
      <guid>https://dev.to/ipt/dissecting-kubewarden-internals-how-its-built-and-its-place-among-policy-engines-57gf</guid>
      <description>&lt;p&gt;Kubernetes offers amazing capabilities to improve compute density compared to older runtimes such as virtual machines. However, in oder to leverage the capabilities of the platform, these tend to host applications from various tenants. This introduces a strong need for properly crafted controls and well-defined compliance to ensure the tenants use the platform correctly and do not affect one another. The RBAC capabilities provided out of the box by Kubernetes are quickly insufficient to address this need. This is where policy engines such as &lt;a href="https://www.kubewarden.io/" rel="noopener noreferrer"&gt;Kubewarden&lt;/a&gt; come into play. In this post we will look at how Kubewarden can be leveraged to ensure correct usage of a platform, how it compares to other policy engines, and how to best adopt it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Policy Engines
&lt;/h1&gt;

&lt;p&gt;Kubernetes provides role-based access control (RBAC) out of the box to control what actions can be performed against the Kubernetes API. Generally, RBAC works by assigning sets of roles to users or groups of users. Capabilities are attached to these roles, and users having a role obtain these capabilities. This simple mechanism is very powerful, mostly because it is quite flexible while allowing a simple overview of a user's capabilities. However, in the case of Kubernetes, the definition of capabilities is very restricted. Roles only allow or deny access to Kuberenetes API endpoints, but to not allow control based on payload content. This means that these capabilities are mostly restricted to CRUD operations on Kubernetes primitives (e.g. &lt;code&gt;Deployments&lt;/code&gt;, &lt;code&gt;Ingresses&lt;/code&gt;, or custom resources). Unfortunately, this is often not enough.&lt;/p&gt;

&lt;p&gt;For instance, it is quite common to allow users to perform actions on some primitives under specific conditions. An example would be that creating &lt;code&gt;Deployments&lt;/code&gt;s is only allowed as long as its name follows some convention and the pods its creates are not privileged and set proper resource requests/limits. The naming convention cannot be enforced by standard RBAC controls as these have no possibility to represent more complex logic. Controlling the configuration of the pods created by a &lt;code&gt;Deployment&lt;/code&gt; is a validation of the payload pushed to the API, and is thus not supported either.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Security contexts and resources on pods can be controlled via methods such as Security Context Constraints or Pod Security Policies and ResourceQuotas. However, these do not reject the creation of the deployment, but will only block the creation of the pods themselves. It is therefore possible to apply a Deployment that is known to not allow the creation of pods. In my personal opinion this is not ideal, as it does not fail early.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These scenarios is where policy engines come into play. They utilise Kubernetes' Dynamic Access Control mechanisms to enable cluster administrators to manage permissions using more complex logic. The exact capabilities of policy engines can vary greatly as these are essentially arbitrary software that validates or mutates Kubernetes requests. However, the majority of major policy engines work similarly. They tend to implement the operator pattern, enabling the configuration of policies using Kubernetes custom resources. In this blog post we will have a look at Kubewarden in more detail, and how it compares to other engines.&lt;/p&gt;

&lt;h1&gt;
  
  
  Kubewarden Architecture
&lt;/h1&gt;

&lt;p&gt;Kubewarden leverages &lt;a href="https://webassembly.org/" rel="noopener noreferrer"&gt;WebAssembly (WASM)&lt;/a&gt; to enable extremely flexible policy evaluation. Essentially, Kubewarden can be seen as a WASM module orchestrator where policies are deployed as serverless functions that get called when necessary. The result of these WASM functions then determines whether an API request against Kubernetes is allowed, denied, or altered (mutated).&lt;/p&gt;

&lt;p&gt;This similee can also help explain Kubewarden's architecture. Essentially, the Kubewarden controller (operator) manages policy servers and admission policies. Policy servers can be seen as hosts for the serverless execution of functions, whereas admissions policies are the functions themselves. Therefore, in order to perform policy validation, one needs at least one policy server running to host the policies one wants to enforce. The controller then takes care of configuring the runtime (policy server) to properly run the adequate policy executable with the appropriate inputs when a policy needs to be evaluated. The diagram below illustrates this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7x0xmdqg4ql6gmp3te9h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7x0xmdqg4ql6gmp3te9h.png" alt="A policy server's internal architecture" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As policies are WASM modules, they can themselves support configuration. This makes policy reuse a major feature of Kubewarden. Complex logic can be contained in the WASM module while exposing some tuning as configuration, allowing a policy to perform a relatively generic task. To understand this better, let us have a look at such a policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policies.kubewarden.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterAdmissionPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cel-policy-replica-example"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry://ghcr.io/kubewarden/policies/cel-policy:v1.0.0&lt;/span&gt;
  &lt;span class="na"&gt;backgroundAudit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;protect&lt;/span&gt;
  &lt;span class="na"&gt;mutating&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;policyServer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apps"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;apiVersions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployments"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replicas"&lt;/span&gt;
        &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object.spec.replicas"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;maxreplicas&lt;/span&gt;
        &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int(5)&lt;/span&gt;
    &lt;span class="na"&gt;validations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variables.replicas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;variables.maxreplicas"&lt;/span&gt;
        &lt;span class="na"&gt;messageExpression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;replicas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;less&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;than&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;equal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;string(variables.maxreplicas)"&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, we are using a WASM module which evaluates a &lt;a href="https://cel.dev/" rel="noopener noreferrer"&gt;Common Expression Language (CEL)&lt;/a&gt; expression to define our policy. Evaluating a CEL expression is not something we want to implement every time ourselves. Thankfully, Kubewarden provides this as a WASM module on their &lt;a href="https://artifacthub.io/packages/search?kind=13&amp;amp;sort=relevance&amp;amp;page=1" rel="noopener noreferrer"&gt;ArtefactHub&lt;/a&gt;. Thus we do not need to implement anything and can reuse that module. It is referenced on the &lt;code&gt;module&lt;/code&gt; line above. Of course we also need to actually define the CEL expression that should be the heart of the policy rule. This is done within the &lt;code&gt;settings&lt;/code&gt; block. Note how we can use object internals (such as replicas defined in a &lt;code&gt;Deployment&lt;/code&gt;) in the validation expression. Finally, we need to define on what objects this policy should be evaluated. In order to do this, we provide &lt;code&gt;rules&lt;/code&gt; that tell Kubewarden on what Kubernetes API endpoints to trigger the policy, and additionally provide information about which namespaces should be affected by the policy with a &lt;code&gt;namespaceSelector&lt;/code&gt;. The remaining options configure the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;backgroundAudit&lt;/code&gt;: informs Kubewarden to report on this policy for objects that are already deployed. In this case, we validate the replicas on created or updated &lt;code&gt;Deployment&lt;/code&gt; objects. However, there might already be &lt;code&gt;Deployments&lt;/code&gt; on the cluster that violate the policy before we start enforcing it. This option will tell Kubewarden to provide reports on such violations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode&lt;/code&gt;: Kubewarden supports enforcing policies (in &lt;code&gt;protect&lt;/code&gt; mode), or monitoring the cluster (in &lt;code&gt;monitor&lt;/code&gt; mode). Using the &lt;code&gt;monitor&lt;/code&gt; mode can be interesting when investigating how people use the Kubernetes cluster or providing them with warnings before enforcing policies.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mutating&lt;/code&gt;: policies can also mutate (change) requests. In this case we are only performing validation to potentially reject requests. Thus we set &lt;code&gt;mutating&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;policyServer&lt;/code&gt;: as explained above, Kubewarden can manage many policy servers. This simply informs
the controller on which policy server this specific policy should be deployed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As one can see based on the sample policy above, while Kubewarden technically uses programs as policies, it is usually not necessary to write any code to use Kubewarden. This is thanks to its strong focus on module configuration and re-usability. The above CEL module alone already enables the configuration of a very wide range of policies. On top of that, other modules shared on ArtefactHub provide more specific validations or mutations that might incorporate more complex logic. If this is not enough, policy groups (a feature we will not cover in this post) can be utilised to combine other policies and express more complex logic as well. Finally, if one has very specific needs that cannot be addressed by any of the publicly shared modules, one can still fall back to writing code and building ones own module with fully arbitrary logic. How such policies can be written, in actual code, might follow in a separate blog post.&lt;/p&gt;

&lt;p&gt;The above architecture of Kubewarden is what makes it stand apart from most other policy engines. Generally policy engines contain the logic fully in the controller, only exposing configuration via the custom resource. Since Kubewarden can essentially execute arbitrary WASM bytecode, it is not bound by the expressiveness of the custom resource declaration.&lt;/p&gt;

&lt;p&gt;All this considered, is Kubewarden the best choice for a policy engine and should be used in all scenarios?&lt;/p&gt;

&lt;h1&gt;
  
  
  Comparison
&lt;/h1&gt;

&lt;p&gt;There are many other policy engines out there, such as &lt;a href="https://kyverno.io/" rel="noopener noreferrer"&gt;Kyverno&lt;/a&gt;, &lt;a href="https://open-policy-agent.github.io/gatekeeper/website/" rel="noopener noreferrer"&gt;Gatekeeper&lt;/a&gt;, or &lt;a href="https://www.fairwinds.com/polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;. So why would you choose Kubewarden over any other?&lt;/p&gt;

&lt;p&gt;As explained above, Kubewarden provides unprecedented flexibility, thanks to the way it evaluates its policies. This has the massive advantage that you will never reach a point that you have a policy that you would like to enforce but are restricted by the policy engine itself. However, it also has some drawbacks. The primary one being complexity. Writing WASM modules is not for the fainthearted, as WebAssembly is not yet incredibly mature, and most developers will not be familiar with it. The complexity issue can however be sidestepped as the vast majority of policies can be expressed using off-the-shelf WASM modules provided by Kubewarden.&lt;/p&gt;

&lt;p&gt;Another aspect that often needs to be considered in enterprise contexts, is support. Kubewarden is an open source project that is loosely backed by SUSE (as it was originally developer for its Rancher offering). Thus enterprise support is only available via a SUSE Rancher Prime. Other tools such as Kyverno are not only more mature, but offer more flexible enterprise support (via Isovalent).&lt;/p&gt;

&lt;p&gt;Finally, another aspect to consider is the featureset of a policy engines. Not all policy engines support mutating requests, and are thus much more restricted in their use. However, in this category Kubewarden offers all the features typically desired from policy engines. Some engines such as Kyverno support more features such as synchronizing &lt;code&gt;Secret&lt;/code&gt; objects. While this can be useful, it is, in my humble opinion, not a feature for a policy engine.&lt;/p&gt;

&lt;p&gt;Of course, there are also personal preference aspects to consider. As an example, Kubewarden and Kyverno handle policy exceptions very differently. Kubewarden has matchers that can be defined as part of the policy itself, which allow to exclude some resources from being validated. Kyverno on the other hand uses a separate CRD called &lt;a href="https://kyverno.io/docs/exceptions/" rel="noopener noreferrer"&gt;&lt;code&gt;PolicyException&lt;/code&gt;&lt;/a&gt;. Both have advantages and disadvantages.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verdict
&lt;/h1&gt;

&lt;p&gt;Kubewarden is a very interesting piece of software. Its internal architecture enables it to be incredibly flexible, at the cost of complexity. However, due to a smart concept of WebAssembly module re-use, that complexity is mostly under the hood, unless one wants or needs to dive deep. In my opinion, Kubewarden can be an absolutely great consideration when ones operates very large Kubernetes clusters what might have quite exceptional requirements. However, even in these cases, I would recommend starting very slow, and slowly building up to the complexity Kubewarden can hold in store.&lt;/p&gt;

&lt;p&gt;If you do not operate a large Kubernetes fleet, or expect to have rather standard requirements in terms of how you want to restrict access to you cluster(s), you might be better off with more mature and simpler tools like Kyverno. Getting support for these tools is likely to also be much simpler.&lt;/p&gt;

&lt;p&gt;A large part of the complexity of Kubewarden also comes from all that is required to even run this in an enterprise context. Unless you allow pulling WASM modules directly from the internet, you will also need a registry to host OCI packaged modules. On top of that, should you decide to write your own modules, you will need a process to do this, and build knowhow in that area. These are some of the aspects I hope to cover in a follow up post.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>kubernetes</category>
      <category>security</category>
    </item>
    <item>
      <title>The Tortoise and the Hare: do AI Agents Really Help for Software Development?</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Wed, 23 Apr 2025 05:37:56 +0000</pubDate>
      <link>https://dev.to/ipt/the-tortoise-and-the-hare-do-ai-agents-really-help-for-software-development-3fo4</link>
      <guid>https://dev.to/ipt/the-tortoise-and-the-hare-do-ai-agents-really-help-for-software-development-3fo4</guid>
      <description>&lt;p&gt;Making my development workflow as fast as possible is a big passion of mine. From customizing my development setup to get the last inkling of efficiency out of it, to thinking how to manage notes and knowledge resources to access them as quickly as possible. With the sudden ubiquity of AI in development tools, I came to wonder how AI could help me write code faster. Being quite the skeptic when it comes to AI actually generating code for me (using tools such as Cursor or GitHub Copilot), I came to investigate AI agents which specialise in code reviews. In this blog post I will share my experience using such an agent on a real world case. I will explore where such agents shine and where they are severely lacking.&lt;/p&gt;

&lt;h1&gt;
  
  
  I am an AI Skeptic
&lt;/h1&gt;

&lt;p&gt;Generally I am not fond of using AI to develop software. My background is mostly in systems software, where correctness of the software can be critical. This means that using tooling that is non-deterministic and might not produce adequate results makes me uneasy. Furthermore, even if AI were to produce amazing results, a developer relying on it could quickly lose understanding of the code. This results in skill atrophy and large risks if the AI reaches the limits of its capabilities. In other words, I am not keen on having any AI generating code for me on a large scale for anything more than a proof of concept or low risk project.&lt;/p&gt;

&lt;p&gt;Nonetheless, one would be foolish to ignore AI's capabilities when it comes to developer tooling.&lt;/p&gt;

&lt;h1&gt;
  
  
  AI Support Agents
&lt;/h1&gt;

&lt;p&gt;Thus starts my journey investigating AI agents that can support me in the software development lifecycle, but whose main use is &lt;em&gt;not&lt;/em&gt; to generate code. Many such agents exist, mostly focusing on reviewing code. I am quite the fan of such a use case, as the AI essentially plays the role of another developer I might work with. It reviews my code, provides feedback, suggestions, and potentially even improvements. It however does this immediately after I have opened a pull request, rather than having to wait for days or weeks on a human review.&lt;/p&gt;

&lt;p&gt;How is this different from using an AI that generates code you might ask? The main difference lies in the fact that I still have to think on how to solve the problem I am working on, and provide a base solution. This forces me to understand the issue at hand. Thus, I am much better prepared to accept or reject any suggestions from an AI than if the AI just generated a first solution for me. Moreover, people (myself included) tend to be slightly defensive about the code they write. Thus I will, in all likelihood, only accept AI generated code improvements if it offers a real improvement, rather than blindly incorporating them into the codebase.&lt;/p&gt;

&lt;p&gt;All in all, it is extremely unlikely that I will lose understanding of the codebase or have my problem solving skills atrophy, but I can iterate on reviews much faster.&lt;/p&gt;

&lt;h1&gt;
  
  
  CodeRabbitAI
&lt;/h1&gt;

&lt;p&gt;In order to gain first experiences with such an AI agent, I chose to try out &lt;a href="https://www.coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbitAI&lt;/a&gt;. This was not a thoroughly researched decision. The main reason I chose CodeRabbitAI is that I could try it out for free during 14 days and that it integrates well with GitHub. I am aware that performance between AI models varies greatly. However, CodeRabbitAI uses Claude under the hood, a model typically known to perform surprising well on programming tasks. I thus expect it to not perform significantly worse than any other state of the art model out there.&lt;/p&gt;

&lt;h1&gt;
  
  
  Starting Small
&lt;/h1&gt;

&lt;p&gt;In my opinion, such agents need to be tested on real world examples. One can see demos using AI to generate a dummy web app all over the place. However, common software projects are significantly larger, contain more complex logic, and are less standardized than these demos. Unfortunately, most software I work on professionally is not publicly available, so I cannot use CodeRabbitAI on these. I therefore picked two (still very small) personal projects of mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://github.com/f4z3r/gruvbox-material.nvim" rel="noopener noreferrer"&gt;NeoVim plugin&lt;/a&gt; providing a colour scheme.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://github.com/f4z3r/sofa" rel="noopener noreferrer"&gt;command execution engine&lt;/a&gt; to run templated commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both projects are extremely small, with under two thousand lines of code. Both projects are written in Lua, a quite uncommon language. I wanted to see how the AI fares against something it is unlikely to have seen too much during its training.&lt;/p&gt;

&lt;p&gt;With that in mind, I wrote a &lt;a href="https://github.com/f4z3r/gruvbox-material.nvim/pull/40" rel="noopener noreferrer"&gt;first pull request&lt;/a&gt; implementing a fix in highlight groups for pop-up menus in NeoVim. I enabled CodeRabbitAI to summarize the PR for me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53fewpgxrfwwxvqln031.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53fewpgxrfwwxvqln031.png" alt="Summary provided by CodeRabbitAI on my first PR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The summary looks good, even though it somehow marks some fixes as features. This is especially intriguing as I use &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" rel="noopener noreferrer"&gt;conventional commits&lt;/a&gt; and explicitly marked these changes as fixes. Additionally, CodeRabbitAI offers a "walkthrough" of the changes made in the PR. In the case of such a simple PR, I found the walkthrough to be mostly confusing. In the case of larger PRs I can however see how this may be appealing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1i5blq1cnvnuqus2w729.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1i5blq1cnvnuqus2w729.png" alt="A walkthrough of the changes in the first PR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In reality, I initially opened the PR with only the fixes the pop-up menus. I then pushed commits introducing the support for additional plugins later on. I would have expected CodeRabbitAI to complain that the new commits introduce changes unrelated to the PR, which is not seen as best practice. It did nothing of the sort.&lt;/p&gt;

&lt;p&gt;While the summary, walkthrough, and disregard for best practices were unsatisfying, one unexpected benefit emerged: the integration of linting feedback directly within the pull request comments. It provided nitpicks from linting tools (in this case &lt;a href="https://github.com/DavidAnson/markdownlint" rel="noopener noreferrer"&gt;&lt;code&gt;markdownlint&lt;/code&gt;&lt;/a&gt;. On one side, it is very disappointing to see that the AI agent did nothing more than lint the code and generate a nice comment out of the output. On the other hand it is quite nice that it introduces "quality gates" such as linting without me having to write a pipeline for it. Moreover, producing easily digestible output from a linter is nothing to be underestimated. The quality of life of having this directly as a comment rather than having to go through pipeline logs to read the raw linter output is quite nice. Is it worth two dozen USD per month? No, definitely not!&lt;/p&gt;

&lt;p&gt;On the upside, it did update the summary of the PR to reflect the other changes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jmypo9fwel98n9x97qp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jmypo9fwel98n9x97qp.png" alt="Updated summary provided by CodeRabbitAI on my first PR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first PR was extremely trivial. It did not introduce any code containing logic. Other than not pointing out that it should probably have been two separate PRs, CodeRabbitAI fared as I would have expected another developer to have reviewed the PR. With two small differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CodeRabbitAI review was close to &lt;strong&gt;immediate&lt;/strong&gt; (took around 30-60 seconds to run). This is amazing to iterate quickly.&lt;/li&gt;
&lt;li&gt;Where I would have expected a human reviewer to point our the nitpick or simply approve, CodeRabbitAI is extremely &lt;strong&gt;verbose&lt;/strong&gt; with explanations, walkthroughs, and so on. This in turn wastes time for the author, as he/she would need to read through this. The verbosity could be nicer on larger PRs, but for small concise PRs this is massive overkill and borderline annoying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To further evaluate CodeRabbitAI's capabilities, I decided to test it on a pull request with more substantial changes...&lt;/p&gt;

&lt;h1&gt;
  
  
  A More Complex PR
&lt;/h1&gt;

&lt;p&gt;Armed with dampened expectations from my first PR, I opened &lt;a href="https://github.com/f4z3r/sofa/pull/3" rel="noopener noreferrer"&gt;another PR&lt;/a&gt; in the command execution repository implementing a feature affecting multiple files. These changes also update existing logic.&lt;/p&gt;

&lt;p&gt;In this second PR, CodeRabbitAI went above and beyond, and generated a walkthrough containing two sequence diagrams showcasing the control flow of the code that was modified! I was actually quite impressed by this. While probably not necessary for the author of a PR, this is great even only for documentation purposes. New team members with less experience may benefit from such visual aids to understand complex logic within the code. Unfortunately the diagrams didn't highlight the &lt;em&gt;specific modifications&lt;/em&gt; introduced by the pull request.&lt;/p&gt;

&lt;p&gt;However, the supporting text suddenly becomes more relevant when considering such PRs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx0eypb4e1v5syzg0gre.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx0eypb4e1v5syzg0gre.png" alt="One of the sequence diagrams generated by CodeRabbitAI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On top of that, CodeRabbitAI actually posted interesting comments. It found the odd nitpick here and there, but also found more meaningful potential issues. For instance, I modified a test configuration to use a different shell. CodeRabbitAI identified that this shell is not listed as a dependency anywhere in the repository, and that it would thus not work off-the-shelf. In this case this was only a test file used to parse the configuration and the configured shell did not affect anything, but this is a great finding generally.&lt;/p&gt;

&lt;p&gt;I also started conversing with CodeRabbitAI about some changes. Requesting it to give me a suggestion on some configurations. It managed just fine, but did not actually provide these as code suggestions that can be applied, but rather as code blocks in comments, which was a bit disappointing.&lt;/p&gt;

&lt;p&gt;Additionally, I decided to try to use CodeRabbitAI's commands feature. This enables ChatOps to control actions taken by CodeRabbitAI. I generated the PR title using one such command. The title turned out generic and not very informative. In CodeRabbitAI's defense, I am quite unsure how I would have named that PR.&lt;/p&gt;

&lt;p&gt;I then tried to get it to write docstrings for new functions that were introduced in the PR. It massively misunderstood the request, and created &lt;a href="https://github.com/f4z3r/sofa/pull/4" rel="noopener noreferrer"&gt;a PR adding docstrings to all functions&lt;/a&gt; in the affected files, even ones that already had docstrings... This goes to show that in some cases, it cannot even do what the most junior of all engineers would be capable of doing thanks to a even so tiny dose of common sense. Moreover, it started adding commits with emojis in the title. This goes to show that these AIs are probably not trained much on professional projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kcqsm6xjkflyk5xpunq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kcqsm6xjkflyk5xpunq.png" alt="CodeRabbitAI not only breaking conventional commits but introducing emojis..."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that first disaster, with significantly less ambition, I requested it creates a PR to change a small typo. CodeRabbitAI informed me that it created a branch with the changes included, but that it was not capable of creating pull requests. This shocked me, considering it had created its first disaster PR no 10 minutes before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwbeqdfikxqu0c5xndu3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwbeqdfikxqu0c5xndu3.png" alt="Fighting with CodeRabbitAI to fix my typo."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After another nudge, CodeRabbitAI however did &lt;a href="https://github.com/f4z3r/sofa/pull/5" rel="noopener noreferrer"&gt;create a PR&lt;/a&gt;. It targeted &lt;code&gt;main&lt;/code&gt; instead of the branch I was initially using. I guess this is my own fault though for not being specific enough.&lt;/p&gt;

&lt;p&gt;Finally, I also tried to get it to update the wording on a commit it did to use conventional commits. Unfortunately it seems that it only has access to the GitHub API and cannot execute any local &lt;code&gt;git&lt;/code&gt; commands. It is therefore not able to perform some relatively common operations in the SDLC that are not part of the GitHub API. However, I am guessing this is subject to change relatively soon with the emergence of technologies such as the &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;model context protocol&lt;/a&gt;, which would enable it to control external tools such as &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All in all, I would say CodeRabbitAI did as I would have expected after the first PR. It corrected nitpicks and allowed me to perform some simple actions. Did it deliver a review of the same quality like a senior engineer familiar with the project would have? No. In fact, in order to test this I intentionally implemented a feature that was already present in the repository, while making a couple design decisions that go against most of what the rest of the repository does. CodeRabbitAI neither detected that the logic I was introducing was already present in the codebase, nor did it complain about the sub-optimal design decisions. This goes to show that such agents are still not capable replacing humans with nuanced understanding of the project's history and architectural principles, potentially leading to the introduction of redundant or suboptimal solutions.&lt;/p&gt;

&lt;h1&gt;
  
  
  Dashboards!
&lt;/h1&gt;

&lt;p&gt;Another feature of AI agents next to the reviews is the analytics capabilities that come with them. In my personal opinion, analytics are important to measure the impact the introduction of such tooling has on the software delivery. CodeRabbitAI provides a couple nice dashboards on how much it is being used, and what kind of errors it helped uncover.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F855vwq57tx742mawk65e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F855vwq57tx742mawk65e.png" alt="Activity dashboard showing engagment with CodeRabbitAI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx3jz0kgur7y6iksl0yq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx3jz0kgur7y6iksl0yq.png" alt="Dashboard showing overall adoption of CodeRabbitAI on the projects"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lay5x98wr9gyulpf91g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lay5x98wr9gyulpf91g.png" alt="Findings dashboard showing errors and suggestions by type"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did not try out CodeRabbitAI for long enough to have any meaningful metrics, but I am confident that the capabilities provided are enough to get a decent understanding of the quality of adoption.&lt;/p&gt;

&lt;p&gt;Moreover, CodeRabbitAI supports reporting. This allows to generate reports based on natural language prompts that could be useful for product owners to get insights of changes made to the software over the course of a sprint.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verdict
&lt;/h1&gt;

&lt;p&gt;While this whole article might seem like a slight rant against such tools, I would in fact wish I could use such tools at work. Not as a replacement for human reviewers, but as an addition to them. For instance, the quite verbose walkthroughs CodeRabbitAI provides can be a very helpful entrypoint to a human reviewer on larger PRs. Moreover, while the quality of the review is insufficient for projects where quality matters, having near instant feedback is amazing.&lt;/p&gt;

&lt;p&gt;Finally, as mentioned above, I believe one major selling point of such agents is in the way we humans interact with them. Even if the agent might do little more than execute linters or similar in the background, having the output of these tools in natural language directly as comments in the PRs is not to be underestimated. This is especially true in the age where more and more responsibility is being shifted to developers. With DevSecOps, developers have to understand and act upon the output of all kinds of tools. Presenting this output in a more understandable format, potentially enriched with explanations, can have a significant impact.&lt;/p&gt;

&lt;p&gt;Therefore, as a final word, I would actually encourage people to explore such agents to augment their workflow &lt;strong&gt;safely&lt;/strong&gt;, albeit with caution and a clear understanding of their &lt;strong&gt;limitations&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/f4z3r" rel="noopener noreferrer"&gt;
        f4z3r
      &lt;/a&gt; / &lt;a href="https://github.com/f4z3r/gruvbox-material.nvim" rel="noopener noreferrer"&gt;
        gruvbox-material.nvim
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Material Gruvbox colorscheme for Neovim written in Lua
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim/archive/master.zip" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flogo.png" alt="Gruvbox Material" width="25%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Gruvbox Material&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/01033da302ce50a77b87423bdc412176b379a892529b9971db88153719c23fd4/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732d616e6f6e2f66347a33722f67727576626f782d6d6174657269616c2e6e76696d"&gt;&lt;img src="https://camo.githubusercontent.com/01033da302ce50a77b87423bdc412176b379a892529b9971db88153719c23fd4/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732d616e6f6e2f66347a33722f67727576626f782d6d6174657269616c2e6e76696d" alt="GitHub contributors"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/a19974e4ca72dabfea4e7782cf9b91efc495c71ad9b4d00890f76e2b28cd6cfe/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f67727576626f782d6d6174657269616c2e6e76696d"&gt;&lt;img src="https://camo.githubusercontent.com/a19974e4ca72dabfea4e7782cf9b91efc495c71ad9b4d00890f76e2b28cd6cfe/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f67727576626f782d6d6174657269616c2e6e76696d" alt="GitHub last commit"&gt;&lt;/a&gt;
&lt;a href="https://repology.org/project/vim%3Agruvbox-material.nvim/versions" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e3fe60bcdb4a06eff8f6d203c67e4bd018cc7834c2e73bc75f6f1326c5434e05/68747470733a2f2f7265706f6c6f67792e6f72672f62616467652f76657273696f6e2d666f722d7265706f2f6e69785f737461626c655f32355f30352f76696d25334167727576626f782d6d6174657269616c2e6e76696d2e737667" alt="nixpkgs stable 25.05 package"&gt;&lt;/a&gt;
&lt;a href="https://repology.org/project/vim%3Agruvbox-material.nvim/versions" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6ba0ae57d5825b36c63ac82e6c84b146e3b921f3004db6bdc7e14882b1a106ca/68747470733a2f2f7265706f6c6f67792e6f72672f62616467652f76657273696f6e2d666f722d7265706f2f6e69785f756e737461626c652f76696d25334167727576626f782d6d6174657269616c2e6e76696d2e737667" alt="nixpkgs unstable package"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;A NeoVim colour scheme in pure Lua allowing for highly flexible configuration and customization.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim#features" rel="noopener noreferrer"&gt;Features&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim#usage-and-configuration" rel="noopener noreferrer"&gt;Usage and Configuration&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim/./docs/api.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-alert markdown-alert-note"&gt;
&lt;p class="markdown-alert-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This is a continuation of the original work from WittyJudge
&lt;a href="https://github.com/WIttyJudge/gruvbox-material.nvim" rel="noopener noreferrer"&gt;https://github.com/WIttyJudge/gruvbox-material.nvim&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;A port of &lt;a href="https://github.com/sainnhe/gruvbox-material" rel="noopener noreferrer"&gt;gruvbox-material&lt;/a&gt; colorscheme for Neovim
written in Lua. It does not aim to be 100% compatible with the mentioned repository, but rather
focuses on keeping the existing scheme stable and to support popular plugins. This colorscheme
supports both &lt;code&gt;dark&lt;/code&gt; and &lt;code&gt;light&lt;/code&gt; themes, based on configured background, and harder or softer
contrasts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dark theme:&lt;/strong&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-medium.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Light theme:&lt;/strong&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-medium.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

    Different contrasts
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contrast&lt;/th&gt;
&lt;th&gt;Dark&lt;/th&gt;
&lt;th&gt;Light&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-hard.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-hard.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-hard.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-hard.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-medium.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-medium.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-soft.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-soft.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-soft.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-soft.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Supported Plugins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-treesitter/nvim-treesitter" rel="noopener noreferrer"&gt;Treesitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-telescope/telescope.nvim" rel="noopener noreferrer"&gt;Telescope&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neovim.io/doc/user/lsp.html" rel="nofollow noopener noreferrer"&gt;LSP Diagnostics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kyazdani42/nvim-tree.lua" rel="noopener noreferrer"&gt;Nvim Tree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/preservim/nerdtree" rel="noopener noreferrer"&gt;NERDTree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mhinz/vim-startify" rel="noopener noreferrer"&gt;Startify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/airblade/vim-gitgutter" rel="noopener noreferrer"&gt;vim-gitgutter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mbbill/undotree" rel="noopener noreferrer"&gt;undotree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liuchengxu/vista.vim" rel="noopener noreferrer"&gt;Vista.vim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/phaazon/hop.nvim" rel="noopener noreferrer"&gt;Hop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liuchengxu/vim-which-key" rel="noopener noreferrer"&gt;WhichKey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yggdroot/indentLine" rel="noopener noreferrer"&gt;indentLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lukas-reineke/indent-blankline.nvim" rel="noopener noreferrer"&gt;Indent Blankline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rcarriga/nvim-notify" rel="noopener noreferrer"&gt;nvim-notify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RRethy/vim-illuminate" rel="noopener noreferrer"&gt;vim-illuminate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hrsh7th/nvim-cmp" rel="noopener noreferrer"&gt;nvim-cmp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-neorg/neorg" rel="noopener noreferrer"&gt;neorg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lukas-reineke/headlines.nvim/" rel="noopener noreferrer"&gt;headlines.nvim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-lualine/lualine.nvim/tree/master" rel="noopener noreferrer"&gt;lualine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;and many more ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please feel free to open an issue if you want some features or other plugins to be included.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/f4z3r/gruvbox-material.nvim" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/f4z3r" rel="noopener noreferrer"&gt;
        f4z3r
      &lt;/a&gt; / &lt;a href="https://github.com/f4z3r/sofa" rel="noopener noreferrer"&gt;
        sofa
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A command execution engine powered by rofi.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/sofa/./assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fsofa%2F.%2Fassets%2Flogo.png" alt="Sofa" width="35%"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Sofa&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/53bf0c03371f1fadda6fe1189a8aa602a39aff60609ed2572c7c4cb35c410169/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f736f66613f6c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f6661253246626c6f622532466d61696e2532464c4943454e5345"&gt;&lt;img src="https://camo.githubusercontent.com/53bf0c03371f1fadda6fe1189a8aa602a39aff60609ed2572c7c4cb35c410169/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f736f66613f6c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f6661253246626c6f622532466d61696e2532464c4943454e5345" alt="GitHub License"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/61c8296308ca8f5d73cb187c82008aa09dc5ac3edb5fa8c42d45187e2b974ea1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f66347a33722f736f66613f6c6f676f3d676974687562266c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f666125324672656c6561736573"&gt;&lt;img src="https://camo.githubusercontent.com/61c8296308ca8f5d73cb187c82008aa09dc5ac3edb5fa8c42d45187e2b974ea1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f66347a33722f736f66613f6c6f676f3d676974687562266c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f666125324672656c6561736573" alt="GitHub Release"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8cf94ab4ef3a06a8418879f24d914bbf59f0f2af92d9ff45be926cb4ff4ef543/68747470733a2f2f696d672e736869656c64732e696f2f6c7561726f636b732f762f66347a33722f736f66613f6c6f676f3d6c7561266c696e6b3d68747470732533412532462532466c7561726f636b732e6f72672532466d6f64756c657325324666347a3372253246736f6661"&gt;&lt;img src="https://camo.githubusercontent.com/8cf94ab4ef3a06a8418879f24d914bbf59f0f2af92d9ff45be926cb4ff4ef543/68747470733a2f2f696d672e736869656c64732e696f2f6c7561726f636b732f762f66347a33722f736f66613f6c6f676f3d6c7561266c696e6b3d68747470732533412532462532466c7561726f636b732e6f72672532466d6f64756c657325324666347a3372253246736f6661" alt="LuaRocks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;A command execution engine powered by &lt;a href="https://github.com/davatorium/rofi" rel="noopener noreferrer"&gt;&lt;code&gt;rofi&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;&lt;code&gt;fzf&lt;/code&gt;&lt;/a&gt;.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/sofa#about" rel="noopener noreferrer"&gt;About&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#examples" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#integration" rel="noopener noreferrer"&gt;Integration&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#configuration" rel="noopener noreferrer"&gt;Configuration&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#development" rel="noopener noreferrer"&gt;Development&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#roadmap" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#license" rel="noopener noreferrer"&gt;License&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;About&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;sofa&lt;/code&gt; is a small utility to enable easy execution of templated commands. It can be used to store
snippets that you often rely on, or fully template complex commands. It is meant to be used with a
shortcut manager to enable launching from anywhere, but can also inject commands into your current
shell session for commands that make more sense to run there (see &lt;a href="https://github.com/f4z3r/sofa#integration" rel="noopener noreferrer"&gt;Integration&lt;/a&gt;).&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Examples&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;For Snippets Management&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;You can use &lt;code&gt;sofa&lt;/code&gt; for standard snippets management. Use the &lt;a href="https://github.com/f4z3r/sofa#integration" rel="noopener noreferrer"&gt;integration&lt;/a&gt; described
below, and have configuration such as:&lt;/p&gt;

Configuration
&lt;div class="highlight highlight-source-yaml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-ent"&gt;namespaces&lt;/span&gt;
  &lt;span class="pl-ent"&gt;lua&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;commands&lt;/span&gt;:
      &lt;span class="pl-ent"&gt;install-local&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;command&lt;/span&gt;: &lt;span class="pl-s"&gt;luarocks --local make --deps-mode {{ deps_mode }} {{ rockspec }}&lt;/span&gt;
        &lt;span class="pl-ent"&gt;description&lt;/span&gt;: &lt;span class="pl-s"&gt;Install rock locally&lt;/span&gt;
        &lt;span class="pl-ent"&gt;tags&lt;/span&gt;:
        - &lt;span class="pl-s"&gt;local&lt;/span&gt;
        - &lt;span class="pl-s"&gt;luarocks&lt;/span&gt;
        &lt;span class="pl-ent"&gt;interactive&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/f4z3r/sofa" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>devops</category>
      <category>devex</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Comprehensive Guide to Managing Large Scale Infrastructure with GitOps</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Tue, 08 Apr 2025 04:31:02 +0000</pubDate>
      <link>https://dev.to/ipt/a-comprehensive-guide-to-managing-large-scale-infrastructure-with-gitops-460c</link>
      <guid>https://dev.to/ipt/a-comprehensive-guide-to-managing-large-scale-infrastructure-with-gitops-460c</guid>
      <description>&lt;p&gt;GitOps is getting adopted more and more. However, there still seems to be some confusion as to what GitOps is, how it differs from regular CI/CD pipelines, and how to best adopt it. In this post we&lt;br&gt;
will quickly cover what GitOps is, and the three main lessons learned from using GitOps to manage infrastructure at scale both on premise and in the cloud.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitOps Overview
&lt;/h2&gt;

&lt;p&gt;GitOps is a set of principles enabling the operation of a system via version controlled, declarative configuration. More specifically, the &lt;a href="https://opengitops.dev/" rel="noopener noreferrer"&gt;OpenGitOps&lt;/a&gt; project defines four principles which define whether a system or set of systems is managed via GitOps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declarative: A system managed by GitOps must have its desired state expressed declaratively.&lt;/li&gt;
&lt;li&gt;Versioned and Immutable: Desired state is stored in a way that enforces immutability, versioning and retains a complete version history.&lt;/li&gt;
&lt;li&gt;Pulled Automatically: Software agents automatically pull the desired state declarations from the source.&lt;/li&gt;
&lt;li&gt;Continuously Reconciled: Software agents continuously observe actual system state and attempt to apply the desired state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;code&gt;git&lt;/code&gt; is not referenced anywhere, as GitOps is not bound to any tooling. However, in layman terms, many consider a system operated via &lt;code&gt;git&lt;/code&gt; to be a GitOps system. This is not quite correct.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitOps is More than CI/CD Pipelines
&lt;/h2&gt;

&lt;p&gt;Taking the "layman's definition" from above, any system that has CI/CD via pipelines triggered on repository changes would be a GitOps system. This is not accurate. Consider an IaC pipeline which applies declaratively defined infrastructure (such as a standard &lt;code&gt;opentofu apply&lt;/code&gt; in a pipeline, or a Docker build followed by a &lt;code&gt;kubectl apply&lt;/code&gt;). While such a system adheres to the first two principles, it does not adhere to the latter two. This implies that changes made to the target system are not corrected (reconciled) until the pipeline runs the next time. Similarly, if the pipeline fails for whatever reason, the desired state does not change the pipeline: a configuration drift is not detected, even if not reconciled.&lt;/p&gt;

&lt;p&gt;This is an important distinction when considering "standard CI/CD" and GitOps. Simply having something declared as code does not make it GitOps.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Advantages of GitOps
&lt;/h2&gt;

&lt;p&gt;GitOps has many advantages over standard ways of managing systems. The advantages of having a declarative desired state, version controlling it, and interacting with the system only via &lt;code&gt;git&lt;/code&gt; (or whatever version control system you use) are tremendous. From improved security and higher efficiency to better change visibility. These are well known to most people and will thus not be covered here.&lt;/p&gt;

&lt;p&gt;Drift detection and automatic reconciliation are the two other aspects that make GitOps absolutely amazing. This is especially true in the current day and age, with the proliferation of complex systems being worked on by many people concurrently. Being able to observe that the system is not in the desired state has massive advantages, such as for standard SRE operations. Continuous reconciliation ensures that manual operational tasks are kept to a minimum, and that systems cannot degrade over time as small undesired changes creep in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tooling
&lt;/h2&gt;

&lt;p&gt;In this post we will mostly focus on using GitOps to manage resources handled via the Kubernetes API, but it should be noted that GitOps as a concept is in no way restricted to Kubernetes. In the Kubernetes space there are two major players for GitOps: &lt;a href="https://argoproj.github.io/cd/" rel="noopener noreferrer"&gt;ArgoCD&lt;/a&gt; and &lt;a href="https://fluxcd.io/" rel="noopener noreferrer"&gt;FluxCD&lt;/a&gt;. We will not go into the details as to what the advantages for each tool are, other than saying that according to our own experience, ArgoCD might be more developer focused, while FluxCD might suit platform engineers with more Kubernetes experience that want more flexibility.&lt;/p&gt;

&lt;p&gt;The rest of this post is tool agnostic and everything we are talking about can be done with either tool (but some aspects might be easier to do with one or the other).&lt;/p&gt;
&lt;h2&gt;
  
  
  Infrastructure: Disambiguation
&lt;/h2&gt;

&lt;p&gt;Before we dive into how to structure your GitOps configuration, it might make sense to draw a line as to where infrastructure starts and where it ends. We consider infrastructure everything that is part of the platform provided to an application team. Hence this line might vary depending on the maturity of the platform you provide your teams. If we consider a simple Kubernetes platform with little additional abstraction for its users, the infrastructure would contain the Kubernetes platform itself as well as all system components that are shared between the teams, such as a central monitoring stack, a central credential management solution, centralized policy enforcement of specific Kubernetes resources, and the like.&lt;/p&gt;

&lt;p&gt;The lower end of the spectrum will likely not be managed by GitOps. That is simply because the GitOps tooling itself typically needs to run somewhere, and also needs to be bootstrapped somehow. Some tools such as FluxCD allow the GitOps controller to manage itself, but even in these cases the runtime for the controller needs to exist when the controller is initially installed, and is thus typically not part of the GitOps configuration.&lt;/p&gt;

&lt;p&gt;Now that this is cleared up, let us consider how the configuration should be managed.&lt;/p&gt;
&lt;h2&gt;
  
  
  App-of-Apps
&lt;/h2&gt;

&lt;p&gt;A very popular pattern for managing configuration via GitOps is the "app-of-apps" pattern. This was popularized by ArgoCD, but is also applicable to other tooling. We will use ArgoCD in the example below, but the same can be implemented using FluxCD Kustomizations.&lt;/p&gt;

&lt;p&gt;Let us consider a component from our infrastructure that we want to manage via GitOps. Typically, we would need to tell the GitOps controller how to manage this component. For instance, let us assume the component is installed via raw Kubernetes manifests. Then we would tell the GitOps controller which repository contains these manifests and in which namespace to install them. Depending on the controller you are using, you might also configure additional parameters such as how often it should be reconciled, whether it depends on other components, and so on. In ArgoCD jargon this would be an "Application" (the root of "app-of-apps" naming), and would look as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sealed-secrets&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;chart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sealed-secrets&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://bitnami-labs.github.io/sealed-secrets&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.16.1&lt;/span&gt;
    &lt;span class="na"&gt;helm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;releaseName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sealed-secrets&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://kubernetes.default.svc"&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeseal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You would then apply this &lt;code&gt;Application&lt;/code&gt; resource to Kubernetes. Your component would then be managed by GitOps, as any changes you push to the manifests repository would be reflected on the Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;Then a second infrastructure component needs to be installed, and you repeat the process. The result would be a second &lt;code&gt;Application&lt;/code&gt; which installs and manages a component. You might also want to version your deployment (such as using version &lt;code&gt;1.16.1&lt;/code&gt; of the Helm chart). This implies that lifecycles require a change to this &lt;code&gt;Application&lt;/code&gt; manifest, and thus a call against the Kubernetes API to edit it.&lt;/p&gt;

&lt;p&gt;The end result is a set of &lt;code&gt;Application&lt;/code&gt; resources, some of which you periodically modify when lifecycling a component. Now imagine you need to deploy your infrastructure elsewhere (for instance a second Kubernetes cluster in our example), or maybe even a couple dozen times. Then you need to manage this entire set of &lt;code&gt;Application&lt;/code&gt; resources on every platform. A better approach is to add an abstraction layer, which itself deploys the &lt;code&gt;Application&lt;/code&gt; resources via GitOps. Hence you put all your &lt;code&gt;Application&lt;/code&gt; resources into a repository, and define another, "higher level" &lt;code&gt;Application&lt;/code&gt; which deploys this repository. This means that when deploying to new platforms, you only need to deploy that one "higher level" &lt;code&gt;Application&lt;/code&gt;, and any changes to the component &lt;code&gt;Application&lt;/code&gt; resources can be made via Git, conforming to our GitOps approach. This "higher level" &lt;code&gt;Application&lt;/code&gt; is only there to deploy the component &lt;code&gt;Application&lt;/code&gt;s thus the name "app-of-apps". Visually, you thus have the following structure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9403giq52kn3ptnxfmlo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9403giq52kn3ptnxfmlo.png" alt="Visual representation of app-of-apps pattern"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It should be noted that this also massively helps when customizing platforms. Typically, components cannot be deployed truly one-to-one in several places, but require slight configuration differences. Consider for instance hostnames for UIs of your components. Two of these components deployed in different locations cannot share the same hostname and routing. Using an "app-of-apps" approach allows you to define variables on the top level application, and inject these into the downstream applications such that they can slightly adapt the way they are installed. We will not dive deeper into how this is done as it is highly dependent on the tooling you use (ArgoCD uses &lt;code&gt;ApplicationSet&lt;/code&gt;, FluxCD uses variable substitution), but know this is enabled by such an approach.&lt;/p&gt;
&lt;h2&gt;
  
  
  Consolidating your Configuration
&lt;/h2&gt;

&lt;p&gt;In the organisation I first used GitOps at scale, we deployed all our components as Helm charts to a Kubernetes cluster. Each component was essentially contained within two different repositories in our version control system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the source code repository which typically built a Docker image as an artefact&lt;/li&gt;
&lt;li&gt;the Helm chart definition which referenced the Docker image from above&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When we then introduced GitOps, we decided to add a third repository containing the exact deployment definition (in our case the &lt;code&gt;Application&lt;/code&gt; declarations) for the component. Using the app-of-apps pattern from above, we could then reference each of these "GitOps repositories" and deploy specific overlays (customizations) of the &lt;code&gt;Application&lt;/code&gt; to specific platforms. This worked well for quite some time. However, with time the number of components we managed increased, and so did the number of target platforms to which these components needed to be deployed. This lead to quite a few issues.&lt;/p&gt;

&lt;p&gt;When a new target platform was introduced, all such "GitOps repositories" needed to be updated to contain a new overlay customizing the &lt;code&gt;Application&lt;/code&gt; to the specific platform. This is very tedious when you have several dozen such repositories.&lt;/p&gt;

&lt;p&gt;Moreover, components had dependencies to other components. This meant that we were referencing components within a repository that were defined in another repository. While not problematic in itself, this can become very tricky when one component has a dependency on a configuration value of another component. The configuration value is then duplicated in both repositories and becomes difficult to maintain. While this sounds like we did not properly separate the components, it is very common to see such cases in infrastructure configurations. Consider for instance a deployment of an ingress controller which defines a hostname suffix for its routes. All components deployed on the same Kubernetes platform that deploy a route/ingress will need to use exactly that hostname suffix in order to have valid routing.&lt;/p&gt;

&lt;p&gt;The above issue also results in tricky situations when configurations need to be changed for components that are dependent on one another. If the deployment configuration is separated into different repositories, PRs to these repositories need to be synchronized to ensure the deployment occurs at the same time.&lt;/p&gt;

&lt;p&gt;Finally, distributing the deployment configuration over so many repositories meant that it became increasingly difficult to have an overview of what is deployed on a target platform. One would need to navigate through dozens of repositories to check this is correctly done.&lt;/p&gt;

&lt;p&gt;After identifying these issues we decided to move all our configuration into a single repository. This repository would then contain a templated definition of the entire set of components which would need to be deployed. A set of platform definitions within the same repository would then feed values to templates to ensure consistent configuration. This massively helped us with to address the issues mentioned above. On top of that, it allows to version the "template" and thus enables rollouts of a versioned infrastructure layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwt8td49qwxva9p5fnj5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwt8td49qwxva9p5fnj5w.png" alt="Grouping Application declarations into a single repository"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find an example repository of such a structure&lt;br&gt;
designed with FluxCD here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/f4z3r" rel="noopener noreferrer"&gt;
        f4z3r
      &lt;/a&gt; / &lt;a href="https://github.com/f4z3r/flux-demo" rel="noopener noreferrer"&gt;
        flux-demo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      This repository shows an example how one can use a single mono-repository to manage multiple clusters' infrastructure in a controlled fashion.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/flux-demo/./assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fflux-demo%2F.%2Fassets%2Flogo.png" alt="FluxCD" width="25%"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Flux Demo&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/d49b85fee643a77a1b3e35707a223e5be358cd0ece3c999f218d7e15485d3531/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f666c75782d64656d6f"&gt;&lt;img src="https://camo.githubusercontent.com/d49b85fee643a77a1b3e35707a223e5be358cd0ece3c999f218d7e15485d3531/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f666c75782d64656d6f" alt="GitHub last commit"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/b726528050260dd1b632df604eb56c379d272e973d0007e2595dc75dd83d002d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f666c75782d64656d6f"&gt;&lt;img src="https://camo.githubusercontent.com/b726528050260dd1b632df604eb56c379d272e973d0007e2595dc75dd83d002d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f666c75782d64656d6f" alt="GitHub License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;An example how one can use a mono-repo to manage large infrastructure in a controlled fashion using FluxCD.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/flux-demo#setup" rel="noopener noreferrer"&gt;Setup&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/flux-demo#structure-of-the-repo" rel="noopener noreferrer"&gt;Structure of the Repo&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/flux-demo#application-vs-infrastructure" rel="noopener noreferrer"&gt;Application vs Infrastructure&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/flux-demo#workflow" rel="noopener noreferrer"&gt;Workflow&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Generate a GitHub PAT&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;See the documentation: &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For fined-grained control, grant the token Admin and content read/write permissions on the
repository.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Setup a Cluster with Flux&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Setup the required tooling with &lt;code&gt;devbox shell&lt;/code&gt;, then&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; install a cluster&lt;/span&gt;
kind create cluster -n demo
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; set the token&lt;/span&gt;
&lt;span class="pl-k"&gt;export&lt;/span&gt; GITHUB_TOKEN=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;redacted&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; onboard flux&lt;/span&gt;
flux bootstrap github \
  --token-auth \
  --owner=f4z3r \
  --repository=flux-demo \
  --branch=main \
  --path=clusters/demo \
  --personal&lt;/pre&gt;

&lt;/div&gt;

Sample output from Flux installation
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;► connecting to github.com
► cloning branch "main" from Git repository "https://github.com/f4z3r/flux-demo.git"
✔ cloned repository
► generating component manifests
✔ generated component manifests
✔ committed component manifests to "main" ("158753158f3c760f741f22ed7f68bdee1b66e475")
► pushing component manifests to "https://github.com/f4z3r/flux-demo.git"
► installing components in&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/f4z3r/flux-demo" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Gitops Bridge
&lt;/h2&gt;

&lt;p&gt;The last challenge we want to address in this blog post is a concept called a "GitOps bridge". In public cloud environments, there is typically a relatively strong cut between infrastructure deployed via Terraform (or any similar tool), and the infrastructure deployed via GitOps. For instance, one might deploy an Azure Kubernetes Service and some surrounding services (such as the required network, a container registry, etc) via Terraform, and them deploy components and applications within the AKS using GitOps. The issue that we face here is that the GitOps configuration very often depends on the Terraform configuration. Consider for instance the container registry. Its address is set up by Terraform, but is used in every image declaration in the GitOps configuration. One option is to duplicate such values in the respective configurations, while another option is to use a GitOps bridge.&lt;/p&gt;

&lt;p&gt;The GitOps bridge is an abstract concept on how to pass configuration values from tooling such as Terraform as inputs to the GitOps configuration. How this is done in practice very much depends on which tools you use. For instance, if looking at Terraform and FluxCD, a common way to achieve this is to have Terraform write a ConfigMap onto the AKS where the FluxCD controller will run containing all variables (and their values) that will be required by the GitOps configuration. The FluxCD controller then supports injecting variables from a ConfigMap via &lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/#post-build-variable-substitution" rel="noopener noreferrer"&gt;variable substitution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using a GitOps bridge has the advantage that changes in the Terraform configurations are much less likely to break the GitOps configuration that builds on top of it. Moreover, it allows Terraform to directly bootstrap the entire GitOps setup when creating new platforms without the need to manually redefine the required variables in the GitOps repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;So, to recap, we have looked at what GitOps really is (and isn't). Understanding these basics is critical to correctly implement GitOps in your projects. On top of that, we looked at three best practices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use an app-of-apps pattern to improve resiliency for when you need to recreate platforms.&lt;/li&gt;
&lt;li&gt;Consider using a mono-repository for all your GitOps configuration as your setup grows.&lt;/li&gt;
&lt;li&gt;Have a look at GitOps bridges to improve the automation when setting up platforms and ensuring your Terraform and GitOps configurations are consistent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I hope this has helped you understand a bit better how to use GitOps at scale. If you have any questions or comments, feel free to let me know below.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>platformengineering</category>
      <category>gitops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>A Very Deep Dive Into Docker Builds</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Tue, 26 Nov 2024 06:25:41 +0000</pubDate>
      <link>https://dev.to/ipt/a-very-deep-dive-into-docker-builds-270n</link>
      <guid>https://dev.to/ipt/a-very-deep-dive-into-docker-builds-270n</guid>
      <description>&lt;p&gt;Containers are everywhere. From Kubernetes for orchestrating deployments and simplifing operations to Dev Containers for flexible yet reproducible development environments. Yet, while they are ubiquitous, images are often built sub-optimally. In this post we will be looking at a full example of a Docker build for a Python application and what best practices to consider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;This is a real world example from an very small component we built in Python for one of our clients. Very few alterations were made to the original configuration (changing URLs, and removing Email addresses mostly). We will go in depth as to why we did every single little thing. While some stuff is quite Python-centric, the same principles apply to other languages, and the text should be broad enough so that it is understandable how to transfer this example to different languages.&lt;/p&gt;

&lt;p&gt;Also, this is a &lt;em&gt;long&lt;/em&gt; article, so if you actually plan on reading it, grab yourself a snack and a fresh drink first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;The goal of this post is to showcase how one can setup a Docker build that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fully reproducible,&lt;/li&gt;
&lt;li&gt;as fast as possible,&lt;/li&gt;
&lt;li&gt;fails early on code issues,&lt;/li&gt;
&lt;li&gt;isolates testing from deployed code,&lt;/li&gt;
&lt;li&gt;is secure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The example we will use for this implements quite a lot to ensure only quality code reaches production, and that it can do so as fast as possible. Going all the way might not be necessary for all projects using Docker. For instance, if you release code to production only once a day (or less) you might care less about release build cache optimization. This example is however meant to show the "extreme" to which you can push Docker, so that you can (in theory) push code to production fully continuously (CI/CD principles). But yeah ...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdf1jdml0g5g4uk006jus.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdf1jdml0g5g4uk006jus.jpeg" alt="A meme that one does not simply push docker images to production" width="568" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Why do we have these goals? Reproducible builds are one of the most important factors for proper compliance, and for easier debugging. Debugging is simpler, since we ensure that no matter the environment, date, or location of the build, if it succeeds, the same input generates the same output. Moreover, it brings stability, as a pipeline might not suddenly fail on a nightly build (if you still do such things) because a new upstream library or program was released that is used somewhere in your supply chain.&lt;/p&gt;

&lt;p&gt;Regarding compliance, we need to be able to tell and revert to the exact state of software that was deployed in the past. Without reproducible builds, using Git to track back to a previous state of deployed code does not help you much, because while you can validate what code you deployed, you don't know what versions of everything else you deployed with it.&lt;/p&gt;

&lt;p&gt;Builds should be fast, and fail fast. The reason here is that no one likes to wait. You don't want to wait for 2 hours to figure out whether a tiny code change breaks tests or does not even compile.&lt;/p&gt;

&lt;p&gt;You will want to isolate test code from deployed code, because more code equals more bugs. While testing frameworks are very good at isolating test code from code being tested, writing tests generates a risk of bugs. Moreover, the test code is unneeded bloat for your runtime application. Thus it should be isolated from it.&lt;/p&gt;

&lt;p&gt;Finally security. While some people think that containers improve security by default, this is not the case. Container technology has the potential to indeed improve the robustness of some security measures and controls. However, in order to achieve this, one needs to correctly utilize containers and build the images with security in mind. For instance, if an image contains certain utilities that allow it to connect to the internet (such as &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;wget&lt;/code&gt;), it suddenly makes the container much more vulnerable to container escape attacks (where an attacker manages to move from the container to the underlying host), and hence the whole isolation benefit of the container (which can be a security control) is broken. The same is true for containers that contain interpreters and allow the runtime user to open, edit and execute arbitrary files. As our container will contain Python code, and hence the Python interpreter, this is definitely something we need to take very seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python Goals
&lt;/h2&gt;

&lt;p&gt;Our example is based on Python, an interpreted language. This is not ideal, as it means that it does not require a compile step. Compilation optimization is however a very important aspect in Docker builds. In order to still address this, I will talk about this, but will not refer to the configuration examples. One could ask why I did not take a compiled language example then. The reason is very simple, I wanted a real world example such that this post is not just theoretical goodness, and most Golang image builds I am currently working on are more basic and not as educational.&lt;/p&gt;

&lt;p&gt;Yet another question could be "why deploy Python in Docker in the first place?". This is a very legitimate question. Python requires a lot of OS bloat to just be able to run. This means that typically a VM is a good choice to host it. For all those saying that Docker is still better because of performance (due to faster startup, no hardware virtualization overhead, etc): this is not true for such cases where a large part of an OS needs to be in the Docker image. A VM of a full init-based Linux system can be launched in less than 250ms on modern technology. A full Ubuntu installation with systemd can be completely booted in around 2.5 seconds. The former is in the same order of magnitude that it might take the Python interpreter to just load the code of a large Python application.&lt;/p&gt;

&lt;p&gt;So performance cannot be said to be better with Docker, why choose Docker then? Better reasons are that you can strip down a Docker image much easier than an OS. This is critical for us due to security requirements. While Python requires a lot of OS features, the majority of the OS is still bloat. Every piece of bloat is a potential attack vector (each of these unused components might have one or more CVEs that we need to patch, even though we don't even use that software). Another reason is that the build process of Docker is much simpler to manage. There are tools such as &lt;a href="https://www.packer.io/" rel="noopener noreferrer"&gt;Packer&lt;/a&gt; that allow similar processes for VMs, but these are not as standardized as the &lt;a href="https://opencontainers.org/" rel="noopener noreferrer"&gt;open container initiative&lt;/a&gt; (OCI - which Docker adheres to).&lt;/p&gt;

&lt;p&gt;Another very important point is the ease of development. Docker and other OCI compliant products provide us with a possibility to build, test, and run our build artefacts (in this case Docker images) everywhere. This makes it very simple and fast for our developers to test the build and perform a test run of an image locally on their development machine. This would not be quite the case with VMs or raw artefacts (JARs, source code archives, ...). Moreover, the OCI ecosystem does not only include specifications on how to interact with images, but also how to setup and configure critical elements such as persistence and networking. These aspects are made very simple with Docker, and would be quite a pain to manage securely with most other technologies.&lt;/p&gt;

&lt;p&gt;Finally the main reason for us is the choice of runtime. We have very decent container runtimes (&lt;a href="https://www.rancher.com/products/secure-kubernetes-distribution" rel="noopener noreferrer"&gt;RKE&lt;/a&gt;, &lt;a href="https://developers.redhat.com/products/openshift/overview" rel="noopener noreferrer"&gt;RHOS&lt;/a&gt;, &lt;a href="https://k3s.io/" rel="noopener noreferrer"&gt;K3s&lt;/a&gt;) available to deploy applications. We are very familiar with them, and they offer us a lot of functionality. These all support containers primarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Tiny Bit of Background
&lt;/h2&gt;

&lt;p&gt;Last before we get into the dirty details, a tiny bit of background into what we are building. The application we will be building here is a sort of a facade reverse proxy. It offers a standardized API to clients, which can connect and perform requests. Based on the content of the request, the component will trigger a routing algorithm that defines where the request needs to be routed. This routing algorithm might require several API calls in the backend to different systems to figure out where the call should go. Once done, the component will relay the call to a backend, and forward the response to the client. The client is never aware that it is talking to more than one component, and only needs to authenticate to that single system. Imagine an API Gateway, but where the routing is extremely complex and requires integration with systems such as Kubernetes, a cloud portal, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Details
&lt;/h2&gt;

&lt;p&gt;Here is an overview of our &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;internal.registry/base/ca-bundle:20220405&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;cert-bundle&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;internal.registry/base/python:3.9.2-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=cert-bundle /certs/ /usr/local/share/ca-certificates/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;update-ca-certificates

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--upgrade&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--ignore-installed&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; pypi.python.org &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; pypi.org &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; files.pythonhosted.org &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;pipenv&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2024.2.0

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PIPENV_VENV_IN_PROJECT=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Pipfile Pipfile&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Pipfile.lock Pipfile.lock&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pipenv &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--deploy&lt;/span&gt;

&lt;span class="c"&gt;### Tester image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pipenv &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="nt"&gt;--deploy&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./pyproject.toml pyproject.toml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./assets/ ./assets&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./features/ ./features&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./tests/ ./tests&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./src/ ./&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;./.mypy_cache/ &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;./.pytest_cache/ &lt;span class="se"&gt;\
&lt;/span&gt;  pipenv run mypy &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pipenv run black &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pipenv run bandit &lt;span class="nt"&gt;-ll&lt;/span&gt; ./&lt;span class="k"&gt;*&lt;/span&gt;.py &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./ pipenv run pytest


&lt;span class="c"&gt;### Runner image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; internal.registry/base/distroless-python:3.9.2&lt;/span&gt;
&lt;span class="k"&gt;LABEL&lt;/span&gt;&lt;span class="s"&gt; maintainer="Redacted &amp;lt;redacted-email&amp;gt;"&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app/&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 1000&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=1000 /app/.venv/lib/python3.9/site-packages ./my-app&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app/my-app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=1000 ./src/ ./&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["python3"]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["./main.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We will go through it line by line and figure out why we did what we did, and why we did not choose a different approach. Let's start!&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;internal.registry/base/ca-bundle:20220405&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;cert-bundle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this line we reference a container image for later use and provide it a name alias &lt;code&gt;cert-bundle&lt;/code&gt;. This container image contains only data: our production network proxy certificates and all internal certificate authorities. We need these CAs as we will connect over TLS to backend components that have internal certificates. We also need the production network proxy certificates as we will pull dependencies straight from the internet, and all that traffic is routed over a gateway proxy. Why distribute these certificates over a Docker image instead of a compressed TAR? The main reason is that we want to have a unified way that we build artefacts and manage CI/CD pipelines. By creating and managing the certificates via Docker, we can use our entire Docker setup (such as UCD/Jenkins/Tekton pipelines for building, registry for distribution, quality gates for security, etc) and do not need to have a different system to manage the certificates. Note that we refer to the exact state of the certificate bundle (&lt;code&gt;20220405&lt;/code&gt;), which refers to the state of the certificates per 5th of April 2022. This is very important to make the build reproducible. If we did not pin the version of the certificates, it would mean that we could build the image maybe today, but it would fail tomorrow, once the certificates change (even though we did not change the code at all). You will note that we will pin every single version in the entire build process.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;internal.registry/base/python:3.9.2-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this line, we reference the base image we will start building from. This is the official Python image for Python version 3.9.2. We use the slim version because we don't need much more than the standard Python installation. We pull this from our own registry, as all Docker images are scanned beforehand to reduce the risk of supply chain attacks. Also here, the version is pinned. We provide this build step the &lt;code&gt;builder&lt;/code&gt; alias. In essence this means that starting from this line we define an image stage that will contain the build process of our application. For Python, this mostly includes downloading dependencies (both software and system level), and injecting the source code, as there will be no compile step.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=cert-bundle /certs/ /usr/local/share/ca-certificates/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This copies our certificates into our build image. We do this by referencing the build step &lt;code&gt;cert-bundle&lt;/code&gt; (see first line of the &lt;code&gt;Dockerfile&lt;/code&gt; again) in the &lt;code&gt;--from&lt;/code&gt; argument of the &lt;code&gt;COPY&lt;/code&gt; command. Note that we could have referenced the image directly in the &lt;code&gt;--from&lt;/code&gt; argument. We choose to use build stage aliases for visibility, and reduce duplication if the certificates need to be copied into different stages. Note that this copies only the raw certificates. A OS specific bundle would still need to be generated.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;update-ca-certificates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here we do exactly this, we generate a certificate bundle for the underlying OS of our builder image (&lt;a href="https://www.debian.org/" rel="noopener noreferrer"&gt;Debian&lt;/a&gt;). This allows our subsequent build steps to use the certificate bundle to validate host certificates on TLS connections.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We then set a working directory. The idea is to have a base directory on which we now operate. This can be nearly any working directory, and will be created if non-existent. We choose &lt;code&gt;/app/&lt;/code&gt; by convention. Moreover, note that we tend to reference directories with the trailing &lt;code&gt;/&lt;/code&gt; to make it more explicit that we are referencing directories and not files. We use this convention throughout the configuration/code.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--upgrade&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--ignore-installed&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; pypi.python.org &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; pypi.org &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; files.pythonhosted.org &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;pipenv&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2024.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We use an environment virtualization technology for Python. This is called &lt;a href="https://pipenv.pypa.io/en/latest/index.html" rel="noopener noreferrer"&gt;&lt;code&gt;pipenv&lt;/code&gt;&lt;/a&gt;. It allows us to have many different versions of the same dependency installed locally, without them conflicting. This is very important when you are developing many applications at the same time locally. By running this line we install version &lt;code&gt;2024.2.0&lt;/code&gt; of &lt;code&gt;pipenv&lt;/code&gt; (pinned). Other than Python itself, these are the only tools required for our Python development environment. If we were using a different language, &lt;code&gt;pipenv&lt;/code&gt; would be substituted with your dependency management tool (such as Maven for Java). Note that we only install &lt;code&gt;pipenv&lt;/code&gt; itself, we do not install the dependencies. Also using the flags provided we ensure a fully clean install of &lt;code&gt;pipenv&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an example where we reach out to the internet and thus needed the network proxy certificates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A very good question here might be "why to use &lt;code&gt;pipenv&lt;/code&gt; at all, considering it is typically used for environment virtualization, which is already covered by Docker itself?". There are two aspects here. The first is to allow us to lock dependencies using their hash, which is not natively supported by &lt;code&gt;pip&lt;/code&gt; (the standard Python package manager). The second is that we want to keep the build process within Docker as close to the build process outside of it. While we do not build artefacts outside of Docker per-se, the IDEs of our developers need to fall back on these technologies to support features such as library-aware code completion, type-checking, test integration, debugging, etc. This could also be achieved by connecting the IDE to an instance running directly in Docker. This however is relatively complex and requires the setup to support remote debugging. In theory, these are not really problems as long as the dev environments are uniform, but we allow each developer to work with the tools he/she desires to develop code. It then suddenly becomes very difficult to have a stable setup that works for everybody, especially considering that some of our developers do not want/know how to configure their environments to that level (client-server debugger setups, network and volume management between the IDE and Docker, ...).&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PIPENV_VENV_IN_PROJECT=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here we set environment variables for &lt;code&gt;pipenv&lt;/code&gt;. Firstly we want the dependencies to be installed directly in the project repository, not centrally. This allows us to ensure that we do not accidentally copy a system Python dependency that installed by default with the base image. The second configures the certificate bundle we generated in the beginning to be used by &lt;code&gt;pipenv&lt;/code&gt;. It does not use the system configured bundle by default, so it needs to be configured manually here.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Pipfile Pipfile&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Pipfile.lock Pipfile.lock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the interesting stuff. Here we copy the dependency files into the image. The first file contains a list of dependencies that we use for our project. The second contains a hash the dependencies should have, including indirect dependencies (dependencies of dependencies), in order to ensure that we always get exactly the same dependency code for very install. The first looks as follows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[source]]&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://pypi.org/simple"&lt;/span&gt;
&lt;span class="py"&gt;verify_ssl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pypi"&lt;/span&gt;

&lt;span class="nn"&gt;[packages]&lt;/span&gt;
&lt;span class="py"&gt;requests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"=&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.28&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="py"&gt;"
pydantic = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="py"&gt;"
# more dependencies ...

[dev-packages]
black = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;23.1&lt;/span&gt;&lt;span class="py"&gt;"
bandit = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.7&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="py"&gt;"
pytest = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;7.2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="py"&gt;"
pytest-mock = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.9&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="py"&gt;"
pytest-bdd = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="py"&gt;"
mypy = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="py"&gt;"
types-Pygments = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.14&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# more dev dependencies ...&lt;/span&gt;

&lt;span class="nn"&gt;[requires]&lt;/span&gt;
&lt;span class="py"&gt;python_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.9"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that we split dependencies into normal packages we require for our application, and packages only required for testing and our quality gates (&lt;code&gt;[dev-packages]&lt;/code&gt;). This is important later on, as we do not wish to have packages only required for testing in our production Docker image.&lt;/p&gt;

&lt;p&gt;I will not show you an example of the lock file, as it contains mostly checksum hashes. Simply trust me that it contains the exact checksum that every package (such as the dependencies of &lt;code&gt;requests&lt;/code&gt;) has to have to be installed. The reason this is required in the first place, is because the dependencies of &lt;code&gt;requests&lt;/code&gt; are likely not pinned to an exact version and might thus change between installations unless locked via our &lt;code&gt;Pipfile.lock&lt;/code&gt;. This would undesired as it would make our builds un-reproducible. The lock file itself is generated by our developers in two different scenarios. The first is when a library is added due to some new feature. In such a case the new library is added to the &lt;code&gt;Pipfile&lt;/code&gt;, and an installation is triggered outside of Docker. This will install the new library and potentially update already installed ones (in case of conflicts). Hence new hashes will be added to the lock file. The second is on a lifecycle of the existing libraries or of our Python version. In such a case we update the pinned version in the &lt;code&gt;Pipfile&lt;/code&gt; and trigger an installation outside of Docker. Again, &lt;code&gt;pipenv&lt;/code&gt; would then update the direct dependencies, and potentially transitive ones, and update their hashes in the lock file.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;pipenv &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here we install the dependencies for our application. The &lt;code&gt;--deploy&lt;/code&gt; flag means that we want to install the dependencies based on the lock file. Moreover, we do not install the dev packages yet, only the ones needed for the production code.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;### Tester image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here we generate a new Docker build stage. We have generated a stage with &lt;code&gt;builder&lt;/code&gt; that contains the required certificates and the production dependencies, and nothing more. We now want to test our code and validate quality gates. We do not want to perform this in the &lt;code&gt;builder&lt;/code&gt; stage, because it would pollute our production dependencies. Moreover, using a different stage allows to trigger builds more granularly with &lt;a href="https://docs.docker.com/build/buildkit/" rel="noopener noreferrer"&gt;BuildKit&lt;/a&gt;. For instance, I would be able to configure (with &lt;code&gt;--target=test&lt;/code&gt;) to only build the image up to the &lt;code&gt;test&lt;/code&gt; stage, and skip any later stages (such as the runtime image in our case). This can be very useful in pipelines, for instance, where we want to run the test on every commit, but are not interested in building a real artefact unless the commit is tagged.&lt;/p&gt;

&lt;p&gt;With this line we essentially say "start a new stage called &lt;code&gt;test&lt;/code&gt; from the latest state of &lt;code&gt;builder&lt;/code&gt;". We also add a comment above to make it more visible that we are starting a new stage in the &lt;code&gt;Dockerfile&lt;/code&gt;. Stage comments are typically the only comments we have in the &lt;code&gt;Dockerfile&lt;/code&gt;s.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;pipenv &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="nt"&gt;--deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this line we now deploy the development dependencies, including tools for quality checks (&lt;code&gt;mypy&lt;/code&gt;, &lt;code&gt;bandit&lt;/code&gt;, &lt;code&gt;black&lt;/code&gt;, see below for details) and for testing. Again, we use the &lt;code&gt;--deploy&lt;/code&gt; flag to ensure we always use the same versions to make the build fully reproducible.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./pyproject.toml pyproject.toml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./assets/ ./assets&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./features/ ./features&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./tests/ ./tests&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./src/ ./&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here is the first time we copy actual content, other than the list of dependencies, into our image. This means that up until now all layers in the build process can be fully cached if we perform code changes. Thinking about this is &lt;em&gt;primordial&lt;/em&gt; if you want an efficient build process. Even here, we copy code in reverse order based on likelihood of change. The first file we copy in configures our tooling and quality gates. This is unlikely to change unless we introduce a new tool or change configuration of an existing one. An example of the file can be seen below.&lt;/p&gt;

&lt;p&gt;The second line copies assets. These are used for testing, such as test configurations for configuration validation etc. These are also quite unlikely to change unless we write new tests of our configuration classes.&lt;/p&gt;

&lt;p&gt;The third line copies in our &lt;a href="https://cucumber.io/docs/installation/python/" rel="noopener noreferrer"&gt;Cucumber&lt;/a&gt; files for BDD testing. These change only when we either define new behavioral tests or add features.&lt;/p&gt;

&lt;p&gt;The fourth line copies our test code, this is quite likely to change, as it contains all our unit tests, and the testing framework for behavioral tests.&lt;/p&gt;

&lt;p&gt;Finally the last line copies in our actual code. This, along with the unit tests, is the code that is most likely to change, and thus comes last. This way on a code change, all lines up to this one (assuming we did not add/change tests) can be used from cache.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.black]&lt;/span&gt;
&lt;span class="py"&gt;line-length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="nn"&gt;[tool.pytest.ini_options]&lt;/span&gt;
&lt;span class="py"&gt;pythonpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"tests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;bdd_features_base_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"features/"&lt;/span&gt;

&lt;span class="nn"&gt;[tool.mypy]&lt;/span&gt;
&lt;span class="py"&gt;exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;'^tests/.*\.py$'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;ignore_missing_imports&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;warn_unused_configs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;warn_redundant_casts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# more settings ...&lt;/span&gt;

&lt;span class="nn"&gt;[[tool.mypy.overrides]]&lt;/span&gt;
&lt;span class="py"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"kubernetes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"parse_types"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c"&gt;# skip libraries without stubs&lt;/span&gt;
&lt;span class="py"&gt;ignore_missing_imports&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;./.mypy_cache/ &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;./.pytest_cache/ &lt;span class="se"&gt;\
&lt;/span&gt;  pipenv run mypy &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pipenv run black &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pipenv run bandit &lt;span class="nt"&gt;-ll&lt;/span&gt; ./&lt;span class="k"&gt;*&lt;/span&gt;.py &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./ pipenv run pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This line aggregates our quality gates and testing. For quality gates we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.mypy-lang.org/" rel="noopener noreferrer"&gt;mypy&lt;/a&gt;: checks typing information where provided. We do not perform strict typing so that type information is required everywhere, but we validate that the provided typing is correct.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://black.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;black&lt;/a&gt;: checks formatting of the code to ensure it is according to your guidelines.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bandit.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;bandit&lt;/a&gt;: performs basic security checks. This is a non-blocking check, meaning that the build will only fail if issues of severity &lt;code&gt;MEDIUM&lt;/code&gt; or higher a found. &lt;code&gt;LOW&lt;/code&gt; severity check fails are ignored.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally we run our testing (with &lt;a href="https://docs.pytest.org/en/7.2.x/" rel="noopener noreferrer"&gt;pytest&lt;/a&gt;). We run the testing last, as it is the most time consuming of the tasks, and it does not need to be executed if the code fails to adhere to our standards. Note that you could add any other gates here, such as a code coverage baseline that needs to be adhered to, various code analysis checks, or security scans. We only perform one more security check against dubious code and supply chain attacks during the build process. This check is however done on the final Docker image and is thus executed by the pipeline itself outside of the Docker build process.&lt;/p&gt;

&lt;p&gt;Note that all commands are executed as one &lt;code&gt;RUN&lt;/code&gt; statement. This is best practice, as none of these commands can be cached individually. Either all have to be executed again if layer it builds upon changed, or none has to run. Putting them into the same &lt;code&gt;RUN&lt;/code&gt; statement generates a single new layer for all four commands, which reduces the layer count and build overhead for Docker.&lt;/p&gt;

&lt;p&gt;Finally, note the &lt;code&gt;--mount&lt;/code&gt; options passed to &lt;code&gt;RUN&lt;/code&gt; (introduced with BuildKit 1.2). These allow to cache content within the Docker build between builds. Here we mount two caches, one for &lt;code&gt;mypy&lt;/code&gt; and one for &lt;code&gt;pytest&lt;/code&gt;. These ensure that if a subsequent Docker build is triggered for code that does not affect some files, the typing checks and tests are not run again for these files, but taken from the cache. For &lt;code&gt;pytest&lt;/code&gt; this is actually done on a "per-test" basis, ensuring tests are not run unless code they are testing is changed. Such caches can &lt;em&gt;massively&lt;/em&gt; increase the speed of your pipelines, especially when your project grows and the test suites start to take more time to run through.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;### Runner image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; internal.registry/base/distroless-python:3.9.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This defines the runner image. We are done with testing and want to build the productive artefact, as all checks have passed. In a compiled setup, this would mean we would now have a release compilation stage (before building the runtime image). This is done after testing as the release binary/JAR will be compiled with optimizations, which can take quite long, and is unnecessary if the tests fail anyways. Thus in a compiled language like Java or Golang, we would now continue from the builder again, copy the code back into the layer, and compile. Here one should be careful, most languages support incremental compilation to reduce compilation times. When this is supported, one needs to mount a build cache, or the incremental compilations from previous builds will be lost every time the code changes, as the entire compilation layer will be discarded from the cache. This is done the same way as in the previous block, with &lt;code&gt;--mount&lt;/code&gt; parameters.&lt;/p&gt;

&lt;p&gt;Once the compilation is completed, and we have our final artefact (binary or JAR), we want to copy it into the runtime image. The idea is again to restrict bloat to reduce our attack surface. For instance, in a Java setup, we only need a working JRE to run our application, we no longer need Maven, the Java compiler, etc. Thus, after the build process, we use a new stage for the runtime image. This is what we did for Python here, since we have no compilation step. We use a different image than our initial &lt;code&gt;internal.registry/base/python:3.9.2-slim&lt;/code&gt; image, as we no longer need &lt;code&gt;pip&lt;/code&gt; (the Python package manager), and other bloat. Instead we use a distroless image, which is essentially a stripped down Debian image containing truly the base minimum to run Python code, but nothing to manage it, etc. Again, we use our own copy of the distroless image from our scanned registry.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;LABEL&lt;/span&gt;&lt;span class="s"&gt; maintainer="Redacted &amp;lt;redacted-email&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This line adds metadata to the image. This is not necessary to have a good image, but useful when using images that are shared across huge organisations. This is the official maintainer label we use, where we reference our team, such that anyone that downloads the image and inspects it can see who built it, and how to get into contact with us in case of issues.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same as before, we copy certificates and configure Python to use our bundle. Note that this time we directly copy the bundle generated in the builder and not from the certificate image, as we need a bundle and cannot create it in this image (&lt;code&gt;update-ca-certificates&lt;/code&gt; is not contained in the distroless image). We need to copy this explicitly since we started from a fresh image. The &lt;code&gt;test&lt;/code&gt; stage had the bundle implicitly configured from the &lt;code&gt;builder&lt;/code&gt; stage, upon which it was set up.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app/&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We set a working directory again. This is also necessary since starting from a fresh image. Also we set a non root user. This is necessary since we do not want to run our code as root for security reasons (reduce the impact of a remote code execution - RCE vulnerability). Note that any statement after the &lt;code&gt;USER&lt;/code&gt; statement will be executed in the context of that user. Therefore I would for instance not be allowed to run &lt;code&gt;update-ca-certificates&lt;/code&gt; (if it was present in the image) in a &lt;code&gt;RUN&lt;/code&gt; statement from now on, as this requires root privileges.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=1000 /app/.venv/lib/python3.9/site-packages ./my-app&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app/my-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here we copy the non-dev packages from the &lt;code&gt;builder&lt;/code&gt; stage into our productive image. Note that we use a path from within the project root (&lt;code&gt;/app/&lt;/code&gt;), since we set &lt;code&gt;pipenv&lt;/code&gt; to install the virtual environment directly in the project (the &lt;code&gt;PIPENV_VENV_IN_PROJECT&lt;/code&gt; variable). We copy the site-packages (the dependencies) directly into a subfolder, in which our application will live. This ensures that they are treated as if we wrote them ourselves, as individual Python modules in our code. They essentially become indistinguishable from our own code. This allows to keep consistency in our module names are resolved. Note we need to add the &lt;code&gt;--chown&lt;/code&gt; flag, as the dependencies were installed by the root user in the &lt;code&gt;builder&lt;/code&gt; image, and they need to be readable by our user 1000 that will run the application. The &lt;code&gt;--chown&lt;/code&gt; flag will change the files' owner (and group) to the provided argument.&lt;/p&gt;

&lt;p&gt;The second line simply sets the new working directory to be the new project directory into which we copied the code from the dependencies.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=1000 ./src/ ./&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here we copy the source code back into the production image. We did this after copying the dependencies, such that the dependency layer can be cached again. Moreover, we only copy the source code, no tests, no assets, no Cucumber features. All these latter ones are not needed to run our application. Finally note that we copy it not from the &lt;code&gt;test&lt;/code&gt; stage, but again back from the outside build context. This is because we mock a lot during testing, changing some code behavior dynamically. Copying it back in from the outside context ensures we do copy the exact code that is in our Git repository, and not something that was accidentally modified during testing, etc.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["python3"]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["./main.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Finally we set an entrypoint and a command. The entrypoint defines what will always be executed on a Docker run (unless explicitly overwritten), and the command provides the default arguments unless overwritten via the Docker run arguments. We always use lists instead of full strings to ensure that the arguments get passed to the Kernel as system calls instead of being executed by a shell. This is important to ensure proper signal handling (when you want to terminate containers), and since there is simply no shell in the distroless image we are building.&lt;/p&gt;
&lt;h2&gt;
  
  
  That's it
&lt;/h2&gt;

&lt;p&gt;Holy molly... There is a lot that goes into building a simple Docker image. And that considering we did not even compile anything, which would require a decent amount of extra work, and that all our tooling can be managed directly via &lt;code&gt;pipenv&lt;/code&gt; and do not need to be installed separately via &lt;code&gt;curl&lt;/code&gt; or some OS package manager.&lt;/p&gt;

&lt;p&gt;So is it worth it? To put so much thought into how a simple Docker image gets built? I would argue yes. I will not start an idiomatic discussion on the benefits of smaller images, security best practices, or having tests being run directly in the Docker build. If you want such a discussion, go to Reddit or Youtube, you find plenty of beef between people fighting about these topics like their life depends on it. All I will say is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I can run &lt;code&gt;docker build&lt;/code&gt; ... after each save on a file, since the caching is optimized to a point where a full build on a code changes takes about 1-2 seconds. Being able to run this so often gives me the confidence that what I will push will actually pass in the pipeline.&lt;/li&gt;
&lt;li&gt;Using proper caching makes me avoid having to wait 2-5 minutes each time I want to compile something. Since 2-5 minutes is typically too little for a context switch to something else, it might be time I would have just sat around thinking about how much it sucks to wait on stuff. So it has considerably improved not only my productivity, but also my mood.&lt;/li&gt;
&lt;li&gt;Docker avoids some "it works on my machine" issues. With proper version pinning and fully reproducible builds, it really nearly eradicates the issue. Now the only time something like this can happen is when running on different Docker versions.&lt;/li&gt;
&lt;li&gt;We all sometimes would like to fix tests by skipping them to "save time" when something needs to go to production quickly. Since testing is fully baked into the build process, changing flags on Jenkins/Tekton/whatever will not allow you to skip any testing or quality checks on the code. The only way would be to comment out the test code or update the &lt;code&gt;Dockerfile&lt;/code&gt;, which would not pass a PR review. This gives me immense peace of mind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the build process and testing is (nearly) fully defined in the &lt;code&gt;Dockerfile&lt;/code&gt; which lies in the git repository, we nearly never need to change pipelines to add/change/remove anything, as all of this can be done in the repository of the corresponding image directly. This also has downsides, as it creates duplication. I would argue that this is beneficial though, as legacy applications might not be able to switch to newer tooling as fast as greenfield projects, which want to leverage that new tooling. Having this "configured" in each repository allows each to move at its own pace. Strict guidelines (such as we don't want to use tool X anymore) can still be enforced on pipeline level via container scanning tools (which you will need either way).&lt;/p&gt;

&lt;p&gt;What's the major downside of this approach? Well I would argue there is one large one. Many people might not understand Docker well enough to figure out how the build process works, or might not have time to invest to learn how to do it correctly. This means that some people might not be able to make changes to the build processes by themselves and need might help. I think this would also be the case without a proper Docker setup, but maybe this problem is augmented by having a slightly more complex Docker build setup.&lt;/p&gt;

&lt;p&gt;I hope this has given you some food for thought. Feel free to comment any questions or remarks below, or to reach out! Do you also take your Docker builds this far?&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__2319323"&gt;
    &lt;a href="/f4z3r" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2319323%2Ffcd09f88-ecfa-41b5-a18d-5ea0f067d475.jpeg" alt="f4z3r image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/f4z3r"&gt;Jakob Beckmann&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/f4z3r"&gt;I am passionate about cloud native software and infrastructure, security tooling, and various programming languages. I am currently a Principal Architect at ipt.ch, working on a variety of mandates.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__user ltag__user__id__9906"&gt;
  &lt;a href="/ipt" class="ltag__user__link profile-image-link"&gt;
    &lt;div class="ltag__user__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9906%2F5d6310d9-6d88-4bf1-a012-dd8f8b7f8ac7.png" alt="ipt image"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
      &lt;a href="/ipt" class="ltag__user__link"&gt;Innovation Process Technology AG (ipt)&lt;/a&gt;
      Follow
    &lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a href="/ipt" class="ltag__user__link"&gt;
        We are a boutique IT consultancy based in Switzerland focused on building individual solutions using leading edge technology. For more information visit our website: https://ipt.ch
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>docker</category>
      <category>sre</category>
      <category>python</category>
      <category>security</category>
    </item>
  </channel>
</rss>
