<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Learning Out Loud: My DevOps Journey]]></title><description><![CDATA[Learning Out Loud: My DevOps Journey]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 25 Jun 2026 07:07:26 GMT</lastBuildDate><atom:link href="https://learning-out-loud-my-devops-journey.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Understanding Kubernetes Gateway API: Why It Exists and What Comes After Ingress (Part 1)]]></title><description><![CDATA[Hello dear readers,
(Quick note: this post is probably best enjoyed in light mode: I’ve added a few fun doodles along the way and they show up much better there.)
Over the past few months, I’ve been t]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/understanding-kubernetes-gateway-api-why-it-exists-and-what-comes-after-ingress-part-1</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/understanding-kubernetes-gateway-api-why-it-exists-and-what-comes-after-ingress-part-1</guid><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Mon, 16 Mar 2026 10:09:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/33aef502-7669-4747-a9fc-f2ea4a4f9882.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello dear readers,</p>
<p>(<em>Quick note: this post is probably best enjoyed in</em> <em><strong>light mode</strong></em><em>: I’ve added a few fun doodles along the way and they show up much better there.)</em></p>
<p>Over the past few months, I’ve been trying to spend more time exploring different parts of the <strong>Kubernetes ecosystem</strong> as part of my open source learning journey. Sometimes that means contributing to a project, sometimes it’s digging through documentation, and other times it simply starts with building a small demo to understand how something works under the hood.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/65ccf82d-ff6a-4ae9-8d59-3b9a47362d0a.png" alt="" style="display:block;margin:0 auto" />

<p>We would all agree that kubernetes has a lot of moving pieces, and I’ve found that the best way for me to truly understand something is to slow down, experiment a bit, and then write about it in a way that would have helped <em>me</em> when I first encountered the topic.</p>
<p>The Kubernetes Steering and Security Response Committees confirmed that <a href="https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/"><strong>Ingress NGINX will be retiring in March 2026</strong></a>, and that <strong>all security patches and bug fixes will end in March</strong>.</p>
<p>Considering that this project currently powers <strong>around 50% of cloud-native environments</strong>, this is quite a significant update for the ecosystem. Many teams are now evaluating different alternatives and approaches for handling ingress traffic moving forward, and <strong>Gateway API is one of the major directions the community has been exploring</strong>.</p>
<p>Seeing this made the topic feel even more relevant to learn about. So I decided to spend some time over the weekend digging into it, reading through the documentation to understand how things actually work.</p>
<p>And as often happens with Kubernetes, that small exploration quickly turned into a deeper learning exercise.</p>
<p>Because of that, I decided to turn this into a <strong>two-part blog</strong>.</p>
<p>In <strong>this first part</strong>, we’ll focus on the concepts and the “why” behind Gateway API. We’ll do a quick recap of what Kubernetes Ingress is, look at some of the limitations people ran into over time, and then understand how Gateway API tries to address those problems through its core resources.</p>
<p>In <strong>the second part</strong>, we’ll move from theory to practice and walk through a small <strong>Gateway API demo</strong> together so we can see how these pieces actually work inside a real Kubernetes cluster.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/4eba6904-0c8a-49b3-b5e4-00607d18bbb0.png" alt="" style="display:block;margin:0 auto" />

<p>So before we jump into Gateway API, let’s take a step back and start with something familiar.</p>
<p>If Gateway API can be thought of as an evolution of Ingress, then the first step is simple:</p>
<p><strong>Let’s quickly revisit what Kubernetes Ingress is and how it works.</strong></p>
<h1>Understanding Kubernetes Ingress</h1>
<p>Before we talk about Gateway API, it helps to step back and look at something that has been part of Kubernetes networking for a long time: <strong>Ingress</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/23480ecb-2d39-4ac1-bf33-25f61d2d7f43.png" alt="" style="display:block;margin:0 auto" />

<p>If you’ve worked with Kubernetes even a little, there’s a good chance you’ve come across it. Ingress is commonly used when we want to expose services inside a cluster to traffic coming from outside. Instead of directly exposing every service, Ingress gives us a way to define <strong>rules for how incoming traffic should be routed</strong>.</p>
<p>At its core, the <strong>Ingress object natively supports two kinds of routing</strong>:</p>
<ul>
<li><p><strong>Host-based routing</strong></p>
</li>
<li><p><strong>Path-based routing</strong></p>
</li>
</ul>
<p>This is something many of us have probably used without thinking too much about it. Inside the <code>spec</code> section of an Ingress resource, we usually define things like <strong>hosts and paths</strong>, which then determine how incoming requests should reach different services.</p>
<pre><code class="language-shell">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
  namespace: mobileapp-ns
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-svc
            port:
              number: 80
</code></pre>
<p>For example, we might define that requests coming to <code>example.com/api</code> should go to one service, while requests to <code>example.com/web</code> should go to another. Kubernetes allows us to describe these routing rules in a structured way through the Ingress resource.</p>
<p>While the <strong>spec section defines the routing logic</strong>, a lot of the other important behavior, especially things related to <strong>how the load balancer should actually be configured</strong>, is often defined somewhere else: <strong>annotations</strong>.</p>
<h1>The Hidden Complexity of Annotations in Ingress</h1>
<p>In Kubernetes, <strong>annotations are essentially key–value metadata attached to objects</strong>. They were originally designed as a way to attach <strong>informational data</strong> to Kubernetes resources.</p>
<p>This information could then be read by other engineers or consumed by external tools.</p>
<pre><code class="language-yaml">metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
</code></pre>
<p>For example, a team might use annotations to attach helpful information to a resource, or tools running inside the cluster might read those annotations and take some action based on them.</p>
<p>Ingress controllers use this mechanism heavily.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/40498c0e-1211-4e8a-93c4-5bdcc01d2ec6.png" alt="" style="display:block;margin:0 auto" />

<p>When we define an Ingress resource, the routing rules go into the <code>spec</code> section. But things like <strong>how the load balancer should behave</strong> are often defined in the <strong>annotations section</strong>. In many setups, this is where we specify configuration related to the load balancer that will actually handle the incoming traffic.</p>
<p>Under the hood, an <strong>Ingress controller is constantly watching Ingress resources</strong> in the cluster. When it detects a new or updated Ingress object, it reads the annotations attached to it and uses them to configure the load balancer accordingly.</p>
<p>Now here’s an interesting detail about how Kubernetes treats these annotations.</p>
<p>When we apply a manifest using <code>kubectl</code>, there are usually two levels of validation happening:</p>
<ol>
<li><p>The <strong>kubectl client</strong> performs an initial verification.</p>
</li>
<li><p>The <strong>Kubernetes API server</strong> performs another round of validation before storing the object.</p>
</li>
</ol>
<p>But annotations are treated differently.</p>
<p>Since annotations are meant to be informational metadata, <strong>Kubernetes itself does not validate their contents</strong>. As long as the YAML syntax is valid and the structure of the manifest is correct, Kubernetes will accept it.</p>
<p>This means that if there is a mistake in the annotation configuration, Kubernetes will not complain. The manifest will still be applied successfully.</p>
<p>However, the <strong>Ingress controller that relies on those annotations may not behave the way we expect</strong>.</p>
<p>If the annotation is misconfigured, the controller may configure the load balancer incorrectly, or sometimes not configure it at all.</p>
<p>Another thing that makes this more complex is that <strong>annotations are often vendor-specific</strong>.</p>
<p>If you are using different ingress controllers such as <strong>NGINX Ingress Controller</strong>, <strong>HAProxy Ingress Controller</strong>, <strong>AWS Load Balancer Controller</strong>, or <strong>Traefik’s ingress controller</strong>, the annotations required for configuration can be completely different.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/25d51643-4743-4bd6-96f9-963278730efa.png" alt="" style="display:block;margin:0 auto" />

<p>So even though the <strong>Ingress resource itself looks standardized</strong>, the behavior behind it often depends heavily on the specific controller being used.</p>
<p>In short:</p>
<ul>
<li><p>The <strong>Ingress object natively supports host-based and path-based routing</strong>.</p>
</li>
<li><p>Many advanced behaviors are implemented through <strong>controller-specific annotations</strong>.</p>
</li>
<li><p>These annotations are <strong>not validated by Kubernetes</strong>.</p>
</li>
<li><p>And they often <strong>vary depending on the ingress controller vendor</strong>.</p>
</li>
</ul>
<p>This design worked for a long time, but as Kubernetes environments became more complex, these limitations started to become more visible.</p>
<h1>What Was Missing in Ingress?</h1>
<p>By now, we’ve seen that Kubernetes <strong>Ingress works well for basic routing</strong>. It allows us to define host-based and path-based rules, and with the help of controllers and annotations we can extend its behavior.</p>
<p>But as Kubernetes environments grew larger and more teams started running multiple applications inside the same cluster, some important limitations of Ingress began to appear. These limitations weren’t always obvious at first, but they became more visible as clusters started hosting <strong>many applications, multiple teams, and more complex networking requirements</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/ef716738-1d9b-4d0e-bd97-e1865ccaca41.png" alt="" style="display:block;margin:0 auto" />

<p>Let’s walk through some of the key things that were <strong>missing or difficult to achieve with Ingress</strong>.</p>
<h3>1. Native Multi-Tenancy and Namespace Isolation</h3>
<p>One of the first challenges comes from how <strong>Ingress resources are scoped</strong>.</p>
<p>An <strong>Ingress resource is namespaced</strong>, which means it belongs to a specific namespace in Kubernetes. At first glance, this seems perfectly normal because most Kubernetes resources are namespaced.</p>
<p>But things become tricky when we think about <strong>how applications are typically deployed in real environments</strong>.</p>
<p>In many organizations:</p>
<ul>
<li><p>Each application is deployed in <strong>its own namespace</strong></p>
</li>
<li><p>This helps maintain <strong>isolation between applications</strong></p>
</li>
<li><p>Different teams manage different namespaces</p>
</li>
<li><p>Services belonging to each application live inside their own namespace</p>
</li>
</ul>
<p>At the same time, exposing applications to the outside world usually involves a <strong>load balancer</strong>.</p>
<p>And load balancers are not something we want to create excessively, especially in cloud environments, because <strong>they incur cost</strong>. So a very common pattern is to use <strong>a single load balancer to expose multiple applications</strong>.</p>
<p>Now imagine we have <strong>10 different applications</strong>, each running in its own namespace:</p>
<pre><code class="language-plaintext">app1 -&gt; namespace: payments
app2 -&gt; namespace: orders
app3 -&gt; namespace: users
...
</code></pre>
<p>Each of these applications has its own service inside its respective namespace.</p>
<p>Ideally, we would want a <strong>single external entry point</strong> (a load balancer) that routes traffic to all these applications.</p>
<p>But here’s where the problem appears.</p>
<p>Inside an <strong>Ingress configuration</strong>, when we define routing rules, we can <strong>only reference services that exist in the same namespace as the Ingress resource</strong>.</p>
<p>That means:</p>
<ul>
<li><p>If the Ingress object is created in namespace <code>payments</code></p>
</li>
<li><p>It can only reference services inside <code>payments</code></p>
</li>
<li><p>It cannot reference services from <code>orders</code>, <code>users</code>, or any other namespace</p>
</li>
</ul>
<p>So if we wanted one Ingress resource to route traffic to services across multiple namespaces, <strong>Ingress does not allow that natively</strong>.</p>
<p>This creates an awkward situation.</p>
<p>On one hand:</p>
<ul>
<li><p>We want <strong>application isolation</strong></p>
</li>
<li><p>We want <strong>each application in its own namespace</strong></p>
</li>
</ul>
<p>But on the other hand:</p>
<ul>
<li><p>We want to <strong>reuse the same load balancer</strong></p>
</li>
<li><p>We want a <strong>single entry point for multiple applications</strong></p>
</li>
</ul>
<p>With the native Ingress resource, achieving this cleanly becomes difficult.</p>
<h3>2. Limited Control Over Who Can Modify Routing Rules</h3>
<p>Another challenge appears when <strong>multiple teams are involved</strong>.</p>
<p>Imagine again that we have multiple applications exposed through the same Ingress resource.</p>
<p>Maybe the routing rules look something like this conceptually:</p>
<pre><code class="language-plaintext">/app1 -&gt; service: app1
/app2 -&gt; service: app2
/app3 -&gt; service: app3
</code></pre>
<p>Now suppose each application is owned by a <strong>different development team</strong>.</p>
<p>Ideally, each team should only be able to modify <strong>their own routing configuration</strong>.</p>
<p>For example:</p>
<ul>
<li><p>The <strong>app1 team</strong> should manage <code>/app1</code></p>
</li>
<li><p>The <strong>app2 team</strong> should manage <code>/app2</code></p>
</li>
<li><p>The <strong>app3 team</strong> should manage <code>/app3</code></p>
</li>
</ul>
<p>But with a single Ingress resource, this becomes difficult.</p>
<p>Because the entire routing configuration lives inside <strong>one resource</strong>, anyone who needs to modify it usually needs <strong>access to the whole Ingress object</strong>.</p>
<p>That means:</p>
<ul>
<li><p>An <code>app1</code> developer could accidentally change the configuration for <code>app3</code></p>
</li>
<li><p>Or modify something unrelated to their application</p>
</li>
</ul>
<p>In other words, Ingress doesn’t provide a clean way to create <strong>clear boundaries between teams managing different routes</strong>.</p>
<h3>3. Limited Protocol Support</h3>
<p>Another limitation is related to <strong>protocol support</strong>.</p>
<p>The Kubernetes Ingress resource primarily focuses on <strong>HTTP and HTTPS traffic</strong>.</p>
<p>For many web applications this works perfectly fine. But modern systems often need to support additional protocols such as:</p>
<ul>
<li><p><strong>TCP</strong></p>
</li>
<li><p><strong>UDP</strong></p>
</li>
<li><p><strong>gRPC</strong></p>
</li>
</ul>
<p>These protocols are not natively supported by the Ingress API itself. In some cases, ingress controllers may provide ways to support them, but again those solutions tend to be <strong>controller-specific extensions rather than part of the core API</strong>.</p>
<h3>4. Important Features Hidden Inside Annotations</h3>
<p>Earlier we discussed how <strong>annotations are heavily used to extend Ingress behavior</strong>.</p>
<p>Over time, many important features ended up being implemented through annotations rather than through the core API itself.</p>
<p>Some examples include:</p>
<p><strong>Traffic splitting (Canary deployments)</strong></p>
<p>Sometimes we want to gradually release a new version of an application. Instead of sending all traffic to the new version immediately, we may want to split traffic like this:</p>
<ul>
<li><p>90% → old version</p>
</li>
<li><p>10% → new version</p>
</li>
</ul>
<p>This type of <strong>weighted routing</strong> or <strong>canary deployment</strong> is not natively defined in the Ingress spec.</p>
<p><strong>SSL Redirects</strong></p>
<p>Sometimes we want to automatically redirect requests from <strong>HTTP to HTTPS</strong>.</p>
<p>For example:</p>
<pre><code class="language-plaintext">&lt;http://myapp.com&gt;
→ redirect internally to
&lt;https://myapp.com&gt;
</code></pre>
<p>This type of behavior is typically implemented through <strong>controller-specific annotations</strong>.</p>
<p><strong>URL Rewrites</strong></p>
<p>Another common requirement is <strong>rewriting URLs</strong>.</p>
<p>For example, a request coming to:</p>
<pre><code class="language-plaintext">/shop
</code></pre>
<p>might need to be internally rewritten to:</p>
<pre><code class="language-plaintext">/store
</code></pre>
<p>Or redirected to another path.</p>
<p>Again, this behavior is usually configured using annotations instead of the native Ingress spec.</p>
<h3>5. Vendor-Specific Annotations</h3>
<p>And finally, we return to the issue we discussed earlier: <strong>annotations are vendor-specific</strong>.</p>
<p>Different ingress controllers use different annotations for configuring behavior.</p>
<p>For example, if today you are using:</p>
<ul>
<li>an <strong>AWS Load Balancer controller</strong></li>
</ul>
<p>and tomorrow you decide to migrate to another ingress controller, you may find that many of the annotations used in your manifests <strong>no longer work</strong>.</p>
<p>Because those annotations were specific to the previous controller.</p>
<p>This means that moving between controllers can require <strong>updating many of the Ingress manifests</strong>, simply because the configuration lives inside vendor-specific annotations.</p>
<h1>What Is Gateway API?</h1>
<p>After seeing some of the limitations of Ingress, the next natural question becomes: <strong>what exactly is Gateway API?</strong></p>
<p>A simple way to think about it is this:</p>
<p><strong>Gateway API is essentially the next generation of Ingress.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/e1d695e6-e557-47cb-b937-903229b68f10.png" alt="" style="display:block;margin:0 auto" />

<p>It was designed by the Kubernetes community to solve many of the problems that started appearing as clusters grew larger, applications became more complex, and more teams began sharing the same infrastructure.</p>
<p>While Ingress focused mainly on <strong>basic HTTP routing</strong>, Gateway API takes a broader approach to traffic management.</p>
<p>One of the first things that stands out is <strong>protocol support</strong>.</p>
<p>With Ingress, the native focus was mostly on <strong>HTTP and HTTPS traffic</strong>. Gateway API expands this significantly by supporting multiple protocols such as:</p>
<ul>
<li><p><strong>HTTP / HTTPS</strong></p>
</li>
<li><p><strong>TCP</strong></p>
</li>
<li><p><strong>UDP</strong></p>
</li>
<li><p><strong>gRPC</strong></p>
</li>
</ul>
<p>This makes it much more flexible for modern environments where different applications might communicate using different protocols.</p>
<p>Another important improvement is around <strong>role separation</strong>.</p>
<p>Earlier, we saw how a single Ingress resource could become difficult to manage when multiple teams needed to modify routing rules. Gateway API introduces a model that allows <strong>clear separation of responsibilities</strong>, so different teams can work on different parts of the configuration without interfering with each other.</p>
<p>In addition to that, Gateway API also focuses on <strong>portable traffic management</strong>. The goal is to reduce reliance on vendor-specific annotations and instead define many important traffic features directly as part of the API itself.</p>
<p>Some of these capabilities are treated as <strong>first-class features</strong>, meaning they are built into the API design rather than being added through controller-specific extensions.</p>
<p>So instead of relying heavily on annotations to enable these behaviors, Gateway API aims to provide <strong>structured ways to define them directly within the API model</strong>.</p>
<h3>How Gateway API Works in Kubernetes</h3>
<p>Another important thing to understand is that <strong>Gateway API is not built into Kubernetes by default</strong>.</p>
<p>Instead, it is introduced into a cluster through <strong>Custom Resource Definitions (CRDs)</strong>.</p>
<p>If you’ve worked with CRDs before, you can think of them as <strong>templates that define new types of resources inside Kubernetes</strong>. Once these CRDs are installed in the cluster, we can start creating <strong>custom resources that follow those definitions</strong>.</p>
<p>In other words:</p>
<ul>
<li><p>The <strong>CRD defines the structure</strong></p>
</li>
<li><p>The <strong>custom resource follows that structure</strong></p>
</li>
</ul>
<p>Gateway API uses this mechanism to introduce new resource types that Kubernetes does not have out of the box.</p>
<p>But defining resources alone is not enough. Something also needs to <strong>watch those resources and make sure the desired behavior actually happens</strong>.</p>
<p>This is where the <strong>controller</strong> comes in.</p>
<p>In Kubernetes, controllers are responsible for making sure that the <strong>desired state matches the actual state</strong>. For example, when we create a <strong>Deployment</strong>, there is a controller that ensures the correct number of pods are always running.</p>
<p>Gateway API follows the same pattern.</p>
<p>Once the Gateway API CRDs are installed, a <strong>compatible controller</strong> must be running in the cluster. That controller continuously watches the Gateway API resources and ensures that whatever configuration we defined is actually implemented.</p>
<p>So the overall flow looks something like this:</p>
<ol>
<li><p>Install the <strong>Gateway API CRDs</strong> into the cluster</p>
</li>
<li><p>Deploy a <strong>compatible controller</strong> that understands those resources</p>
</li>
<li><p>Create <strong>Gateway API resources</strong> that follow the CRD definitions</p>
</li>
<li><p>The controller watches those resources and <strong>configures the underlying infrastructure accordingly</strong></p>
</li>
</ol>
<h3>Who Builds and Maintains Gateway API?</h3>
<p>Gateway API is not maintained by a single vendor.</p>
<p>It is developed by the <strong>Kubernetes networking Special Interest Group (SIG)</strong>, which is a community group responsible for networking-related features in Kubernetes.</p>
<p>Because it is developed under the Kubernetes SIG umbrella, the goal is to create a <strong>standardized and portable networking API</strong> that can work across different environments and implementations.</p>
<h1>3 Stable API Kinds:</h1>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/ef297719-a0c1-4d00-841b-48fdc6fb9bea.png" alt="" style="display:block;margin:0 auto" />

<h2>1. GatewayClass</h2>
<p>When I first started looking into Gateway API, one thing that helped me a lot was thinking about it as a <strong>set of building blocks</strong>. Instead of one large resource like Ingress trying to do everything, Gateway API breaks things down into smaller, clearer pieces.</p>
<p>One of the first pieces you’ll come across is something called a <strong>GatewayClass</strong>.</p>
<p>At first glance, the name might sound a bit abstract, but the idea behind it is actually quite simple.</p>
<p>A <strong>GatewayClass</strong> is essentially a <strong>template that describes how gateways should behave</strong>.</p>
<p>You can think of it as a <strong>blueprint for gateways</strong>.</p>
<p>Just like in Kubernetes we have things like a <strong>StorageClass</strong> that defines how storage should be provisioned, a <strong>GatewayClass</strong> defines how a gateway should be implemented by a particular controller.</p>
<p>Let’s unpack that step by step.</p>
<h3>Step 1: Connecting Gateway API to a Controller</h3>
<p>Earlier we discussed that Gateway API works using <strong>controllers</strong>. A controller is responsible for watching resources and making sure the infrastructure behaves the way we defined.</p>
<p>GatewayClass is where we tell Kubernetes <strong>which controller should handle our gateway configuration</strong>.</p>
<p>For example:</p>
<pre><code class="language-plaintext">apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: example-gatewayclass
spec:
  controllerName: example.com/gateway-controller
  description: GatewayClass with parameters
  parametersRef:
    group:""
    kind: ConfigMap
    name: gatewayclass-config
    namespace: default
</code></pre>
<p>The most important part here is:</p>
<pre><code class="language-plaintext">controllerName: example.com/gateway-controller
</code></pre>
<p>What this line is saying is:</p>
<blockquote>
<p>This GatewayClass should be managed by the controller called <code>example.com/gateway-controller</code>.</p>
</blockquote>
<p>So whenever a gateway references this GatewayClass, <strong>that specific controller will take responsibility for configuring the underlying infrastructure</strong>, such as provisioning or managing a load balancer.</p>
<h3>Step 2: GatewayClass Does Not Create Infrastructure</h3>
<p>Another important thing to understand is that <strong>GatewayClass itself does not create anything at runtime</strong>.</p>
<p>It does not spin up a load balancer.</p>
<p>It does not route traffic.</p>
<p>Instead, it simply <strong>defines the template or policy that gateways will follow</strong>.</p>
<p>The actual runtime component comes later when we create a <strong>Gateway resource</strong>, which will reference this GatewayClass.</p>
<p>So the relationship looks something like this:</p>
<pre><code class="language-plaintext">GatewayClass → defines how gateways should behave
Gateway → uses that definition to create actual networking infrastructure
</code></pre>
<p>In other words:</p>
<ul>
<li><p><strong>GatewayClass defines the template</strong></p>
</li>
<li><p><strong>Gateway consumes that template</strong></p>
</li>
</ul>
<h3>Step 3: Provider-Specific Configuration</h3>
<p>Another thing a GatewayClass can hold is <strong>provider-specific parameters and default policies</strong>.</p>
<p>Remember earlier how Ingress relied heavily on annotations that were often vendor-specific?</p>
<p>Gateway API tries to organize that configuration in a more structured way.</p>
<p>Instead of scattering configuration across annotations, a GatewayClass can reference a configuration object, such as a <strong>ConfigMap</strong>, which contains provider-specific settings.</p>
<p>In the example above, this part does that:</p>
<pre><code class="language-plaintext">parametersRef:
  kind: ConfigMap
  name: gatewayclass-config
</code></pre>
<p>This allows the platform team to define <strong>default settings</strong> that should apply to gateways using this class.</p>
<h3>Step 4: Who Usually Creates GatewayClass?</h3>
<p>In most environments, <strong>GatewayClass is not something application developers create</strong>.</p>
<p>Instead, it is typically managed by the <strong>infrastructure or platform team</strong>.</p>
<p>Why?</p>
<p>Because GatewayClass defines things like:</p>
<ul>
<li><p>which controller is responsible</p>
</li>
<li><p>default policies</p>
</li>
<li><p>infrastructure-level configuration</p>
</li>
</ul>
<p>These are decisions that usually affect the <strong>entire cluster</strong>, so they are typically standardized by the platform team to ensure consistency.</p>
<p>Application teams then simply reference the existing GatewayClass when creating their gateways.</p>
<h2>2. The Gateway:</h2>
<p>In the previous section, we looked at <strong>GatewayClass</strong> and saw that it acts like a <strong>template or blueprint</strong> for gateways. It tells Kubernetes <strong>which controller should handle the gateway and what kind of configuration it should follow</strong>.</p>
<p>But GatewayClass alone does not create anything in the cluster. It simply defines how gateways should behave.</p>
<p>The resource that actually <strong>results in something real being created</strong>, such as a load balancer, is the <strong>Gateway</strong>.</p>
<p>A <strong>Gateway</strong> is essentially the configuration that tells the controller:</p>
<blockquote>
<p>“Create an entry point into the cluster that listens for traffic and routes it to applications.”</p>
</blockquote>
<p>So if GatewayClass is the <strong>blueprint</strong>, the <strong>Gateway</strong> is the actual <strong>building plan that gets executed</strong>.</p>
<p>When a Gateway is created, the controller responsible for the referenced GatewayClass reads the configuration and provisions the infrastructure required to expose applications. In many environments, this results in a <strong>load balancer being created in the cloud</strong>.</p>
<p>Let’s look at an example Gateway resource.</p>
<pre><code class="language-plaintext">apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: example-gateway
  namespace: ngf-gatewayapi-ns
spec:
  gatewayClassName: example-gatewayclass
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    hostname: example.com
    allowedRoutes:
      namespaces:
        from: All
</code></pre>
<p>Let’s break down what’s happening here in a simple way.</p>
<h3>Connecting the Gateway to Its GatewayClass</h3>
<p>One of the first things you’ll notice in the specification is this field:</p>
<pre><code class="language-plaintext">gatewayClassName: example-gatewayclass
</code></pre>
<p>This value matches the <strong>name of the GatewayClass we defined earlier</strong>.</p>
<p>What this means is:</p>
<blockquote>
<p>This Gateway should follow the configuration defined in the <code>example-gatewayclass</code>, and the controller associated with that class should manage it.</p>
</blockquote>
<p>Since the GatewayClass already tells us <strong>which controller is responsible</strong>, the controller now knows it should watch this Gateway resource and provision the required infrastructure.</p>
<p>Another interesting detail here is scope.</p>
<p>The <strong>GatewayClass is cluster-scoped</strong>, which means it does not belong to a specific namespace. That’s why when we defined it earlier, we did not include a namespace field.</p>
<p>However, <strong>Gateway resources themselves are namespaced</strong>.</p>
<p>In this example:</p>
<pre><code class="language-plaintext">namespace: ngf-gatewayapi-ns
</code></pre>
<p>This means the Gateway exists within a specific namespace, even though it references a GatewayClass that exists at the cluster level.</p>
<h3>Defining Listeners:</h3>
<p>Another important part of the Gateway configuration is the <strong>listeners</strong> section.</p>
<pre><code class="language-plaintext">listeners:
- name: http
  protocol: HTTP
  port: 80
  hostname: example.com
</code></pre>
<p>A listener defines <strong>how the gateway should accept incoming traffic</strong>.</p>
<p>You can think of a listener as a <strong>port on the load balancer that waits for incoming requests</strong>.</p>
<p>In this example, the configuration is saying:</p>
<ul>
<li><p>The gateway should listen for <strong>HTTP traffic</strong></p>
</li>
<li><p>It should listen on <strong>port 80</strong></p>
</li>
<li><p>It should accept requests for the hostname <a href="http://example.com"><strong>example.com</strong></a></p>
</li>
</ul>
<p>When the controller processes this configuration, it ensures that the resulting load balancer is configured to <strong>listen on port 80 for HTTP traffic directed at that host</strong>.</p>
<h3>Allowed Routes: Solving a Major Ingress Limitation</h3>
<p>One of the most interesting parts of this configuration is this section:</p>
<pre><code class="language-plaintext">allowedRoutes:
  namespaces:
    from: All
</code></pre>
<p>Earlier when we were discussing Ingress, we saw that <strong>Ingress resources could only reference services from the same namespace</strong>. That made it difficult to expose applications across multiple namespaces using a single entry point.</p>
<p>Gateway API introduces a cleaner way to handle this.</p>
<p>With <code>allowedRoutes</code>, we can specify <strong>which namespaces are allowed to attach their routes to this gateway</strong>.</p>
<p>In this example, the configuration says:</p>
<blockquote>
<p>Routes from <strong>all namespaces</strong> are allowed to attach to this gateway.</p>
</blockquote>
<p>This means that the load balancer created by this Gateway can serve applications that live in <strong>multiple namespaces</strong>.</p>
<p>This directly addresses one of the key limitations we discussed earlier with Ingress.</p>
<h3>Who Usually Creates Gateways?</h3>
<p>In most environments, <strong>Gateways are typically created by cluster operators or platform teams</strong>.</p>
<p>Why?</p>
<p>Because a Gateway usually represents <strong>shared infrastructure</strong>, such as a load balancer that multiple applications might use.</p>
<p>Application teams then attach their routing rules to that gateway without needing to manage the infrastructure themselves.</p>
<h2>3. Route Objects:</h2>
<p>If you remember how <strong>Ingress</strong> worked, all the routing logic lived inside a <strong>single Ingress resource</strong>.</p>
<p>That Ingress manifest usually contained:</p>
<ul>
<li><p>The <strong>host or path rules</strong></p>
</li>
<li><p>The <strong>backend service</strong></p>
</li>
<li><p>The <strong>port to forward traffic to</strong></p>
</li>
</ul>
<p>For example, an Ingress rule might say:</p>
<blockquote>
<p>If a request comes to <code>/mobileapp</code>, send it to the service <code>mobileapp-svc</code>.</p>
</blockquote>
<p>That service is typically fronting a <strong>Deployment</strong>, and that deployment runs <strong>pods that serve the application</strong>. For example, it could be pods serving an application specifically designed for <strong>mobileapp users such as iPhone clients</strong>.</p>
<p>So in the Ingress world, <strong>all of this routing logic was bundled inside a single resource</strong>.</p>
<h1>How Gateway API Handles Routing</h1>
<p>Gateway API follows the <strong>same logical idea</strong>, but with a cleaner separation of responsibilities.</p>
<p>Instead of putting everything into one object, Gateway API splits the pieces into <strong>different resources with different responsibilities</strong>.</p>
<ul>
<li><p><strong>GatewayClass</strong> → defines the gateway implementation and controller</p>
</li>
<li><p><strong>Gateway</strong> → creates the entry point (load balancer)</p>
</li>
<li><p><strong>Routes</strong> → define how traffic is routed to services</p>
</li>
</ul>
<p>The <strong>Route objects</strong> are where the actual <strong>traffic routing rules live</strong>.</p>
<p>For HTTP traffic, the most common route resource is <strong>HTTPRoute</strong>.</p>
<p>An HTTPRoute defines rules like:</p>
<ul>
<li><p>What <strong>path or hostname</strong> to match</p>
</li>
<li><p>Which <strong>backend service</strong> should receive the request</p>
</li>
<li><p>Which <strong>port</strong> of the service should be used</p>
</li>
</ul>
<p>Here is the example HTTPRoute.</p>
<pre><code class="language-plaintext">apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: mobileapp-httproute
  namespace: mobileapp-ns
spec:
  parentRefs:
  - name: example-gateway
    namespace: ngf-gatewayapi-ns
  hostnames:
  - example.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /mobileapp
    backendRefs:
    - name: mobileapp-svc
      port: 80
</code></pre>
<p>Let’s break this down step by step.</p>
<h2>Connecting the Route to the Gateway</h2>
<p>One important field in this configuration is <code>parentRefs</code>.</p>
<pre><code class="language-plaintext">parentRefs:
- name: example-gateway
  namespace: ngf-gatewayapi-ns
</code></pre>
<p>This tells Kubernetes:</p>
<blockquote>
<p>Attach this route to the <strong>Gateway called</strong> <code>example-gateway</code> that exists in the namespace <code>ngf-gatewayapi-ns</code>.</p>
</blockquote>
<h2>Defining the Routing Rule</h2>
<p>Now let’s look at the actual routing rule.</p>
<pre><code class="language-plaintext">matches:
- path:
    type: PathPrefix
    value: /mobileapp
</code></pre>
<p>This rule means:</p>
<blockquote>
<p>If the request path starts with <code>/mobileapp</code>, this rule should match.</p>
</blockquote>
<p>So if a request comes to:</p>
<pre><code class="language-plaintext">example.com/mobileapp
</code></pre>
<p>or even</p>
<pre><code class="language-plaintext">example.com/mobileapp/products
</code></pre>
<p>it will match this route.</p>
<h2>Sending Traffic to the Backend Service</h2>
<p>Once the request matches the rule, it is forwarded to the backend service defined here:</p>
<pre><code class="language-plaintext">backendRefs:
- name: mobileapp-svc
  port: 80
</code></pre>
<p>This means the traffic will be sent to:</p>
<ul>
<li><p>Service: <strong>mobileapp-svc</strong></p>
</li>
<li><p>Port: <strong>80</strong></p>
</li>
</ul>
<p>That service then forwards the traffic to the <strong>pods running the application</strong>.</p>
<h1>Wrapping Up:</h1>
<p>And that brings us to the end of the theory part of this post.</p>
<p>I realize that going through concepts like these purely through explanations can sometimes feel a bit overwhelming. Kubernetes documentation can be quite extensive, and when you're encountering something for the first time, it’s not always easy to connect all the pieces together immediately.</p>
<p>In this post, I tried to explain the ideas the same way I understood them while learning i.e. slowly breaking them down and focusing on the “why” behind the design decisions. My goal was simply to make the concepts a little easier to approach, especially if you’re seeing Gateway API for the first time.</p>
<p>That said, sometimes things only truly click once we see them working in practice.</p>
<p>So in the <strong>next part of this blog</strong>, we’ll move away from theory and walk through a <strong>hands-on demo</strong>, where we’ll actually set up these resources inside a Kubernetes cluster and see how they work together in a real environment.</p>
<p>And if some of the concepts in this post still feel a bit abstract right now, that’s completely fine, I’m fairly confident that once we go through the demo together, the pieces will start making a lot more sense.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/03ecffcd-7635-40d3-83d4-e61e9d6b1eb4.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[A Beginner’s Guide to eBPF and Cilium for Kubernetes (Part 1)]]></title><description><![CDATA[Recently, I’ve been contributing to an open-source Kubernetes project called the Node Readiness Controller. As part of that journey, I was assigned an issue that involved understanding its integration]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/a-beginner-s-guide-to-ebpf-and-cilium-for-kubernetes-part-1</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/a-beginner-s-guide-to-ebpf-and-cilium-for-kubernetes-part-1</guid><category><![CDATA[Open Source]]></category><category><![CDATA[cilium]]></category><category><![CDATA[eBPF]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Sun, 01 Mar 2026 06:37:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/f0068aa7-ee0f-44a8-8579-0d948a000b06.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I’ve been contributing to an open-source Kubernetes project called the <a href="https://github.com/kubernetes-sigs/node-readiness-controller">Node Readiness Controller</a>. As part of that journey, I was assigned an issue that involved understanding its integration with Cilium.</p>
<p>To understand the integration, I needed to understand Cilium. And to understand Cilium, I needed to understand eBPF. And to understand eBPF… I had to go all the way down to the Linux kernel.</p>
<p>This series is not meant to be an expert guide. It’s not a replacement for official documentation. It’s simply a reflection of my learning process written in a way that I hope feels approachable, especially if you’re someone who is new to these concepts.</p>
<p>Sometimes when we read about networking, kernels, or Kubernetes internals, it can feel overwhelming. There are so many layers. So many abstractions.</p>
<p>If you’re curious about how modern Kubernetes networking works…and you’ve heard terms like Cilium or eBPF but never quite understood them…Or if you just enjoy learning by walking through someone else’s thought process…</p>
<p>Then I hope this journey resonates with you.</p>
<p>Let’s start at the beginning, with something called eBPF.</p>
<h2>Before Cilium: Meeting eBPF for the First Time</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/d4448789-1c3d-4c30-80a3-fab2c2f2246f.png" alt="" style="display:block;margin:0 auto" />

<p><strong>What exactly is eBPF?</strong></p>
<p>A simple way to think about it is this:</p>
<blockquote>
<p>eBPF is a kernel technology that lets us inspect and manipulate calls made to the Linux kernel.</p>
</blockquote>
<p>Let’s make it more concrete.</p>
<p>Imagine the Linux kernel as one giant API.</p>
<p>Every time you do something on your computer i.e. open a file, create a network socket, connect to the internet, change directories, create a file system, you’re indirectly making a call to that kernel API. Whether you’re inside a container or running something directly on the host, the kernel is always involved.</p>
<p>In a way, everything eventually talks to the kernel.</p>
<p>Now here’s where eBPF becomes interesting.</p>
<p>eBPF allows us to attach small programs to different points in that kernel API surface. When a specific event happens, like a network socket being opened, we can “catch” that moment. And once we catch it, we can decide:</p>
<ul>
<li><p>Do we just observe what’s happening?</p>
</li>
<li><p>Do we collect context?</p>
</li>
<li><p>Or do we influence what happens next?</p>
</li>
</ul>
<p>And the most fascinating part is that this happens directly inside the kernel.</p>
<h2>Why People Compare eBPF to JavaScript</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/a1a401c3-1de0-4ccb-83ec-488cd909f8bc.png" alt="" style="display:block;margin:0 auto" />

<p>eBPF is sometimes described as “JavaScript for the Linux kernel.”</p>
<p>Think about how JavaScript works in a browser.</p>
<p>When you open a web page, JavaScript is shipped along with it. That code runs inside the browser environment. It listens for events i.e. a button click, a page load, a form submission and when those events happen, it executes logic.</p>
<p>eBPF works in a somewhat similar way.</p>
<p>You write an eBPF program and decide where it should attach in the system stack. Once attached, whenever the relevant event occurs, the kernel runs that program.</p>
<p>So imagine this scenario:</p>
<p>A program maybe inside a container, maybe on the host, opens a network socket.<br />The kernel notices that event.<br />There’s an eBPF program attached to that type of event.<br />The kernel executes that eBPF program.<br />The result of that program helps determine what happens next.</p>
<p>It’s happening at the kernel layer itself.</p>
<p>Instead of adding more and more layers on top, eBPF works by embedding logic directly where events originate.</p>
<h2>Cilium’s Journey With eBPF</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/6c61d1bd-8c2c-4d4f-af28-17c471be8d41.png" alt="" style="display:block;margin:0 auto" />

<p>What I found particularly interesting while learning about Cilium is that the project didn’t just <em>use</em> eBPF from day one in its current form.</p>
<p>In its early years, a significant focus was actually on helping eBPF mature to a place where something like Cilium could even be built on top of it.</p>
<p>In the beginning, eBPF wasn’t yet ready to support a full-fledged networking and security solution the way Cilium envisioned. There was groundwork to be done. Capabilities had to improve. The ecosystem needed to grow.</p>
<p>So part of Cilium’s journey was contributing to making eBPF production-ready for this kind of use case.</p>
<p>Because when you look at a polished networking solution in Kubernetes, you don’t always see the years of foundational work underneath. You don’t see the kernel-level innovation that made it possible.</p>
<p>But it’s there.</p>
<h2>So, What Is Cilium?</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/28e28497-349a-4f92-96d2-0e87a4b550f8.png" alt="" style="display:block;margin:0 auto" />

<p>Now that we’ve spent some time understanding eBPF, at least at an intuitive level we can finally ask the obvious question:</p>
<p><strong>What is Cilium?</strong></p>
<p>At its core, Cilium is a networking technology.</p>
<p>But that sentence alone doesn’t do it justice.</p>
<p>In the Kubernetes world, networking is about enabling pods to talk to other pods. It’s about allowing communication across nodes. Sometimes even across clusters. Sometimes across different cloud environments. And sometimes between cloud and physical or legacy environments.</p>
<p>In simpler terms, Cilium is focused on solving connectivity.</p>
<p>Connectivity between pods.<br />Connectivity between nodes.<br />Connectivity across clusters.<br />Connectivity across cloud and physical environments.</p>
<p>And not just connectivity, but doing it with a focus on security and observability as well.</p>
<p>Before understanding how Cilium does what it does, we had to understand something more foundational:</p>
<p><strong>CNI.</strong></p>
<h2>Laying the Groundwork: What Happens When a Pod Is Created?</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/14be7f92-d2ba-4475-86c3-abf8bde500c2.png" alt="" style="display:block;margin:0 auto" />

<p>Let’s walk through something simple.</p>
<p>A pod is created in Kubernetes.</p>
<p>Behind the scenes, kubelet is responsible for making that pod actually come to life on a node. But part of that process is about giving that pod networking.</p>
<p>And this is where CNI comes in.</p>
<p>CNI (Container Network Interface) is the mechanism kubelet calls when it needs networking for a pod.</p>
<p>So imagine kubelet saying:</p>
<blockquote>
<p>“Here’s a new pod. I need a networking stack for it.”</p>
</blockquote>
<p>If Cilium is the configured CNI, kubelet calls Cilium at this moment.</p>
<p>What does that mean in practice?</p>
<p>It means Cilium needs to create the networking stack for that pod.</p>
<p>And at a very practical level, that networking stack involves creating something called a <strong>veth pair</strong>.</p>
<h2>The veth Pair: Why You Only See One IP Inside a Pod</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/7bb8534c-9660-43bb-8719-856598777e3e.png" alt="" style="display:block;margin:0 auto" />

<p>A veth pair is essentially two connected virtual network interfaces.</p>
<p>You can think of it like a virtual cable with two ends.</p>
<ul>
<li><p>One end lives inside the container’s network namespace.</p>
</li>
<li><p>The other end lives in the host’s network stack (the underlying node).</p>
</li>
</ul>
<p>So when a pod is created:</p>
<ul>
<li><p>One side of the veth pair is placed inside the pod’s network namespace.</p>
</li>
<li><p>The other side remains in the host’s networking stack.</p>
</li>
</ul>
<p>That’s why when you exec into a pod and run something like:</p>
<pre><code class="language-shell">ip addr
</code></pre>
<p>You only see one interface (with one IP address). You don’t see all the host interfaces. You’re inside the pod’s own isolated network namespace.</p>
<p>That isolation is deliberate.</p>
<p>And Cilium is responsible for setting this up when kubelet makes the CNI call.</p>
<p>But Cilium doesn’t stop at just creating a veth pair.</p>
<h2>Identity and eBPF; Where Cilium Becomes Different</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/0dc76b7e-5ea8-47f8-9104-7a20fd2567d9.png" alt="" style="display:block;margin:0 auto" />

<p>When kubelet requests networking for a pod, Cilium:</p>
<ol>
<li><p>Creates the veth pair</p>
</li>
<li><p>Places one end in the container</p>
</li>
<li><p>Places the other end in the host network namespace</p>
</li>
</ol>
<p>But then it does something additional.</p>
<p>It generates an identity for that particular pod.</p>
<p>That identity is unique.</p>
<p>And then, this is the key part, Cilium attaches an eBPF program to the pod’s network namespace.</p>
<p>Now think back to what we discussed earlier about eBPF.</p>
<p>We said eBPF allows us to attach logic to kernel-level events.</p>
<p>So what Cilium is doing here is powerful:</p>
<p>Instead of handling networking decisions purely through traditional mechanisms, it attaches eBPF programs that operate at the kernel layer for that pod’s traffic.</p>
<p>So whenever traffic flows in or out of that pod, the eBPF logic can evaluate it.</p>
<p>So, now we are talking about:</p>
<ul>
<li><p>Enforcing identity-based decisions</p>
</li>
<li><p>Observing traffic at the kernel layer</p>
</li>
<li><p>Making networking decisions dynamically</p>
</li>
</ul>
<p>All without adding heavy user-space components and this is where eBPF becomes the engine underneath Cilium’s networking model.</p>
<h2>From Observing Traffic to Understanding It</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/7ba6e0a5-719a-424e-a43a-63864d364d78.png" alt="" style="display:block;margin:0 auto" />

<p>So now we have something important in place.</p>
<p>When Cilium sets up a pod’s networking stack, it doesn’t just create a veth pair. It also attaches an eBPF program to that pod’s network namespace.</p>
<p>And because of that, we’ve already solved one big piece of the puzzle:</p>
<p><strong>Observability.</strong></p>
<p>Why?</p>
<p>Because now, anything that happens inside that pod’s network namespace can be seen.</p>
<p>Any new connection.<br />Any new packet.<br />Any network interaction.</p>
<p>All of it can be captured as an event stream.</p>
<p>Remember what we discussed about eBPF earlier? It allows us to “catch” kernel-level events. So when traffic flows in or out of that pod, the eBPF program can observe it directly at the kernel layer.</p>
<p>The first thing this gives us is visibility.</p>
<p>Whenever a network connection is made inside that namespace, a network event can be generated. Instead of guessing what’s happening or relying purely on external monitoring tools, we can see traffic at the source, right where it enters or leaves the pod.</p>
<p>But Cilium doesn’t stop at just observing.</p>
<h2>From Observability to Control: Network Policies</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/6a387b6b-e522-49bc-a488-cea2058f8bf1.png" alt="" style="display:block;margin:0 auto" />

<p>In Kubernetes, there’s an important default behavior that often surprises beginners:</p>
<p>By default, every pod can talk to every other pod in the cluster.</p>
<p>There are no restrictions unless you explicitly define them.</p>
<p>This is where network policies come in.</p>
<p>Network policies are rules that restrict which pods can communicate with which other pods. They exist to enforce boundaries, because in real systems, not everything should be allowed to talk to everything else.</p>
<p>Cilium implements network policies using eBPF.</p>
<p>Just like we used eBPF to generate network events, we can extend that same mechanism to enforce policy.</p>
<p>So instead of only saying:</p>
<blockquote>
<p>“Hey, I saw a connection happen.”</p>
</blockquote>
<p>We can now say:</p>
<blockquote>
<p>“I saw this connection attempt… should it be allowed?”</p>
</blockquote>
<p>And because this logic runs in the kernel through eBPF, the evaluation happens at native kernel speed.</p>
<p>That’s an important detail.</p>
<p>We’re not sending traffic up to user space for evaluation. We’re not depending on an external proxy to make a decision. We’re evaluating policy directly inside the Linux kernel.</p>
<p>Once a network policy is defined, Cilium evaluates that policy for every packet that moves through that networking stack.</p>
<p>Every packet.</p>
<p>Which means the enforcement is extremely close to the traffic itself.</p>
<h2>As Close to the Wire as Possible</h2>
<p>If you think about where this enforcement is happening, it’s almost as close to the traffic as you can possibly get.</p>
<p>Because this logic is implemented at the kernel layer, you can think of it as allowing or denying traffic at the socket level i.e. even before it fully becomes just another packet moving through the system.</p>
<p>You’re affecting the traffic at its origin point.</p>
<p>We started with eBPF as a way to “inspect and manipulate kernel calls.”</p>
<p>Then we saw how Cilium uses it to attach logic to a pod’s network namespace.</p>
<p>First, that gave us observability.<br />Then, it gave us policy enforcement.</p>
<h2>The Networking Side of eBPF: Reducing the Detour</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/8cadfddd-940e-4360-94e5-9e17529c644c.png" alt="" style="display:block;margin:0 auto" />

<p>So far, we’ve seen how eBPF helps with observability and policy enforcement.</p>
<p>But there’s another piece of the story and this one is about actual packet movement.</p>
<p>Let’s imagine something simple.</p>
<p>Two pods.<br />Same host.<br />They want to talk to each other.</p>
<p>You might assume that since they’re on the same node, communication would be straightforward and fast.</p>
<p>But traditionally, that’s not always the case.</p>
<p>Here’s what typically happens without something like eBPF handling things at the kernel layer:</p>
<ol>
<li><p>A packet leaves the first pod.</p>
</li>
<li><p>It goes into the kernel.</p>
</li>
<li><p>It travels through user space mechanisms like iptables to evaluate network policies.</p>
</li>
<li><p>It gets routed appropriately.</p>
</li>
<li><p>It enters the network namespace of the second pod.</p>
</li>
<li><p>The second pod receives it.</p>
</li>
<li><p>And responses go back through the same path again.</p>
</li>
</ol>
<p>Even though both pods are sitting on the same machine, the packet still goes through multiple layers of processing.</p>
<p>Each layer adds a little overhead.</p>
<p>And those small costs add up, especially at scale.</p>
<p>If two sockets on the same machine are trying to communicate, why should they take such a long path?</p>
<h2>Short-Circuiting the Path With eBPF</h2>
<img src="https://cdn.hashnode.com/uploads/covers/684ef947de5fe28bbc52673f/3921b643-21c1-46e5-b9bc-83c0b9084ef4.png" alt="" style="display:block;margin:0 auto" />

<p>This is where eBPF changes the game.</p>
<p>Because Cilium’s logic runs at the kernel layer, it understands what’s happening at the socket level.</p>
<p>If Pod A is trying to talk to Pod B, and both are on the same host, the eBPF program can see that.</p>
<p>It knows:</p>
<ul>
<li><p>Which sockets are involved.</p>
</li>
<li><p>What identities are associated.</p>
</li>
<li><p>What the network policy says.</p>
</li>
</ul>
<p>So instead of letting the traffic travel down through multiple layers and come back up, we can evaluate the policy right there in the kernel and make a decision immediately.</p>
<p>If the policy allows it, the traffic can be short-circuited.</p>
<p>In other words, instead of sending packets through a longer processing path, we can allow those two sockets to communicate more directly.</p>
<p>This is often referred to as operating at the socket layer or even enabling socket-level load balancing.</p>
<p>We’re not waiting to process the packet after it has already traveled through multiple networking layers.</p>
<p>We’re making the decision as close to the origin of the traffic as possible.</p>
<p>And when this applies across many pods, many nodes, and potentially many clusters, the impact becomes significant.</p>
<p>And that’s the deeper story of how Cilium uses eBPF.</p>
]]></content:encoded></item><item><title><![CDATA[Why I want to attend KubeCon Mumbai 2026]]></title><description><![CDATA[Coming from a commerce background, I followed a non-traditional path into technology. Unlike many in the cloud-native community, I did not have formal computer science education and had to navigate learning independently. This perspective has shaped ...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/why-i-want-to-attend-kubecon-mumbai-2026</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/why-i-want-to-attend-kubecon-mumbai-2026</guid><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Thu, 22 Jan 2026 15:22:02 GMT</pubDate><content:encoded><![CDATA[<p>Coming from a commerce background, I followed a non-traditional path into technology. Unlike many in the cloud-native community, I did not have formal computer science education and had to navigate learning independently. This perspective has shaped the way I approach challenges, contributions, and community engagement, and I hope to use my experience to support others from similar non-technical backgrounds as they explore cloud-native technologies. I wrote a blog about my transition from non-it to tech here: <a target="_blank" href="https://learning-out-loud-my-devops-journey.hashnode.dev/curiosity-confusion-and-cloud">https://learning-out-loud-my-devops-journey.hashnode.dev/curiosity-confusion-and-cloud</a></p>
<p>My journey into technology did not begin with a single defining moment, it grew gradually. I come from a commerce background, but during my +2 years I studied “Information Practices,” an introduction to computer science that I truly enjoyed and performed well in. Even though my formal academic path continued in commerce, that subject created an early connection with technology that stayed with me.</p>
<p>During graduation, we were required to choose electives outside our core curriculum, and semester after semester I chose technology-focused subjects such as programming, databases, and cryptography. These courses allowed me to remain close to something I genuinely enjoyed learning. Looking back, I realize that my interest in technology was steadily growing alongside my primary field of study.</p>
<p>In my current role in Quality Engineering, our team encouraged us to prepare for cloud certifications, and I began learning AWS through A Cloud Guru. What initially felt like a learning requirement soon turned into something much deeper as the practical, visual explanations helped me understand how cloud services solve real-world problems. Until then, I associated “cloud” mostly with storage, but exploring compute, networking, IAM, serverless, and auto-scaling helped me appreciate the engineering behind systems. Concepts such as durability in S3 reshaped how I thought about reliability and system design.</p>
<p>Around this time, I also began exploring open source. At first, it felt overwhelming, large repositories, new workflows, and conversations that seemed far ahead of where I was. Gradually, I learned that contributions are not limited to complex code. They also include documentation, clarity, reviews, and small improvements that help others learn more comfortably. This realization encouraged me to take my first step.</p>
<p>That understanding led me toward the Kubernetes ecosystem. I was drawn to it because of its impact and the strength of the community behind it (also I wanted to make new friends). I spent time watching KubeCon talks, reading documentation, and going through beginner resources. My first contribution was a small documentation improvement i.e. removing a duplicate paragraph in a blog, but it shaped my perspective in a meaningful way. It taught me that no contribution is too small.</p>
<p>Most of my journey until then had been online, but that changed when I attended my first CNCF meetup in Kolkata. As it was my first in-person tech event, I went simply to listen and learn. What stayed with me most was the inclusiveness of the community, I felt welcomed not for my background, but for my willingness to learn. I wrote about my experiences in a blog here: <a target="_blank" href="https://learning-out-loud-my-devops-journey.hashnode.dev/2025-a-year-of-learning-community-and-firsts">https://learning-out-loud-my-devops-journey.hashnode.dev/2025-a-year-of-learning-community-and-firsts</a></p>
<p>Today, I have begun contributing more intentionally. I am an active participant in SIG-ContribEx and SIG Node (node-readiness-controller), where I attend meetings to learn from maintainers and understand contributor workflows. My current contributions focus on beginner-friendly issues and documentation clarity, and I plan to expand this involvement as I gain more ecosystem understanding. </p>
<p>I am also committed to creating a multiplier impact from my journey. I hope to share what I learn at KubeCon with the broader community through blogs, helping others navigate their first steps in open source. I write blogs regularly and plan to continue documenting my learning so that beginners, especially those transitioning from non-traditional backgrounds, can benefit from approachable explanations and real experiences. </p>
<p>Attending KubeCon Mumbai 2026 would be an important milestone as I see it as an opportunity to learn directly from practitioners, contributors, and mentors who shape the cloud-native ecosystem.</p>
<p>At the same time, coming from a modest background means that travel and conference costs are significant. I have invested considerable personal time and resources into learning cloud native technologies and contributing to open source, and without financial assistance, I would not be able to attend KubeCon. I do not have corporate sponsorship, and this support would make participation possible.</p>
<p>I’ve met many individuals from non-technical backgrounds who sometimes question whether they can transition into tech with just curiosity and a willingness to learn. Sharing the insights, experiences, and lessons I gain from KubeCon could help make that journey feel more approachable for newcomers, regardless of their background.</p>
<p>Regards,</p>
<p>Arnab Nandi</p>
]]></content:encoded></item><item><title><![CDATA[2025: A Year of Learning, Community, and Firsts]]></title><description><![CDATA[Introduction: December reflections
Hi everyone, I hope you’re doing well and finding a little time to breathe as the year comes to a close.
With only a handful of days left on the calendar, December always invites me to slow down a little. It’s my fa...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/2025-a-year-of-learning-community-and-firsts</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/2025-a-year-of-learning-community-and-firsts</guid><category><![CDATA[CNCF]]></category><category><![CDATA[Kolkata]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Blogging]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Tue, 23 Dec 2025 15:45:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766478019293/78785ce6-fbff-4dbd-96af-8d2e259cdf91.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction-december-reflections">Introduction: December reflections</h1>
<p>Hi everyone, I hope you’re doing well and finding a little time to breathe as the year comes to a close.</p>
<p>With only a handful of days left on the calendar, December always invites me to slow down a little. It’s my favorite month, it feels like the world slows down just enough to let us catch up with ourselves, space to rest after a long year, space to think about what we tried, what worked, and what didn’t.</p>
<p>Some people plan resolutions, some take breaks they’ve been postponing for months, and some simply allow themselves small joys in the form of good food, long conversations, or doing absolutely nothing without guilt.</p>
<p>I find myself reflecting a lot during this time. Thinking about where I started the year, how I felt back then, and how different or maybe similar I feel now.</p>
<p>So I decided to write this. To look back at 2025 and reflect on the moments that challenged me, the habits that slowly formed, and the growth that didn’t announce itself loudly but stayed with me anyway.</p>
<p>Before I talk about the “firsts” or the highlights, I wanted to begin here by saying that 2025, for me, wasn’t only about doing more or learning more. It was about becoming a little more aware, a little more consistent, and a little more confident than I was before.</p>
<h1 id="heading-learning-to-be-proud-of-progress">Learning to Be Proud of Progress</h1>
<p>As I look back, one of the things I feel most proud of this year is the progress I’ve made: not just professionally, but mentally and emotionally as well. It’s easy to overlook this kind of growth because it doesn’t come with certificates or announcements. But it changes how you show up every day, and that matters.</p>
<p>I wrote my very <a target="_blank" href="https://learning-out-loud-my-devops-journey.hashnode.dev/curiosity-confusion-and-cloud">first blog</a> on <strong>16 June 2025</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766466609458/93486619-1813-4bc0-8092-31b5ebf53e74.png" alt class="image--center mx-auto" /></p>
<p>Before <strong>16 June 2025</strong>, if someone had told me I’d be writing blogs regularly, I probably would’ve laughed it off. I never saw myself as a writer. That label felt too big, too serious, like something meant for people who always knew what they were doing. I didn’t. And honestly, I was afraid of being judged. Afraid that what I wrote wouldn’t be good enough, or worse, wouldn’t be worth anyone’s time.</p>
<p>Still, the idea of writing kept coming back.</p>
<p>In the early days, the hardest part was this question that kept looping in my head: <em>Why am I even doing this?</em> There were moments when that doubt felt louder than any motivation. But I had already made a decision that I would keep showing up. No matter how the first few posts were received, or even if they weren’t received at all.</p>
<p>That decision mattered more than confidence ever could.</p>
<p>I started writing with the intention of learning in public. Not to teach, not to impress, just to document what I was figuring out as I went along. I held onto a quote from <a target="_blank" href="https://www.youtube.com/@TechWorldwithNana">Techworld with Nana</a> that felt uncomfortably close to home:</p>
<blockquote>
<p><em>“Stop waiting to feel ready, stop waiting for the perfect moment: start messy, start scared, and start before you feel qualified.”</em></p>
</blockquote>
<p>So I did. I wrote while unsure. I published while questioning myself.</p>
<p>Once I made that promise to myself, to keep publishing no matter what, the pressure eased a little. I wasn’t writing to prove anything anymore. I was writing because I had decided to show up.</p>
<p>In the beginning, consistency didn’t feel natural. Some days I’d sit in front of my screen wondering what to write, questioning whether my thoughts made sense, or whether anyone would relate to them. But instead of waiting for motivation, I leaned on routine. I wrote on days when I felt confident, and I wrote on days when I didn’t.</p>
<p>I noticed that I was clearer in how I understood new concepts, simply because I knew I’d try to explain them later. Learning felt more intentional. More honest.</p>
<p>Over time, the numbers added up: <strong>48 blogs and still counting,</strong> by the end of the year. But what stayed with me was the discipline that formed in the background. The realization that consistency isn’t about pushing yourself endlessly; it’s about creating a rhythm that you can actually sustain. And without realizing it, that curiosity began nudging me toward something I had admired from afar for a long time.</p>
<p>It nudged me toward open source.</p>
<h1 id="heading-taking-the-first-step-into-open-source">Taking the First Step Into Open Source</h1>
<p>For a long time, open source felt like a world that existed somewhere far away from me.</p>
<p>Before this year, my understanding of open source was very limited. I knew Linux was open source, that much I had heard over and over again but beyond that, everything felt confusing. Big repositories, unfamiliar workflows, endless files, and conversations that seemed to assume you already knew how things worked. It felt vast, complex, and honestly, a little overwhelming. Too big to even know where to begin.</p>
<p>Out of curiosity, I started exploring what open source actually meant. I watched videos explaining the basics i.e. why open source exists, how communities work, and how people collaborate across the world. I spent time understanding what it means to contribute, learning that it’s not always about writing complex code. Sometimes it’s about documentation. Sometimes it’s about improving clarity. Sometimes it’s about fixing something small that helps someone else.</p>
<p>This journey led me to the <a target="_blank" href="https://github.com/kubernetes/kubernetes"><strong>Kubernetes project</strong></a>, the second largest open source project in the world after the Linux kernel. Even writing that sentence still feels a little unreal.</p>
<p>I didn’t choose it because it was easy. I chose it because it fascinated me. I watched KubeCon talks, beginner guides, and “how to get started with Kubernetes” videos, trying to make sense of this massive ecosystem. The more I learned, the more curious I became. And slowly, that curiosity replaced the fear of starting.</p>
<p>Eventually, I decided to try.</p>
<p>My first contribution wasn’t code-heavy. In fact, it was something very small: removing a duplicate paragraph from a blog.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766482096750/97db254a-1a88-46e8-855d-ce4356ea6158.png" alt class="image--center mx-auto" /></p>
<p>You can check out my first contribution <a target="_blank" href="https://github.com/kubernetes/website/pull/53236">here</a>.</p>
<p>On the surface, it might seem insignificant. But the moment my pull request was merged, it genuinely made my day.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766482179695/6c4a01cb-ef90-429f-a5a3-0e589e6077af.png" alt class="image--center mx-auto" /></p>
<p>It marked the beginning of my open source journey. It told me that I could participate. That I could contribute. That I didn’t need to be an expert to take the first step.</p>
<p>I never hesitated just because the contribution wasn’t code. By then, I had already understood that: <strong>no contribution is too small</strong>. Even showing up for yourself, choosing to be curious, choosing to learn, choosing to engage, is a contribution in its own way.</p>
<p>That first merged pull request gave me confidence, not in my skills alone, but in the idea that I belonged. That I could grow into this space.</p>
<p>And that’s what I hope anyone new to tech takes away from this: you don’t need to know everything to start. You don’t need to be perfect. Small contributions matter. Curiosity matters. And sometimes, all it takes is deciding to begin.</p>
<p>And while open source gave me a sense of belonging online, the next experience helped me feel that same connection in person, for the very first time.</p>
<h1 id="heading-experiencing-the-community-in-person-my-first-cncf-meetup">Experiencing the Community in Person: My First CNCF Meetup</h1>
<p>Up until this point, most of my learning and interactions had lived behind a screen. Talks, tutorials, discussions, everything happened online. I had spent hours watching KubeCon sessions, listening to people speak about communities, meetups, and the incredible work happening across the cloud native ecosystem. It always sounded exciting, but also distant, like something I was only meant to observe from afar.</p>
<p>That changed when I finally attended my first <strong>CNCF meetup in Kolkata</strong>.</p>
<p>When I entered the venue, the first thing I noticed was how quiet it was. There were only five or six people inside at that moment. I didn’t know anyone there, so I did the simplest thing I could i.e. I picked what felt like the best seat in the hall and sat down. (P.S. I’m the one in the red sweater)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766483151346/960ae761-0e65-4f07-a3ec-7dccdd3b0748.jpeg" alt class="image--center mx-auto" /></p>
<p>Since this was my first in-person tech event, I went in without many expectations. I didn’t feel the need to prepare or prove anything. I just wanted to listen, learn, and experience what a community meetup actually felt like. I had a feeling the talks would be insightful and yes, I was also excited about the swag, but what stayed with me was so much more than that.</p>
<p>The sessions covered a wide range of topics, each offering a glimpse into different parts of the cloud native world. From intelligent workflows and chaos engineering to observability, security, and multi-architecture Kubernetes, every talk brought something new to the table. Even when some topics felt advanced, the way the speakers shared their experiences made them approachable and engaging.</p>
<p>There were moments of excitement too, like recognising tools I had only used in my local projects. I had used Grafana in my local Kubernetes projects before, and finally getting a sticker for something I had actually worked with felt surprisingly rewarding. And the tote bag, beautifully designed with Kolkata’s heritage, was a lovely touch too.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766483953925/982851b2-72b0-4656-b75a-fef096823615.jpeg" alt class="image--center mx-auto" /></p>
<p>I went for the learnings and stayed for the swags but I left with something far more valuable.</p>
<p>For the first time, the community felt real. Not just names on slides or voices in videos, but people sitting next to me, asking questions, sharing experiences, and learning together. It reminded me that tech communities aren’t built only through code and conferences, they’re built through presence, conversations, and shared curiosity.</p>
<p>And just when I thought the day couldn’t get any more memorable, I had an experience that made the meetup truly unforgettable.</p>
<h1 id="heading-meeting-someone-i-admired-a-moment-to-remember">Meeting Someone I Admired: A Moment to Remember</h1>
<p>One of the surreal moments of the meetup was getting the chance to meet <a target="_blank" href="https://www.linkedin.com/in/kunaldaskd/"><strong>Kunal Das</strong></a> in person. I had been following his work for quite some time, so seeing him there felt a little unreal.</p>
<p>A few days back, I attended an event where he spoke about <strong>migrating a live Minecraft server without any downtime</strong>. It was a fun and engaging talk, technical, yes, but explained in a way that made it easy to follow and genuinely enjoyable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766484248997/78872241-5439-4dd0-95b3-343219b60a1f.png" alt class="image--center mx-auto" /></p>
<p>The idea of moving something live, something people are actively using, without disruption was fascinating, especially for someone still learning how large systems work behind the scenes.</p>
<p>What meant even more to me was getting the opportunity to have a short, casual conversation with him afterward. I thanked him for the work he’s doing and for sharing his knowledge so openly. We even managed to take a picture together! 👇</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766484374105/c1c245cd-0973-4d81-b376-f3214dc7638e.jpeg" alt class="image--center mx-auto" /></p>
<p>Encounters like this make you realize how accessible the community really is. The people you learn from online are often just as kind and approachable in person.</p>
<p>As I headed home that day, I found myself reflecting on how much had changed over the past year and how differently this December felt compared to the last.</p>
<h1 id="heading-looking-back-and-looking-ahead">Looking Back, and Looking Ahead</h1>
<p>As I reflect on the year now, that contrast feels meaningful. <strong>December 2024</strong> ended with me clearing my first AWS certification i.e. the <strong>AWS Cloud Practitioner</strong>. At the time, it felt like a big personal win, a sign that I was moving in the right direction.</p>
<p>And now, <strong>December 2025</strong> ends with me attending my very first CNCF event, surrounded by people who are building, learning, and sharing in the open. Somewhere between those two Decembers, a lot changed.</p>
<p>This year has been about <strong>small beginnings</strong> turning into meaningful experiences.</p>
<p>Each “first” reminded me that growth doesn’t always arrive with dramatic fanfare, it often comes quietly, step by step, through curiosity, persistence, and the courage to show up.</p>
<p>I feel deeply grateful for everything I’ve experienced this year. For the curiosity that pushed me to start blogging. For the courage to start writing even when I wasn’t sure. For the confidence that came from small, consistent efforts. For the first open source contribution that showed me I belonged. And for a community that welcomed me, both online and in person.</p>
<p>I decided to write this blog because there are so many people out there doing incredible work: quietly, passionately and more people deserve to know about it. Communities like these need visibility. They need participation. They need students and working professionals alike to feel that they, too, have a place here.</p>
<p>And as I step into the next year, I carry that lesson with me: excited, grateful, and hopeful for all the learning and firsts still waiting ahead.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766484866523/15d2954a-0610-43d1-8b04-eee73bfb288b.jpeg" alt class="image--center mx-auto" /></p>
<p>Here’s to a reminder that <strong>beginnings don’t need to be perfect, contributions don’t need to be massive, and showing up matters more than you think</strong>.</p>
<p>May 2026 be filled with more learning, more firsts, and more moments that shape us in ways we don’t yet imagine.</p>
<p>With that, I hope this season brings you rest, warmth, and joy.</p>
<p>Wishing you a Merry Christmas and a Happy New Year!</p>
]]></content:encoded></item><item><title><![CDATA[Day #47: [DAY-22] Two Tier Architecture Setup on AWS Using Terraform]]></title><description><![CDATA[Introduction
Hello everyone, and welcome back to Day 22.
If you’ve been following along on this learning journey with us, thank you for being here again. And if you’re just joining in, you’re very welcome too. This series has always been about learni...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-47-day-22-two-tier-architecture-setup-on-aws-using-terraform</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-47-day-22-two-tier-architecture-setup-on-aws-using-terraform</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Mon, 22 Dec 2025 17:23:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766423595267/6582f7e7-0485-445b-9117-36f5abbe5d44.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello everyone, and welcome back to Day 22.</p>
<p>If you’ve been following along on this learning journey with us, thank you for being here again. And if you’re just joining in, you’re very welcome too. This series has always been about learning together: without rushing through the fundamentals.</p>
<p>In the previous blog, we spent our time understanding <strong>AWS Policy and Governance using Terraform</strong>. We talked about how guardrails, permissions, and governance are not just “enterprise things,” but essential building blocks for any real system, no matter how small it starts. That foundation matters, because once we begin creating actual infrastructure, those decisions quietly shape everything that follows.</p>
<p>Today, we’re taking the next natural step.</p>
<p>Instead of talking only about policies and structure, we’re going to <strong>build something tangible</strong> i.e. a small but meaningful <strong>two-tier architecture on AWS using Terraform</strong>.</p>
<p><img src="https://www.eukhost.com/blog/wp-content/uploads/2010/05/Characteristics-Of-Client-Server-800x360-1.png" alt="Top Features of Client-Server Architecture" class="image--center mx-auto" /></p>
<p>If a <em>mini</em> project carries this much complexity, just imagine what a real-world production setup looks like. This is also why challenges like our <strong>30 days of learning</strong> exist, to expose us to these layers without overwhelming us all at once.</p>
<p>In this blog, we’ll walk through an architecture where:</p>
<ul>
<li><p>A web application runs on an EC2 instance</p>
</li>
<li><p>A managed MySQL database lives securely in RDS</p>
</li>
<li><p>Networking, security groups, and access boundaries are clearly defined</p>
</li>
<li><p>Sensitive database credentials are handled safely, not hardcoded</p>
</li>
</ul>
<p>We’ll move step by step, telling the story of <em>why</em> each piece exists, how it connects to the next, and how Terraform helps us keep everything organized through custom modules.</p>
<p>So take your time, read slowly, and don’t worry if some concepts feel new.</p>
<p>Before we dive in here’s a fact for today:</p>
<blockquote>
<p>Fact #22: I really like having routines and a clear plan… but somehow, I rarely stick to them exactly as I imagine. Some days I’m on track, and other days I’m juggling a dozen things at once. That’s part of why I took up this #30daysofawsterraform challenge as it gave me a framework that I could follow along.</p>
</blockquote>
<h1 id="heading-understanding-the-two-tier-architecture-were-building">Understanding the Two-Tier Architecture We’re Building</h1>
<p>Before we touch Terraform or look at any files, it’s important that we first <strong>picture the system in our minds</strong>. When things feel confusing later, and they will at some point, this mental picture is what helps everything fall back into place.</p>
<p>At a very high level, a <strong>two-tier architecture</strong> simply means we are separating responsibilities into two clear layers.</p>
<p>The <strong>first tier</strong> is what users interact with.<br />The <strong>second tier</strong> is where the data lives.</p>
<p>In our case, the first tier is a <strong>web server running on an EC2 instance</strong>, and the second tier is a <strong>MySQL database running on Amazon RDS</strong>. Both of these live inside AWS, but they live very different lives.</p>
<p>Let’s start from the user’s perspective.</p>
<p>A user sends an HTTP request from their browser. That request enters AWS through an <strong>Internet Gateway</strong> and reaches our <strong>web server</strong>, which is hosted on an EC2 instance inside a <strong>public subnet</strong>. This server runs a simple <strong>Flask application on Ubuntu</strong>, and it listens on port <strong>80</strong>, just enough to serve requests and respond.</p>
<p>This web server is protected by a <strong>security group</strong> that allows:</p>
<ul>
<li><p>HTTP traffic on port 80 from the internet</p>
</li>
<li><p>SSH access (something we should always restrict to our own IP)</p>
</li>
</ul>
<p>This is the only part of our system that the outside world can see.</p>
<p>Now, here’s where the second tier comes in.</p>
<p>Behind the scenes, our Flask application needs a place to store data. For that, we use <strong>Amazon RDS running MySQL</strong>. This database does <strong>not</strong> sit in the public subnet. Instead, it lives inside a <strong>private subnet</strong>, completely shielded from direct internet access.</p>
<p>And this is a very intentional decision.</p>
<p>The database:</p>
<ul>
<li><p>Has <strong>no public access</strong></p>
</li>
<li><p>Accepts traffic only on port <strong>3306</strong></p>
</li>
<li><p>Allows connections <strong>only from the web tier’s security group</strong></p>
</li>
</ul>
<p>In other words, even if someone knows the database exists, they cannot talk to it directly. The <strong>only trusted path</strong> to the database is through our web server.</p>
<p>There’s one more important piece that connects these two tiers in a secure way: <strong>AWS Secrets Manager</strong>.</p>
<p>Instead of writing database usernames and passwords inside our application or worse, inside Terraform files, we let Secrets Manager handle that responsibility. It generates the database password for us, stores it securely, and makes it available to the application when needed.</p>
<p>This is how our web server learns <em>how</em> to talk to the database without ever hardcoding sensitive information.</p>
<p>This is the essence of the two-tier architecture we’re building.</p>
<p>In the next section, we’ll start looking at <strong>how Terraform helps us organize all of this using custom modules</strong>, and how the root module ties everything together without becoming messy.</p>
<h1 id="heading-how-we-organize-this-infrastructure-using-terraform-modules">How We Organize This Infrastructure Using Terraform Modules</h1>
<p>As soon as infrastructure grows beyond a few resources, things can get overwhelming very quickly. This is exactly why, in this project, we’re not placing everything into one large Terraform file. Instead, we’re using <strong>custom Terraform modules</strong> to keep each responsibility clearly separated.</p>
<p>If custom modules are still new to you, that’s completely okay. We’ve already explored them in detail earlier in this journey, especially in the blog where we implemented a real-time project using modules. The idea here is the same, we’re simply applying that structure to a two-tier architecture.</p>
<p>At the center of everything, we have the <strong>root module</strong>. This is the starting point.</p>
<p>When we open the root module, the first thing we define is the Terraform setup itself.</p>
<p>We specify the Terraform version we expect and the AWS provider version we want to use. This helps ensure consistency, especially when others pull the project and run it on their machines. We also configure the AWS provider using a region that comes from variables, keeping the setup flexible and reusable.</p>
<p>Once that foundation is in place, we begin calling our custom modules: one by one.</p>
<h2 id="heading-first-module-secrets-module"><strong>First module: Secrets module</strong></h2>
<p>Before we create servers or databases, we take a small but thoughtful pause to answer an important question:<br /><strong>Where should our database credentials live?</strong></p>
<p>In many early projects, it’s common to see usernames and passwords written directly into code or configuration files. It works, but it also creates risk. In this setup, we deliberately choose a safer path by letting <strong>AWS Secrets Manager</strong> handle those sensitive details for us.</p>
<p>In the root module, the Secrets module is called very early.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421099434/02c409c8-3f23-452f-a927-1cb140c5f930.png" alt class="image--center mx-auto" /></p>
<p>Notice what we <em>don’t</em> pass into it, we never pass a password. The only database-related value we provide is the <strong>database username</strong>, along with the project name and environment. Everything else is handled automatically.</p>
<p>Inside the Secrets module, the first thing we do is generate a <strong>random password</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421146977/0116e5f1-d9a9-42a2-a05c-a3b26dedbaf4.png" alt class="image--center mx-auto" /></p>
<p>Terraform’s <code>random_password</code> resource allows us to define simple rules, such as:</p>
<ul>
<li><p>The password length</p>
</li>
<li><p>Whether special characters are included</p>
</li>
<li><p>Which special characters are allowed</p>
</li>
</ul>
<p>This means the password is strong, unpredictable, and never manually created. Once generated, it exists only in Terraform’s state and in Secrets Manager, not in our code.</p>
<p>To avoid naming conflicts, we also generate a small <strong>random ID suffix</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421185308/1c089296-79b6-4978-8aa6-2f3330853ca9.png" alt class="image--center mx-auto" /></p>
<p>This helps ensure that each secret has a unique name, even if we deploy the same project multiple times across environments.</p>
<p>With these pieces in place, we create an <strong>AWS Secrets Manager secret</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766422437299/041f5bf2-415c-4d71-96c8-c5c5c9621d2c.png" alt class="image--center mx-auto" /></p>
<p>The name of the secret includes the project name, environment, and purpose, making it easy to identify later. We also add tags so the secret remains discoverable and organized inside AWS.</p>
<p>Next, we create a <strong>secret version</strong>, which is where the actual data lives.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421424938/18e03897-416c-4f13-8e10-991434553bac.png" alt class="image--center mx-auto" /></p>
<p>Here, we store the credentials as a JSON structure. This JSON includes:</p>
<ul>
<li><p>The database username passed from the root module</p>
</li>
<li><p>The randomly generated password</p>
</li>
<li><p>The database engine (MySQL)</p>
</li>
<li><p>A placeholder for the database host</p>
</li>
</ul>
<p>Even though the database hasn’t been created yet, this is perfectly fine. Terraform allows us to define this structure now and connect the missing pieces later, once the database exists.</p>
<p>What’s important is the intent.<br />At no point do we hardcode secrets, and at no point does the application need to “know” where passwords come from. It simply consumes them securely when needed.</p>
<p>This approach gives us a few quiet but powerful benefits:</p>
<ul>
<li><p>Credentials are centrally managed</p>
</li>
<li><p>Passwords can be rotated later without changing code</p>
</li>
<li><p>Sensitive values are never exposed in plain text files</p>
</li>
</ul>
<p>With secrets handled safely, we can now move forward with confidence.</p>
<h2 id="heading-second-module-vpc-module">Second module: <strong>VPC module</strong>.</h2>
<p>This module is responsible for laying down the networking foundation. It creates:</p>
<ul>
<li><p>A VPC</p>
</li>
<li><p>A public subnet for the web server</p>
</li>
<li><p>Multiple private subnets for RDS, spread across availability zones</p>
</li>
<li><p>An internet gateway</p>
</li>
<li><p>Route tables and their associations</p>
</li>
</ul>
<p>Because RDS is a managed service designed for high availability, it needs multiple private subnets.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421615874/4fc225da-dbee-4005-959e-b7fe2bf4544a.png" alt class="image--center mx-auto" /></p>
<p>That’s why we don’t just create one private subnet, we create more than one, ensuring resilience if an availability zone goes down.</p>
<p>It’s also important to remember that private subnets still need outbound access. This is where <strong>NAT gateways</strong> come into play.</p>
<p>They allow resources like RDS to communicate outward when required, without exposing them to inbound internet traffic.</p>
<h2 id="heading-third-module-security-groups">Third module: <strong>Security groups</strong></h2>
<p>Here, responsibilities are clearly split:</p>
<ul>
<li>The web security group allows HTTP traffic on port 80 and SSH access (ideally restricted to our IP)</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421863516/f9df3f79-b847-44a2-8656-58cb02aeaea9.png" alt class="image--center mx-auto" /></p>
<ul>
<li>The database security group allows MySQL traffic on port 3306, but only from the web security group</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421926743/1de3c440-0d42-4736-bc80-19be7d3c801a.png" alt class="image--center mx-auto" /></p>
<p>This setup ensures that the database trusts only the web tier and nothing else.</p>
<h2 id="heading-fourth-module-rds-module">Fourth module: <strong>RDS module</strong>.</h2>
<p>This module receives:</p>
<ul>
<li><p>Private subnet IDs from the VPC module</p>
</li>
<li><p>The database security group ID</p>
</li>
<li><p>Database name and username from variables</p>
</li>
<li><p>The database password directly from the Secrets module</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766422051372/dcc32d58-c37f-434e-9517-3cd5d8f5a52e.png" alt class="image--center mx-auto" /></p>
<p>Notice how nothing is duplicated. Each module produces outputs, and the root module simply wires them together. This is how Terraform helps us build complex systems without losing clarity.</p>
<h2 id="heading-final-module-ec2-module">Final module: <strong>EC2 module</strong></h2>
<p>With the database securely running in the background, the final piece of our architecture is the <strong>web server</strong>. This is the part users interact with directly, and it’s created using the <strong>EC2 module</strong>.</p>
<p>When the EC2 module is called from the root module, it receives several important inputs from other parts of the system:</p>
<ul>
<li><p>The <strong>public subnet ID</strong> from the VPC module, so the instance knows where to live</p>
</li>
<li><p>The <strong>web security group ID</strong>, so traffic rules are applied correctly</p>
</li>
<li><p>The <strong>database endpoint</strong> from the RDS module</p>
</li>
<li><p>The <strong>database credentials</strong> from the Secrets Manager module</p>
</li>
</ul>
<p>Once again, nothing is duplicated. Each module provides exactly what it owns, and the EC2 module simply consumes what it needs.</p>
<p>Now, a natural question comes up here:<br /><strong>When does the application actually get deployed?</strong></p>
<p>The answer is, <strong>at instance launch time</strong>.</p>
<p>Inside the EC2 module, we use a <strong>user data script</strong>. This script lives in a templates folder and is passed into the EC2 instance as part of its metadata. When the instance starts for the first time, this script runs automatically.</p>
<p>Within that user data script:</p>
<ul>
<li><p>Required system packages are installed</p>
</li>
<li><p>Application dependencies are set up</p>
</li>
<li><p>A simple <strong>Flask application</strong> is created</p>
</li>
<li><p>Database connection details are injected into the application</p>
</li>
</ul>
<p>This is how the application learns:</p>
<ul>
<li><p>Which database to connect to</p>
</li>
<li><p>Which username and password to use</p>
</li>
</ul>
<p>All of this happens without logging into the server manually.</p>
<p>The Flask application itself is intentionally simple. It has:</p>
<ul>
<li><p>A home page</p>
</li>
<li><p>A health endpoint</p>
</li>
<li><p>A database info endpoint</p>
</li>
<li><p>Basic insert and read operations for messages</p>
</li>
</ul>
<p>There are no delete or update operations here. The goal isn’t to build a full product, it’s to clearly demonstrate how a <strong>web tier and database tier communicate securely</strong>.</p>
<p>Once the EC2 instance is up and running, Terraform outputs the <strong>public DNS name</strong> of the server. This becomes the application URL that we can open in a browser.</p>
<p>At this stage, the entire flow is complete:</p>
<ul>
<li><p>Users access the application via the public DNS</p>
</li>
<li><p>Requests hit the EC2 instance through the internet gateway</p>
</li>
<li><p>The Flask app talks to RDS using secure credentials</p>
</li>
<li><p>Data is stored safely in a private subnet</p>
</li>
</ul>
<p>Everything we planned earlier is now working together.</p>
<h1 id="heading-provisioning-the-infrastructure-and-testing-the-application">Provisioning the Infrastructure and Testing the Application</h1>
<p>At this point, all the pieces are defined. The modules are wired together, dependencies are clear, and Terraform understands the order in which everything needs to be created. Now comes the part where we simply let Terraform do its job.</p>
<p>From the project directory, we initialize Terraform. This step prepares the working directory, downloads the required provider, and gets everything ready for provisioning. Once that’s done, we run a Terraform plan.</p>
<p>The plan shows us exactly what Terraform is about to create. In our case, it lists all the resources i.e. VPC components, security groups, secrets, RDS, and the EC2 instance. Seeing this plan is reassuring. Nothing is hidden. Everything is explicit.</p>
<p>Once we’re comfortable with the plan, we apply it.</p>
<p>Terraform now starts creating resources in the correct sequence. You’ll notice that some resources come up quickly, while others, especially <strong>RDS</strong>, take a bit of time. This is expected. Managed database services don’t appear instantly, and Terraform patiently waits until they’re fully ready before moving on.</p>
<p>Eventually, the process completes, and all resources are provisioned.</p>
<p>Terraform then provides us with a few useful <strong>outputs</strong>:</p>
<ul>
<li><p>The <strong>RDS endpoint</strong></p>
</li>
<li><p>The <strong>public DNS name of the web server</strong></p>
</li>
<li><p>The application URL</p>
</li>
</ul>
<p>This is our moment of truth.</p>
<p>We take the public DNS name and open it in a browser. The page loads, and we see the application running. This confirms a few important things all at once:</p>
<ul>
<li><p>The EC2 instance is reachable</p>
</li>
<li><p>The Flask application started successfully</p>
</li>
<li><p>Networking and security groups are working as expected</p>
</li>
</ul>
<p>To test database connectivity, we try saving a message through the application. Once submitted, the message is stored in the MySQL database running in RDS.</p>
<p>And it works.</p>
<p>The message is saved, retrieved, and displayed, proving that:</p>
<ul>
<li><p>The web server can talk to the database</p>
</li>
<li><p>Secrets were injected correctly</p>
</li>
<li><p>The private subnet and security group setup is doing its job</p>
</li>
</ul>
<p>The application also exposes a few helpful endpoints. There’s a <strong>health endpoint</strong> that confirms database connectivity, and a <strong>database info endpoint</strong> that shows details like the database name, host, and MySQL version. These endpoints help us verify that everything behind the scenes is wired together correctly.</p>
<p>What’s important here isn’t the complexity of the application. It’s the clarity of the architecture. We now have a fully functioning two-tier system:</p>
<ul>
<li><p>A public-facing web tier</p>
</li>
<li><p>A private, secure database tier</p>
</li>
<li><p>Clean separation of responsibilities</p>
</li>
<li><p>Everything managed through Terraform</p>
</li>
</ul>
<p>Before we wrap up, there’s one very important reminder.</p>
<p>Resources like <strong>RDS cost money</strong>. Once you’re done exploring and testing, make sure to destroy the infrastructure. Terraform makes this easy, and it’s a habit worth building early.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And with that, we’ve completed Day 22 of the 30 Days of Terraform Challenge. Today’s demo covered something really important: understanding how we can <strong>build a two-tier architecture step by step while keeping everything secure and modular</strong>.</p>
<p>Sometimes things just make more sense visually, and for that, here’s a helpful video by Piyush Sachdeva, which explains everything including the demo clearly, with some troubleshooting steps you can follow along.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/7XcqRDVMv3o?si=RKKH6HjH0i6-NLsu">https://youtu.be/7XcqRDVMv3o?si=RKKH6HjH0i6-NLsu</a></div>
<p> </p>
<p>We’ll continue our journey tomorrow, exploring the next steps together. Until then, happy terraforming!</p>
]]></content:encoded></item><item><title><![CDATA[DAY #46: [DAY-21] AWS Policy and Governance Setup Using Terraform]]></title><description><![CDATA[Introduction
Hello and welcome back to 30 Days of AWS Terraform.
In the previous part of this journey, we spent our time building Terraform custom modules for EKS. We focused on structure, reusability, and how to keep our infrastructure code clean an...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-46-day-21-aws-policy-and-governance-setup-using-terraform</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-46-day-21-aws-policy-and-governance-setup-using-terraform</guid><category><![CDATA[AWS]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Fri, 19 Dec 2025 13:08:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766149352604/f324bf8c-ae1c-47a5-9840-22d5cb6f8a5a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back to <strong>30 Days of AWS Terraform</strong>.</p>
<p>In the previous part of this journey, we spent our time building <strong>Terraform custom modules for EKS</strong>. We focused on structure, reusability, and how to keep our infrastructure code clean and manageable as things start to grow. That work was important and it gave us a solid foundation and showed us how real-world teams avoid repeating themselves when managing cloud infrastructure.</p>
<p>But as our setup grows, something interesting starts to happen.</p>
<p>It’s no longer just about <em>creating</em> resources. It’s also about making sure everyone uses them in the right way.</p>
<p>This brings us to <strong>Day 21</strong>, where we take the next natural step in our real-world project: <strong>AWS Policy and Governance using Terraform</strong>.</p>
<p><img src="https://assets.nationbuilder.com/commongrace/pages/4020/meta_images/original/feature_Day_21.jpg?1576816591" alt="Day 21 - What are we longing for? - Common Grace" /></p>
<p>Think of it this way, once multiple people are working in the same AWS environment, we need a shared understanding of what’s allowed and what’s not. We need some guardrails. Not to slow anyone down, but to protect the system, the data, and the team itself. This is where policy and governance step in and do their job.</p>
<p>In this blog, we’re building a <strong>real-world audit and compliance setup</strong>, the kind you’d actually see inside an organization. Step by step, we’ll look at how we can:</p>
<ul>
<li><p>Prevent certain risky actions before they happen</p>
</li>
<li><p>Detect resources that don’t follow our standards</p>
</li>
<li><p>Keep a clear, secure record of what’s happening in our AWS account</p>
</li>
</ul>
<p>And we’ll do all of this using <strong>Terraform</strong>, so the entire setup is automated, repeatable, and easy to understand.</p>
<p>Let’s take this next step together and start putting some thoughtful rules and visibility around the infrastructure we’ve already built.</p>
<h1 id="heading-why-policy-and-governance-matter-and-why-we-need-both">Why Policy and Governance Matter (and Why We Need Both)</h1>
<p>Before we touch any Terraform files or AWS services, it’s important that we understand <strong>what we really mean by policy and governance</strong>. These words often sound heavy or intimidating, they’re actually very simple and very practical.</p>
<p>In any real-world setup, whether it’s a company, a team, or even a shared home, there are always <strong>some basic rules</strong>. These rules aren’t there to restrict creativity; they’re there to keep things safe, predictable, and manageable. The same idea applies to our AWS environment.</p>
<p>Policy and governance are two sides of that same idea.</p>
<p>Let’s start with <strong>policy</strong>.</p>
<p>When we talk about policy in AWS, we’re really talking about <strong>rules that are enforced upfront</strong>. These rules decide what actions are allowed and what actions should be blocked. If something breaks the rule, it simply doesn’t go through.</p>
<p><img src="https://media3.giphy.com/media/v1.Y2lkPWVjZjA1ZTQ3d3B1dm80NDFvaHIxZXN3eGltdzZjeDlybWFtZGZpNWNiN2RwbzU0ayZlcD12MV9naWZzX3NlYXJjaCZjdD1n/IQIn0hrwSvHFt0l5H6/giphy.gif" alt="Happy Text GIF by Pixel Parade App" class="image--center mx-auto" /></p>
<p>For example, imagine a rule that says:<br />“If a user does not have Multi-Factor Authentication (MFA) enabled, they should not be allowed to delete important resources.”</p>
<p>That’s a policy. The moment someone tries to perform that action without MFA, AWS checks the rule and immediately denies the request. Nothing happens. The system protects itself.</p>
<p>Another example could be around data security. Let’s say we want to make sure that whenever someone uploads a file to an S3 bucket, that data is <strong>secure while it’s being transferred</strong>. In simple terms, that means uploads must happen over HTTPS, not plain HTTP. If someone tries to upload data in an insecure way, the request is blocked right there.</p>
<p>We can even create policies around <strong>tags</strong>. For instance, we might decide that every resource created in AWS must have an <code>Environment</code> tag like <code>dev</code>, <code>staging</code>, or <code>prod</code>. If someone forgets to add it, the resource creation fails. This helps keep things organized as the environment grows.</p>
<p>All of these are examples of <strong>IAM policies</strong>. They act like strict gatekeepers. If the request doesn’t meet the rules, it doesn’t get through. Simple as that.</p>
<p>Now, here’s where <strong>governance</strong> comes in.</p>
<p><img src="https://i.gifer.com/6wxk.gif" alt="Governance GIFs - Get the best gif on GIFER" class="image--center mx-auto" /></p>
<p>Governance doesn’t stop actions before they happen. Instead, it focuses on <strong>visibility and accountability</strong>. It helps us answer questions like:</p>
<ul>
<li><p>Are our resources following the rules?</p>
</li>
<li><p>What changed, and when?</p>
</li>
<li><p>Which resources are compliant, and which ones are not?</p>
</li>
</ul>
<p>In our real-world project, we’ll handle governance using <strong>AWS Config</strong>.</p>
<p><img src="https://i.ytimg.com/vi/MJDuAvNEv64/maxresdefault.jpg" alt="Config Tool – AWS Config – Amazon Web Services" class="image--center mx-auto" /></p>
<p>AWS Config continuously looks at the resources in our account and checks them against a set of rules. If a resource doesn’t follow a rule, AWS Config doesn’t block it, but it <strong>records it as non-compliant</strong>.</p>
<p>For example, an S3 bucket might get created without encryption. AWS Config will allow it, but it will flag it as non-compliant. That way, we always know what needs attention. This is extremely important for audits, security reviews, and long-term maintenance.</p>
<p>So to put it simply:</p>
<ul>
<li><p><strong>Policy</strong> prevents bad actions from happening</p>
</li>
<li><p><strong>Governance</strong> keeps track of what’s happening and what needs fixing</p>
</li>
</ul>
<p>In this project, we’ll use both together. We’ll prevent risky behavior using IAM policies, and we’ll monitor and record compliance using AWS Config. Finally, we’ll store all of this audit and compliance data securely in an S3 bucket.</p>
<p>This combination is what makes the setup feel real and practical, exactly how things are done in real organizations.</p>
<h1 id="heading-laying-the-foundation-setting-up-a-secure-audit-bucket">Laying the Foundation: Setting Up a Secure Audit Bucket</h1>
<p>Before we start enforcing rules or checking compliance, we need a <strong>safe place to store our records</strong>. In any real-world organization, audits and compliance data are treated very carefully. These logs tell the story of what happened in the environment, and losing them, or exposing them can be risky.</p>
<p>So in our project, the very first thing we do is create a <strong>dedicated S3 bucket</strong> that will store:</p>
<ul>
<li><p>AWS Config audit logs</p>
</li>
<li><p>Compliance and non-compliance details</p>
</li>
<li><p>Configuration history over time</p>
</li>
</ul>
<p>This bucket becomes the backbone of our governance setup.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1200/0*juR1tUliN8XuSJy9" alt="Data sources episode 2: AWS S3 to Postgres Data Sync using Singer | by  Community Post | Mage AI" class="image--center mx-auto" /></p>
<p>We define all of this inside our <code>main.tf</code> file.</p>
<p>We start by creating the S3 bucket itself. To make sure the bucket name is unique (since S3 bucket names are global), we attach a <strong>random suffix</strong> at the end. This way, we don’t have to worry about name conflicts, and the bucket still clearly belongs to our project.</p>
<p>Once the bucket is created, we immediately add <strong>tags</strong> to it. These tags help us identify the purpose of the bucket and keep it aligned with the rest of our infrastructure. Tagging might feel small right now, but it becomes extremely valuable as environments grow.</p>
<p>Next, we enable <strong>versioning</strong> on the bucket.</p>
<p>Versioning ensures that even if a file is overwritten or deleted, previous versions are still preserved. For audit data, this is especially important. We never want to lose historical information about what happened in the past.</p>
<p>After that, we enable <strong>server-side encryption</strong> on the bucket.</p>
<p>Here, we’re telling AWS that every object stored in this bucket must be encrypted at rest. We use a standard encryption algorithm provided by AWS, which means the data is protected automatically without any extra effort from users or services writing to the bucket.</p>
<p>Then comes one of the most important steps: <strong>blocking public access</strong>.</p>
<p>This is an audit bucket. It should never be exposed to the internet. So we explicitly block:</p>
<ul>
<li><p>Public ACLs</p>
</li>
<li><p>Public bucket policies</p>
</li>
<li><p>Any form of public access</p>
</li>
</ul>
<p>By setting all of these to true, we make sure the bucket stays private by default. This aligns with best practices and prevents accidental exposure.</p>
<p>Now that the bucket itself is secure, we attach a <strong>bucket policy</strong>.</p>
<p>This policy serves two main purposes.</p>
<p>First, it allows the <strong>AWS Config service</strong> to interact with the bucket. AWS Config needs permissions to:</p>
<ul>
<li><p>Check the bucket’s ACL</p>
</li>
<li><p>List the bucket</p>
</li>
<li><p>Upload audit files</p>
</li>
</ul>
<p>Without this policy, AWS Config wouldn’t be able to store its data.</p>
<p>Second, the bucket policy also <strong>denies any insecure traffic</strong>. If someone, or some service, tries to access the bucket over HTTP instead of HTTPS, the request is denied. This enforces encryption in transit and ensures data is always transferred securely.</p>
<p>At this point, we’ve done something very important for our real-world project.</p>
<p>We haven’t enforced any rules yet. We haven’t checked compliance yet.<br />But we’ve created a <strong>secure, private, versioned, and encrypted foundation</strong> where all governance-related data will live.</p>
<p>With this base in place, we’re now ready to move into the next part of the story i.e. <strong>using IAM policies to prevent risky actions before they happen</strong>.</p>
<h1 id="heading-putting-guardrails-in-place-enforcing-rules-with-iam-policies">Putting Guardrails in Place: Enforcing Rules with IAM Policies</h1>
<p>Now that we have a secure audit bucket in place, we move to the next important layer of our real-world project: <strong>preventing risky actions before they happen</strong>.</p>
<p>This is where <strong>IAM policies</strong> come into the picture.</p>
<p>In simple terms, IAM policies act like strict rules. If a request doesn’t meet the rule, AWS doesn’t even allow the action to happen. There’s no warning, no log first, the request is just blocked. This makes IAM policies perfect for enforcing non-negotiable security requirements.</p>
<p>Let’s start with the first policy.</p>
<h2 id="heading-enforcing-mfa-for-deleting-s3-objects">Enforcing MFA for Deleting S3 Objects</h2>
<p>Deleting data from S3 is a sensitive action. In real environments, we never want this to happen casually. That’s why our first policy ensures that <strong>MFA must be enabled before any S3 object can be deleted</strong>.</p>
<p>Here’s the Terraform code for that policy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766145931515/e4bfa006-bd8b-4e14-94e3-f4f71547bec5.png" alt class="image--center mx-auto" /></p>
<p>Let’s slowly understand what’s happening here.</p>
<p>We’re creating a <strong>custom IAM policy</strong>. The effect is set to <code>Deny</code>, which is important, in AWS, an explicit deny always wins. The action we are targeting is <code>s3:DeleteObject</code>, and we apply it broadly to all resources.</p>
<p>The real heart of this policy is the <strong>condition</strong>.</p>
<p>AWS checks whether <code>aws:MultiFactorAuthPresent</code> is false. If the user does not have MFA enabled, the delete request is denied. If MFA is enabled, the policy does nothing and allows the request to proceed.</p>
<p>This single rule already removes a huge amount of risk from our environment.</p>
<h2 id="heading-enforcing-encryption-in-transit-for-s3-uploads">Enforcing Encryption in Transit for S3 Uploads</h2>
<p>Next, we focus on protecting data <strong>while it’s being transferred</strong>.</p>
<p>Whenever someone uploads an object to S3, that data travels over the network. We want to make sure it always travels securely using HTTPS.</p>
<p>Here’s the policy that enforces that:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146002218/249cd09c-1624-4747-9c3a-6b1f21ed708a.png" alt class="image--center mx-auto" /></p>
<p>This policy is very direct.</p>
<p>If someone tries to upload an object using insecure transport, meaning HTTP instead of HTTPS, AWS sets <code>aws:SecureTransport</code> to false. When that happens, the policy denies the request.</p>
<p>The user doesn’t have to remember anything. The rule enforces secure behavior automatically.</p>
<h2 id="heading-requiring-tags-when-creating-ec2-instances">Requiring Tags When Creating EC2 Instances</h2>
<p>As environments grow, <strong>untagged resources quickly turn into chaos</strong>. So in real-world projects, tagging is not optional, it’s enforced.</p>
<p>Here’s the policy that makes sure EC2 instances are created with the right tags:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146071764/e3a638b4-c4a9-487c-bb28-a5e4026c7b14.png" alt class="image--center mx-auto" /></p>
<p>This policy does two checks during EC2 creation.</p>
<p>First, it checks whether the <code>Environment</code> tag exists and whether its value is one of <code>dev</code>, <code>staging</code>, or <code>prod</code>. If not, the request is denied.</p>
<p>Second, it checks whether the <code>Owner</code> tag exists at all. If it’s missing, the request is denied.</p>
<p>Together, these rules ensure that every EC2 instance has context and accountability from day one.</p>
<h2 id="heading-creating-a-demo-user-to-see-this-in-action">Creating a Demo User to See This in Action</h2>
<p>To demonstrate how these policies behave, we create a simple IAM user:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146113579/fdb06b45-aa3e-4983-b326-8834ef9badca.png" alt class="image--center mx-auto" /></p>
<p>And then we attach the MFA delete policy to this user:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146145472/94b485e9-61b0-4700-9563-fb7cc0221eb4.png" alt class="image--center mx-auto" /></p>
<p>This helps us clearly see how the policy behaves when MFA is missing versus when it’s enabled.</p>
<p>At this point, we’ve done something powerful.</p>
<p>We’ve <strong>stopped certain bad actions from ever happening</strong>.<br />But we still don’t know whether all existing resources are compliant.</p>
<p>That visibility comes next, and that’s where <strong>AWS Config</strong> enters the story.</p>
<h1 id="heading-keeping-an-eye-on-everything-introducing-aws-config">Keeping an Eye on Everything: Introducing AWS Config</h1>
<p>So far in our real-world project, we’ve put <strong>strong guardrails</strong> in place using IAM policies. Certain risky actions are now blocked before they can even happen. That already gives us a safer environment.</p>
<p>But in real organizations, prevention alone is not enough.</p>
<p>We also need <strong>visibility</strong>.</p>
<p>We need to know:</p>
<ul>
<li><p>What resources exist in the account?</p>
</li>
<li><p>Are they following our standards?</p>
</li>
<li><p>If something drifts from what we expect, can we see it clearly?</p>
</li>
</ul>
<p>This is where <strong>AWS Config</strong> becomes an essential part of governance.</p>
<p>AWS Config does not stop actions. Instead, it <strong>observes, records, and evaluates</strong> what’s happening in the account.</p>
<p><img src="https://media4.giphy.com/media/v1.Y2lkPTZjMDliOTUyeXQ2YWNjb2xkaDc4a3c5YnI0d3Q4a2Y0Z2Fob25oaDRieDA5czZsOSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/L17xM7PvLcqJggsCYa/200.gif" alt="Takingnotes GIFs - Find &amp; Share on GIPHY" class="image--center mx-auto" /></p>
<p>It continuously checks resources against predefined rules and tells us whether they are <strong>compliant or non-compliant</strong>.</p>
<p>To make AWS Config work, we need three things:</p>
<ol>
<li><p>A role that AWS Config can assume</p>
</li>
<li><p>A recorder to capture configuration changes</p>
</li>
<li><p>A delivery channel to store the data</p>
</li>
</ol>
<p>Let’s walk through each of these, one step at a time.</p>
<h2 id="heading-giving-aws-config-the-right-permissions">Giving AWS Config the Right Permissions</h2>
<p>Even though we already allowed AWS Config access to our S3 bucket using a bucket policy, AWS Config itself still needs permission to act inside our account. For that, we create an <strong>IAM role</strong> that AWS Config can assume.</p>
<p>Here’s the role definition:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146310303/325048eb-6d4a-414b-9b6d-0b30a87285ef.png" alt class="image--center mx-auto" /></p>
<p>This role is very focused. We’re simply telling AWS:<br />“Allow the AWS Config service to assume this role.”</p>
<p>Next, we attach the <strong>AWS-managed policy</strong> that gives AWS Config its standard permissions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146363594/6d173c1c-1775-4401-b4a7-e9be57eb3d38.png" alt class="image--center mx-auto" /></p>
<p>This managed policy allows AWS Config to evaluate resources, record configuration changes, and perform its core functionality.</p>
<p>But we’re not done yet.</p>
<p>AWS Config also needs permission to <strong>write data into our audit S3 bucket</strong>. For that, we attach an additional inline policy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146400886/bec40c64-2b19-4271-9506-42e7021918f3.png" alt class="image--center mx-auto" /></p>
<p>This policy allows AWS Config to check bucket versioning and read and write objects inside the bucket. Together with the bucket policy we created earlier, this completes the permission setup cleanly and securely.</p>
<h2 id="heading-turning-on-aws-config-recording">Turning On AWS Config Recording</h2>
<p>Now that permissions are in place, we can tell AWS Config <strong>what to record</strong>.</p>
<p>We start by creating a <strong>configuration recorder</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146446378/d0997c40-2320-41be-b2b6-aa29d7dc3787.png" alt class="image--center mx-auto" /></p>
<p>Here, we’re asking AWS Config to record:</p>
<ul>
<li><p>All supported resource types</p>
</li>
<li><p>Global resources as well (like IAM)</p>
</li>
</ul>
<p>This ensures broad visibility across the account.</p>
<p>Next, we define a <strong>delivery channel</strong>, which tells AWS Config where to store the recorded data:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146475945/2e3762ac-5dbd-4b34-b564-b0e8561a35ce.png" alt class="image--center mx-auto" /></p>
<p>We point it to the secure audit bucket we created earlier. This connects everything together.</p>
<p>Finally, we enable the recorder:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146500725/fc97d23d-07d6-4d7b-a0cd-de41eabe06e4.png" alt class="image--center mx-auto" /></p>
<p>This step is important. AWS Config does nothing until the recorder is enabled. We also make sure it only starts <strong>after</strong> the delivery channel exists, so data has somewhere to go.</p>
<p>At this point, AWS Config is alive and watching.</p>
<p>But it’s not evaluating anything yet.</p>
<p>That’s where <strong>Config Rules</strong> come in and that’s what we’ll build next.</p>
<h1 id="heading-checking-whats-compliant-defining-aws-config-rules">Checking What’s Compliant: Defining AWS Config Rules</h1>
<p>Now that AWS Config is up and running, recording everything and sending data to our secure audit bucket, it’s time to answer the most important governance question:</p>
<p><strong>Are our resources actually following the rules?</strong></p>
<p>This is where <strong>AWS Config rules</strong> come into play.</p>
<p>Config rules don’t block anything. Instead, they continuously evaluate resources and tell us whether they are <strong>compliant or non-compliant</strong>. This distinction is important. IAM policies prevent actions, while Config rules observe and report.</p>
<p>In real-world environments, this combination is powerful. We prevent critical mistakes upfront, and we detect everything else so it can be reviewed, fixed, and audited later.</p>
<p>Let’s walk through the rules we define in this project.</p>
<h2 id="heading-making-sure-s3-buckets-dont-allow-public-write-access">Making Sure S3 Buckets Don’t Allow Public Write Access</h2>
<p>The first rule ensures that <strong>no S3 bucket allows public write access</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146730644/56349cd3-b7c8-480e-970e-24c594a53b3f.png" alt class="image--center mx-auto" /></p>
<p>This is an <strong>AWS-managed rule</strong>, which means AWS already knows how to evaluate it. We simply reference it using the source identifier.</p>
<p>Once this rule is active, AWS Config continuously checks every S3 bucket. If any bucket allows public write access, it is immediately flagged as <strong>non-compliant</strong>.</p>
<h2 id="heading-ensuring-s3-buckets-are-encrypted-at-rest">Ensuring S3 Buckets Are Encrypted at Rest</h2>
<p>Next, we make sure that all S3 buckets have <strong>server-side encryption enabled</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146776645/d93365e2-e20c-4150-bfdb-49e35570979d.png" alt class="image--center mx-auto" /></p>
<p>If a bucket is created without encryption, AWS Config doesn’t block it, but it records it as non-compliant. This gives us visibility and a clear action item to fix the issue.</p>
<h2 id="heading-blocking-public-read-access-on-s3-buckets">Blocking Public Read Access on S3 Buckets</h2>
<p>Public read access can expose sensitive data unintentionally. So we add another rule to detect that:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766146807861/61201911-3b83-41ae-8900-ba24fbfff944.png" alt class="image--center mx-auto" /></p>
<p>Together with the previous rule, this ensures that S3 buckets are neither publicly writable nor readable.</p>
<p>At this stage, something important has happened in our project.</p>
<ul>
<li><p>IAM policies are <strong>preventing risky actions</strong></p>
</li>
<li><p>AWS Config is <strong>recording everything</strong></p>
</li>
<li><p>Config rules are <strong>telling us what’s compliant and what isn’t</strong></p>
</li>
<li><p>All audit data is stored securely in our S3 bucket</p>
</li>
</ul>
<p>This is exactly how policy and governance come together in real environments.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>As we reach the end of <strong>Day 21</strong> of our <em>30 Days of AWS Terraform</em> journey, it’s worth taking a quiet moment to look back at what we’ve built together in this real-world project.</p>
<p>We designed a <strong>thoughtful policy and governance setup</strong>, the kind that real teams rely on every day to keep their cloud environments safe, visible, and manageable.</p>
<p>This was a slightly longer blog, and that’s completely okay. You don’t have to absorb everything in one go. I chose not to shorten it because policy and governance have many moving pieces, and writing them down step by step helps show how everything connects in a real setup. Even skimming through and coming back later is perfectly fine.</p>
<p>If you feel that things make more sense when you <em>see</em> them happening rather than just reading about them, there’s also a video by <strong>Piyush Sachdeva</strong> that walks through the entire setup, including the EKS demo, in a very clear and practical way. It’s a great companion if you want to follow along visually or understand some common troubleshooting steps.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/sAtbDGi-82A?si=gPaQPxknsDSC-50q">https://youtu.be/sAtbDGi-82A?si=gPaQPxknsDSC-50q</a></div>
<p> </p>
<p>That’s it for <strong>Day 21</strong>.</p>
<p>Thank you for learning along with us, for taking the time to understand not just the <em>how</em>, but the <em>why</em>. Tomorrow, we’ll be back with <strong>Day 22</strong>, where we’ll continue building more hands-on, real-world projects and deepen our understanding step by step.</p>
<p>Until then: keep learning, keep experimenting, and keep moving forward.</p>
]]></content:encoded></item><item><title><![CDATA[DAY #45: [DAY-20] AWS Terraform Modules]]></title><description><![CDATA[Introduction
Welcome back to Day 20 of our 30 Days of AWS Terraform journey.
Last day, we explored how Terraform provisioners help us go beyond simply declaring infrastructure and how they allow us to bridge that small but important gap between creat...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-45-day-20-aws-terraform-modules</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-45-day-20-aws-terraform-modules</guid><category><![CDATA[AWS]]></category><category><![CDATA[EKS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Thu, 18 Dec 2025 14:07:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766066765533/735b7dfe-8039-410e-b4f6-945cf52a4d96.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Welcome back to Day 20 of our <strong>30 Days of AWS Terraform</strong> journey.</p>
<p>Last day, we explored how Terraform provisioners help us go beyond simply declaring infrastructure and how they allow us to bridge that small but important gap between <em>creating</em> resources and actually <em>preparing</em> them for real use.</p>
<p>With <strong>only 10 days left</strong> in our 30-day challenge, it feels like the perfect moment to start thinking about structure, organization, and reusability.</p>
<p><img src="https://flipanim.com/gif/o/6/O6gefeUj.gif" alt="Birthday countdown #2 - FlipAnim" class="image--center mx-auto" /></p>
<p>Up until now, most of our Terraform work lived comfortably inside a single folder, with files like <code>main.tf</code>, <code>variables.tf</code>, and <code>outputs.tf</code> sitting together and doing their job. That approach is perfectly fine when we’re learning or experimenting. But as our projects start to look more like something we’d run in the real world, a question naturally begins to surface: <em>how do we keep this clean, reusable, and manageable as it grows?</em></p>
<p>This is where today’s topic enters the picture. In this blog, we will not include the full EKS cluster setup, as going through the entire cluster creation here would be overwhelming. Instead, we’ll focus on <strong>understanding modules and how they make complex Terraform projects manageable</strong>, using simplified examples that illustrate the concepts clearly.</p>
<p>We’ll understand <em>why</em> modules exist, <em>what problem they solve</em>, and <em>how</em> they help us structure Terraform code in a way that makes sense for real-world use. Think of today as the moment where everything we’ve learned so far starts to come together in a more organized, intentional way.</p>
<p>By the end of this blog, we’ll have infrastructure that’s thoughtfully designed, reusable, and easy to extend.</p>
<p>Before we dive in, a little about myself:</p>
<blockquote>
<p><strong>Fact #20: I love asking “why?” more than I probably should.</strong> I find myself questioning things constantly, why a process works a certain way, why a decision was made, why one approach is chosen over another. Sometimes it can be a bit much for people around me. :P</p>
</blockquote>
<h1 id="heading-what-are-modules">What are modules?</h1>
<p>Before we talk about <em>custom</em> modules, it’s important that we slow down for a moment and understand something more fundamental: <strong>what Terraform modules actually are</strong>.</p>
<p>At its core, a module in Terraform is simply a <strong>reusable piece of code</strong>. That’s it. It’s just a way of grouping Terraform configuration so we can use it again without rewriting the same logic over and over.</p>
<p>Now, a natural question comes up at this point: <em>how is this different from a function in a programming language?</em> And that’s a very good question.</p>
<p>Terraform isn’t a fully-fledged programming language. It’s a <strong>configuration language</strong>, which means it gives us a limited set of built-in functions, but it doesn’t allow us to define our own functions the way we would in languages like Python or Java. So when we want reuse, not just reuse of values, but reuse of entire infrastructure logic, Terraform gives us modules instead.</p>
<p>Modules let us take a chunk of infrastructure logic, package it together, and reuse it wherever we need. They help us <strong>encapsulate complexity</strong>. Instead of looking at dozens of resources every time we open our configuration, we can hide that complexity behind a module and interact with it using just a few well-defined inputs and outputs.</p>
<p>This becomes incredibly important as soon as we move beyond small demos. Think about something like a VPC. A real VPC isn’t just one resource, it usually includes subnets, route tables, gateways, and more. Writing all of that repeatedly would be tiring, error-prone, and hard to maintain. A module allows us to define that complexity once and then simply <em>use</em> it wherever needed.</p>
<p>And this is where our journey today really begins. Since we’re building a <strong>production-grade EKS setup</strong>, we’ll have multiple moving parts i.e. networking, IAM, authentication, secrets, and the cluster itself. Each of these can live in its own module, neatly organized and clearly separated. Instead of one massive Terraform file, we’ll have a structure that feels intentional and easy to reason about.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766066641671/97b9199d-7245-41e4-a06d-a42c577a80fd.png" alt class="image--center mx-auto" /></p>
<p>So as we move forward, keep this simple idea in mind: <strong>a Terraform module is just a reusable container for Terraform code</strong>.</p>
<h1 id="heading-types-of-modules">Types of Modules</h1>
<p>Now that we’re comfortable with what a module is, let’s take the next small step and talk about the <strong>different types of modules</strong> we’ll come across in Terraform. This is an important part of the story, because it helps us understand <em>why</em> we’re choosing to build our own custom modules in this project instead of relying entirely on what already exists.</p>
<p>When we explore the Terraform ecosystem, we’ll quickly notice that not all modules are created or maintained in the same way. Broadly speaking, Terraform modules fall into three categories: <strong>public modules</strong>, <strong>partner modules</strong>, and <strong>custom modules</strong>.</p>
<p>Let’s start with <strong>public modules</strong>. These are modules that are typically published and maintained by Terraform providers themselves i.e. providers like AWS, Google Cloud, Azure, and others. We’ll find them listed in the Terraform Registry, and they’re designed to solve common infrastructure problems in a standardized way. For example, there are public modules for VPCs, EKS clusters, IAM configurations, and many other services. These modules are widely used, well-tested, and a great way to get started quickly.</p>
<p>Closely related to public modules are <strong>partner modules</strong>. These are created and maintained by organizations that have an official partnership with HashiCorp. When we browse the Terraform Registry and look at the available filters, we’ll notice provider filters such as AWS, Azure, Google, Helm, and others and within those, we’ll also see modules labeled as partner modules. These are jointly maintained and follow certain standards agreed upon with HashiCorp. In simple terms, partner modules sit somewhere between community contributions and official provider-backed modules.</p>
<p>Both public and partner modules are incredibly useful, especially when we want to move fast or follow widely accepted patterns. But this naturally leads to a question that many beginners ask at this point: <em>if these modules already exist, why would we ever need to create our own?</em></p>
<p>This is where <strong>custom modules</strong> come into the picture.</p>
<p>A custom module is a module that’s <strong>not managed by a provider or a partner</strong>. It’s created and maintained by us, our team, or our organization. Anyone can create a custom module. The process is straightforward: we write the Terraform code, place it in its own folder or repository, and optionally publish it to a GitHub repository. From there, we can tag releases, manage versions, and evolve the module over time.</p>
<p>One of the biggest advantages of custom modules is <strong>control</strong>. With public or partner modules, the design decisions are made by someone else. If something breaks, or if a change doesn’t align with our requirements, we’re dependent on the maintainers to fix or update it. With a custom module, we own the entire lifecycle. We decide what goes in, what stays out, and who is allowed to modify it.</p>
<p>Custom modules also allow us to <strong>lock down certain values</strong> intentionally. If there are configurations that shouldn’t be changed casually, especially in production, we can hardcode or tightly control them inside the module itself. That way, anyone using the module interacts only with the inputs we expose, and any deeper changes require deliberate updates to the module code.</p>
<p>This is exactly why most real-world organizations rely heavily on custom modules. They provide consistency, enforce standards, and reduce accidental changes, all while keeping Terraform code clean and reusable.</p>
<h1 id="heading-how-terraform-organises-modules">How Terraform organises modules?</h1>
<p>Now that we understand <em>why</em> custom modules matter, let’s shift our focus to <strong>how Terraform actually organizes modules</strong>. This part is especially important, because once the structure becomes clear, everything else starts to feel far less mysterious.</p>
<p>Whenever we write Terraform code, no matter how small or simple, it always lives inside a module. Even if we don’t explicitly create one, Terraform still treats our configuration as a module. The folder where Terraform execution begins is known as the <strong>root module</strong>.</p>
<p>Think back to what we’ve been doing so far in this series. We usually created a directory, and inside it we had files like <code>main.tf</code>, <code>variables.tf</code>, <code>outputs.tf</code>, maybe a <code>backend.tf</code> or a <code>.tfvars</code> file. All of that together formed our Terraform project. Even though we never called it a module, that entire folder was actually the <strong>root module</strong>.</p>
<p>So when we say “root module,” we’re simply referring to the top-level Terraform configuration, the place where <code>terraform init</code>, <code>terraform plan</code>, and <code>terraform apply</code> are executed. This is where Terraform starts reading and understanding what we want to build.</p>
<p>Now, when we move into more realistic projects, like the one we’re building today, we don’t want everything to live in that single folder. Instead, we break things down into smaller, focused pieces. And this is where <strong>custom modules</strong> come in.</p>
<p>Inside our root module, we create subfolders. Each of these subfolders represents a custom module, and each one is responsible for a specific part of the infrastructure. For example, in this Day 20 project, our root module lives at the top level, and inside it we have a <code>modules</code> directory. Within that directory, we create separate folders for things like <strong>VPC</strong>, <strong>EKS</strong>, <strong>IAM</strong>, and <strong>Secrets Manager</strong>.</p>
<p>(add image)</p>
<p>Each of these module folders looks very familiar. Just like the root module, they contain files such as <code>main.tf</code>, <code>variables.tf</code>, and <code>outputs.tf</code>. The difference is not in the file names, but in their role. The root module orchestrates everything, while the custom modules focus on doing one thing well.</p>
<p>So, at a high level, the structure looks something like this: we have a root module that serves as the entry point, and inside it, multiple custom modules that handle networking, security, and the Kubernetes cluster itself. The root module pulls these modules together and passes values to them, while the modules return outputs back to the root module.</p>
<p>This relationship is very similar to calling a function with parameters. The root module provides the inputs, the module performs its work, and then it returns outputs that other parts of the configuration can use.</p>
<h1 id="heading-first-module-vpc">First Module: VPC</h1>
<p>We’ll start small and focused, because this is how custom modules truly make sense.</p>
<p>Imagine we’re at <strong>Day 20</strong>, and this is our project layout:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766062354347/47497a6d-ac0f-4e70-95f0-c39a5f25b3ea.png" alt class="image--center mx-auto" /></p>
<p>The <code>code/</code> directory is our <strong>root module</strong>.<br />The <code>modules/vpc/</code> directory is our <strong>first custom module</strong>.</p>
<h2 id="heading-step-1-writing-the-vpc-logic-inside-the-custom-module">Step 1: Writing the VPC logic <em>inside</em> the custom module</h2>
<p>Let’s go inside <code>modules/vpc/main.tf</code>.</p>
<p>This file contains the actual infrastructure logic. In our case, we start with the VPC itself:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766062425067/297b63f6-a2d8-4429-babc-779b73ed9165.png" alt class="image--center mx-auto" /></p>
<p>Now pause here for a second and notice something important.</p>
<p>We are <strong>not hardcoding</strong> values like the CIDR block or the name.<br />Instead, we’re using variables such as <code>var.vpc_cidr</code> and <code>var.name_prefix</code>.</p>
<p>This tells us something very clearly:</p>
<blockquote>
<p>This module expects someone else to provide these values.</p>
</blockquote>
<p>That “someone else” is the <strong>root module</strong>.</p>
<h2 id="heading-step-2-declaring-what-the-module-expects-variables">Step 2: Declaring what the module expects (variables)</h2>
<p>Since we’re using <code>var.vpc_cidr</code>, we must define it.<br />That happens inside <code>modules/vpc/variables.tf</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766062514702/da31666e-0bf0-493d-a10d-6344230f2282.png" alt class="image--center mx-auto" /></p>
<p>This file is not assigning values.<br />It’s simply saying:</p>
<blockquote>
<p>“If you want to use this VPC module, these are the values you must give me.”</p>
</blockquote>
<p>This separation is one of the biggest reasons modules feel clean and professional.</p>
<h2 id="heading-step-3-defining-values-in-the-root-module">Step 3: Defining values in the root module</h2>
<p>Now we move back to the <strong>root module</strong>, inside <code>code/variables.tf</code> (check the friendly project layout above)</p>
<p>Here’s where the values actually come from:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766063377431/c069e896-41d5-4eba-9e69-8acdbc68e26d.png" alt class="image--center mx-auto" /></p>
<p>This is the moment where things start to click.</p>
<p>The <strong>same variable names</strong> exist in both places:</p>
<ul>
<li><p>Declared in the module</p>
</li>
<li><p>Defined in the root module</p>
</li>
</ul>
<p>Terraform wires them together for us.</p>
<h2 id="heading-step-4-calling-the-custom-module-from-the-root-module">Step 4: Calling the custom module from the root module</h2>
<p>Now comes the most important part: <strong>using the module</strong>.</p>
<p>Inside <code>code/main.tf</code>, we write:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766063496579/9ed6991f-0d1e-4f5d-af1d-150a6470fd84.png" alt class="image--center mx-auto" /></p>
<p>Let’s slow down:</p>
<ul>
<li><p><code>module "vpc"</code> → we are creating an instance of our custom module</p>
</li>
<li><p><code>source = "./modules/vpc"</code> → this tells Terraform <strong>where the module lives</strong></p>
</li>
<li><p>Everything below that → values being passed <em>into</em> the module</p>
</li>
</ul>
<p>This is exactly like calling a function and passing arguments.</p>
<p>The module doesn’t care <em>where</em> the values came from.<br />The root module doesn’t care <em>how</em> the VPC is built internally.</p>
<p>Each has a single responsibility and that’s what makes this production-ready.</p>
<p>At this point, we’ve achieved something very important:</p>
<ul>
<li><p>The VPC logic is <strong>encapsulated</strong></p>
</li>
<li><p>The root module stays <strong>clean and readable</strong></p>
</li>
<li><p>We can reuse this VPC module anywhere</p>
</li>
<li><p>We can control changes by modifying the module itself</p>
</li>
</ul>
<h1 id="heading-extending-the-vpc-module-subnets-and-availability-zones">Extending the VPC module: Subnets and Availability Zones</h1>
<p>Let’s continue exactly where we left off.</p>
<h2 id="heading-step-1-deciding-where-logic-should-live">Step 1: Deciding <em>where</em> logic should live</h2>
<p>Before we write any code, we ask an important design question:</p>
<blockquote>
<p>Should the VPC module decide <em>which</em> availability zones to use, or should the root module decide?</p>
</blockquote>
<p>For production-grade projects, the answer is usually:<br /><strong>the root module decides</strong>, and the custom module consumes.</p>
<p>This keeps the module reusable across regions and environments.</p>
<h2 id="heading-step-2-fetching-availability-zones-in-the-root-module">Step 2: Fetching availability zones in the root module</h2>
<p>Inside the root module (<code>code/main.tf</code>), we add a data source:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766063704766/f7496f96-20e8-4441-be7d-84aa5867b6a0.png" alt class="image--center mx-auto" /></p>
<p>We don’t hardcode zone names. Instead, we ask AWS what’s available in the current region.</p>
<p>Now we take only the first three zones:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766063758047/85234d75-2e8d-419d-b6a7-b5862513586e.png" alt class="image--center mx-auto" /></p>
<p>This <code>locals</code> block is just helping us keep things readable.<br />At this point, the root module knows which availability zones to use.</p>
<h2 id="heading-step-3-passing-availability-zones-into-the-vpc-module">Step 3: Passing availability zones into the VPC module</h2>
<p>We now pass these availability zones into the VPC module:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766063800992/baa981dd-ef47-4c6a-b3b2-dd434a441f01.png" alt class="image--center mx-auto" /></p>
<p>Notice something subtle but important.</p>
<p><code>azs</code>, <code>public_subnets</code>, and <code>private_subnets</code> are <strong>not AWS resource arguments</strong>.<br />They are <strong>inputs to our custom module</strong>.</p>
<p>Terraform allows this because we define what these values mean <em>inside</em> the module.</p>
<h2 id="heading-step-4-declaring-these-variables-inside-the-vpc-module">Step 4: Declaring these variables inside the VPC module</h2>
<p>Inside <code>modules/vpc/variables.tf</code>, we add:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766064438216/6dfc262f-8cc1-4519-978d-d1783858732c.png" alt class="image--center mx-auto" /></p>
<p>Again, no values here, just expectations.</p>
<h2 id="heading-step-5-using-these-values-inside-the-vpc-module">Step 5: Using these values inside the VPC module</h2>
<p>Now comes the part where everything connects.</p>
<p>Inside <code>modules/vpc/main.tf</code>, we create public subnets:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766064499788/deb0d86d-346b-4cf5-9152-186adf9a3896.png" alt class="image--center mx-auto" /></p>
<p>Let’s slow this down.</p>
<ul>
<li><p><code>count</code> lets us create multiple subnets</p>
</li>
<li><p>Each subnet gets:</p>
<ul>
<li><p>a CIDR from <code>public_subnets</code></p>
</li>
<li><p>an AZ from <code>azs</code></p>
</li>
</ul>
</li>
<li><p>The index ties them together</p>
</li>
</ul>
<p>This is a <strong>very common real-world pattern</strong>, and it works beautifully with modules.</p>
<h2 id="heading-step-6-why-this-is-such-an-important-moment">Step 6: Why this is such an important moment</h2>
<p>At this point, something fundamental has happened:</p>
<ul>
<li><p>The <strong>root module controls decisions</strong></p>
</li>
<li><p>The <strong>custom module executes logic</strong></p>
</li>
</ul>
<p>We’ve built a clean contract between the two.</p>
<p>If tomorrow we want to change regions, availability zones, or subnet layouts, we don’t touch the module logic, we only change inputs.</p>
<p>That’s the real power of custom modules.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And with that, we’ve completed <strong>Day 20 of the 30 Days of Terraform Challenge</strong>!</p>
<p>Today’s demo covered something truly important: the idea of <strong>custom modules</strong>. While it might sound technical at first, modules are really just a way to <strong>simplify our lives</strong>, they let us <strong>encapsulate complexity, reuse code, and maintain control</strong> over our infrastructure.</p>
<p>We also explored <strong>how modules interact with root modules</strong>, passed values using variables, and saw some <strong>hands-on code examples</strong> that demonstrate the concept. I intentionally did not include the full EKS cluster setup in this blog, because going through the entire cluster creation here would have been overwhelming. But don’t worry, there’s a <strong>video by Piyush Sachdeva</strong> that explains everything, including the complete EKS demo, with helpful troubleshooting steps along the way.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/a_j6Gq-KtxE?si=my-YiKKq8Ulo72rq">https://youtu.be/a_j6Gq-KtxE?si=my-YiKKq8Ulo72rq</a></div>
<p> </p>
<p>So take your time to <strong>digest these concepts</strong>, play around with the examples we went over, and feel free to experiment. Tomorrow, we’ll continue our journey and explore the next exciting step in the challenge.</p>
<p>Happy Terraforming, and see you in the next session!</p>
]]></content:encoded></item><item><title><![CDATA[DAY #44: [DAY-19] AWS Terraform Provisioners]]></title><description><![CDATA[Introduction
Hello and welcome back to 30 Days of AWS Terraform. This is Day 19 of the series.
If you’ve been following along from the early days, give yourself a quiet pat on the back, showing up consistently and learning something new every day is ...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-44-day-19-aws-terraform-provisioners</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-44-day-19-aws-terraform-provisioners</guid><category><![CDATA[Terraform]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Mon, 15 Dec 2025 15:09:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765808205638/7dd1c4a6-f8fd-46cb-9b75-d3d26fb08f20.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back to <strong>30 Days of AWS Terraform</strong>. This is <strong>Day 19</strong> of the series.</p>
<p>If you’ve been following along from the early days, give yourself a quiet pat on the back, showing up consistently and learning something new every day is no small thing.</p>
<p>So far, we’ve covered quite a lot. We didn’t just talk about concepts in isolation; we slowly built them up and even worked through a few mini projects and a couple of real-world–style scenarios. Most recently, we spent time on a <strong>mini project around Image Processing using AWS Lambda</strong>, where we saw how serverless components can work together to process images efficiently. That project was an important step because it helped us move from “writing Terraform code” to actually thinking in Terraform, orchestrating multiple resources to achieve a real task.</p>
<p>Today, we’re stepping into another concept that often comes up once our infrastructure starts feeling more real. We can think of this as another mini project if you like, or simply as a focused learning exercise that adds one more useful tool to our Terraform toolbox. We’ll be talking about <strong>Terraform provisioners</strong>.</p>
<p><img src="https://blog.xmi.fr/assets/img/terraform-vs-bicep/02-execution-mode-terraform.webp" alt="Terraform vs Bicep: the differences you should really know | Xavier Mignot" class="image--center mx-auto" /></p>
<p>Provisioners tend to show up right at that moment when creating infrastructure alone is not enough. The reason we’re learning this topic is simple, sometimes creating resources is not enough. Once an EC2 instance, server, or other infrastructure exists, we often need to <strong>perform additional tasks automatically</strong>, like running commands, installing software, or copying files. Provisioners give us a safe, structured way to handle these tasks without manually logging into the server each time. i.e. maybe run a command, copy a file, or prepare a server just enough so that it’s ready for use.</p>
<p>By the end, provisioners would feel like another practical option we can reach for when the situation calls for it.</p>
<blockquote>
<p>Fact #19: I usually avoid plain socks and prefer ones with small patterns, subtle designs, or little motifs. Maybe a tiny spaceship, a little Mickey Mouse logo, or something fun like that.</p>
</blockquote>
<h1 id="heading-what-is-a-provisioner">What is a Provisioner?</h1>
<p>Let’s start with a very simple question.</p>
<p>What exactly is a <strong>provisioner</strong> in Terraform?</p>
<p>At its core, a provisioner is something that <strong>performs a task</strong>. That’s it. A task could be as small as running a single command, executing a script, copying a file, or doing some kind of operation at a particular moment in time. When Terraform creates or recreates a resource, a provisioner gives us a way to say, <em>“Now that this thing exists, please do this extra step.”</em></p>
<p>This idea becomes easier to grasp if we think back to what we’ve already been doing. Until now, most of our Terraform work has focused on <strong>creating infrastructure</strong> i.e. VPCs, subnets, security groups, EC2 instances, and so on. Terraform is excellent at that. But sometimes, creating the resource is only part of the story. Sometimes we also want to prepare that resource in a small way.</p>
<p>That’s where provisioners come in.</p>
<p>Terraform gives us <strong>three types of provisioners</strong>, and each one is meant for a slightly different situation. Understanding which one to use and why is much more important than memorizing syntax. So we’ll take them one at a time and connect them back to real situations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809166075/8f80074f-1f11-4fc5-8d62-884cd00b30ff.png" alt class="image--center mx-auto" /></p>
<p>The first thing to keep in mind is that all provisioners exist to <strong>do some work</strong>, not to define infrastructure itself. They are helpers, not the foundation. With that mental model in place, the differences between them start to make sense.</p>
<p>The three provisioners we’ll be working with today are:</p>
<ul>
<li><p><code>local-exec</code></p>
</li>
<li><p><code>remote-exec</code></p>
</li>
<li><p><code>file</code></p>
</li>
</ul>
<p>Let’s begin with the <strong>local-exec provisioner</strong>, because it’s the easiest place to start and helps set the stage for everything that comes after.</p>
<h1 id="heading-what-is-a-local-provisioner">What is a Local Provisioner?</h1>
<p>Let’s talk about the <strong>local-exec provisioner</strong>.</p>
<p>As the name suggests, <code>local-exec</code> is used when we want to run a command <strong>locally</strong>. And by locally, we mean on the machine where Terraform itself is running. In our case, throughout this series, we’ve been running Terraform from our own laptops. For example, if you’re following along on a MacBook or a personal system, that machine becomes the “local” environment for Terraform.</p>
<p>So when we use <code>local-exec</code>, Terraform is not reaching out to AWS, and it’s not logging into any EC2 instance. It’s simply running a command right there on our own system.</p>
<p>This is an important distinction to pause on.</p>
<p>If we think about how we’ve been working so far, every time we run <code>terraform apply</code>, those commands are already being executed locally. The <code>local-exec</code> provisioner just gives us a structured way to attach an extra local command to the lifecycle of a resource. It might be logging something, printing output, or triggering a small helper script.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809623622/a8a4b6ee-463e-4a03-b842-0c5961f81962.png" alt class="image--center mx-auto" /></p>
<p>To see this in action, let’s build on the infrastructure we already created.</p>
<p>We already have our provider block in place:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809791902/82669c60-61fd-4f28-a83d-9cd2699e0a1b.png" alt class="image--center mx-auto" /></p>
<p>We also use a data source to fetch the Ubuntu AMI, because we don’t want to hardcode image IDs:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809828932/2d72c4e2-528c-4ae9-b0cf-f0d331f9c66c.png" alt class="image--center mx-auto" /></p>
<p>This gives us a clean and reliable way to always get the latest Ubuntu image. Nothing new here, we’ve already seen this pattern before.</p>
<p>Next, we create a security group that allows SSH access, simply so we can connect to the instance later:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809871067/988f1400-ab2d-45b9-952c-4436e5112a38.png" alt class="image--center mx-auto" /></p>
<p>With that in place, we move on to creating the EC2 instance itself:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809904011/4d68c255-86a2-4f6a-b005-9274a2283e3a.png" alt class="image--center mx-auto" /></p>
<p>Up to this point, everything should feel familiar. We’re pulling the AMI from the data source, reading values from variables, and attaching the security group we just created.</p>
<p>Now, before we add any provisioners, we need one more important piece: the <strong>connection block</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809937532/f8cedea0-6ebd-4f45-abaa-cd18a34d02b7.png" alt class="image--center mx-auto" /></p>
<p>Even though <code>local-exec</code> itself doesn’t need SSH, this connection block becomes important as we move forward. It tells Terraform how to connect to <em>this specific resource</em>. The keyword <code>self</code> simply means “this resource right here.” So when we say <code>self.public_ip</code>, we’re referring to the public IP of this EC2 instance.</p>
<p>Now comes the local-exec provisioner itself:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765809983762/5b1912c1-a916-46c4-87c2-e7e7ab66be8b.png" alt class="image--center mx-auto" /></p>
<p>Let’s slow down and really look at what’s happening here.</p>
<p>The <code>command</code> field is just a normal shell command. We’re using <code>echo</code> to print a message. Inside that message, we’re interpolating values from the EC2 instance itself, like the instance ID and its public IP. Even though the command runs locally, Terraform already knows about the resource, so it can safely substitute those values.</p>
<p>When this provisioner runs, the command is executed <strong>on our local machine</strong>, not on the EC2 instance. That’s the key idea to remember.</p>
<p>If we run <code>terraform apply</code> at this point, Terraform may say there are no changes, because provisioners don’t change the infrastructure itself. To force Terraform to recreate the resource and trigger the provisioner again, we can mark the instance as tainted:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765810039900/cb5bb344-9a86-4d11-aed6-3747df248ea7.png" alt class="image--center mx-auto" /></p>
<p>This tells Terraform that the resource should be destroyed and recreated.</p>
<p>When we apply again, Terraform destroys the old instance, creates a new one, and during that creation, we see the <code>local-exec</code> provisioner run. In the output, we’ll see the echoed message showing the instance ID and IP address.</p>
<p>This is a small example, but it clearly shows what <code>local-exec</code> is good at: running <strong>local helper commands</strong> that react to infrastructure events.</p>
<p>With that foundation in place, we’re ready to move from “local” actions to running commands <strong>on the remote machine itself</strong>. That’s where the next provisioner comes in.</p>
<h1 id="heading-what-is-a-remote-exec-provisioner">What is a Remote-exec provisioner?</h1>
<p>Let’s now move on to the <strong>remote-exec provisioner</strong>.</p>
<p>Now that we’re comfortable with what <strong>local-exec</strong> does, the next step feels very natural.</p>
<p>Until now, every command we talked about was running <strong>on our own machine</strong>. But at some point, we usually want to go one step further. We don’t just want to <em>know</em> that an EC2 instance was created, we want to actually <strong>do something on that instance</strong>.</p>
<p>This is where the <strong>remote-exec provisioner</strong> comes in.</p>
<p>As the name suggests, <code>remote-exec</code> allows us to run commands <strong>remotely</strong> on a machine. In our case, that remote machine is the EC2 instance we just created using Terraform. Unlike <code>local-exec</code>, this provisioner does not run on our laptop. Instead, Terraform connects to the instance over <strong>SSH</strong> and executes the commands there.</p>
<p>This is an important shift, so let’s pause and absorb it.</p>
<p>For <code>remote-exec</code> to work, Terraform needs a way to log in to the server. That’s why the <strong>connection block</strong> we added earlier suddenly becomes very important. The SSH user, private key, and host information tell Terraform exactly how to reach the instance and authenticate itself. Without this, remote execution simply wouldn’t be possible.</p>
<p>Now, think about common real-life situations. When a server comes up for the first time, we often want to do some basic setup. Maybe we want to update packages, create a file, or install something simple. That initial preparation step is a very common use case for <code>remote-exec</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765810371968/244da810-9a79-4887-baf9-178f3ac213f2.png" alt class="image--center mx-auto" /></p>
<p>Let’s see how this looks in our existing EC2 resource.</p>
<p>We add a new provisioner block, this time of type <code>remote-exec</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765810441928/c6283f0a-5e76-4d62-a33a-c9b834d53564.png" alt class="image--center mx-auto" /></p>
<p>Instead of a single <code>command</code>, we now use <code>inline</code>. This lets us provide a list of commands that Terraform will run one after another on the remote machine. These commands are executed over SSH, exactly as if we had logged into the instance ourselves and typed them manually.</p>
<p>The first command updates the package list. The second command creates a file inside the <code>/tmp</code> directory and writes a simple message into it. There’s nothing complex here and that’s intentional. The goal is not to do something fancy, but to clearly see the difference between <strong>local execution</strong> and <strong>remote execution</strong>.</p>
<p>At this point, if we run <code>terraform apply</code> again, Terraform may still say there are no changes. Just like before, provisioners don’t count as infrastructure changes. So once again, we mark the instance as tainted:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765810499444/299a467c-43bd-49d8-aacc-4603b9429ff7.png" alt class="image--center mx-auto" /></p>
<p>And then we apply the changes.</p>
<p>Terraform destroys the existing instance, creates a new one, and during that creation process, it connects to the EC2 instance over SSH and runs the <code>remote-exec</code> commands. This time, the work is happening <strong>inside the server</strong>, not on our local machine.</p>
<p>To really confirm this, we can log into the EC2 instance after it’s created. Once connected, we check the <code>/tmp</code> directory and see the file <code>remote_exec.txt</code>. When we open it, the content matches exactly what we defined in the Terraform configuration.</p>
<p>That small file is our proof.</p>
<p>It shows us that <code>remote-exec</code> truly runs commands <strong>on the remote machine</strong>, using the connection details we provided. This is the key difference between <code>local-exec</code> and <code>remote-exec</code>, and once that difference is clear, both concepts feel much easier to reason about.</p>
<p>Now that we’ve seen how to run commands locally <em>and</em> remotely, there’s one more very practical scenario left, copying files from our machine to the server.</p>
<p>That’s where the <strong>file provisioner</strong> comes into the picture, and that’s what we’ll explore next.</p>
<h1 id="heading-what-is-a-file-provisioner">What is a File Provisioner?</h1>
<p>Finally, let’s explore the <strong>file provisioner</strong>, which builds naturally on what we’ve learned so far.</p>
<p>Where <code>remote-exec</code> lets us run commands on the remote machine, the <code>file</code> provisioner is all about <strong>transferring files</strong> from our local environment to the instance. This is useful when we want scripts, configuration files, or other assets to be available on the machine right after it’s created.</p>
<p>Here’s a simple example based on our setup:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765810937817/b8cae9c3-9983-4b73-9b43-2e60be9ef2f4.png" alt class="image--center mx-auto" /></p>
<p>Let’s break this down:</p>
<ul>
<li><p><code>source</code> points to a file in our local project directory, in this case, a script <code>welcome.sh</code> inside a <code>scripts</code> folder.</p>
</li>
<li><p><code>destination</code> specifies where that file should be placed on the EC2 instance. We’re putting it in <code>/tmp/welcome.sh</code>, but it could be any absolute path you like.</p>
</li>
</ul>
<p>Once Terraform applies this provisioner, the file is copied <strong>over SSH</strong> to the remote machine. Notice that, just like <code>remote-exec</code>, the connection block we defined earlier is required for authentication.</p>
<p>After the file is transferred, we could even run it using another <code>remote-exec</code> provisioner if needed, for example, to configure the system or create additional files based on the script.</p>
<p>To see it in action, we taint the resource again:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765811125717/e6b039e0-2669-4466-8b65-6bcace35a1d3.png" alt class="image--center mx-auto" /></p>
<p>After the instance is recreated, logging into it will show our <code>welcome.sh</code> script in the <code>/tmp</code> directory. Alongside the file created by <code>remote-exec</code>, we now have tangible evidence that the provisioners executed successfully.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765811011140/6e87e3d3-59dc-4c25-8f97-10392c07c25a.png" alt class="image--center mx-auto" /></p>
<p>The file provisioner is simple, yet powerful. It’s a straightforward way to <strong>move files where they’re needed</strong> immediately after creating a resource, no manual copying, no extra steps.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And with that, we’ve completed <strong>Day 19 of the 30 Days of Terraform Challenge</strong>! Today’s session introduced us to <strong>Terraform provisioners</strong>, which might seem like small helpers at first glance, but they are actually <strong>powerful tools that allow us to go beyond just creating infrastructure</strong>. They let us automate tasks that happen right after a resource is created, whether it’s running commands locally, configuring a server remotely, or copying important files over. In other words, provisioners help bridge the gap between <em>infrastructure as code</em> and <em>infrastructure ready to use</em>. And that’s why this topic is so important, it gives us the ability to make our resources immediately practical without manual intervention, saving time and reducing errors.</p>
<p>Compared to some of the previous blogs, today’s session was <strong>short and fun</strong>, and the concepts were very approachable. Even if you’re just starting out, you can follow along easily. The examples were simple enough to understand the mechanics, but also practical enough that you can apply them in real-world scenarios.</p>
<p>For those who like a <strong>step-by-step visual guide</strong>, there’s a helpful video by <strong>Piyush Sachdeva</strong> where he explains all the concepts covered today. The video also goes through some common troubleshooting steps, which can be a lifesaver if your first attempt at running provisioners doesn’t go exactly as expected.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/DkhAgYa0448?si=wfy8fZ_-Mcrm03E4">https://youtu.be/DkhAgYa0448?si=wfy8fZ_-Mcrm03E4</a></div>
<p> </p>
<p>So, that wraps up <strong>Day 19</strong>. Tomorrow, we’ll continue our journey, building on this foundation and exploring more exciting features of Terraform. Until then, take a moment to reflect on what you’ve learned, maybe even experiment a little with your own scripts and provisioners, and enjoy the process.</p>
<p><strong>Happy Terraforming!</strong></p>
]]></content:encoded></item><item><title><![CDATA[DAY #43: [DAY-18] AWS Terraform: Image Processing Serverless Project using AWS Lambda]]></title><description><![CDATA[Introduction
Hello, and welcome back to the blog!
This is Day 18 of our 30 Days of AWS Terraform journey, and if you’ve been following along, we’ve already covered quite a bit together. Last day, we explored a mini-project on AWS Terraform Blue-Green...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-43-day-18-aws-terraform-image-processing-serverless-project-using-aws-lambda</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-43-day-18-aws-terraform-image-processing-serverless-project-using-aws-lambda</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[lambda]]></category><category><![CDATA[Lambda function]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Sun, 14 Dec 2025 12:51:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765716511983/ccc81f9f-0be2-4e82-96ce-c313e43e70c5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello, and welcome back to the blog!</p>
<p>This is <strong>Day 18</strong> of our <strong>30 Days of AWS Terraform journey</strong>, and if you’ve been following along, we’ve already covered quite a bit together. Last day, we explored a mini-project on <strong>AWS Terraform Blue-Green Deployment using Elastic Beanstalk</strong>. Today, we’re building on that learning, but we’re taking a slightly different turn.</p>
<p>Instead of focusing on networking or long-running infrastructure, we’re going to explore something lighter, more <strong>event-driven</strong>. Our focus for today is <strong>AWS Lambda</strong>, the star of the serverless world.</p>
<p><img src="https://media.tenor.com/voz91UlCh80AAAAM/half-life-hecu.gif" alt="Lambda GIF - Lambda - Discover &amp; Share GIFs" class="image--center mx-auto" /></p>
<p>This blog remains <strong>Terraform-focused</strong>, just like the rest of the series. But instead of provisioning servers that must run all the time, we’ll see how Terraform can help us create something that <strong>runs only when it’s needed</strong>.</p>
<p>To make this practical, we’ll walk through an <strong>end-to-end image processing project</strong> i.e. from uploading a file to S3, to automatically processing it using a Lambda function, all orchestrated through Terraform.</p>
<p><img src="https://i.makeagif.com/media/3-02-2021/L3g9pt.gif" alt="Introduction to AWS Lambda - Serverless Compute on Amazon Web Services on  Make a GIF" /></p>
<p>Before touching any code or Terraform files, there’s one important step: <strong>understanding what AWS Lambda really is</strong>, how serverless works, and why this approach fits so well with event-driven workflows. Once we have that mental picture, everything else in the project will start to make a lot more sense.</p>
<blockquote>
<p><strong>Fact #18:</strong> I’m not really good at cooking, but if you put me in front of a pan and some rice, I can make <strong>omurice</strong> pretty well.</p>
</blockquote>
<h1 id="heading-what-exactly-is-aws-lambda-and-what-do-we-mean-by-serverless">What Exactly Is AWS Lambda? And What Do We Mean by “Serverless”?</h1>
<p>So, what exactly is Lambda?</p>
<p>At its core, <strong>AWS Lambda is a serverless function</strong>.</p>
<p>Now, that word <em>serverless</em> can sound a bit confusing at first. It’s not the same as someone saying, “There are no servers at all,” which isn’t really true. There <em>are</em> servers involved, but the difference is <strong>we don’t manage them</strong>.</p>
<p>Let’s break this down.</p>
<p>Whenever we build a traditional application, the usual flow looks something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765715820822/b24ab052-19b9-48c2-8952-4a6be076ccf8.png" alt class="image--center mx-auto" /></p>
<p>That server has to be running <strong>all the time</strong>, especially if it’s hosting a live application. Even if no one is using the app at that moment, the server is still there, still running, still costing money.</p>
<p>This is the model most of us are familiar with.</p>
<p>Now, serverless changes this mindset.</p>
<p>With Lambda, <strong>we don’t provision servers at all</strong>. Instead of thinking about machines, we think about <strong>functions</strong>. We simply write our application code, package it, and upload it as a Lambda function.</p>
<p>AWS takes care of everything else.</p>
<p>The servers still exist, but AWS manages them for us. We don’t worry about operating systems, scaling, or uptime. Our responsibility ends with the code.</p>
<p>And here’s the key difference:<br /><strong>a Lambda function does not run all the time.</strong></p>
<p>A Lambda function runs <strong>only when something triggers it</strong>.</p>
<p>This trigger is called an <strong>event</strong>.</p>
<p>An event could be:</p>
<ul>
<li><p>A file being uploaded to an S3 bucket</p>
</li>
<li><p>A scheduled time (for example, every Monday at 7 AM)</p>
</li>
<li><p>A real-time system event</p>
</li>
</ul>
<p>When the event happens:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765715326667/2c6bd0ce-a2ec-46e8-ba60-fb4f60a635fd.png" alt class="image--center mx-auto" /></p>
<p>That’s it.</p>
<p>There’s no server sitting idle in the background. The function exists until it’s needed, does its job, and then disappears again.</p>
<p>This makes Lambda especially useful for tasks that:</p>
<ul>
<li><p>Run for a <strong>short time</strong></p>
</li>
<li><p>React to <strong>events</strong></p>
</li>
<li><p>Don’t need to be running continuously</p>
</li>
</ul>
<p>Typically, Lambda functions are used for executions that last <strong>a few seconds to a few minutes</strong>. AWS recommends not using Lambda if your code needs to run longer than <strong>15 minutes</strong>, and that’s an important boundary to keep in mind.</p>
<p>Now that we have a clearer picture of what Lambda is and how serverless works, we’re ready to look at <strong>how this idea fits perfectly with the project we’re building today</strong> i.e. an image processing pipeline driven by events.</p>
<h1 id="heading-understanding-the-project-flow">Understanding the Project Flow</h1>
<p>Now that we have a clear understanding of what AWS Lambda is and how serverless works, it’s the perfect time to look at <strong>what we’re actually building</strong> in this project.</p>
<p>Before we touch Terraform files or Python code, let’s first form a simple mental picture. If the architecture makes sense in our head, the implementation will feel much easier later on.</p>
<p>At a high level, this project is an <strong>image processing pipeline</strong>. And the beauty of it is that once it’s set up, everything happens <strong>automatically</strong>.</p>
<p>Here’s the idea.</p>
<p>We’ll have <strong>two S3 buckets</strong>:</p>
<ul>
<li><p>One bucket where we upload the original image</p>
</li>
<li><p>Another bucket where the processed images will be stored</p>
</li>
</ul>
<p>The first bucket is our <strong>source bucket</strong>. Whenever we upload an image to this bucket, that upload creates an <strong>S3 event</strong>.</p>
<p>And remember what we discussed earlier, events are exactly what serverless functions like Lambda are waiting for.</p>
<p>So as soon as an image is uploaded, that S3 event will <strong>trigger our Lambda function</strong>.</p>
<p>This Lambda function is where all the image processing logic lives. It will take the original image and automatically generate:</p>
<ul>
<li><p>A JPEG image with <strong>85% quality</strong></p>
</li>
<li><p>Another JPEG image with <strong>60% quality</strong></p>
</li>
<li><p>A <strong>WebP</strong> version</p>
</li>
<li><p>A <strong>PNG</strong> version</p>
</li>
<li><p>And a <strong>thumbnail image</strong> resized to <strong>200 by 200</strong></p>
</li>
</ul>
<p>All of this happens without us clicking any extra buttons or running any manual commands.</p>
<p>We simply upload a file using the AWS Console, AWS CLI, or even an API call and the rest of the workflow takes care of itself.</p>
<p>Once the processing is done, the Lambda function uploads all five generated images into the <strong>destination bucket</strong>. That’s where the final output lives.</p>
<p>Now, for this to work smoothly, <strong>permissions</strong> play a very important role.</p>
<p>The S3 bucket must be allowed to <strong>trigger</strong> the Lambda function.<br />The Lambda function must be allowed to:</p>
<ul>
<li><p>Read files from the source bucket</p>
</li>
<li><p>Write processed files to the destination bucket</p>
</li>
<li><p>Write logs so we can see what’s happening behind the scenes</p>
</li>
</ul>
<p>These permissions are handled through <strong>IAM roles and policies</strong>, and we’ll be creating them using Terraform by giving the function exactly what it needs and nothing more.</p>
<p>This follows a principle we’ve touched on before: <strong>least privilege access</strong>. We don’t give broad permissions like full S3 access. We only allow what is strictly required.</p>
<p>There’s one more important piece in this setup.</p>
<p>For image manipulation, we’ll be using <strong>Pillow</strong>, which is a Python library designed for image processing. Since Lambda doesn’t include Pillow by default, we’ll package it as a <strong>Lambda layer</strong> and attach it to our function.</p>
<p>This keeps our Lambda function clean and makes dependencies easier to manage.</p>
<p>So to summarize the flow in simple terms:</p>
<ol>
<li><p>We upload an image to the source S3 bucket</p>
</li>
<li><p>An S3 event is created</p>
</li>
<li><p>The event triggers the Lambda function</p>
</li>
<li><p>The Lambda function processes the image using Pillow</p>
</li>
<li><p>Processed images are saved to the destination S3 bucket</p>
</li>
<li><p>Logs are written to CloudWatch for visibility</p>
</li>
</ol>
<p>All of this infrastructure i.e. buckets, permissions, Lambda, layers, and triggers will be created <strong>using Terraform</strong>.</p>
<p>Now that we understand the full picture, we’re ready to dive into the repository and start seeing how this architecture translates into Terraform code.</p>
<h1 id="heading-getting-started-cloning-the-repository-and-setting-the-stage">Getting Started: Cloning the Repository and Setting the Stage</h1>
<p>Now that we understand <strong>what we’re building</strong> and <strong>how the pieces fit together</strong>, it’s time to get our hands a little closer to the actual project.</p>
<p>For this project, we’ll continue using the <strong>same GitHub repository</strong> that we’ve been working with throughout this series.</p>
<p>The first step is to clone the repository and move into the <strong>Day 18</strong> directory.</p>
<p>The repository lives <a target="_blank" href="https://github.com/arnab-logs/30-Days-of-Terraform.git">here</a></p>
<p>Once we clone it, navigate into the <strong>day-18</strong> folder and then into the <strong>terraform</strong> directory. This is where all the Terraform files for today’s project live.</p>
<p>Before we create any real AWS resources, there’s one small but important thing we need to handle.</p>
<h1 id="heading-making-resource-names-unique">Making Resource Names Unique</h1>
<p>Some AWS resources, like <strong>S3 buckets</strong>, must have globally unique names. That means even if the bucket name makes sense to us, AWS might reject it if someone else in the world is already using it.</p>
<p>To solve this, we use a small helper resource in Terraform i.e. a <strong>random suffix</strong>.</p>
<p>In our <code>main.tf</code>, the first thing we create looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765712466574/f8b5c13e-4450-4633-a2c2-73da53ea91a6.png" alt class="image--center mx-auto" /></p>
<p>What this does is simple. Terraform generates a short random value, and we attach that value to our resource names. This way, we don’t have to manually keep changing bucket names every time we deploy.</p>
<p>Next, we define some <strong>locals</strong>. Locals allow us to build names once and reuse them everywhere, which keeps the configuration clean and readable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765712520890/ca454fe8-dbd3-4dfe-8019-d4d40cffc433.png" alt class="image--center mx-auto" /></p>
<p>Here’s what’s happening:</p>
<ul>
<li><p>We build a common <strong>bucket prefix</strong> using the project name and environment</p>
</li>
<li><p>We create two bucket names i.e. one for uploads and one for processed images</p>
</li>
<li><p>We append the random suffix so the names stay unique</p>
</li>
<li><p>We also define a clear name for our Lambda function</p>
</li>
</ul>
<p>This approach might feel repetitive if you’ve been following the series, and that’s a good thing. It means the concepts are starting to feel familiar.</p>
<p>With naming out of the way, we’re now ready to create the first real AWS resources for this project i.e. S3 buckets.</p>
<h1 id="heading-creating-the-source-s3-bucket">Creating the Source S3 Bucket</h1>
<p>Every story has a starting point, and in our case, it’s the place where the original image first arrives.</p>
<p>This project begins with an <strong>S3 bucket</strong> that acts as the <strong>source bucket</strong>. Any image we upload here will eventually kick off the entire image processing workflow.</p>
<p>We start by creating the bucket itself.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765712646989/5c197335-027a-4692-8e66-d5a1ce82ae1e.png" alt class="image--center mx-auto" /></p>
<p>There’s nothing complicated happening here. We’re simply creating an S3 bucket and giving it the name we already prepared using locals. This bucket will hold the original image exactly as it’s uploaded.</p>
<p>But creating a bucket is only the first step. There are a few important decisions we need to make to ensure this bucket is <strong>safe, reliable, and production-friendly</strong>, even though this is a learning project.</p>
<h2 id="heading-enabling-versioning">Enabling Versioning</h2>
<p>The next thing we do is enable <strong>bucket versioning</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765712707509/94666b21-eb8f-4a66-a7e7-2e253c55b9cd.png" alt class="image--center mx-auto" /></p>
<p>Versioning helps us keep track of changes. If the same file name is uploaded again, S3 doesn’t overwrite the old object, it stores a new version instead.</p>
<p>This is especially useful when files are important or when multiple updates might happen. Even if we don’t strictly need it for this demo, it’s a good habit to build and reinforces real-world practices.</p>
<h2 id="heading-adding-server-side-encryption">Adding Server-Side Encryption</h2>
<p>Next, we enable <strong>server-side encryption</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765712746610/2371e562-61b0-4899-883f-f0e77cec9a0b.png" alt class="image--center mx-auto" /></p>
<p>This ensures that any image uploaded to the bucket is encrypted at rest using AES-256. We don’t need to manage encryption keys manually, AWS takes care of that for us.</p>
<p>Again, this is about forming good habits early. Even simple projects deserve sensible security defaults.</p>
<h2 id="heading-blocking-public-access">Blocking Public Access</h2>
<p>Finally, we make sure the bucket is <strong>private</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765712787961/fce0f804-3305-4f1c-a2f6-14af3536789d.png" alt class="image--center mx-auto" /></p>
<p>For this project, there’s no need for the upload bucket to be publicly accessible. Images are uploaded intentionally, and access is controlled.</p>
<p>By blocking public ACLs and policies, we make sure the bucket isn’t accidentally exposed. In real production systems, public access is usually handled through controlled layers in front of S3, not directly on the bucket itself.</p>
<p>Now, with the source bucket in place, we now need somewhere to store the processed images.</p>
<h1 id="heading-creating-the-destination-s3-bucket">Creating the Destination S3 Bucket</h1>
<p>This second S3 bucket is our <strong>destination bucket</strong>. Every image generated by the Lambda function i.e. all five variants, will be stored here.</p>
<p>The setup for this bucket looks very similar to the source bucket, and that’s intentional. Consistency makes infrastructure easier to understand, maintain, and secure.</p>
<h2 id="heading-we-start-by-creating-the-bucket-itself">We start by creating the bucket itself</h2>
<p>Just like before, we’re using a name generated from locals, which includes the project name, environment, and a random suffix to ensure uniqueness.</p>
<h2 id="heading-enabling-versioning-again">Enabling Versioning Again</h2>
<p>Next, we enable versioning for the processed bucket as well.</p>
<p>Processed images may change over time, maybe we adjust compression levels, add new formats, or improve the logic later. Versioning ensures we don’t lose older outputs if something changes.</p>
<h2 id="heading-encrypting-the-processed-data">Encrypting the Processed Data</h2>
<p>We then apply <strong>server-side encryption</strong> to the destination bucket.</p>
<p>This ensures that all generated images are encrypted at rest, just like the originals. Consistent security across buckets is always a good practice.</p>
<h2 id="heading-keeping-the-bucket-private">Keeping the Bucket Private</h2>
<p>Finally, we block public access for the processed bucket as well.</p>
<p>There’s no reason for processed images to be publicly accessible in this setup. Everything is handled internally by AWS services, and access is controlled through IAM permissions.</p>
<p>At this stage, we now have:</p>
<ul>
<li><p>A source bucket for uploads</p>
</li>
<li><p>A destination bucket for processed outputs</p>
</li>
<li><p>Both buckets encrypted, versioned, and private</p>
</li>
</ul>
<p>But these buckets don’t yet know how to talk to our Lambda function. And the Lambda function itself doesn’t exist yet either.</p>
<p>Before we can create the function, we need to solve an important question:<br /><strong>Who is allowed to do what?</strong></p>
<p>That’s where <strong>IAM roles and policies</strong> come into play.</p>
<h1 id="heading-iam-roles-and-policies-giving-lambda-just-enough-permission">IAM Roles and Policies: Giving Lambda Just Enough Permission</h1>
<p>So far, we’ve created two S3 buckets. They exist, they’re secure, and they’re ready. But right now, they’re just storage.</p>
<p>For our image processing workflow to actually work, we need a way for <strong>AWS Lambda to interact with these buckets</strong>, and to do that safely.</p>
<p>This is where <strong>IAM roles and policies</strong> come in.</p>
<p>Instead of hard-coding permissions or credentials, AWS uses roles to define <strong>what a service is allowed to do</strong>. In our case, we want the Lambda function to:</p>
<ul>
<li><p>Write logs so we can see what’s happening</p>
</li>
<li><p>Read images from the source bucket</p>
</li>
<li><p>Write processed images to the destination bucket</p>
</li>
</ul>
<p>Nothing more, nothing less.</p>
<h2 id="heading-creating-the-iam-role-for-lambda">Creating the IAM Role for Lambda</h2>
<p>We start by creating an IAM role that Lambda can assume.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765713089417/6c365e2a-37bf-4415-bab3-0e2e67bc4051.png" alt class="image--center mx-auto" /></p>
<p>This role doesn’t give any permissions yet. It simply says:</p>
<p>“<strong>This role can be assumed by AWS Lambda</strong>.”</p>
<p>We define this using a policy document. While the JSON might look a bit intimidating at first, it’s really just a structured way of expressing trust. AWS even provides a policy generator to help create these documents, which makes life easier when you’re starting out.</p>
<p>Once this role exists, we can start attaching actual permissions to it.</p>
<h2 id="heading-defining-the-permissions-with-an-iam-policy">Defining the Permissions with an IAM Policy</h2>
<p>Next, we create a policy that tells AWS exactly what this Lambda function is allowed to do.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765713194387/bad7f050-97e9-4ea3-8e1c-69ed399c533e.png" alt class="image--center mx-auto" /></p>
<p>The policy may look intimidating, but here’s what it does:</p>
<p>The <strong>first statement</strong> allows the Lambda function to <strong>create log groups, log streams, and write logs</strong>. Without this, we’d have no visibility into what the function is doing, especially if something goes wrong.</p>
<p>The <strong>second statement</strong> allows Lambda to <strong>read objects from the</strong> <strong>source bucket</strong>. This is how it gets access to the uploaded image.</p>
<p>The <strong>third statement</strong> allows Lambda to <strong>write objects to the destination bucket</strong>. This is where all the processed images will be stored.</p>
<p>Notice something important here?</p>
<p>We are <strong>not</strong> giving full S3 access. We’re not using broad permissions like “S3FullAccess.” Instead, we’re granting access only to:</p>
<ul>
<li><p>Specific actions</p>
</li>
<li><p>Specific buckets</p>
</li>
<li><p>Specific object paths</p>
</li>
</ul>
<p>This is a perfect example of the <strong>principle of least privilege</strong>. We give the Lambda function exactly what it needs to work and nothing more.</p>
<p>With this IAM role and policy in place, our Lambda function will be able to:</p>
<ul>
<li><p>Read images</p>
</li>
<li><p>Process them</p>
</li>
<li><p>Store the results</p>
</li>
<li><p>Write logs for us to inspect</p>
</li>
</ul>
<p>Now that permissions are sorted, we’re ready to create something truly interesting, the Lambda function itself.</p>
<h1 id="heading-lambda-layers-bringing-image-processing-capabilities-to-lambda">Lambda Layers: Bringing Image Processing Capabilities to Lambda</h1>
<p>Now that our permissions are in place, we’re almost ready to create the Lambda function itself. But before we do that, there’s one important challenge we need to solve.</p>
<p>Our Lambda function is going to process images, and for that, we’ll be using <strong>Pillow</strong>, which is a Python library designed for image manipulation.</p>
<p>The problem is simple:<br /><strong>Pillow is not included by default in AWS Lambda.</strong></p>
<p>So how do we use it?</p>
<p>This is where <strong>Lambda layers</strong> come into the picture.</p>
<p>A Lambda layer is a way to package external libraries and dependencies separately from our function code. Instead of bundling everything inside the function zip, we place shared or heavy dependencies into a layer and then attach that layer to the Lambda function.</p>
<p>This keeps the function code clean and makes dependencies easier to manage.</p>
<h2 id="heading-creating-the-lambda-layer-resource">Creating the Lambda Layer Resource</h2>
<p>In Terraform, we define the layer like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765713597336/135a284f-70f5-414b-a54d-2f1e797f21ca.png" alt class="image--center mx-auto" /></p>
<p>Here’s what’s happening:</p>
<ul>
<li><p><code>filename</code> points to a zip file that contains the Pillow library</p>
</li>
<li><p><code>layer_name</code> gives the layer a clear, readable name</p>
</li>
<li><p><code>compatible_runtimes</code> ensures this layer works with Python 3.12</p>
</li>
<li><p>The description tells us exactly what this layer is for</p>
</li>
</ul>
<p>But this raises another question.</p>
<p>How do we create the <code>pillow_layer.zip</code> file in the first place?</p>
<p>Because AWS Lambda runs on Linux, the dependencies inside the layer must also be built for a <strong>Linux environment</strong>. This is important, especially if you’re working on macOS or Windows.</p>
<p>To solve this, we use some help from our friend <strong>Docker</strong>.</p>
<h2 id="heading-building-the-pillow-layer-using-docker">Building the Pillow Layer Using Docker</h2>
<p>Inside the <code>scripts</code> folder, there’s a helper script that takes care of building the layer correctly.</p>
<p>This script runs a Linux-based Python container, installs Pillow inside the exact folder structure Lambda expects, and then packages it into a zip file.</p>
<p>The important thing to understand here isn’t every line of the script, it’s the idea behind it.</p>
<p>We’re ensuring that:</p>
<ul>
<li><p>Pillow is installed in a Linux environment</p>
</li>
<li><p>The directory structure matches what Lambda expects</p>
</li>
<li><p>The final output is a clean zip file we can reuse</p>
</li>
</ul>
<p>Once this zip file is created, Terraform can reference it and upload it as a Lambda layer.</p>
<p>With the layer ready, we finally have everything needed to create the Lambda function itself:</p>
<ul>
<li><p>Storage (S3 buckets)</p>
</li>
<li><p>Permissions (IAM role and policy)</p>
</li>
<li><p>Dependencies (Pillow via a Lambda layer)</p>
</li>
</ul>
<p>In the next section, we’ll create the <strong>Lambda function</strong>, attach the layer, configure memory and timeout, and pass environment variables that the function will use at runtime.</p>
<h1 id="heading-creating-the-lambda-function">Creating the Lambda Function</h1>
<p>With our storage, permissions, and dependencies ready, we can finally create the <strong>Lambda function</strong> that will do the actual image processing work.</p>
<p>But before Terraform can create the function, it needs one important thing:<br /><strong>the function code packaged as a zip file</strong>.</p>
<h2 id="heading-packaging-the-lambda-function-code">Packaging the Lambda Function Code</h2>
<p>Our Lambda function is written in Python and lives inside the repository. To package it correctly, we use a Terraform data source.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765714065067/307ed8cf-451c-4960-8ec0-b07de93ad5e8.png" alt class="image--center mx-auto" /></p>
<p>This data source takes the Python file, compresses it into a zip archive, and makes it ready for deployment.</p>
<p>Even though we’re working with a local file here, Terraform treats this as data it needs to reference during deployment which is exactly what data sources are designed for.</p>
<h2 id="heading-defining-the-lambda-function">Defining the Lambda Function</h2>
<p>Now we define the Lambda function itself.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765714184457/6e78f80a-d763-439f-8515-e7e8ef08b359.png" alt class="image--center mx-auto" /></p>
<p>Let’s walk through this.</p>
<ul>
<li><p><code>filename</code> points to the zip file created earlier</p>
</li>
<li><p><code>function_name</code> gives the Lambda function a clear identity</p>
</li>
<li><p><code>role</code> attaches the IAM role we created, allowing the function to access S3 and logs</p>
</li>
<li><p><code>handler</code> tells Lambda where execution begins in the Python file</p>
</li>
<li><p><code>runtime</code> specifies Python 3.12</p>
</li>
<li><p><code>timeout</code> is set to 60 seconds, which is more than enough for image processing</p>
</li>
<li><p><code>memory_size</code> is set to 1024 MB to give the function enough resources</p>
</li>
</ul>
<p>We also attach the <strong>Pillow layer</strong> here. This is what enables image manipulation inside the function.</p>
<p>Finally, we pass in a couple of <strong>environment variables</strong>:</p>
<ul>
<li><p>The name of the processed images bucket</p>
</li>
<li><p>The log level</p>
</li>
</ul>
<p>This keeps our code flexible and avoids hard-coding values.</p>
<h1 id="heading-creating-the-cloudwatch-log-group">Creating the CloudWatch Log Group</h1>
<p>To make sure logs are retained in a predictable way, we also create a CloudWatch log group.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765714329472/28c74ea5-e125-4bd7-b062-a857d99407ec.png" alt class="image--center mx-auto" /></p>
<p>This ensures that logs are stored for seven days and then automatically cleaned up.</p>
<p>At this point, the Lambda function exists, but it still doesn’t know <strong>when</strong> to run.</p>
<p>Remember, Lambda functions don’t run on their own. They need an <strong>event trigger</strong>.</p>
<p>In the next section, we’ll connect the source S3 bucket to the Lambda function so that every image upload automatically triggers the processing workflow.</p>
<h1 id="heading-wiring-the-event-trigger-letting-s3-invoke-lambda-automatically">Wiring the Event Trigger: Letting S3 Invoke Lambda Automatically</h1>
<p>Our Lambda function now exists. It has the code, the permissions, and the image processing library attached. But right now, it’s still just waiting.</p>
<p>For Lambda to actually run, it needs an <strong>event</strong>.</p>
<p>In our project, that event is simple and very natural:<br /><strong>an image being uploaded to the source S3 bucket</strong>.</p>
<p>To make this work, we need to do two things:</p>
<ol>
<li><p>Allow S3 to invoke the Lambda function</p>
</li>
<li><p>Tell the S3 bucket <em>which</em> events should trigger the function</p>
</li>
</ol>
<h2 id="heading-allowing-s3-to-invoke-the-lambda-function">Allowing S3 to Invoke the Lambda Function</h2>
<p>First, we explicitly give S3 permission to invoke our Lambda function.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765714412317/5e143334-d937-45fb-81e2-77dd8ec42411.png" alt class="image--center mx-auto" /></p>
<p>This resource acts like a handshake.</p>
<p>It tells AWS:</p>
<ul>
<li><p>S3 is allowed to invoke this Lambda function</p>
</li>
<li><p>The permission applies only to our upload bucket</p>
</li>
<li><p>The action allowed is strictly <code>InvokeFunction</code></p>
</li>
</ul>
<p>Without this permission, S3 events would never be able to trigger the function, even if everything else was configured correctly.</p>
<h2 id="heading-creating-the-s3-event-notification">Creating the S3 Event Notification</h2>
<p>Now that permission is in place, we configure the S3 bucket to send events to Lambda.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765714477493/e9c311ec-0368-419d-884c-0873b39035c2.png" alt class="image--center mx-auto" /></p>
<p>This tells the source bucket:</p>
<p>“Whenever an object is created, in any way, invoke this Lambda function.”</p>
<p>It doesn’t matter whether the file is uploaded through:</p>
<ul>
<li><p>The AWS Console</p>
</li>
<li><p>The AWS CLI</p>
</li>
<li><p>An API call</p>
</li>
</ul>
<p>As long as an object is created in the bucket, the event fires.</p>
<p>The <code>depends_on</code> is important here. It ensures Terraform creates the permission first, so S3 is allowed to invoke the function before the notification is set up.</p>
<p>At this point, the entire flow is connected:</p>
<ul>
<li><p>Upload an image</p>
</li>
<li><p>S3 generates an event</p>
</li>
<li><p>Lambda is invoked</p>
</li>
<li><p>Image processing happens automatically</p>
</li>
</ul>
<p>Now all that’s left is deployment.</p>
<p>In the next section, we’ll look at the <strong>deployment scripts</strong>, how the Lambda layer is built automatically, and how everything is deployed using a single command.</p>
<h1 id="heading-deploying-everything-bringing-the-project-to-life">Deploying Everything: Bringing the Project to Life</h1>
<p>At this point, all the pieces of our architecture are defined. We’ve described the buckets, the permissions, the Lambda function, the layer, and the event trigger. Now comes the satisfying part, <strong>actually deploying everything</strong>.</p>
<p>To make this process smooth and beginner-friendly, we’ll not run long list of commands manually. Instead, the repository includes a small script that takes care of everything in the right order.</p>
<h2 id="heading-the-deploysh-script">The <code>deploy.sh</code> Script</h2>
<p>Inside the <code>scripts</code> folder, we’ll find a file called <code>deploy.sh</code>. This script is designed to guide the entire deployment process from start to finish.</p>
<p>When we run this script, here’s what it does.</p>
<p>First, it performs a few <strong>basic checks</strong>. It makes sure:</p>
<ul>
<li><p>AWS CLI is installed</p>
</li>
<li><p>Terraform is installed</p>
</li>
</ul>
<p>If either of these is missing, the script stops and tells us exactly what’s wrong. This saves time and avoids confusion later.</p>
<p>Next, the script builds the <strong>Lambda layer</strong>.</p>
<p>This is an important step. Remember, the Pillow library needs to be compiled in a Linux environment to work correctly with AWS Lambda. Instead of doing this manually, the script calls another helper script that uses Docker to:</p>
<ul>
<li><p>Spin up a Linux-based Python environment</p>
</li>
<li><p>Install Pillow in the correct directory structure</p>
</li>
<li><p>Package everything into a <code>pillow_layer.zip</code> file</p>
</li>
</ul>
<p>Once that’s done, the script moves into the Terraform directory and runs the familiar commands:</p>
<ul>
<li><p><code>terraform init</code></p>
</li>
<li><p><code>terraform plan</code></p>
</li>
<li><p><code>terraform apply</code></p>
</li>
</ul>
<p>All in the correct order.</p>
<p>Terraform then takes over and creates every AWS resource we discussed:</p>
<ul>
<li><p>Both S3 buckets</p>
</li>
<li><p>IAM roles and policies</p>
</li>
<li><p>Lambda layer</p>
</li>
<li><p>Lambda function</p>
</li>
<li><p>CloudWatch log group</p>
</li>
<li><p>S3 event trigger</p>
</li>
</ul>
<p>When the deployment finishes, the script prints out something very useful, the names of the buckets and the Lambda function that were just created.</p>
<p>This makes it easy to know exactly where to upload your image.</p>
<h1 id="heading-testing-the-workflow">Testing the Workflow</h1>
<p>Once deployment is complete, testing the project is beautifully simple.</p>
<p>We just upload an image to the <strong>upload bucket</strong>.</p>
<p>It can be any JPG or JPEG file. We can upload it using:</p>
<ul>
<li><p>The AWS Console</p>
</li>
<li><p>The AWS CLI</p>
</li>
<li><p>Any method that creates an object in the bucket</p>
</li>
</ul>
<p>The moment the file is uploaded, the event is triggered.</p>
<p>Behind the scenes:</p>
<ul>
<li><p>Lambda starts</p>
</li>
<li><p>The image is processed</p>
</li>
<li><p>Five new images are generated</p>
</li>
<li><p>All processed files appear in the destination bucket</p>
</li>
</ul>
<p>If we open CloudWatch, we can also see the logs generated by the Lambda function, helpful for understanding what happened and for troubleshooting if something goes wrong.</p>
<p>And just like that, we’ve built an <strong>event-driven, serverless image processing system</strong> using Terraform.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And with that, we’ve completed <strong>Day 18 of the 30 Days of Terraform Challenge</strong>.</p>
<p>Today’s demo walked us through the <strong>serverless side of AWS</strong>, which is such a crucial piece when we talk about building truly cloud-native solutions. Services like Lambda, S3 event triggers, and IAM roles show us how powerful the cloud can be when we design systems that react to events instead of relying on always-running servers. This mindset shift is important, and today was a solid step in that direction.</p>
<p>It’s completely okay if the codebase felt a bit overwhelming at first glance. That feeling is part of the journey. As developers, we’re not expected to understand everything instantly. What really matters is how we approach it. We can lean on <strong>LLMs, documentation, blogs, and videos</strong> to slowly break things down and understand what each line of code is doing. Reading code carefully, line by line, is a skill in itself, and it plays a huge role in becoming a better, more confident programmer over time.</p>
<p>To make things even easier, there’s a great video by <strong>Piyush Sachdeva</strong> that explains this demo clearly and walks through the entire flow. It’s especially helpful if you want to follow along step by step or need guidance with troubleshooting, definitely worth checking out alongside the code.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/l0RYCxczgyk?si=zyofQ5tLvgeR7RgP">https://youtu.be/l0RYCxczgyk?si=zyofQ5tLvgeR7RgP</a></div>
<p> </p>
<p>We’ll stop here for today, take a moment to absorb what we’ve learned, and come back refreshed tomorrow to continue the journey.</p>
<p>Until then, happy terraforming, see you on <strong>Day 19</strong></p>
]]></content:encoded></item><item><title><![CDATA[DAY #42: [DAY-17] AWS Terraform Blue-Green Deployment using Elastic Beanstalk]]></title><description><![CDATA[Introduction
Welcome back to Day 17 of our 30 Days of AWS Terraform journey.It nice to have you here again! Over the past few days, we’ve quietly built up little blocks of understanding together. Yesterday, we spent time with IAM authentication using...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-42-day-17-aws-terraform-blue-green-deployment-using-elastic-beanstalk</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-42-day-17-aws-terraform-blue-green-deployment-using-elastic-beanstalk</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Fri, 12 Dec 2025 05:23:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765516907641/dd387e4c-ce57-48e9-a0fb-346e2ef69879.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction"><strong>Introduction</strong></h1>
<p>Welcome back to Day 17 of our 30 Days of AWS Terraform journey.<br />It nice to have you here again! Over the past few days, we’ve quietly built up little blocks of understanding together. Yesterday, we spent time with IAM authentication using Terraform, and even though IAM can feel a bit strict and serious, we made it through with patience.</p>
<p>Today, we’re opening a new chapter, something practical, something used by real teams every single day, yet something that can feel intimidating when we first hear its name**: blue-green deployment.**</p>
<p><img src="https://codefresh.io/wp-content/uploads/2023/06/Blue-green-simple.gif" alt="What Is Blue/Green Deployment?" class="image--center mx-auto" /></p>
<p>If that phrase feels a bit heavy, that’s okay. Most things in tech sound complicated before someone explains them. And that’s exactly what we’re going to do today, breathe through it, and understand it like a story.</p>
<p>It’s a concept that might sound big but is actually just a simple idea wrapped in a slightly fancy name. By the time we reach the end, we’ll see how naturally all the pieces fit.</p>
<p>Our mini-project today uses AWS Elastic Beanstalk and Terraform to create two separate environments i.e. blue and green. We’ll package a small application, upload it to S3, create application versions, and watch how each environment gets its own version of the app. The most interesting part comes later, where with a simple “swap,” traffic can gently shift from one environment to the other, giving us a sense of how real deployments happen with almost no downtime.</p>
<p>So take a breath, settle in, and let’s begin our slow and steady walk into the world of blue-green deployments together.</p>
<p>Here’s a fact before we proceed:</p>
<blockquote>
<p>Fact #17: During my school and college years, drawing little sketches and doodles was something I kept coming back to. It was a hobby I really enjoyed.</p>
</blockquote>
<h1 id="heading-understanding-blue-green-deployment"><strong>Understanding Blue Green Deployment</strong></h1>
<p>Before we dive into any Terraform blocks or AWS resources, let’s take a pause. I want you to imagine something simple.</p>
<p>Picture our application as a cozy little café.</p>
<p><img src="https://media4.giphy.com/media/v1.Y2lkPTZjMDliOTUyanRwNHV1Z3RxZzI3aGR4cW1tdnVyaGt2eHFnbTFpMndnMWQyaHZ4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/kTEqpBl5W9X2w/giphy.gif" alt="Cafe GIFs - Find &amp; Share on GIPHY" class="image--center mx-auto" /></p>
<p>Our customers walk in, take a seat, enjoy the calm atmosphere, and everything just… works.<br />This café is our <strong>blue environment</strong> i.e. the one everyone knows, trusts, and uses every day. It’s open, it’s running, and people depend on it.</p>
<p>Now, suppose we want to renovate a few things, maybe update the menu, repaint the walls, or try new lighting. But doing that while customers are sitting inside? That’s stressful, messy, and risky. We don’t want people slipping on wet paint or being disturbed by hammering noises.</p>
<p>So instead, we quietly build an <strong>identical café right next door</strong>. Same furniture, same setup, same layout, everything in the same place. This second café is our <strong>green environment</strong>. It looks just like the blue one, but no customers are sitting inside yet. It’s empty… peaceful… safe to experiment with.</p>
<p>And that is the beauty of it.</p>
<p>In the green café, we can try all our improvements without disturbing anyone. Paint the walls, test new recipes, adjust the lighting, whatever we want. If something goes wrong, no one sees it. No one is affected. We simply fix it and try again.</p>
<p>Only when everything feels perfect i.e. when the chairs are aligned, the lights are warm, and the menu is polished, we gently swap the entrances.</p>
<p>Suddenly, customers walk into the green café without even realizing it, because from the outside, the signboard (our DNS name) is exactly the same. All that changed was the door they’re stepping through.</p>
<p>The old blue café becomes the standby, still there, still safe, still intact. If we ever need to switch back because we found a problem later, we simply swap the doors again. Everything stays smooth.</p>
<p>In real applications, this is exactly how a blue-green deployment works.</p>
<ul>
<li><p><strong>Blue</strong> is our production environment i.e. the live one.</p>
</li>
<li><p><strong>Green</strong> is our duplicate i.e. the safe testing ground.</p>
</li>
<li><p>And the <strong>swap</strong> is the seamless transition our users never feel.</p>
</li>
</ul>
<p>Maybe our blue environment started with version 1.0, and when we created green, it also mirrored 1.0 to stay identical. After checking everything, you upgraded green to 2.0, tested it, and once we felt confident, we swapped the environments. Traffic that once flowed to blue now flows to green, gracefully, without a single customer getting disturbed.</p>
<p>This is why teams love the blue-green approach: it keeps users happy, avoids those stressful downtimes, and gives us a cushion to experiment safely while ensuring our deployments feel smooth and effortless.</p>
<h1 id="heading-preparing-s3-and-application-packaging"><strong>Preparing S3 and Application Packaging</strong></h1>
<p>Before we even talk about environments or deployments, I want to pause with you for a moment and slowly walk into the first stepping stone of this whole process.</p>
<p>Every application, no matter how small, needs a home where its packaged files can stay safely before being deployed. In our case, that home is an S3 bucket. Instead of imagining S3 as a complicated AWS service, picture it as a little storage room where we keep different versions of our application, each carefully labeled and tucked away.</p>
<p>So the very first thing we do is upload our application package to this bucket. The package is simply a zip file named <strong>app-v1.zip</strong>, we can think of it like wrapping your application in a clean, sealed envelope. Terraform picks up this envelope and stores it in the S3 room so that Elastic Beanstalk can come later, open it, and use it to create a version of our application.</p>
<p>This simple but important step looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765515610842/886c4b8f-97a8-4931-885c-aa9ea2ae8876.png" alt class="image--center mx-auto" /></p>
<p>Let’s walk through this:</p>
<ul>
<li><p><strong>resource "aws_s3_object" "app_v1"</strong><br />  This is Terraform saying, “I want to upload a file to S3, and I’ll refer to this upload as app_v1.”</p>
</li>
<li><p><strong>bucket = aws_s3_bucket.app_versions.id</strong><br />  This tells Terraform which S3 bucket to place the file on. The bucket already exists somewhere in our setup.</p>
</li>
<li><p><strong>key = "app-v1.zip"</strong><br />  Think of this as the label on the file once it’s inside the bucket. The name we want it to have.</p>
</li>
<li><p><strong>source = "${path.module}/app-v1/app-v1.zip"</strong><br />  This is the real physical location of our zip file on our computer or project folder.<br />  Terraform looks here and picks up the file.</p>
</li>
<li><p><strong>etag = filemd5(...)</strong><br />  This is just Terraform checking the file’s fingerprint to know if it has changed.<br />  We don’t need to worry about the details, just think of it as a “change detector.”</p>
</li>
<li><p><strong>tags = var.tags</strong><br />  This attaches your usual tags so everything stays organized.</p>
</li>
</ul>
<p>And that’s all it is.</p>
<p>A tiny resource, doing a tiny job, but it sets everything else in motion. Without this file sitting in S3, Elastic Beanstalk wouldn’t know where to pull the application version from.</p>
<h1 id="heading-blue-environment-details">Blue Environment Details</h1>
<p>Now that our application package is safely stored in S3, it’s time to take the next step: creating our <strong>blue environment</strong> in Elastic Beanstalk. Think of the blue environment as the steady, reliable “home base” for our application, the environment that will first serve users and carry the version 1.0 of our app.</p>
<p>Before we actually deploy the environment, we need to define the <strong>application version</strong>. This tells Elastic Beanstalk exactly which package to deploy. Here’s the Terraform code that does that:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473722828/2ebe88c9-65e9-47ab-830b-bdd78a256451.png" alt class="image--center mx-auto" /></p>
<p>Let’s pause and understand this:</p>
<ul>
<li><p><code>name</code> is just a label for this version. We’re calling it <code>v1</code> to match our first release.</p>
</li>
<li><p><code>application</code> links this version to the Elastic Beanstalk application we created earlier.</p>
</li>
<li><p><code>description</code> is a friendly note to remind us this is version 1.0, the initial release.</p>
</li>
<li><p><code>bucket</code> and <code>key</code> tell Elastic Beanstalk <strong>where to find the zip file</strong> for this version. The bucket is our S3 bucket, and the key is the file name inside it (<code>app-v1.zip</code>).</p>
</li>
<li><p><code>tags</code> are simple labels we attach to this version for easy identification and organization.</p>
</li>
</ul>
<p>Once the version is ready, we can create the <strong>blue environment</strong> itself:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473802309/662c0d76-93ce-4ff3-9fc8-4a243046d64e.png" alt class="image--center mx-auto" /></p>
<p>Here’s what’s happening in this code:</p>
<ul>
<li><p><code>name</code> gives our environment a friendly identifier: <code>blue</code>.</p>
</li>
<li><p><code>application</code> points back to the Elastic Beanstalk app we’re deploying.</p>
</li>
<li><p><code>solution_stack_name</code> tells Elastic Beanstalk what type of environment to run, like which operating system, web server, and platform to use.</p>
</li>
<li><p><code>tier</code> is set to <code>WebServer</code> because this environment will serve HTTP requests to our users.</p>
</li>
<li><p><code>version_label</code> tells the environment <strong>which version of the app to deploy</strong>, here, it is <code>v1</code>.</p>
</li>
</ul>
<p>But a web application is not just a single instance; it has many moving parts. That’s why we add <strong>settings</strong> to configure the environment properly. These settings handle things like IAM roles, instance types, load balancing, scaling, health checks, and environment variables. Let’s take a few examples:</p>
<p>IAM instance profile (gives EC2 instances permission to access AWS resources):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473852984/8e9cdec8-4f4a-409f-8e52-03c65e97b482.png" alt class="image--center mx-auto" /></p>
<p>Service role (allows Elastic Beanstalk itself to manage resources):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473880824/a0dedff6-9fde-44b1-9eb7-1002e9262e92.png" alt class="image--center mx-auto" /></p>
<p>Instance type (defines the size of the virtual machine running our app):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473901360/508a8a5d-3b8f-4a67-a51e-272e8c1fcc37.png" alt class="image--center mx-auto" /></p>
<p>Load balancer configuration (ensures traffic is distributed across instances):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473921879/f6b30d50-8df1-423f-9a0e-d06d7d9da4ab.png" alt class="image--center mx-auto" /></p>
<p>Autoscaling (minimum number of instances):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473946425/da39ca36-a1af-4242-a05e-ed9666334585.png" alt class="image--center mx-auto" /></p>
<p>Health reporting (helps monitor application status):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765473977954/b428135a-d793-44eb-b05e-52baee15809c.png" alt class="image--center mx-auto" /></p>
<p>Environment variables (used by our app to know it’s running in blue):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765474001025/6ceaba5a-cd93-4743-b9c7-44953bc0a66c.png" alt class="image--center mx-auto" /></p>
<p>Finally, we add <strong>tags</strong> to identify the environment:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765474020291/3306bcc3-a7c3-42de-867e-f2cacaec14d2.png" alt class="image--center mx-auto" /></p>
<p>These tags are helpful when you have multiple environments and want to quickly filter resources by environment or role.</p>
<p>When we put all these pieces together, we now have a fully configured <strong>blue environment</strong>, ready to serve our first users with version 1.0 of our application. Every setting ensures our application runs reliably, is secure, and can scale when traffic increases.</p>
<h1 id="heading-green-environment-details">Green Environment Details</h1>
<p>Now that our blue environment is up and running, serving version 1.0 of the application to users, it’s time to bring the <strong>green environment</strong> to life. Think of the green environment as the “practice stage” or the <strong>staging environment</strong>, it’s an exact twin of blue, but it will host the next version of our application, version 2.0.</p>
<p>The beauty of blue-green deployment is that we don’t touch the live production environment while preparing the next release. Instead, we work safely in the green environment, test everything thoroughly, and only then make it live.</p>
<h1 id="heading-iam-roles-and-profiles">IAM Roles and Profiles</h1>
<p>Now that our blue and green environments are taking shape, let’s pause and explore one of the important parts of this journey: <strong>IAM roles and instance profiles</strong>. Think of IAM as the permission system in AWS, like a backstage pass that lets our application and the underlying servers do their jobs safely. Without proper roles, our Elastic Beanstalk environments wouldn’t be able to access S3, update themselves, or scale properly.</p>
<p>Let’s start with the <strong>EC2 role</strong>. Each EC2 instance in Elastic Beanstalk needs permission to perform certain actions on AWS services. Here’s how we define it in Terraform:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765474395180/c07259b5-c189-4681-9208-b979d3528e53.png" alt class="image--center mx-auto" /></p>
<p>Let’s unpack this:</p>
<ul>
<li><p><code>name</code> is simply the friendly name of this role. We’re using a variable so it stays dynamic.</p>
</li>
<li><p><code>assume_role_policy</code> is a JSON that tells AWS <strong>who can “assume” this role</strong>. In our case, the EC2 service itself (<code>ec2.amazonaws.com</code>) is allowed to assume it.</p>
</li>
<li><p><code>tags</code> help us organize and identify resources, which is especially useful in real-world projects.</p>
</li>
</ul>
<p>Next, we attach AWS-managed policies to this role. These policies define <strong>what actions the EC2 instances can actually perform</strong>. For example, here’s the attachment for the web tier:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765474432777/5013f56c-788e-4d1e-b2b5-6d72f90cd073.png" alt class="image--center mx-auto" /></p>
<p>This is like giving your EC2 instances a pre-configured toolbox for web applications. We can also attach other policies for worker tiers or multi-container Docker if needed, these ensure that the EC2 instances can do everything required by the environment without manually assigning individual permissions.</p>
<p>Now that the role is ready, we create an <strong>instance profile</strong>, which essentially <strong>links the IAM role to the EC2 instances</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765474457779/59598435-f5d9-4c38-9b5f-48118b3a24f7.png" alt class="image--center mx-auto" /></p>
<p>It carries the IAM role badge and gives it to the EC2 instances so they know what they’re allowed to do.</p>
<p>Finally, Elastic Beanstalk itself needs a role to perform operations on our behalf, like deploying new versions, reporting health, or performing managed updates. Here’s the service role:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765474491008/b4bcc139-1750-4e1e-ab38-cec043821510.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Here, the principal is <code>elasticbeanstalk.amazonaws.com</code>, this tells AWS that Elastic Beanstalk can assume this role.</p>
</li>
<li><p>We attach policies for <strong>enhanced health reporting</strong> and <strong>managed updates</strong>, so the service can monitor and maintain the environments automatically.</p>
</li>
</ul>
<p>All these pieces together i.e. EC2 role, policies, instance profile, and service role form the <strong>IAM foundation</strong> of our Elastic Beanstalk environments. Without them, nothing would work. This is one of those parts of the deployment that runs silently, allowing the environments to function reliably and securely.</p>
<h1 id="heading-app-versions-deployment-flow-and-swap">App Versions, Deployment Flow and Swap</h1>
<p>Now that we have both our <strong>blue and green environments</strong> set up, let’s take a moment to imagine how traffic flows between them. At this stage, each environment is running its own version of the application: blue is our stable production version, and green is our updated version waiting in the wings.</p>
<p>The magic of blue-green deployment lies in the <strong>swap</strong>. When we perform the swap, behind the scenes, the DNS entry is updated so that all traffic now flows to the green environment.</p>
<p>This approach is what makes blue-green deployment so powerful. It allows us to:</p>
<ul>
<li><p><strong>Test safely</strong>: Any updates are first applied to the green environment without affecting users.</p>
</li>
<li><p><strong>Minimize downtime</strong>: The switch happens quickly; users barely notice.</p>
</li>
<li><p><strong>Roll back easily</strong>: If something goes wrong with the green environment, a simple swap can revert traffic back to blue, restoring the stable version instantly.</p>
</li>
</ul>
<p>In short, the swap is the heart of this strategy. It lets us update our application confidently, keeping users happy and the system reliable, all without a stressful downtime.</p>
<h1 id="heading-demo-version-10-and-version-20">Demo: Version 1.0 and Version 2.0</h1>
<p>Now comes one of my favorite parts, the moment where the concepts we’ve been discussing start to feel real. Imagine we’ve carefully built your blue and green environments, uploaded your application packages, and set everything up. It’s time to see them in action.</p>
<p>First, we visit the <strong>blue environment</strong>. This is our steady, live production environment, the one that our users are currently interacting with. When we open the URL, you are greeted with a friendly message:</p>
<p>“Welcome to the blue-green deployment demo. This is version 1.0.”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766987511404/212c51d3-40d1-486c-a010-da344fe9ef9a.png" alt class="image--center mx-auto" /></p>
<p>Alongside this, we might see some metadata, like the server time and host name. These little details give us reassurance that our environment is alive and stable. The blue environment represents the foundation, it’s reliable, predictable, and serving users exactly as expected.</p>
<p>Next, we take a peek at the <strong>green environment</strong>. This is the twin environment, waiting in the wings. It mirrors the blue environment in every way, but now it carries <strong>version 2.0</strong>, the updated release. When we open the green URL, we’ll see a message like:</p>
<p>“It is in the status staging, and these are the new features released in version v2.0.”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766987561497/343c8718-0dd6-4667-8f46-ec2c342686fa.png" alt class="image--center mx-auto" /></p>
<p>Notice how the green environment isn’t yet live for users, it’s in staging. It’s a safe space to verify that everything works as intended. We can explore the new features, check performance, and make sure the updates behave correctly, all without touching the live blue environment.</p>
<p>So, at this stage, the setup is clear:</p>
<ul>
<li><p>Blue is live, serving version 1.0 to users.</p>
</li>
<li><p>Green is staging, hosting version 2.0 and ready for testing.</p>
</li>
</ul>
<p>This careful pacing ensures we can appreciate why blue-green deployment works so elegantly. The green environment is not rushing ahead, it’s waiting, tested and ready, until it is time to take the stage.</p>
<h1 id="heading-rollback-and-destroy">Rollback and Destroy</h1>
<p>Now, imagine this: we’ve done the swap, and the green environment is live with the new version.</p>
<p>Everything seems fine, but what if something unexpected pops up? Maybe a small bug, or a feature isn’t behaving exactly as we hoped. Don’t worry, that’s the beauty of blue-green deployment.</p>
<p>Because the blue environment still exists, untouched, we have a safety net. Rolling back is as simple as performing another swap. The traffic will flow back to the stable blue environment, and our users won’t even notice the hiccup.</p>
<p>Once we’ve completed our testing, observed the new version, and are satisfied with how everything works, it’s time to tidy up. Terraform makes this easy. Running a <code>terraform destroy</code> will carefully remove all the resources we created for this demo.</p>
<p>Taking this approach not only keeps your AWS account clean but also reinforces a disciplined, thoughtful workflow, especially helpful for beginners. Step by step, you’re learning to manage deployments safely, predictably, and with confidence.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And just like that, we reach the gentle end of today’s little adventure together. I hope you’ve been able to follow along at a comfortable pace and felt the story of blue-green deployment unfold.</p>
<p>I truly hope that today’s journey has given us not only a clearer understanding of blue-green deployment but also a sense of confidence. And if at any point things feel overwhelming, that’s perfectly okay too. It simply means there’s something new asking for a little extra time and attention. With concepts like these, the only real way to feel confident is through gentle, steady hands-on practice.</p>
<p>So if you’d like to revisit today’s topic in a more visual way, there’s a wonderful video by Piyush Sachdeva that walks through the same idea slowly and clearly.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/fTVx2m5fEbQ?si=48zLQYRGq0aqnVsO">https://youtu.be/fTVx2m5fEbQ?si=48zLQYRGq0aqnVsO</a></div>
<p> </p>
<p>Tomorrow, we’ll continue our shared journey with another story, another mini-project, and more pieces of the AWS Terraform puzzle. Thank you for walking through today’s blog with me.</p>
<p>I’m grateful for your curiosity, your patience, and the steady effort you’re putting into understanding something new. Until then, take a moment to appreciate how far we’ve come and I’ll see you tomorrow.</p>
]]></content:encoded></item><item><title><![CDATA[DAY #41: [DAY-16] AWS Terraform IAM Authentication]]></title><description><![CDATA[Introduction
Hello and welcome back to the blog! I hope you’re keeping warm in this chilly weather!
Yesterday, we wrapped up a small but meaningful mini-project around AWS VPC Peering using Terraform, a topic that can seem intimidating but we walked ...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-41-day-16-aws-terraform-iam-authentication</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-41-day-16-aws-terraform-iam-authentication</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Wed, 10 Dec 2025 15:33:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765379116972/3e78153e-9675-40c7-82b4-95752a66dc4d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back to the blog! I hope you’re keeping warm in this chilly weather!</p>
<p>Yesterday, we wrapped up a small but meaningful mini-project around AWS VPC Peering using Terraform, a topic that can seem intimidating but we walked through it step by step, making it approachable and understandable.</p>
<p>Today, we’re easing into <strong>Day 16 of our 30 Days of AWS Terraform journey</strong>, and we’re tackling a question every cloud beginner eventually wonders about:<br /><strong>How do we manage users in AWS, especially when there are dozens of them?</strong></p>
<p><img src="https://media.tenor.com/Ii_MoTuaIXEAAAAM/whos-managing-this-costumer.gif" alt="Management GIFs | Tenor" class="image--center mx-auto" /></p>
<p>Before jumping into code, imagine this: you start a new job and discover that thirty new employees are joining on the same day. Someone has to create their AWS accounts, assign access, place them into the right groups, and set temporary passwords so they can log in. Doing all that manually? Exhausting. Definitely a job made for automation.</p>
<p>Here’s what we’ll cover in today’s mini-project:</p>
<ul>
<li><p>Prepare a CSV file containing user details</p>
</li>
<li><p>Convert that CSV into a list of maps</p>
</li>
<li><p>Apply loops, conditionals, IAM groups, dynamic assignments, and login profiles</p>
</li>
<li><p>Keep everything beginner-friendly while connecting each step to the bigger picture</p>
</li>
</ul>
<p>We are not rushing into IAM resources just yet; we’ll set the stage first.</p>
<p>In the next section, we’ll start by creating our CSV file i.e. the heart of our bulk user-creation workflow and discuss why it’s important and how it will be used.</p>
<p>Before we proceed, a little personal fact for Day 16</p>
<blockquote>
<p>Fact #16: If you’re not aware, I consider myself a bit of a movie connoisseur, and my favorite movie of 2025 was <strong>Sinners</strong>! So instead of looking it up, I’d suggest watching it without having any expectations!</p>
</blockquote>
<h1 id="heading-creating-the-csv-file-laying-the-foundation">Creating the CSV File: Laying the Foundation</h1>
<p>Before we write a single line of Terraform code, let’s start with something simple and familiar: a <strong>CSV file</strong>. This small, plain text file will become the backbone of our entire mini-project. This file will hold all the user information we want Terraform to work with, allowing us to create many users at once, without having to type each one manually.</p>
<p>Let’s begin!</p>
<p>Instead of thinking of <strong>“user management”</strong> as a big, overwhelming AWS task, imagine it like organizing a classroom on the first day of school. We have a list of students, each with their own details, and our job is to place them in the right groups and give them what they need. In our case, the CSV file is that <strong>student list</strong>.</p>
<p>We’ll store information for around <strong>30 users</strong>, and each row will contain just <strong>four simple fields</strong>:</p>
<ul>
<li><p><code>first_name</code></p>
</li>
<li><p><code>last_name</code></p>
</li>
<li><p><code>department</code></p>
</li>
<li><p><code>job_title</code></p>
</li>
</ul>
<p>We don’t want to complicate things. Terraform will use <strong>exactly these fields</strong> to create IAM users, tag them, add them to groups, and manage their login profiles.</p>
<p>Here’s a small preview of what the CSV might look like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765375821586/3ef2b910-b5df-446b-9973-313939792182.png" alt class="image--center mx-auto" /></p>
<p>We actual file will have around 30 entries, but this small snippet is enough to understand the structure.</p>
<p>Once we save this file as <code>users.csv</code> inside our Terraform project directory, we’ll be ready for the next step: teaching Terraform <strong>how to read this file</strong> and convert it into something it can actually use.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765380028665/9a76f2e7-f532-4c7d-9376-4b6e6e41e8bd.png" alt class="image--center mx-auto" /></p>
<p>Before moving ahead, take a moment to appreciate this:<br />Even something as simple as a CSV becomes <strong>powerful</strong> when Terraform interprets it. Instead of manually creating 30 IAM users, Terraform will handle everything for us.</p>
<p>In the next section, we’ll explore how to convert this CSV into a format Terraform understands using a very handy function: <code>csvdecode()</code>.</p>
<h1 id="heading-converting-the-csv-into-a-terraform-friendly-format-understanding-csvdecode">Converting the CSV into a Terraform-Friendly Format: Understanding <code>csvdecode()</code></h1>
<p>Now that our CSV file is ready, the next step is to <strong>help Terraform understand it</strong>. At first glance, the file might seem simple i.e. just rows and columns of text but Terraform doesn’t see it the same way we do. To Terraform, it’s just raw text. It can’t yet loop through it, read values, or create users dynamically.</p>
<p>That’s where one of Terraform’s very helpful functions comes in: <code>csvdecode()</code>.</p>
<h2 id="heading-why-do-we-need-csvdecode">Why do we need <code>csvdecode()</code>?</h2>
<p>Terraform cannot directly perform operations on raw text files. If we tried to work with the CSV as-is, every step would require <strong>complicated parsing</strong> or <strong>messy manual loops</strong> which would defeat the purpose of using Terraform in the first place.</p>
<p><code>csvdecode()</code> takes the entire CSV file and transforms it into a <strong>list of maps</strong>. Each row in our CSV becomes a map (think of it like a small dictionary), and each column in that row becomes a key-value pair.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765380613842/5dfd570a-6739-4158-8844-1995250aeeb1.png" alt class="image--center mx-auto" /></p>
<p>This transformation makes our data <strong>predictable</strong>, <strong>easy to use</strong>, and <strong>ready for dynamic operations</strong> inside Terraform.</p>
<h2 id="heading-the-code">The Code</h2>
<p>Here’s the local block we’ll use:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765376015060/e601f8f7-9cdd-46a5-929e-6f3aeae16715.png" alt class="image--center mx-auto" /></p>
<p>Let’s break this down:</p>
<ul>
<li><p><code>file("users.csv")</code> → reads the CSV file as plain text</p>
</li>
<li><p><code>csvdecode(...)</code> → converts that plain text into structured data</p>
</li>
<li><p><code>users</code> → a variable that becomes a list of maps, where each map looks something like this:</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765376070744/b6624b0e-619f-4d51-9823-3be9c40d6cd4.png" alt class="image--center mx-auto" /></p>
<p>We can almost imagine Terraform laying out all 30 users in front of it, each with their own labels and details.</p>
<h2 id="heading-why-is-this-conversion-important">Why is this conversion important?</h2>
<p>Once the data becomes a list of maps, Terraform can:</p>
<ul>
<li><p>Loop through it easily</p>
</li>
<li><p>Extract specific fields like <code>user.first_name</code> or <code>user.department</code></p>
</li>
<li><p>Create IAM users dynamically</p>
</li>
<li><p>Use conditions to assign users to groups</p>
</li>
<li><p>Tag resources properly</p>
</li>
</ul>
<p>Without this conversion, we would be stuck writing repetitive logic or manually handling text, something Terraform is <strong>not designed for</strong>.</p>
<h2 id="heading-an-important-mindset-shift">An Important Mindset Shift</h2>
<p>Instead of thinking of the CSV as just an external file, consider it as <strong>part of Terraform’s data model</strong>. Once decoded, Terraform treats the CSV exactly like any other structured variable.</p>
<p>In the next section, we’ll add another small but crucial piece: retrieving our <strong>AWS account ID</strong> using a datasource. This helps Terraform understand <strong>where</strong> it’s creating these resources, keeping everything organized and consistent.</p>
<h1 id="heading-retrieving-our-aws-account-id-using-a-data-source">Retrieving Our AWS Account ID Using a Data Source</h1>
<p>Now that Terraform understands our CSV and has neatly organized all the user data, the next step is helping Terraform understand <em>where</em> these resources will be created.<br />In AWS, every action i.e. creating users, policies, roles happens inside our unique AWS account.</p>
<p>Terraform can automatically fetch this account ID for us using a special feature called a <strong>data source</strong>.</p>
<h2 id="heading-what-is-a-data-source">What is a data source?</h2>
<p>A data source in Terraform is like asking AWS a question and getting back an answer.<br />We’re not creating anything, we’re simply retrieving information that AWS already knows.</p>
<p>In this case, we want Terraform to ask AWS:</p>
<p><em>“What is my account ID?”</em></p>
<p>This ID is important because it helps Terraform correctly reference the AWS resources it creates, especially when generating ARNs.</p>
<h2 id="heading-the-code-1">The Code</h2>
<p>Here’s the data source we’ll use:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765376260123/2d35e792-b5e4-4039-bb5b-cb50433282b0.png" alt class="image--center mx-auto" /></p>
<p>When Terraform reads this, it contacts AWS and fetches three pieces of information:</p>
<ul>
<li><p><code>account_id</code></p>
</li>
<li><p><code>arn</code></p>
</li>
<li><p><code>user_id</code></p>
</li>
</ul>
<p>But the one we care about most right now is:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765376308624/263e3421-78b0-481a-8d10-739524e73606.png" alt class="image--center mx-auto" /></p>
<p>Terraform will now know our account ID without us typing it anywhere, which keeps our code clean, safe, and reusable.</p>
<h2 id="heading-why-is-this-useful">Why is this useful?</h2>
<p>Once Terraform knows the account ID, we can:</p>
<ul>
<li><p>construct IAM ARNs dynamically</p>
</li>
<li><p>avoid hardcoding sensitive values</p>
</li>
<li><p>write modules that work in any AWS account</p>
</li>
<li><p>reduce mistakes when referencing users or roles</p>
</li>
<li><p>keep your configuration future-proof</p>
</li>
</ul>
<p>Now that Terraform understands:</p>
<ul>
<li><p>our CSV users</p>
</li>
<li><p>our AWS account context</p>
</li>
</ul>
<p>We’re ready to write the IAM user creation logic, the part where Terraform creates users dynamically using loops.</p>
<h1 id="heading-creating-iam-users-dynamically-with-foreach">Creating IAM Users Dynamically With <code>for_each</code></h1>
<p>Now that Terraform understands our CSV data and knows our AWS account ID, we can finally move to the exciting part i.e. <strong>creating IAM users automatically</strong>.</p>
<p>Instead of writing 30 separate IAM user blocks (which would be painful, repetitive, and easy to make mistakes), Terraform allows us to create all users in <strong>one clean, elegant block</strong> using <code>for_each</code>.</p>
<h2 id="heading-why-use-foreach">Why use <code>for_each</code>?</h2>
<p>When we decoded our CSV using <code>csvdecode</code>, Terraform turned the plain text into a <strong>list of maps</strong>. Each map contains the user’s details like <code>first_name</code>, <code>last_name</code>, <code>department</code>, and <code>job_title</code>.</p>
<p>Now, we want Terraform to go through this list and create a corresponding IAM user for every single row. That’s exactly what <code>for_each</code> does, it loops through the list and handles each user automatically.</p>
<p>Each map in that list represents <strong>one user</strong>, so Terraform will:</p>
<ul>
<li><p>take the first row → create user 1</p>
</li>
<li><p>take the second row → create user 2</p>
</li>
<li><p>…and continue for every row in the CSV</p>
</li>
</ul>
<p>This makes the whole configuration <strong>short, clean, and scalable</strong>, no matter how many users we have.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765380745016/33917930-6c2d-48d5-bd51-9935f5217bd0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-code-2">The Code</h2>
<p>Here’s how we create all users from the CSV:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765377498469/bc257873-0bbb-4ded-b7b8-508be93e6c8f.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>for_each = { for user in local.users : user.first_name =&gt; user }</code></p>
<ul>
<li><p>Here, Terraform is taking each user from the list we got from the CSV.</p>
</li>
<li><p><code>user.first_name</code> is used as the key because Terraform needs a unique identifier for each item.</p>
</li>
<li><p>This loop ensures that Terraform creates a resource for every single user automatically.</p>
</li>
</ul>
</li>
<li><p><code>name = lower("${substr(each.value.first_name, 0, 1)}${each.value.last_name}")</code></p>
<ul>
<li><p>This creates a username for AWS.</p>
</li>
<li><p>We take the <strong>first letter of the first name</strong> and combine it with the <strong>last name</strong>.</p>
</li>
<li><p><code>lower()</code> converts the username to lowercase so it’s consistent.</p>
</li>
<li><p>Example: John Doe → <code>jdoe</code></p>
</li>
</ul>
</li>
<li><p><code>tags = { ... }</code></p>
<ul>
<li><p>Tags are like little labels attached to the user.</p>
</li>
<li><p>Here, we store the <strong>full name</strong>, <strong>department</strong>, and <strong>job title</strong>.</p>
</li>
<li><p>Tags help us organize users and make it easier to find them later in AWS.</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-why-this-approach-is-so-powerful">Why this approach is so powerful</h2>
<ul>
<li><p>We can create 10, 30, or even 10,000 users with <strong>the same code</strong>.</p>
</li>
<li><p>Adding a new user to the CSV automatically creates that IAM user.</p>
</li>
<li><p>Removing a user from the CSV tells Terraform to <strong>destroy that user</strong> safely.</p>
</li>
<li><p>Every user receives tags that match your CSV data.</p>
</li>
<li><p>The configuration remains <strong>readable, scalable, and beginner-friendly</strong>.</p>
</li>
</ul>
<p>In short, Terraform does the heavy lifting, we only need to maintain the CSV, and everything else happens automatically.</p>
<h1 id="heading-assigning-iam-users-to-groups-based-on-their-department">Assigning IAM Users to Groups Based on Their Department</h1>
<p>Now that we have our IAM users created from the CSV, it’s time to place them into the correct groups.<br />Why is this important? In AWS, groups help us manage <strong>permissions and access</strong> for multiple users at once. For example:</p>
<ul>
<li><p>Users in the <strong>Education</strong> department might need different access than users in <strong>Engineering</strong>.</p>
</li>
<li><p>Instead of assigning permissions one by one, we can assign permissions to groups, and anyone in that group automatically inherits them.</p>
</li>
</ul>
<p>In our mini-project, we’ll create <strong>three groups</strong>: Education, Managers, and Engineers. Then, we’ll assign users dynamically based on the <strong>Department</strong> tag from our CSV. This way, Terraform does all the heavy lifting, and we don’t have to manually add each user to a group.</p>
<h2 id="heading-step-1-create-the-groups">Step 1: Create the Groups</h2>
<p>Let’s first define our IAM groups in a new file, for example <a target="_blank" href="http://groups.tf"><code>groups.tf</code></a>. Here’s how it looks:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765378192380/f1e4dca9-4cd4-4181-931f-4955e02a9ee4.png" alt class="image--center mx-auto" /></p>
<p><strong>What’s happening here?</strong></p>
<ul>
<li><p><code>name</code> → the name of the IAM group in AWS.</p>
</li>
<li><p><code>path</code> → a folder-like structure for organizing groups (optional but recommended).</p>
</li>
</ul>
<p>Now we have three groups ready. The next step is adding users to these groups dynamically.</p>
<h2 id="heading-step-2-add-users-to-a-group-dynamically">Step 2: Add Users to a Group Dynamically</h2>
<p>Instead of manually listing each user, Terraform lets us use a <strong>for loop</strong> to assign users automatically based on the <code>Department</code> tag.</p>
<p>Here’s an example for the Education group:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765378231798/6cbb54c9-e393-4941-bd10-089b6a174e6a.png" alt class="image--center mx-auto" /></p>
<p><strong>Let’s break this down:</strong></p>
<ul>
<li><p><code>for user in aws_iam_user.users</code> → loops through every IAM user we created earlier.</p>
</li>
<li><p><code>user.name</code> → grabs the username for each user.</p>
</li>
<li><p><code>if user.tags.Department == "Education"</code> → only includes users whose Department tag is “Education.”</p>
</li>
</ul>
<p>This means Terraform <strong>automatically finds all users in the Education department</strong> and adds them to the Education group.</p>
<p>We don’t have to type each user manually. This is especially useful when dealing with 30 or more users.</p>
<h2 id="heading-step-3-repeat-for-other-groups">Step 3: Repeat for Other Groups</h2>
<p>The same logic can be applied to the Managers and Engineers groups:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765378317491/c1f118ed-c05c-4581-8c73-23c1a0bd6cda.png" alt class="image--center mx-auto" /></p>
<p>This keeps everything <strong>consistent and dynamic</strong>. Adding a new user to the CSV automatically assigns them to the correct group without touching the Terraform code.</p>
<h2 id="heading-step-4-how-this-works-together">Step 4: How This Works Together</h2>
<ol>
<li><p>Terraform loops through all users created from the CSV.</p>
</li>
<li><p>Checks their Department tag.</p>
</li>
<li><p>Automatically adds them to the correct IAM group.</p>
</li>
</ol>
<p>Once users are in their groups, the next step is to <strong>create IAM login profiles</strong>. This will allow each user to log in to the AWS console, set their own password, and start working.</p>
<h1 id="heading-creating-iam-login-profiles-so-users-can-sign-in">Creating IAM Login Profiles So Users Can Sign In</h1>
<p>Now that we have our IAM users ready, it’s time to give them a way to actually <strong>sign in to the AWS console</strong>.</p>
<p>Imagine this like handing each new team member a <strong>temporary key to the office</strong>. They can enter, take a look around, but they’ll set up their own permanent key (password) the first time they log in.</p>
<p>We’ll do the same with Terraform by creating <strong>login profiles</strong> for each user.</p>
<h2 id="heading-why-do-we-need-login-profiles">Why do we need login profiles?</h2>
<p>Without a login profile, a user can exist in AWS but <strong>cannot log in to the console</strong>.<br />We want each user to:</p>
<ul>
<li><p>Access the console easily</p>
</li>
<li><p>Receive a temporary password automatically</p>
</li>
<li><p>Reset the password on their first login</p>
</li>
</ul>
<p>Terraform will create these <strong>for every user automatically</strong>, even if there are 30 or more users.</p>
<h2 id="heading-the-code-3">The Code</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765378765466/04ca0670-6de2-40aa-876c-d9d410bc0dca.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-lets-walk-through-it">Let’s walk through it</h3>
<ul>
<li><p><code>for_each = aws_iam_user.users</code><br />  Think of this as Terraform going down the list of all users we created earlier.</p>
</li>
<li><p><code>user = each.value.name</code><br />  This assigns the login profile to the correct user.</p>
</li>
<li><p><code>password_reset_required = true</code><br />  This is like giving them a <strong>temporary key</strong> and saying:<br />  <em>“Please change this key when you enter for the first time.”</em></p>
</li>
<li><p><code>lifecycle { ignore_changes = [...] }</code><br />  This tells Terraform to leave certain settings alone, so the login profile doesn’t get accidentally changed in the future.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766938640751/4b45c8e0-87af-4bb1-9242-5ff88df5d0b2.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-seeing-the-login-profiles-demo-output">Seeing the login profiles (demo output)</h2>
<p>For our demo, we can create an output to <strong>check that Terraform has created login profiles</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765378868812/d29504cf-1249-4d8d-8eac-5a91b61a16aa.png" alt class="image--center mx-auto" /></p>
<p>This does <strong>not</strong> show real passwords (AWS keeps them secure), but it gives us a <strong>confirmation message for each user</strong>.</p>
<p><code>sensitive = true</code> ensures the messages don’t accidentally show up in log.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766938522230/b832462d-4672-469c-9906-d2518fe767e3.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And with that, we’ve completed <strong>Day 16 of the 30 Days of Terraform Challenge</strong>.</p>
<p>I hope you found today’s blog a bit lighter and more approachable compared to the last two sessions. I say this because, having personally gone through the previous two blogs hands-on, I noticed how much more comfortable things start to feel once you get some practical experience under your belt. That’s one of the beautiful aspects of this journey: we slowly transition from simply learning theoretical concepts to <strong>actively applying them in real scenarios</strong>!</p>
<p>What I particularly enjoy about these hands-on sessions, which doesn’t always appear in the blog itself, is <strong>troubleshooting</strong>. Troubleshooting is often the hidden gem of learning, it’s where we really understand what’s happening behind the scenes. Sometimes Terraform throws an error, a loop doesn’t work as expected, or a resource isn’t created exactly how you anticipated. Figuring out why, experimenting with small changes, and eventually resolving the issue is where deep learning happens. These steps are usually messy and time-consuming, and the blogs don’t always show them, but trust me, that process is invaluable.</p>
<p>Personally, I like to dedicate uninterrupted time, often late at night, to complete a demo in one go.</p>
<p>I think I talked enough, the real learning comes from <strong>doing</strong>. Take the CSV file you prepared, apply the Terraform configuration, and observe how the automation unfolds. If you run into issues, take your time to explore the errors, check resource states, and practice fixing them.</p>
<p>To make this process easier, here’s a helpful video by <strong>Piyush Sachdeva</strong> that explains the workflow step by step and even walks through some common troubleshooting scenarios. Following along with such a resource can make the learning curve smoother and boost your confidence.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/33dWo4esH1U?si=J55BVG_s3ddCQjPq">https://youtu.be/33dWo4esH1U?si=J55BVG_s3ddCQjPq</a></div>
<p> </p>
<p>We’ll continue tomorrow with Day 17, diving deeper into Terraform’s capabilities, so take some time to practice today’s mini-project, reflect on the lessons learned, and enjoy the satisfaction of seeing your automation work flawlessly. Until then, happy terraforming!</p>
]]></content:encoded></item><item><title><![CDATA[DAY #40: [DAY-15] AWS VPC Peering Using Terraform]]></title><description><![CDATA[Introduction
Hello and welcome back to 30 Days of AWS Terraform. Today is Day 15, and if you’ve been following this journey from the start, congratulations, we’ve made it halfway!

Staying consistent, especially while learning new technology, takes r...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-40-day-15-aws-vpc-peering-using-terraform</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-40-day-15-aws-vpc-peering-using-terraform</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Tue, 09 Dec 2025 19:12:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765305666293/5ea86ac0-dab8-4e36-8349-1c4c600281dc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back to <strong>30 Days of AWS Terraform</strong>. Today is <strong>Day 15</strong>, and if you’ve been following this journey from the start, congratulations, we’ve made it halfway!</p>
<p><img src="https://media2.giphy.com/media/v1.Y2lkPTZjMDliOTUyeWxhenZvdzcydm1lb3V4YWZhYmZ0ZnU1ejI3cm90Njl6ZWowMjZvdSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/5fE0uLFQrPCFy/200.gif" alt="Half Way There GIFs - Find &amp; Share on GIPHY" class="image--center mx-auto" /></p>
<p>Staying consistent, especially while learning new technology, takes real curiosity and effort. So truly… <strong>well done</strong>. Take a moment to appreciate yourself for showing up and sticking with it.</p>
<p>Over the past few days, we’ve been moving from <strong>understanding basic Terraform concepts</strong> to trying out <strong>mini projects</strong>. If you remember, our last hands-on adventure was <strong>hosting a static website on AWS S3</strong>. It was an example that showed that we can actually build something real in the cloud. Then, we made it even more exciting by integrating <strong>CloudFront</strong> to distribute our website globally. That small project taught us how Terraform can manage real AWS infrastructure, and how small changes can have a big impact.</p>
<p>Today, we’ll take another <strong>step forward</strong>. We’re going to explore <strong>AWS VPC Peering using Terraform</strong>. At first glance, networking concepts like VPC peering can feel intimidating. We’ll begin by <strong>understanding the “why”</strong> behind VPC peering, and slowly see how it fits naturally into the cloud architectures we’ve been creating.</p>
<p>A personal fact before we move on:</p>
<blockquote>
<p>Fact #15: I’ve always been fascinated by <strong>Vietnam</strong>, its history, culture, and landscapes. I hope to visit someday and see it for myself; it’s been on my travel wish list for a long time.</p>
</blockquote>
<h1 id="heading-what-vpc-peering-really-is">What VPC Peering Really Is</h1>
<p>Alright, let’s slow down for a moment and imagine this scenario.</p>
<p>Think of <strong>two separate VPCs</strong> as <strong>two neighborhoods</strong> in the vast city of AWS. Each neighborhood has its own streets, houses, and landmarks i.e. in AWS terms, this means each VPC has its own <strong>network space, subnets, and EC2 running services</strong>.</p>
<p>Now, sometimes these neighborhoods need to communicate. Perhaps one neighborhood has a <strong>web server</strong>, and the other has a <strong>backend database</strong>. Or maybe they’re <strong>sharing resources for an application</strong>.</p>
<p>Without VPC peering, these neighborhoods would have to shout across the public internet just to talk to each other. Not only is this <strong>less secure</strong>, it’s also <strong>slower and inefficient</strong>. We want them to talk <strong>privately</strong>, as if they were on the <strong>same internal street</strong>.</p>
<p>This is exactly what <strong>VPC peering</strong> does.</p>
<p>It creates a <strong>private bridge</strong> between two VPCs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765307209200/f34ee57f-4d29-407e-bfdf-eb5885b6d158.png" alt class="image--center mx-auto" /></p>
<p>Some important things to keep in mind:</p>
<ol>
<li><p><strong>Bidirectional</strong>: The connection works both ways. Traffic can flow from A → B <strong>and</strong> from B → A. If either side isn’t properly connected, the peering <strong>won’t work</strong>.</p>
</li>
<li><p><strong>Non-overlapping CIDR ranges</strong>: Imagine street addresses: two houses cannot have the same number in different neighborhoods. Our VPCs must have distinct IP ranges.</p>
</li>
<li><p><strong>No transitive peering</strong>: A common beginner mistake is expecting A → B and B → C to automatically allow A → C. It doesn’t. Every pair of VPCs needs its <strong>own explicit peering connection</strong>.</p>
</li>
</ol>
<h2 id="heading-a-simple-example">A Simple Example</h2>
<ul>
<li><p><strong>Primary VPC (A)</strong>: 10.0.0.0/16</p>
</li>
<li><p><strong>Secondary VPC (B)</strong>: 10.1.0.0/16</p>
</li>
</ul>
<p>We want instances in <strong>A</strong> to communicate with instances in <strong>B</strong> privately. Here’s what happens:</p>
<ol>
<li><p>Create a <strong>peering request</strong> from A → B.</p>
</li>
<li><p>Accept the request on B → A.</p>
</li>
</ol>
<p>Now, both neighborhoods can communicate <strong>securely and privately</strong>, without using the internet.</p>
<h2 id="heading-transitive-peering-what-not-to-do">Transitive Peering: What Not to Do</h2>
<p>Let’s imagine adding a <strong>third VPC (C)</strong>:</p>
<ul>
<li><p>A is peered with B</p>
</li>
<li><p>B is peered with C</p>
</li>
</ul>
<p>Some might expect that A can automatically talk to C. But <strong>it won’t work</strong>, this is called <strong>transitive peering</strong>. If A needs to communicate with C, we must create <strong>a direct peering connection between A and C</strong>. Each pair of VPCs needs its own bridge.</p>
<h1 id="heading-setting-up-terraform-for-multi-region-vpc-peering">Setting Up Terraform for Multi-Region VPC Peering</h1>
<p>Before we build anything, let’s take a moment to think about <strong>what we are trying to achieve</strong>.</p>
<p>We want <strong>two separate networks (VPCs)</strong> to communicate securely with each other. One VPC will live in <strong>US East (us-east-1)</strong>, and the other will live in <strong>US West (us-west-2)</strong>.</p>
<p>Now, Terraform doesn’t automatically know where each VPC should go. Think of it like trying to <strong>organize two separate offices in different cities</strong>. If you don’t tell the movers which office belongs where, things could end up in the wrong place! That’s exactly what the <strong>provider</strong> does, it tells Terraform <strong>“this resource belongs to this region”</strong>.</p>
<h2 id="heading-step-1-introducing-the-providers">Step 1: Introducing the Providers</h2>
<p>To handle two regions, we need <strong>two providers</strong>. Each provider is like a <strong>workspace for a region</strong>.</p>
<p>Here’s how we define them:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765302804809/5194e53d-8823-474e-a211-fb9d1931e1af.png" alt class="image--center mx-auto" /></p>
<p>Now, let’s break it down:</p>
<ul>
<li><p><code>region</code>: This tells Terraform <strong>which AWS region</strong> the resources will live in.</p>
</li>
<li><p><code>alias</code>: This is a <strong>nickname</strong> so Terraform knows which provider to use for each resource.</p>
</li>
</ul>
<p>Later, when we create a VPC in <strong>US East</strong>, we will tell Terraform:</p>
<blockquote>
<p>“Use the <strong>primary</strong> workspace.”</p>
</blockquote>
<p>And for the VPC in <strong>US West</strong>, we will say:</p>
<blockquote>
<p>“Use the <strong>secondary</strong> workspace.”</p>
</blockquote>
<p>This way, Terraform <strong>never gets confused</strong>, even though we are managing multiple regions in a single project.</p>
<h2 id="heading-step-2-making-it-flexible-with-variables">Step 2: Making It Flexible with Variables</h2>
<p>Hardcoding values like regions or IP ranges in multiple places can be messy. Instead, we define <strong>variables</strong> to make our configuration <strong>clean, readable, and reusable</strong>.</p>
<p>We set important parameters once, and Terraform reads them everywhere it needs to.</p>
<p>Here’s the basic setup:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765302905986/31716c58-74d8-486d-b814-0e91beb20b53.png" alt class="image--center mx-auto" /></p>
<p>Let’s understand each piece:</p>
<ul>
<li><p><strong>Primary and Secondary Region</strong>: These are the regions where Terraform will create the VPCs.</p>
</li>
<li><p><strong>CIDR Blocks</strong>: Think of a CIDR block like an <strong>address range</strong> for our network. Each VPC needs a unique range so that devices in one network don’t conflict with the other.</p>
<ul>
<li><p>Primary: <code>10.0.0.0/16</code></p>
</li>
<li><p>Secondary: <code>10.1.0.0/16</code></p>
</li>
</ul>
</li>
</ul>
<p>Using variables keeps your code <strong>tidy</strong>. If we ever need to change the region or IP range, we just change it in one place, and everything else updates automatically.</p>
<h2 id="heading-step-3-planning-the-multi-region-network">Step 3: Planning the Multi-Region Network</h2>
<p>Now that Terraform knows <strong>where each VPC belongs</strong> and we have defined the <strong>CIDR ranges</strong>, we can visualize what we are building:</p>
<ol>
<li><p><strong>Primary VPC in us-east-1</strong></p>
<ul>
<li><p>Subnet</p>
</li>
<li><p>Internet gateway</p>
</li>
<li><p>Route table</p>
</li>
<li><p>Security group</p>
</li>
</ul>
</li>
<li><p><strong>Secondary VPC in us-west-2</strong></p>
<ul>
<li><p>Subnet</p>
</li>
<li><p>Internet gateway</p>
</li>
<li><p>Route table</p>
</li>
<li><p>Security group</p>
</li>
</ul>
</li>
<li><p><strong>VPC Peering Connection</strong></p>
<ul>
<li><p>Links the primary and secondary VPCs</p>
</li>
<li><p>Updates route tables so instances can talk privately</p>
</li>
</ul>
</li>
</ol>
<p>Think of it like building <strong>two houses in different cities</strong>, each with its own doors, streets, and security fences. Then, you build a <strong>private bridge</strong> between them so people inside can visit each other <strong>without going through public roads</strong>.</p>
<h1 id="heading-creating-primary-and-secondary-vpcs">Creating Primary and Secondary VPCs</h1>
<p>In this section, we’ll create <strong>two VPCs (Virtual Private Clouds)</strong> in AWS using Terraform:</p>
<ul>
<li><p><strong>Primary VPC</strong> in <code>us-east-1</code></p>
</li>
<li><p><strong>Secondary VPC</strong> in <code>us-west-2</code></p>
</li>
</ul>
<p>We’ll understand each piece, and focus on <strong>why</strong> it’s needed. By the end, you’ll understand how VPCs, subnets, internet gateways, and route tables work together.</p>
<h2 id="heading-primary-vpc">Primary VPC</h2>
<p>Here’s the Terraform code for the primary VPC:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765345372976/0a7d7a36-5005-4666-86c5-de7de92c3ff6.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p><code>provider = aws.primary</code></p>
<ul>
<li><p>This tells Terraform <strong>which AWS account and region</strong> to create the VPC in.</p>
</li>
<li><p>We’ve set up a <strong>primary provider</strong> with <code>us-east-1</code> as the region.</p>
</li>
</ul>
</li>
<li><p><code>cidr_block = var.primary_vpc_cidr</code></p>
<ul>
<li><p>Every VPC needs an <strong>IP range</strong> (like an address range).</p>
</li>
<li><p>We’re using a variable (<code>10.0.0.0/16</code>) so it’s easy to change later.</p>
</li>
</ul>
</li>
<li><p><code>enable_dns_hostnames</code> &amp; <code>enable_dns_support</code></p>
<ul>
<li><p>These allow instances inside the VPC to have <strong>DNS names</strong>.</p>
</li>
<li><p>This means instead of remembering IP addresses, you can use friendly names.</p>
</li>
</ul>
</li>
<li><p><code>tags</code></p>
<ul>
<li>Tags are <strong>labels</strong> in AWS that help you organize and identify resources.</li>
</ul>
</li>
</ol>
<p>By defining the primary VPC like this, we’ve created the <strong>foundation</strong> for our network.</p>
<h2 id="heading-primary-subnet">Primary Subnet</h2>
<p>Next, we create a subnet inside the VPC:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765345423852/9e745506-ea6b-4158-a4f1-3cb9f2ac1619.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p><code>vpc_id = aws_vpc.primary_vpc.id</code></p>
<ul>
<li>This links the subnet to our <strong>primary VPC</strong>.</li>
</ul>
</li>
<li><p><code>cidr_block = "10.0.1.0/24"</code></p>
<ul>
<li>Each subnet has its own smaller <strong>IP range</strong> within the VPC.</li>
</ul>
</li>
<li><p><code>availability_zone</code></p>
<ul>
<li><p>AWS divides regions into <strong>availability zones (AZs)</strong>.</p>
</li>
<li><p>We use a <strong>data source</strong> so Terraform automatically selects one, instead of hardcoding.</p>
</li>
</ul>
</li>
<li><p><code>map_public_ip_on_launch = true</code></p>
<ul>
<li>Instances launched in this subnet <strong>automatically get public IPs</strong>, so we can connect via SSH.</li>
</ul>
</li>
<li><p><code>tags</code></p>
<ul>
<li>Again, tags help us <strong>identify and organize</strong> resources in AWS.</li>
</ul>
</li>
</ol>
<h2 id="heading-internet-gateway">Internet Gateway</h2>
<p>The <strong>internet gateway</strong> allows traffic to flow in and out of the VPC:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765345473025/1f01b5bb-7651-4f17-b79b-651ceb780763.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>vpc_id</code>: Attaches the gateway to our primary VPC.</p>
</li>
<li><p>Once attached, instances in the VPC can <strong>communicate with the internet</strong>.</p>
</li>
<li><p>Later, when we set up <strong>VPC peering</strong>, traffic between VPCs can flow <strong>privately</strong>, without using the internet.</p>
</li>
</ul>
<h1 id="heading-route-table">Route Table</h1>
<p>The route table tells AWS <strong>how network traffic should flow</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765345611379/31f9bea2-608b-452d-b2aa-c09a6b3305ed.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p><code>route { cidr_block = "0.0.0.0/0" }</code></p>
<ul>
<li>This means <strong>all traffic going outside the VPC</strong> should use the internet gateway.</li>
</ul>
</li>
<li><p><code>route_table_association</code></p>
<ul>
<li>Links the <strong>route table to the subnet</strong>, so the rules actually apply to our subnet.</li>
</ul>
</li>
</ol>
<h2 id="heading-secondary-vpc">Secondary VPC</h2>
<p>The secondary VPC in <strong>US West 2</strong> is created the <strong>same way</strong>, but with a few differences:</p>
<ul>
<li><p><code>cidr_block = 10.1.0.0/16</code> (non-overlapping with primary)</p>
</li>
<li><p>All resources (subnet, internet gateway, route table) are configured <strong>similarly</strong>.</p>
</li>
<li><p>The <code>provider</code> points to <code>aws.secondary</code> instead of <code>aws.primary</code>.</p>
</li>
</ul>
<p>This ensures we have <strong>two independent VPCs</strong>, ready to be connected using <strong>VPC Peering</strong>.</p>
<h1 id="heading-connecting-the-neighborhoods-vpc-peering">Connecting the Neighborhoods: VPC Peering</h1>
<p>Imagine we have <strong>two separate VPCs</strong>, one in <strong>US East (Primary)</strong> and one in <strong>US West (Secondary)</strong>. Right now, they are completely isolated. If a server in Primary wants to communicate with a server in Secondary, the traffic would have to travel over the <strong>public internet</strong>. That’s not ideal. We want the traffic to stay <strong>private and secure</strong> inside AWS.</p>
<p>We have discussed all of this in the above sections, now lets see them in action!</p>
<h2 id="heading-step-1-request-a-peering-connection-primary-secondary">Step 1: Request a Peering Connection (Primary → Secondary)</h2>
<p>Here’s the Terraform code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765345651327/a37c773f-2acd-43f6-9f8c-1207992c6102.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>resource "aws_vpc_peering_connection" "primary_to_secondary"</code> → We are creating a <strong>VPC Peering Connection resource</strong>.</p>
</li>
<li><p><code>provider = aws.primary</code> → This tells Terraform which AWS account/region to use. Here, it’s the <strong>Primary VPC</strong> in US East.</p>
</li>
<li><p><code>vpc_id = aws_vpc.primary_vpc.id</code> → This is the <strong>ID of the VPC we want to connect from</strong> (Primary).</p>
</li>
<li><p><code>peer_vpc_id = aws_vpc.secondary_vpc.id</code> → This is the <strong>VPC we want to connect to</strong> (Secondary).</p>
</li>
<li><p><code>peer_region = var.secondary_region</code> → Since the Secondary VPC is in a different region, we tell AWS where to find it.</p>
</li>
<li><p><code>auto_accept = false</code> → AWS won’t automatically connect; the Secondary VPC must <strong>accept</strong> this connection.</p>
</li>
<li><p><code>tags = {...}</code> → These are labels to help us identify the connection in AWS. Think of them as <strong>name tags</strong> for your resources.</p>
</li>
</ul>
<h2 id="heading-step-2-accept-the-peering-connection-secondary-primary">Step 2: Accept the Peering Connection (Secondary → Primary)</h2>
<p>Now, the Secondary VPC needs to <strong>accept the connection</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765345697125/74e0e4eb-f04b-4ee1-b062-c5ea92b64f22.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>resource "aws_vpc_peering_connection_accepter"</code> → This resource accepts the peering request.</p>
</li>
<li><p><code>provider = aws.secondary</code> → We’re now working in the <strong>Secondary region/VPC</strong>.</p>
</li>
<li><p><code>vpc_peering_connection_id = aws_vpc_peering_connection.primary_to_secondary.id</code> → Refers to the connection request we created earlier.</p>
</li>
<li><p><code>auto_accept = true</code> → Secondary is saying <strong>“Yes, Primary, you can connect to me.”</strong></p>
</li>
<li><p><code>tags = {...}</code> → Helpful labels to organize the resource in AWS.</p>
</li>
</ul>
<p>Now the private connection is <strong>fully active</strong>.</p>
<h2 id="heading-step-3-update-route-tables">Step 3: Update Route Tables</h2>
<p>Even after creating the connection, AWS doesn’t yet know how to send traffic between the VPCs. We need to <strong>update the route tables</strong> in both VPCs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765304173576/581daf5c-2047-476a-8fdf-18fcdd5e85bf.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>provider = aws.primary</code> → Route is for Primary VPC.</p>
</li>
<li><p><code>route_table_id = aws_route_table.primary_rt.id</code> → Specifies which route table to update.</p>
</li>
<li><p><code>destination_cidr_block = var.secondary_vpc_cidr</code> → Any traffic destined for the Secondary VPC should follow this route.</p>
</li>
<li><p><code>vpc_peering_connection_id = aws_vpc_peering_connection.primary_to_secondary.id</code> → Use the peering connection we just created.</p>
</li>
<li><p><code>depends_on = [...]</code> → Ensures Terraform waits for the peering to be accepted before adding the route.</p>
</li>
</ul>
<p>The <strong>secondary route</strong> works the same way:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765304265319/33be5753-ff0f-4f06-89bd-d9182918a08d.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Now traffic from Secondary knows how to reach Primary, completing the private connection.</li>
</ul>
<h1 id="heading-ec2-instances-amp-security-groups">EC2 Instances &amp; Security Groups</h1>
<p>Now that our <strong>private bridge (VPC Peering)</strong> is ready, it’s time to launch <strong>EC2 instances</strong></p>
<p>We want these instances to:</p>
<ul>
<li><p>Communicate privately across the peering connection.</p>
</li>
<li><p>Be reachable for management tasks (like SSH).</p>
</li>
<li><p>Serve a simple webpage to verify connectivity.</p>
</li>
</ul>
<h2 id="heading-step-1-security-groups-controlling-access">Step 1: Security Groups: Controlling Access</h2>
<p><strong>Security Groups</strong> act like virtual firewalls or “guards” around your instances. They control <strong>who can talk to your EC2 instances</strong> and what type of traffic is allowed.</p>
<p><strong>Primary Security Group</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765345808259/35628320-3103-4920-ad59-92a1b51fe4cc.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>provider = aws.primary</code> → Security group is for the <strong>Primary VPC</strong>.</p>
</li>
<li><p><code>vpc_id = aws_vpc.primary_vpc.id</code> → Associates this security group with the Primary VPC.</p>
</li>
<li><p><strong>Ingress Rules</strong> → Control incoming traffic:</p>
<ul>
<li><p><strong>SSH from anywhere (port 22)</strong> → Allows us to log in to the instance from our computer.</p>
</li>
<li><p><strong>ICMP from Secondary VPC</strong> → Allows “ping” commands from the Secondary VPC to test connectivity.</p>
</li>
<li><p><strong>All traffic from Secondary VPC</strong> → Lets applications like NGINX communicate freely between the two VPCs.</p>
</li>
</ul>
</li>
<li><p><strong>Egress Rule</strong> → Outgoing traffic:</p>
<ul>
<li>Allow all outbound traffic so the instance can reach the internet or other services if needed.</li>
</ul>
</li>
</ul>
<blockquote>
<p>The Secondary VPC’s security group is a mirror of the Primary, allowing <strong>two-way communication</strong>.</p>
</blockquote>
<h2 id="heading-step-2-ec2-instances">Step 2: EC2 Instances</h2>
<p>With guards in place, we can now launch the <strong>EC2 instances</strong>.</p>
<h4 id="heading-primary-vpc-instance">Primary VPC Instance</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765346008482/13475e86-55db-4a4b-b14e-38f7ce00c1eb.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>provider = aws.primary</code> → This instance is in the Primary VPC.</p>
</li>
<li><p><code>ami</code> → Specifies which operating system image to use.</p>
</li>
<li><p><code>instance_type</code> → Determines the size and power of the EC2 instance.</p>
</li>
<li><p><code>subnet_id</code> → Places the instance inside the <strong>Primary subnet</strong>.</p>
</li>
<li><p><code>vpc_security_group_ids</code> → Attaches the security group we created earlier.</p>
</li>
<li><p><code>key_name</code> → Name of the SSH key to connect to this instance.</p>
</li>
<li><p><code>user_data</code> → A script that runs on first boot to install Apache/Nginx and display a webpage.</p>
</li>
<li><p><code>depends_on</code> → Ensures the instance is created <strong>after the VPC peering is active</strong>.</p>
</li>
</ul>
<blockquote>
<p>The Secondary instance is configured the same way, just using <strong>aws.secondary</strong>, its subnet, and its security group.</p>
</blockquote>
<h2 id="heading-step-3-user-data">Step 3: User Data</h2>
<p><strong>User Data</strong> is a script that runs when an EC2 instance starts. Here, we use it to <strong>install Apache and display a simple webpage</strong> showing which VPC/region the instance belongs to.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765304892484/b6031bc0-b609-4da2-ad01-26836978deae.png" alt class="image--center mx-auto" /></p>
<p><strong>Explanation:</strong></p>
<ul>
<li><p>Updates the instance and installs Apache web server.</p>
</li>
<li><p>Starts and enables Apache so the server runs automatically on reboot.</p>
</li>
<li><p>Creates a simple <strong>webpage showing the VPC region and private IP</strong>.</p>
</li>
<li><p>When we access this page from the other VPC, we <strong>see the bridge (peering) working</strong>.</p>
</li>
</ul>
<h1 id="heading-testing-connectivity-between-vpcs">Testing Connectivity Between VPCs</h1>
<p>Now that we have set up <strong>VPCs, subnets, route tables, security groups, and EC2 instances</strong>, it’s time to <strong>see if our private bridge is working</strong>.</p>
<ol>
<li><p><strong>Log into the EC2 Instances</strong></p>
<ul>
<li><p>One instance is in <strong>US East 1 (Primary VPC)</strong></p>
</li>
<li><p>The other is in <strong>US West 2 (Secondary VPC)</strong></p>
</li>
<li><p>We can connect via <strong>SSH</strong> using the key pairs we created earlier.</p>
</li>
</ul>
</li>
<li><p><strong>Test Private Connectivity with Ping</strong></p>
<ul>
<li><p>From the <strong>Primary VPC instance</strong>, ping the <strong>private IP</strong> of the <strong>Secondary instance</strong>:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765305140149/508b06e5-c9e7-410a-83c0-09bc09134001.png" alt class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934201043/f1d401bb-2dd4-4158-871b-692b94c1b5f5.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>From the <strong>Secondary VPC instance</strong>, ping the <strong>private IP</strong> of the <strong>Primary instance</strong>:</p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765305163793/4d3242b7-3e16-4b51-a3d5-4008356571f8.png" alt class="image--center mx-auto" /></p>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934258422/e371a8b3-2692-4d6e-9737-119eedbd8bdf.png" alt class="image--center mx-auto" /></p>
<ul>
<li>If the pings are successful, it confirms that <strong>ICMP traffic</strong> works and the peering connection is active.</li>
</ul>
<ol start="3">
<li><p><strong>Test Web Connectivity with Nginx</strong></p>
<ul>
<li><p>Each instance runs a simple <strong>Nginx server</strong> serving a webpage with its <strong>region</strong> and <strong>private IP</strong>.</p>
</li>
<li><p>Use <strong>curl</strong> to access the other instance’s private IP:</p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765305209842/50dbfaef-194e-4ccd-954a-101a086b1d0c.png" alt class="image--center mx-auto" /></p>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934367125/aa0998cc-7fc2-41e0-a30b-dc4778319d78.png" alt class="image--center mx-auto" /></p>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934393361/bcd5ba93-4430-4dcb-ab45-61a3f18494a3.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>We should see the HTML page showing the <strong>region and private IP</strong> of the other instance.</p>
</li>
<li><p>This confirms that <strong>TCP traffic</strong> (used by web servers) works across the peering connection.</p>
</li>
</ul>
<ol start="4">
<li><p><strong>Key Takeaways</strong></p>
<ul>
<li><p>The <strong>VPCs can now communicate privately</strong> across regions.</p>
</li>
<li><p>Both <strong>ping (ICMP)</strong> and <strong>web traffic (TCP)</strong> work.</p>
</li>
<li><p>Your <strong>VPC peering setup is successfully complete</strong>!</p>
</li>
</ul>
</li>
<li><p><strong>Clean Up After Testing</strong></p>
<ul>
<li>Remember to <strong>destroy your Terraform resources</strong> to avoid unnecessary AWS charges:</li>
</ul>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765305241253/46a90162-1cc4-4edd-92b3-b367bb9ec01c.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934594337/16cf4054-a0c0-44f8-bcea-c265ade48ce2.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934652965/b5cfc55a-33a1-44b1-993b-5e713f88c167.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Congratulations! With today’s blog, we are officially <strong>halfway through the 30 Days of Terraform challenge</strong>.</p>
<p>I know this session had a lot of code blocks, and just by reading it, anyone might feel a little <strong>overwhelmed</strong> and that’s perfectly okay. Feeling overwhelmed is actually a good sign. It tells us that there is something new here, something worth putting in the effort to truly understand.</p>
<p>Remember: <strong>nothing builds confidence faster than hands-on practice</strong>. Reading alone is never enough. The real understanding comes when you try, make mistakes, troubleshoot, and see things work with your own eyes.</p>
<p>To help you along, here’s a <strong>video by Piyush Sachdeva</strong> that explains everything clearly, step by step. You can follow along, try the exercises, and even troubleshoot with the tips he provides.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/WGt000THDmQ?si=FCQqYTSvKmOHKWt6">https://youtu.be/WGt000THDmQ?si=FCQqYTSvKmOHKWt6</a></div>
<p> </p>
<p>So take your time, practice today’s VPC peering setup, explore the EC2 instances, and test your connections. Tomorrow, we’ll continue building on this knowledge and dive into something new.</p>
<p>Happy Terraforming!</p>
]]></content:encoded></item><item><title><![CDATA[DAY #39: [DAY-14] AWS Terraform: Hosting A Static Website using AWS S3 And Cloudfront]]></title><description><![CDATA[Introduction
Hello and welcome back!
If you’ve been following the series, you know that yesterday, on Day 13, we spent time exploring Terraform data sources. We saw how Terraform can fetch information from AWS and reuse it, helping us keep our config...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-39-day-14-aws-terraform-hosting-a-static-website-using-aws-s3-and-cloudfront</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-39-day-14-aws-terraform-hosting-a-static-website-using-aws-s3-and-cloudfront</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Mon, 08 Dec 2025 19:05:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765219627421/bc4aef02-af8e-4172-9a50-2d1680401e52.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back!</p>
<p>If you’ve been following the series, you know that yesterday, on <strong>Day 13</strong>, we spent time exploring <strong>Terraform data sources</strong>. We saw how Terraform can fetch information from AWS and reuse it, helping us keep our configurations clean, organized, and dynamic.</p>
<p>And today, we finally begin that <strong>“something bigger”</strong>.</p>
<p>This is <strong>Day 14 of 30 Days of AWS Terraform</strong>, and until now, we’ve been learning concepts one by one. But from today onward, we’re stepping into <strong>real mini-projects</strong>.</p>
<p><img src="https://miro.medium.com/1*t_5uOLpt_Z5V8HtzGKutEA.gif" alt="Onboarding the Power of Terraform | by Aiswarya🦋 | Medium" class="image--center mx-auto" /></p>
<p>To start this phase, we’ll tackle a project that many beginners find comforting: <strong>hosting a static website using an S3 bucket, with CloudFront in front of it</strong>. We may have tried this manually in the AWS console at some point i.e. uploading HTML files to a bucket, enabling static website hosting, maybe even connecting a domain. But doing all of this with <strong>Terraform</strong> is different, and it shows how we can automate and manage infrastructure efficiently.</p>
<p>Before we dive into code, it’s important to understand <strong>what we’re building and why it matters</strong>. CloudFront and S3 aren’t just two AWS services placed together, they solve real problems. For example, when our website has visitors from different parts of the world, these services work together to make the website <strong>secure, fast, and cost-efficient</strong>, all while keeping the S3 bucket private.</p>
<p>Before we dive in, here’s a personal tidbit:</p>
<blockquote>
<p>Fact #14: What I love about working in tech is that there’s <strong>always something new to learn</strong>. Sometimes it can feel a bit intimidating with so much happening all the time, but that’s also what makes it exciting.</p>
</blockquote>
<p>And by the end of today’s blog, you’ll have your first real Terraform-powered static website up and running.</p>
<h1 id="heading-understanding-s3-cloudfront-together">Understanding S3 + CloudFront Together</h1>
<p>Before we touch any Terraform code, let’s take a moment to understand <strong>why we need both S3 and CloudFront</strong> and how they work together to make our website fast, secure, and reliable.</p>
<p>Imagine our website has visitors scattered all around the world. Some might be in India, others in the United States, Australia, Latin America, or Europe. Now, if all our website files i.e. like HTML pages, CSS styles, JavaScript, images, and videos are stored in a single S3 bucket, every visitor is trying to reach that same bucket, no matter where they are.</p>
<p>If we serve the website <strong>directly from S3</strong>, a few challenges arise:</p>
<ul>
<li><p><strong>Cost</strong>: Every time a visitor far from the bucket downloads a file, AWS charges for the data transfer. These charges can add up quickly if our website has many visitors.</p>
</li>
<li><p><strong>Performance</strong>: Visitors far away will notice slower loading times, because the files have to travel long distances across the internet.</p>
</li>
<li><p><strong>Security</strong>: To make the website publicly accessible, the S3 bucket must be open to everyone. This can expose our files to attacks and isn’t safe for production use.</p>
</li>
</ul>
<p>This is where <strong>CloudFront</strong> comes in. We can think of CloudFront as a network of helpers, stationed all around the globe. These helpers are called <strong>edge locations</strong>. When someone requests a file from our website, CloudFront serves it from the <strong>nearest edge location</strong> instead of the original S3 bucket.</p>
<p>Here’s what this achieves:</p>
<ul>
<li><p><strong>Faster delivery</strong>: Files are stored temporarily at the edge locations. When someone requests the same file nearby, it is served immediately without going all the way to S3.</p>
</li>
<li><p><strong>Lower cost</strong>: Since the data travels a shorter distance, transfer fees are smaller.</p>
</li>
<li><p><strong>Better security</strong>: Users never access the S3 bucket directly. Only CloudFront can fetch files, keeping our bucket private.</p>
</li>
</ul>
<p>Each cached file has a <strong>TTL (time to live)</strong>, usually 24 hours by default. This means that if another user nearby requests the same file within this period, CloudFront serves it from its cache. This reduces load on the S3 bucket and makes the website feel snappy.</p>
<p>By combining S3 and CloudFront in this way, we create a website that is <strong>secure, fast, and cost-efficient</strong>, no matter where our users are.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765220266089/0d368db5-5401-4c14-9047-b0534f8daab6.png" alt class="image--center mx-auto" /></p>
<p>Keeping this in mind will help us understand why we create each Terraform resource: the private S3 bucket, origin access control, bucket policies, CloudFront distribution, and finally uploading our static files. Each step in Terraform has a purpose, and together they tell the story of a website that works smoothly for everyone.</p>
<h1 id="heading-setting-up-the-s3-bucket-with-terraform">Setting Up the S3 Bucket with Terraform</h1>
<p>Now that we understand <strong>why</strong> we’re using S3 and CloudFront together, let’s move into the “how.” We’ll start with the S3 bucket i.e. the heart of our static website. This is a place where all our website files will live: your HTML, CSS, JavaScript, images, and any other static content.</p>
<p>In Terraform, creating this bucket is surprisingly straightforward, but before we rush to the code, let’s pause and understand what we are doing. We want the bucket to have a <strong>unique, reusable name</strong> so we don’t run into conflicts with other buckets in AWS. Terraform allows us to do this using a <strong>variable-based prefix</strong>, which is a way of letting us define part of the name dynamically. Here’s what that looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765216589158/41e3b463-2399-46cd-9119-9e28cd75c0db.png" alt class="image--center mx-auto" /></p>
<p>Here’s what’s happening in beginner-friendly terms:</p>
<ul>
<li><p><code>aws_s3_bucket</code> tells Terraform, “Hey, I want you to create a new S3 bucket.”</p>
</li>
<li><p><code>bucket_prefix</code> lets us build a unique bucket name by combining a fixed string (<code>day-14-demo-</code>) with whatever value we set in <code>var.bucket_prefix</code>.</p>
</li>
<li><p>The magic of using a prefix is that if we run this code multiple times, Terraform can still create new, unique buckets without manual renaming.</p>
</li>
</ul>
<p>Once we have the bucket, we need to make sure it’s <strong>private</strong>. Even though AWS buckets are private by default, explicitly blocking public access is like double-checking and making sure it is really secure. It prevents anyone from accidentally accessing our buckets. Here’s the Terraform block for that:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765216721686/a3a5e04f-fbbc-4bcf-82cb-e5b5cbb472d8.png" alt class="image--center mx-auto" /></p>
<p>Let’s unpack this:</p>
<ul>
<li><p><strong>block_public_acls = true</strong> → Any access control lists (ACLs) that are public will be blocked.</p>
</li>
<li><p><strong>block_public_policy = true</strong> → No public bucket policies can be applied.</p>
</li>
<li><p><strong>ignore_public_acls = true</strong> → Existing public ACLs are ignored to avoid accidental exposure.</p>
</li>
<li><p><strong>restrict_public_buckets = true</strong> → The bucket cannot be made public accidentally in the future.</p>
</li>
</ul>
<p>With the bucket created and secured, we’ve now <strong>laid the foundation</strong> for a website that’s both safe and ready to serve content efficiently. The next step will be setting up <strong>Origin Access Control</strong>, which allows CloudFront to fetch files from this private bucket safely</p>
<h1 id="heading-configuring-origin-access-control-and-bucket-policy">Configuring Origin Access Control and Bucket Policy</h1>
<p>After creating and securing our S3 bucket, the next step is to allow <strong>CloudFront</strong> to access the bucket safely. Because the bucket is private, users cannot directly access it. Only CloudFront should fetch the files. To achieve this, we use <strong>Origin Access Control (OAC)</strong> and a <strong>bucket policy</strong>.</p>
<h3 id="heading-1-origin-access-control-oac">1. Origin Access Control (OAC)</h3>
<p>Origin Access Control is a configuration in CloudFront that ensures <strong>all requests from CloudFront to the S3 bucket are authenticated and secure</strong>. Without it, CloudFront cannot read from a private bucket. Terraform allows us to create OAC easily:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765217086001/7f04390b-e39b-4a4b-98db-11d6988cb4ae.png" alt class="image--center mx-auto" /></p>
<p><strong>Detailed explanation of each field:</strong></p>
<ul>
<li><p><strong>name</strong><br />  A descriptive name for the OAC. We can choose any name, but it’s best to keep it clear and consistent.</p>
</li>
<li><p><strong>description</strong><br />  Provides additional context about the purpose of the OAC. Helpful when managing multiple distributions.</p>
</li>
<li><p><strong>origin_access_control_origin_type = "s3"</strong><br />  Specifies that the origin CloudFront will access is an <strong>S3 bucket</strong>. This is required because the signing and permissions differ depending on the origin type.</p>
</li>
<li><p><strong>signing_behavior = "always"</strong><br />  Ensures every request from CloudFront to the S3 bucket is signed. Signing is required for authentication, allowing CloudFront to access a private bucket.</p>
</li>
<li><p><strong>signing_protocol = "sigv4"</strong><br />  Specifies the AWS signature protocol used to sign requests. SigV4 is the latest secure protocol that encrypts and validates the request.</p>
</li>
</ul>
<p>This configuration guarantees that CloudFront can securely read from the private S3 bucket while keeping the bucket inaccessible to other users or services.</p>
<h3 id="heading-2-s3-bucket-policy">2. S3 Bucket Policy</h3>
<p>While OAC enables secure communication, the <strong>bucket policy</strong> defines <strong>explicit permissions</strong> on the S3 bucket. It tells AWS: “This CloudFront distribution is allowed to read these files.”</p>
<p>Terraform configuration:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765217185738/61437ee3-95dd-4e0a-8e18-fd616cb53591.png" alt class="image--center mx-auto" /></p>
<p><strong>Detailed explanation:</strong></p>
<ul>
<li><p><strong>bucket</strong><br />  Specifies the S3 bucket to which the policy applies.</p>
</li>
<li><p><strong>depends_on</strong><br />  Ensures Terraform creates this bucket policy <strong>after</strong> the public access block and CloudFront distribution are ready. This prevents errors during deployment.</p>
</li>
<li><p><strong>policy = jsonencode({...})</strong><br />  The policy itself, written in JSON, defines who can do what on the bucket.</p>
<ul>
<li><p><strong>Version</strong><br />  AWS uses this to determine the policy syntax version.</p>
</li>
<li><p><strong>Statement</strong><br />  Contains the permission rules. Each statement describes one rule.</p>
<ul>
<li><p><strong>Sid</strong>: A unique identifier for the statement.</p>
</li>
<li><p><strong>Effect = "Allow"</strong>: Grants the permission.</p>
</li>
<li><p><strong>Principal = { Service = "cloudfront.amazonaws.com" }</strong>: Grants access only to CloudFront. No other user or service can read the files.</p>
</li>
<li><p><strong>Action = ["s3:GetObject"]</strong>: Specifies that CloudFront can <strong>read objects</strong> (but cannot modify or delete them).</p>
</li>
<li><p><em>Resource = bucket ARN + /</em> *: Applies to all objects in the bucket.</p>
</li>
<li><p><strong>Condition = { StringEquals = { "AWS:SourceArn" = CloudFront distribution ARN } }</strong>: Ensures only this specific CloudFront distribution can access the bucket.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Together, OAC and the bucket policy make the S3 bucket <strong>private, secure, and accessible only to CloudFront</strong>. Users will never access the S3 bucket directly, but CloudFront can fetch and cache files efficiently for global access.</p>
<h1 id="heading-uploading-static-website-files-to-the-s3-bucket">Uploading Static Website Files to the S3 Bucket</h1>
<p>Now that our S3 bucket is private and CloudFront has its secure key to access it, we can start placing our website files into the bucket.</p>
<p>Our website consists of <strong>static files</strong>: <code>index.html</code>, CSS for styling, JavaScript for interactivity, and maybe some images or videos. These are the files our users will see and interact with when they visit our site.</p>
<p>Terraform provides a very neat way to do this using the <code>aws_s3_object</code> resource. Here’s what it looks like in code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765217451382/6028a533-24e5-4a03-b643-306e8115754b.png" alt class="image--center mx-auto" /></p>
<p>Let’s understand this, step by step:</p>
<p>Let’s go <strong>slowly</strong>, so it’s easy to understand:</p>
<ol>
<li><p><strong>for_each = fileset("${path.module}/www", "</strong>/*")**</p>
<ul>
<li><p>This tells Terraform to look in the <code>www</code> folder and find <strong>all files and subfolders</strong>.</p>
</li>
<li><p>Terraform will create an S3 object for each file it finds.</p>
</li>
</ul>
</li>
<li><p><strong>bucket = aws_s3_bucket.day_14_demo_bucket.id</strong></p>
<ul>
<li>This is the S3 bucket we created earlier. Every file will be uploaded here.</li>
</ul>
</li>
<li><p><strong>key = each.value</strong></p>
<ul>
<li>The “key” is the name or path of the file inside the S3 bucket. This is how CloudFront and users will reference the file.</li>
</ul>
</li>
<li><p><strong>source = "${path.module}/www/${each.value}"</strong></p>
<ul>
<li>This points to the <strong>actual file on our computer</strong>, so Terraform knows what to upload.</li>
</ul>
</li>
<li><p><strong>etag = filemd5("${path.module}/www/${each.value}")</strong></p>
<ul>
<li>Terraform uses this to check if a file has changed. If the file is updated locally, Terraform will upload the new version.</li>
</ul>
</li>
<li><p><strong>content_type = lookup(...)</strong></p>
<ul>
<li><p>This tells the browser what type of file it is, such as <code>text/html</code> for HTML, <code>text/css</code> for CSS, or <code>image/png</code> for PNG images.</p>
</li>
<li><p>If Terraform doesn’t recognize the file type, it uses a generic type (<code>application/octet-stream</code>).</p>
</li>
</ul>
</li>
</ol>
<p>By using this resource, we ensure that <strong>all website files are uploaded correctly, with the right type, and ready for CloudFront to serve</strong>.</p>
<p>Once the files are uploaded, our website is ready to be delivered through CloudFront, making it fast, secure, and globally accessible.</p>
<p>Once all the files are safely uploaded, we’ll move to the next step: <strong>creating the CloudFront distribution</strong>. This is where our website will start becoming <strong>fast, globally accessible, and secure</strong>.</p>
<h1 id="heading-creating-the-cloudfront-distribution">Creating the CloudFront Distribution</h1>
<p>Now that our S3 bucket is ready and all our website files are uploaded, the next step is to make the website fast, secure, and accessible from anywhere in the world. That’s what <strong>CloudFront</strong> does.</p>
<p>CloudFront is a service that delivers our website to users efficiently. It ensures that:</p>
<ul>
<li><p>Our private S3 bucket stays private.</p>
</li>
<li><p>Files are delivered quickly to users, no matter where they are.</p>
</li>
<li><p>Visitors connect securely using HTTPS.</p>
</li>
</ul>
<p>We will use Terraform to create a CloudFront distribution, which is just a configuration that tells CloudFront how to serve our website. Here’s the code we’ll use:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765218277436/e66d5c9d-3c7e-4448-9369-10de09420533.png" alt class="image--center mx-auto" /></p>
<p>Let’s carefully go through this <strong>line by line</strong>, so it’s easy to understand:</p>
<ol>
<li><p><strong>origin</strong></p>
<ul>
<li><p>This tells CloudFront <strong>which S3 bucket to use</strong>.</p>
</li>
<li><p>We also provide the Origin Access Control we created earlier. This is necessary so <strong>CloudFront can access the bucket safely</strong>, while keeping it private from everyone else.</p>
</li>
</ul>
</li>
<li><p><strong>enabled = true</strong></p>
<ul>
<li>This simply turns the CloudFront distribution on. Without this, it won’t serve any content.</li>
</ul>
</li>
<li><p><strong>is_ipv6_enabled = true</strong></p>
<ul>
<li>This ensures visitors using newer internet addresses (IPv6) can access your website.</li>
</ul>
</li>
<li><p><strong>default_root_object = "index.html"</strong></p>
<ul>
<li>When someone visits our main website address, CloudFront will serve the <code>index.html</code> page automatically.</li>
</ul>
</li>
<li><p><strong>default_cache_behavior</strong></p>
<ul>
<li><p><strong>allowed_methods</strong> and <strong>cached_methods</strong>: Only GET and HEAD requests are allowed. For a static website, this is all we need.</p>
</li>
<li><p><strong>forwarded_values</strong>: We are not sending cookies or query strings, which makes caching simpler and faster.</p>
</li>
<li><p><strong>viewer_protocol_policy = "redirect-to-https"</strong>: Ensures visitors connect securely using HTTPS.</p>
</li>
<li><p><strong>min_ttl, default_ttl, max_ttl</strong>: These settings tell CloudFront <strong>how long to keep a copy of each file cached at the edge locations</strong>. Cached files load faster for users and reduce requests to your S3 bucket.</p>
</li>
</ul>
</li>
<li><p><strong>price_class = "PriceClass_100"</strong></p>
<ul>
<li>Limits which locations CloudFront uses to deliver our website, keeping costs reasonable.</li>
</ul>
</li>
<li><p><strong>viewer_certificate</strong></p>
<ul>
<li>CloudFront provides a default HTTPS certificate so our site is secure without extra setup.</li>
</ul>
</li>
</ol>
<p>Once this distribution is active, we can use Terraform outputs to get the website URL, distribution ID, and bucket name for easy reference.</p>
<h1 id="heading-understanding-terraform-outputs">Understanding Terraform Outputs</h1>
<p>At this point, we have created:</p>
<ul>
<li><p>A private S3 bucket containing all our website files</p>
</li>
<li><p>A CloudFront distribution to deliver the site globally</p>
</li>
</ul>
<p>Everything is working behind the scenes. But after running Terraform, how do we <strong>quickly find the important information</strong> we need? For example:</p>
<ul>
<li><p>What is the website URL?</p>
</li>
<li><p>What is the CloudFront distribution ID?</p>
</li>
<li><p>What is the S3 bucket name?</p>
</li>
</ul>
<p>We could look through the AWS console to find these, but that can be <strong>slow and confusing</strong>, especially for beginners.</p>
<p>This is where <strong>Terraform outputs</strong> help. Outputs are like <strong>a summary table that Terraform shows us</strong> after deployment. They make it easy to see and use the information you need, without searching.</p>
<p>Here’s how we define them:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765218720741/97e2a98d-3b69-46f4-b536-5944f787b8ed.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-1-website-url">1. Website URL</h3>
<p>After deployment, this output gives the <strong>link to our live website</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766919664460/4de921d3-98b0-429c-bb99-13316a1f495b.png" alt class="image--center mx-auto" /></p>
<ul>
<li>We can copy this link and open it in a browser.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766919423020/ac63036d-30a1-4a36-9249-cc248ac88540.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>CloudFront serves our website files from here, so we <strong>don’t access the S3 bucket directly</strong>.</p>
</li>
<li><p>This makes it <strong>fast, secure, and globally accessible</strong>.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766919748466/23dc8292-53d3-44a9-a181-71560b04cf0a.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-2-cloudfront-distribution-id">2. CloudFront Distribution ID</h3>
<p>Every CloudFront distribution has a <strong>unique identifier</strong>.</p>
<ul>
<li><p>We may need this ID later if we want to <strong>update the distribution</strong> or check its settings in AWS.</p>
</li>
<li><p>Terraform shows it to us automatically, so we <strong>don’t need to search in the console</strong>.</p>
</li>
</ul>
<p>For beginners, knowing this ID is helpful because it connects the configuration we wrote in Terraform to the actual CloudFront service in AWS.</p>
<h3 id="heading-3-s3-bucket-name">3. S3 Bucket Name</h3>
<p>This output shows the name of our <strong>private S3 bucket</strong>.</p>
<ul>
<li><p>We can use this name if we want to <strong>look at the files in the bucket</strong>, check permissions, or connect it with other AWS services.</p>
</li>
<li><p>Even though the bucket is private, Terraform makes it easy to <strong>reference the exact bucket name whenever needed</strong>.</p>
</li>
</ul>
<p>By defining these outputs, you have <strong>all the critical information in one place</strong> after deployment.</p>
<ul>
<li><p>We don’t need to hunt in the AWS console.</p>
</li>
<li><p>We can quickly access the website, manage CloudFront, or check your S3 bucket.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>With this, we have completed <strong>Day 14</strong> of our 30-day AWS Terraform journey. Today was our <strong>first demo session</strong>, where we saw an S3 bucket and CloudFront distribution in action.</p>
<p>I know this blog contains <strong>many moving parts</strong>, and some sections, especially when we look at the code, might feel <strong>hard to understand at first</strong>. That’s completely normal, and it’s part of the learning process.</p>
<p>One suggestion that usually helps me make sense of things is this:</p>
<ol>
<li><p>Read the <strong>explanation for a piece of code</strong>.</p>
</li>
<li><p>Then go to the <strong>actual line of code</strong> and try to connect it with the explanation.</p>
</li>
<li><p>Repeat this slowly for each section.</p>
</li>
</ol>
<p>It does take <strong>time and effort</strong>, but this approach usually helps the concepts “click” and makes the learning much more meaningful.</p>
<p>If some parts still feel confusing, which is completely understandable, here is a <strong>video by Piyush Sachdeva</strong> that explains everything clearly and walks you through the demo step by step.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/bK6RimAv2nQ?si=oYLDYeM2aZvZAGz5">https://youtu.be/bK6RimAv2nQ?si=oYLDYeM2aZvZAGz5</a></div>
<p> </p>
<p>Remember, learning cloud infrastructure and Terraform is a <strong>gradual process</strong>, and every small effort adds up. Keep practicing, and soon these concepts will feel much more natural.</p>
<p>Happy learning!</p>
]]></content:encoded></item><item><title><![CDATA[DAY #38: [DAY-13] AWS Terraform Data Sources]]></title><description><![CDATA[Introduction
Hello and welcome back to Day 13 of 30 Days of AWS Terraform.

If you’ve been following along, you know that yesterday we explored Terraform functions i.e. those helpers that make our configurations cleaner, smarter, and flexible. We too...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-38-day-13-aws-terraform-data-sources</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-38-day-13-aws-terraform-data-sources</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Sun, 07 Dec 2025 15:34:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765121588034/fa1db6a7-9062-4694-bade-bf58ac1808a8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back to Day 13 of 30 Days of AWS Terraform.</p>
<p><img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExMW92Nno3Nnd2c2R6Nmk5dHpuaWkwcmducGc0eWFjbHM2b2thZnh0ZCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/v3YBN2BCGeQRq3o0X6/giphy.gif" alt="Stay Warm Fall Season GIF by Jessica Lau" class="image--center mx-auto" /></p>
<p>If you’ve been following along, you know that yesterday we explored <strong>Terraform functions</strong> i.e. those helpers that make our configurations cleaner, smarter, and flexible. We took things one step at a time, carefully understanding not just <em>what</em> these functions do, but <em>why</em> they matter when we start building real environments.</p>
<p>Today, we’re stepping into something that becomes incredibly useful once our Terraform projects start touching real AWS environments: <strong>data sources</strong>.</p>
<p>Instead of treating them as just another “feature” to check off a list, think of them as Terraform’s way of observing what already exists in AWS, picking up ready-made details, and helping us avoid hardcoding values that might change over time.</p>
<p>By the end of today’s blog, we’ll walk through why data sources are needed, how they fit into a simple EC2 example, and how they help when working in shared environments, like a pre-existing VPC and subnets used by multiple teams.</p>
<p>Before we dive in, here’s a little personal note:</p>
<blockquote>
<p>Fact #13: I love collecting interesting metaphors to explain tech concepts. They help ideas stick, spark curiosity, and make tricky topics easier to understand. Over time, these metaphors have become a small treasure I revisit when learning or teaching.</p>
</blockquote>
<h1 id="heading-what-exactly-is-a-data-source">What Exactly Is a Data Source?</h1>
<p>Imagine we want to provision a simple EC2 instance. Nothing fancy.<br />Just an instance that needs a few basic things: instance type, subnet, tags… and of course, an <strong>AMI</strong>.</p>
<p>If you’re new to AWS, an AMI is basically the machine image i.e. the operating system template our instance will use. And there isn’t just one AMI. There are multiple versions for Amazon Linux, Ubuntu, CentOS, and many others. These images get new releases over time, so the list keeps growing.</p>
<p>What’s interesting is that AMIs aren’t stored <em>inside</em> our AWS account.<br />They live outside, almost like a big open catalogue. And because there are so many of them, each release ends up having its own unique AMI ID.</p>
<p>Now here’s the challenge:<br />When we create an EC2 instance in Terraform, we need to provide that AMI ID. And yes, we <em>can</em> manually go to the AMI release page, copy the AMI ID, and paste it in our Terraform file… but that’s not something we want to rely on once we start automating things.</p>
<p>In an automated setup, the goal is simple:<br /><strong>no manual intervention, or as little as possible.</strong></p>
<p>So hardcoding the AMI ID isn’t a good idea. It becomes outdated very quickly, and every update forces us to go hunting for the latest one.</p>
<p>This is where Terraform’s <strong>data sources</strong> step in.</p>
<p>A data source is like saying to Terraform,<br />“Hey, instead of me manually telling you the AMI ID, can <em>you</em> go and fetch the correct one for me?”</p>
<p>And Terraform says,<br />“Sure. Just tell me what type of AMI you want i.e. Amazon Linux 2, Ubuntu, anything, and I’ll go find the most recent one.”</p>
<p>With just that, Terraform fetches the latest Amazon Linux 2 image and uses that AMI ID when creating our EC2 instance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765120490827/83fa5c3a-41bf-4980-8a45-84a96dd4d247.png" alt class="image--center mx-auto" /></p>
<p>That’s the core idea behind data sources.<br />They help Terraform <strong>pull in information</strong> that already exists, whether it’s an AMI, a VPC, a subnet, or anything else, so that we don’t have to create everything from scratch or maintain static values.</p>
<h1 id="heading-a-real-world-scenario-using-what-already-exists">A Real-World Scenario: Using What Already Exists</h1>
<p>Now that we understand why data sources are helpful, let’s step into a slightly more practical example. We’ll try to see how Terraform behaves when we’re working with resources that are <em>already</em> present in AWS.</p>
<p>Imagine an organization where multiple teams share the same VPC.<br />It’s pretty common: the DevOps team, the development team, QA… everyone uses this one shared VPC that is already created and maintained in the AWS environment. Inside this shared VPC, imagine there are two subnets i.e. Subnet 1 and Subnet 2, and they’re also already set up.</p>
<p>Now, for <strong>our</strong> task, we need to create a few EC2 instances:<br />two in Subnet 1 and two in Subnet 2.<br />Simple enough, but here’s the important part:</p>
<p><strong>We’re are not creating a new VPC or new subnets.</strong><br />We have to use the existing ones.</p>
<p>And this brings us back to the value of data sources.</p>
<p>Because if the VPC and subnets are already present in AWS, Terraform doesn’t know their IDs. It needs a way to <em>look them up</em>, just like it looked up the AMI earlier. Hardcoding these IDs is not ideal, especially when we’re working in environments where different teams might update or rename things over time.</p>
<p>So, just like we used a data source to fetch the latest Amazon Linux AMI, we’ll use data sources again, but this time for the <strong>VPC</strong> and the <strong>subnets</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765121480766/329e5167-d424-4db9-a57f-0f35233ddf07.png" alt class="image--center mx-auto" /></p>
<p>This small setup i.e a shared VPC, shared subnets, and a few new EC2 instances, is the perfect place to learn how data sources help us connect Terraform to infrastructure that already exists. We’re still keeping things small, but this is the kind of approach enterprises use: slowly building on top of shared components without recreating everything from scratch.</p>
<h1 id="heading-creating-the-data-source-for-the-vpc">Creating the Data Source for the VPC</h1>
<p>Now that we understand <em>why</em> we need data sources, let’s start actually writing them. And just like always, we’ll take it one small step at a time.</p>
<p>Before this, whenever we looked up resources in Terraform documentation, we focused on the <strong>resource</strong> section, because we were creating things like EC2, VPC, security groups, and so on.<br />But now that we want to <em>read</em> something that already exists, we have to shift our attention to the <strong>data source</strong> section of the documentation instead.</p>
<p>So let’s begin with the first thing we need: the <strong>existing VPC</strong>.</p>
<p>In our AWS account, this shared VPC already exists and it has a tag named “day-13-vpc”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117876253/78e7db5d-3d0f-4765-a977-b5daebf7f2b7.png" alt class="image--center mx-auto" /></p>
<p>To fetch this VPC, we use Terraform’s data source for AWS VPC:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117160931/b5ba0859-5f95-4f09-8e7b-e782d8fc32e9.png" alt class="image--center mx-auto" /></p>
<p>This tiny block does something very powerful.<br />It tells Terraform:<br />“Look through all the VPCs in my AWS environment and find the one whose Name tag is set to ‘day-13-vpc’. Once you find it, make its details available to me.”</p>
<p>We’re not creating anything here.<br />We are simply <em>filtering</em> from what already exists.</p>
<p>And once Terraform finds this VPC, it will store all its details, like its ID, CIDR block, and more inside this data source. Later, when we create our EC2 instances, we can reference this VPC simply by calling:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117187562/2b1e6eb6-9fbd-4b05-8737-765f17ec0dfe.png" alt class="image--center mx-auto" /></p>
<p>It like asking Terraform,<br />“Can you show me the ID of that VPC we fetched earlier?”</p>
<p>This is the beauty of data sources.<br />They quietly connect Terraform to the real AWS environment without us having to hardcode anything or recreate resources that are already there.</p>
<h1 id="heading-creating-the-data-source-for-the-subnet">Creating the Data Source for the Subnet</h1>
<p>Now that Terraform knows how to fetch the existing VPC, the next step is to help it find the <strong>subnet</strong> inside that VPC.<br />Remember, both the VPC and the subnets already exist in our AWS environment, we’re simply asking Terraform to look them up.</p>
<p>So just like before, we go to the data source section in the Terraform documentation, but this time we search for <strong>AWS Subnet</strong>.</p>
<p>Let’s say the subnet we want has a tag called “day-13-subnet”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117919674/1adcf5f3-3771-4231-b0eb-62d033140416.png" alt class="image--center mx-auto" /></p>
<p>To fetch this specific subnet, we can write the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117360653/99f08a20-9c1e-4cbd-a89d-b27c9313d188.png" alt class="image--center mx-auto" /></p>
<p>Let’s pause and understand what’s happening here.</p>
<p>First, we apply a filter that searches for a subnet whose <strong>Name</strong> tag matches “day-13-subnet”.<br />This makes sure Terraform doesn’t return just any subnet but only the exact one we want.</p>
<p>Second, we link it to the VPC data source we created earlier:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117420401/7e152495-809b-47d8-be55-bad24b0160de.png" alt class="image--center mx-auto" /></p>
<p>With this small piece, Terraform now knows exactly which existing subnet to use when we later create our EC2 instance.</p>
<p>Nothing is being created here yet.<br />We’re still just gathering information and preparing Terraform for the final configuration.</p>
<p>In the next part, we’ll do the same thing for the <strong>Amazon Linux 2 AMI</strong>, because every EC2 instance we create needs a proper AMI. And again, we don’t want to hardcode it.</p>
<h1 id="heading-creating-the-data-source-for-the-ami-amazon-linux-2">Creating the Data Source for the AMI (Amazon Linux 2)</h1>
<p>Now that our VPC and subnet data sources are ready, there’s one more important piece we need before creating an EC2 instance: the <strong>AMI</strong>.</p>
<p>Just like we discussed earlier, an AMI (Amazon Machine Image) is basically the operating system template our EC2 instance will use. And there’s not just one AMI, there are multiple versions released over time. So instead of manually searching and copying AMI IDs, we’ll let Terraform fetch the latest one for us.</p>
<p>Terraform provides a data source just for this purpose.<br />Let’s build it.</p>
<p>Here’s how we define the Amazon Linux 2 AMI data source:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117526880/2b0c98ab-9962-4251-8608-2710b0704537.png" alt class="image--center mx-auto" /></p>
<p>Let’s break this down:</p>
<ul>
<li><p><code>owners = ["amazon"]</code> ensures we only pull AMIs officially published by Amazon, not community images or anything random.</p>
</li>
<li><p><code>most_recent = true</code> tells Terraform, “Always pick the latest Amazon Linux 2 image available.”</p>
</li>
<li><p>The first filter looks for AMIs whose <strong>name pattern</strong> matches the Amazon Linux 2 naming format. The stars (<code>*</code>) allow Terraform to match any version.</p>
</li>
<li><p>The second filter restricts the virtualization type to <strong>HVM</strong>, which is the standard for modern Amazon Linux AMIs.</p>
</li>
</ul>
<p>We can add more filters if needed, like root device type, specific naming patterns, or architecture but for our example, this is more than enough.</p>
<p>With this, Terraform will always fetch the latest, correct Amazon Linux 2 AMI, without us ever needing to manually update anything. This tiny block brings a lot of reliability to our configurations, especially when AMIs get updated frequently.</p>
<p>Now we have all three data sources ready:</p>
<ul>
<li><p>VPC</p>
</li>
<li><p>Subnet</p>
</li>
<li><p>AMI</p>
</li>
</ul>
<p>And in the next part, we'll finally use these data sources inside our <strong>aws_instance</strong> resource i.e. the part that actually creates the EC2 instance.</p>
<h1 id="heading-creating-the-ec2-instance-using-data-sources">Creating the EC2 Instance Using Data Sources</h1>
<p>Now that we have everything ready i.e. the VPC, the subnet, and the Amazon Linux 2 AMI, it’s time to bring it all together and create our EC2 instance.</p>
<p>Here’s what the resource block looks like in Terraform:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765117774738/93891877-3072-4eca-bcad-be858694bf41.png" alt class="image--center mx-auto" /></p>
<p>Let’s go through this:</p>
<ul>
<li><p><code>ami = data.aws_ami.linux2.id</code><br />  This tells Terraform to use the <strong>latest AMI</strong> we fetched using the data source. No hardcoding just the correct, up-to-date image.</p>
</li>
<li><p><code>instance_type = "t3.micro"</code><br />  We are keeping it simple with a small instance type.</p>
</li>
<li><p><code>subnet_id = data.aws_subnet.shared.id</code><br />  Terraform will place the EC2 instance in the subnet we fetched from the data source. Again, no manual ID lookup is needed.</p>
</li>
<li><p><code>tags = var.tags</code><br />  Tags help us identify and organize our resources. We’re reusing the same tags variable as before.</p>
</li>
</ul>
<p>We would be using the following values in var.tags:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765118285736/73d04634-fd33-4c45-83f4-63cc12d46ca8.png" alt class="image--center mx-auto" /></p>
<p>Now, after writing this, you can save the file and run:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765118301933/0d587919-f392-445f-b1d1-24708ebebce0.png" alt class="image--center mx-auto" /></p>
<p>We’ll notice that Terraform recognizes <strong>one new resource</strong> i.e. the EC2 instance. It will use the existing VPC, the existing subnet, and the latest AMI.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765119242264/a065f0c0-9d78-4bd6-b1f1-36f471fe125d.png" alt class="image--center mx-auto" /></p>
<p>We didn’t create any VPCs or subnets; Terraform simply referenced the ones we fetched using data sources.</p>
<p>Once you’re ready, running <code>terraform apply</code> will create the instance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765119310627/d21ce2de-45ba-44a8-b165-c79ca48901b9.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765119353819/948d9cc0-2412-4fa5-9496-28569717cab3.png" alt class="image--center mx-auto" /></p>
<p>After the demo, make sure to destroy the resource to avoid unnecessary costs.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And that’s it for Day 13!</p>
<p>With today’s demo, we wrapped up our first <strong>hands-on example</strong> using Terraform data sources.</p>
<p>This being our <strong>first practical demo</strong>, I hope it gave you a feel for what a <strong>hands-on experience</strong> is like. There’s something truly valuable about seeing concepts in action i.e. how small, careful steps in Terraform build up into something real, and why practicing hands-on is so important for understanding and confidence.</p>
<p>Before we close, a small reminder: after you’ve applied this demo, don’t forget to <strong>destroy the instance</strong> when you’re done. This keeps costs under control while you continue experimenting and learning.</p>
<p>If some parts feel a little confusing, here’s a <strong>video by Piyush Sachdeva</strong> that explains everything clearly, step by step. Watching it can help reinforce what we covered today and give you another perspective on hands-on Terraform practice.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/MSr67lWCyD8?si=pC-EeEbdtcjCNmaU">https://youtu.be/MSr67lWCyD8?si=pC-EeEbdtcjCNmaU</a></div>
<p> </p>
<p>Looking ahead, Day 14 will be a mini project where we combine what we’ve learned so far into a slightly larger example. I’m excited to continue this journey with you.</p>
<p>Thank you for staying patient and curious today. I hope you feel encouraged by your first hands-on experience and I’ll see you tomorrow for Day 14!</p>
<p><img src="https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExamZ5MHh1cmsyOTYxM2NxN3FyeDczd3JseDZkMm95aXhkbjNmMHhxMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kVI8LkPMHIaCLONauz/giphy.gif" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[DAY #37: [DAY-12] AWS Terraform Functions (Part-2)]]></title><description><![CDATA[Introduction
Hello and welcome back, it’s good to see you here again.
We’re now on Day 12 of our 30-day AWS Terraform journey, and it really does feel like we’re continuing a conversation we paused only yesterday. That steady, day-by-day progress is ...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-37-day-12-aws-terraform-functions-part-2</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-37-day-12-aws-terraform-functions-part-2</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[Devops]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Fri, 05 Dec 2025 18:21:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764958849359/8d4e4dd3-d3e2-415c-9a7d-2beb182d495d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction"><strong>Introduction</strong></h1>
<p>Hello and welcome back, it’s good to see you here again.</p>
<p>We’re now on <strong>Day 12</strong> of our 30-day AWS Terraform journey, and it really does feel like we’re continuing a conversation we paused only yesterday. That steady, day-by-day progress is what makes this whole thing manageable and, honestly, kind of enjoyable.</p>
<p>If you remember, on <strong>Day 11</strong> we opened the door to Terraform functions: we saw what they are, ran a few demos, and began to get a feel for how Terraform “thinks.” That day was about getting comfortable with the ideas and seeing how small functions fit into a larger workflow. Today we pick up right where we left off, carrying the same slow, steady pace.</p>
<p>So settle in, make yourself comfortable, and let’s explore this next chapter together.</p>
<p><img src="https://memesgenerator.hu/wp-content/uploads/mememe/2023/08/i-am-so-ready-for-learning-terraform-75087-1.jpg" alt="I am so ready for learning terraform - MeMes Generator" class="image--center mx-auto" /></p>
<p>Personal tidbit time:</p>
<blockquote>
<p>Fact #12: I’m not a big fan of mathematics, as a kid I used to dread it. And I think a big part of that was because I never really tried to understand it. Looking back, I actually feel a little sad for my younger self, if only I could explain things to that version of me in a simpler way, maybe it wouldn’t have felt so overwhelming.</p>
</blockquote>
<h1 id="heading-understanding-validation-functions"><strong>Understanding Validation Functions</strong></h1>
<p>Before we jump into the Terraform code, let’s take a moment to understand <em>why</em> validation functions matter.</p>
<p>Imagine we’re asking someone to fill out a small form maybe choosing an instance type or providing a backup name. Now, people make mistakes. They might type something too short, too long, or in a completely different format. If Terraform went ahead without checking anything, we’d only discover the mistake much later, often after the infrastructure has already been created incorrectly.</p>
<p>That’s where <strong>validation functions</strong> step in.</p>
<p>And the best part? These validations happen <em>before</em> Terraform even starts planning anything.</p>
<h2 id="heading-why-validation-goes-inside-the-variable-block"><strong>Why validation goes inside the variable block</strong></h2>
<p>Unlike other checks we’ve done before, validation has to live <strong>inside the variable declaration</strong> itself.<br />That’s because we’re telling Terraform:</p>
<blockquote>
<p>“Whenever someone provides a value for this variable, please check these rules first.”</p>
</blockquote>
<p>Here’s the example:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764950192014/90e70b73-ce3a-4a0e-af48-43318bc03603.png" alt class="image--center mx-auto" /></p>
<p>Let’s break this down.</p>
<h2 id="heading-1-first-validation-checking-the-length"><strong>1. First validation: checking the length</strong></h2>
<p>Terraform first checks whether the instance type is at least 2 characters long and at most 20.</p>
<p>Why?<br />Because sometimes someone may type something extremely short like <code>t</code> or something very long accidentally. This rule keeps things sensible.</p>
<p>The validation says:</p>
<ul>
<li><p>if the length is <strong>2–20</strong>, we’re good</p>
</li>
<li><p>if not, Terraform stops you with the message:<br />  <em>“instance type must be between 2 and 20 characters”</em></p>
</li>
</ul>
<p>Simple and helpful.</p>
<h2 id="heading-2-second-validation-checking-if-it-starts-with-t2-or-t3"><strong>2. Second validation: checking if it starts with t2 or t3</strong></h2>
<p>This one is more interesting.</p>
<p>Here’s the condition again:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764951616452/073aa096-eda2-41e0-8e7f-c3ce87b62cbe.png" alt class="image--center mx-auto" /></p>
<p>Let’s break it into very easy language:</p>
<ul>
<li><p><code>regex()</code><br />  → checks whether a value matches a specific pattern</p>
</li>
<li><p>the pattern <code>^t[2-3]\.</code> means:<br />  “The value should start with <strong>t2.</strong> or <strong>t3.</strong>”</p>
</li>
<li><p><code>can()</code><br />  → used so Terraform can safely test the regex without crashing</p>
</li>
</ul>
<p>Think of the regex like a rulebook:</p>
<ul>
<li><p><code>^</code> = start of the string</p>
</li>
<li><p><code>t</code> = must start with the letter "t"</p>
</li>
<li><p><code>[2-3]</code> = followed by either 2 or 3</p>
</li>
<li><p><code>\.</code> = then there must be a dot (<code>.</code>)</p>
</li>
</ul>
<p>So valid examples are:</p>
<ul>
<li><p>t2.micro</p>
</li>
<li><p>t2.small</p>
</li>
<li><p>t3.large</p>
</li>
</ul>
<p>Invalid ones would be:</p>
<ul>
<li><p>m5.large</p>
</li>
<li><p>t4.micro</p>
</li>
<li><p>t2micro (missing dot)</p>
</li>
</ul>
<p>And if the input doesn’t follow the pattern, Terraform returns our custom error:</p>
<blockquote>
<p>“Instance type must start with T2 or T3.”</p>
</blockquote>
<p>This makes the error more human-friendly and less confusing.</p>
<h2 id="heading-why-can-is-needed"><strong>Why</strong> <code>can()</code> is needed</h2>
<p>If we used only <code>regex()</code>, Terraform might throw a harsh technical error if the pattern didn’t match.<br />But with <code>can()</code>, Terraform simply checks:</p>
<ul>
<li><p>If regex works → return <strong>true</strong></p>
</li>
<li><p>If regex fails → return <strong>false</strong>, but gracefully (no crashes)</p>
</li>
</ul>
<h2 id="heading-an-example-to-relate"><strong>An example to relate</strong></h2>
<p>Imagine we're checking someone’s ID:</p>
<ul>
<li><p>First, check if the ID length is reasonable (not too short, not too long).</p>
</li>
<li><p>Then check if it starts with a certain prefix like "AB" or "BC".</p>
</li>
</ul>
<p>If both checks pass, great, let them in.<br />If not, we politely say, “Sorry, this ID doesn’t look right.”</p>
<p>Terraform is doing exactly that for our variables.</p>
<h1 id="heading-a-practical-validation-ensuring-a-backup-name-ends-with-backup"><strong>A Practical Validation: Ensuring a Backup Name Ends With</strong> <code>-backup</code></h1>
<p>Now that we understand how validations work with <code>instance_type</code>, let’s look at another everyday example, something even simpler.</p>
<p>Imagine we’re creating a variable called <strong>backup name</strong>.<br />We want every backup to follow a proper naming pattern so it’s easy to identify later.</p>
<p>Let’s say the rule is:</p>
<blockquote>
<p>“Every backup name must end with <code>-backup</code>.”</p>
</blockquote>
<p>Why is this useful?<br />Because when our systems grow, naming patterns save us from chaos.<br />Backups become easy to filter, group, search, and automate around, all because they follow a familiar ending.</p>
<h2 id="heading-the-terraform-validation"><strong>The Terraform validation</strong></h2>
<p>Here’s how it might look:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764954386610/168b08c3-656b-4298-828c-fa00862fafd7.png" alt class="image--center mx-auto" /></p>
<p>Let’s unpack this.</p>
<h3 id="heading-1-endswith-function"><strong>1. endswith() function</strong></h3>
<p><code>endswith(string, substring)</code> simply checks whether a value ends with a specific text.</p>
<p>So:</p>
<ul>
<li><p><code>"mydata-backup"</code> → valid ✔</p>
</li>
<li><p><code>"backup-mydata"</code> → invalid ❌</p>
</li>
<li><p><code>"mydata-backup123"</code> → invalid ❌</p>
</li>
</ul>
<p>Terraform evaluates the condition, and if it fails, it shows you the custom message we provided.</p>
<p>We can enforce structure <strong>before</strong> Terraform even plans anything.</p>
<h1 id="heading-understanding-sensitive-variables-why-some-values-stay-hidden"><strong>Understanding Sensitive Variables (Why Some Values Stay Hidden)</strong></h1>
<p>Some Terraform variables hold harmless information, like names or flags.<br />But others may contain things that should <em>never</em> appear in logs or screens i.e. things like passwords, tokens, keys, or database connection strings.</p>
<p>Terraform gives us a simple way to protect such values:</p>
<blockquote>
<p><strong>Mark the variable as</strong> <code>sensitive = true</code>.</p>
</blockquote>
<p>This doesn’t encrypt the value, but it does hide it from output, logs, and accidental exposure during <code>terraform plan</code> or <code>terraform apply</code>.</p>
<h2 id="heading-terraform-example"><strong>Terraform example</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764954557417/b295beae-5395-4804-b66e-d861a795d2b9.png" alt class="image--center mx-auto" /></p>
<p>Now, even if someone tries to print it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764954586718/8b9698a9-f8fe-4d86-b012-ba18ea880bcd.png" alt class="image--center mx-auto" /></p>
<p>Terraform will hide it.<br />You’ll see something like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764954612556/4171ef70-5a54-47af-98f7-50965e2c11d3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-why-this-matters"><strong>Why this matters</strong></h2>
<p>Sensitive variables protect us from:</p>
<ul>
<li><p>accidental screen-sharing leaks</p>
</li>
<li><p>logs exposing secrets</p>
</li>
<li><p>commit mistakes when values accidentally get echoed</p>
</li>
<li><p>teammates seeing secrets they don’t need to</p>
</li>
</ul>
<p>It’s not encryption, but it’s a safety layer that prevents embarrassment and accidents.</p>
<h1 id="heading-validations-using-regex"><strong>Validations Using Regex</strong></h1>
<p>So far, our validations checked simple conditions like:</p>
<ul>
<li><p>Does this value belong to a list?</p>
</li>
<li><p>Does this value end with “-backup”?</p>
</li>
</ul>
<p>But sometimes we need <strong>more powerful rules</strong>, rules that check patterns, formats, or structures.</p>
<p>This is where <strong>regex</strong> (regular expressions) comes in.</p>
<p>Regex sounds scary, but in Terraform, we’ll mostly use it for simple checks like:</p>
<ul>
<li><p>Does the value contain only letters and numbers?</p>
</li>
<li><p>Does the name follow a specific pattern?</p>
</li>
<li><p>Does it start with something specific?</p>
</li>
<li><p>Does it avoid special characters?</p>
</li>
</ul>
<h2 id="heading-a-simple-explanation-of-regex"><strong>A simple explanation of regex</strong></h2>
<p>Think of regex as a <strong>pattern detective</strong>.</p>
<p>Instead of checking a single condition, it looks for a <em>shape</em> in the text.</p>
<p>Example in plain English:</p>
<blockquote>
<p>“The name must start with a letter and can only contain letters, numbers, and hyphens.”</p>
</blockquote>
<p>Regex lets Terraform enforce that in one line.</p>
<h2 id="heading-terraform-regex-example"><strong>Terraform regex example</strong></h2>
<p>Let’s say we want our environment names to follow this rule:</p>
<blockquote>
<p>“Must start with a letter, and can contain letters, numbers, and hyphens.”</p>
</blockquote>
<p>The regex for that (very common pattern) is:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764954761503/ef5e33b8-8ad5-4e6a-ba16-672b688053e2.png" alt class="image--center mx-auto" /></p>
<p>We’ll understand what the above gibbersh means in a while.</p>
<p>Here’s how we apply it in Terraform:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764954813707/059f0da9-66c3-4a6e-a526-4001780cd6af.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-breaking-this-down-in-simple-terms"><strong>Breaking this down in simple terms</strong></h2>
<h3 id="heading-1-and"><strong>1. ^ and $</strong></h3>
<p>These mean “start” and “end”.</p>
<p>Terraform checks the <em>whole</em> string, not just a part.</p>
<h3 id="heading-2-a-za-z"><strong>2. [a-zA-Z]</strong></h3>
<p>The name must start with a letter.</p>
<h3 id="heading-3-a-za-z0-9"><strong>3. [a-zA-Z0-9-]</strong>*</h3>
<p>After the first letter, we can have:</p>
<ul>
<li><p>letters</p>
</li>
<li><p>numbers</p>
</li>
<li><p>hyphens</p>
</li>
<li><p>repeated any number of times (<code>*</code>)</p>
</li>
</ul>
<h2 id="heading-why-regex-matters-in-terraform"><strong>Why regex matters in Terraform</strong></h2>
<p>When our project grows, naming mistakes create chaos:</p>
<ul>
<li><p>S3 buckets with weird characters</p>
</li>
<li><p>IAM roles with spaces</p>
</li>
<li><p>Environment names with typos</p>
</li>
<li><p>Random uppercase/lowercase mixtures</p>
</li>
</ul>
<p>Regex is our gatekeeper.</p>
<p>It stops wrong values <strong>before</strong> they spread through our infrastructure.</p>
<p>If this section is good, we’ll move to the next one:</p>
<h1 id="heading-validation-rules-for-lists"><strong>Validation Rules for Lists</strong></h1>
<p>So far, we’ve seen validations for single values: an instance type, a backup name, or an environment name.<br />But what if our variable is a <strong>list of values</strong>? For example, a list of regions, instance types, or users.</p>
<p>Terraform lets us apply validations <strong>to every item in the list</strong>, ensuring consistency across the board.</p>
<h2 id="heading-why-this-matters-1"><strong>Why this matters</strong></h2>
<p>Imagine we have a list of regions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955312945/e8841ad9-2678-4df3-bfa2-20912242cd61.png" alt class="image--center mx-auto" /></p>
<p>We want all regions to:</p>
<ul>
<li><p>Follow the proper format</p>
</li>
<li><p>Only contain letters, numbers, and hyphens</p>
</li>
<li><p>Avoid typos or accidental spaces</p>
</li>
</ul>
<p>Without validation, Terraform would happily accept a wrong entry, and our deployments could fail later.</p>
<p>With validations, Terraform stops mistakes <strong>upfront</strong>.</p>
<h2 id="heading-terraform-example-1"><strong>Terraform example</strong></h2>
<p>Suppose we have a variable for <strong>allowed regions</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955377696/dc94a6fe-1656-47d9-b369-7b6ee3e35ae1.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-breaking-it-down"><strong>Breaking it down</strong></h3>
<ul>
<li><p><strong>for r in var.allowed_regions</strong><br />  Iterates over every region in the list.</p>
</li>
<li><p><strong>regex("^[a-z]{2}-[a-z]+-[0-9]$", r)</strong><br />  Ensures each region follows AWS naming conventions i.e.</p>
<ul>
<li><p>Accepted values: <code>us-east-1</code></p>
</li>
<li><p>Rejected values: <code>USEAST1</code>, <code>us-east</code></p>
</li>
</ul>
</li>
<li><p><strong>can()</strong><br />  Handles errors gracefully if the regex fails.</p>
</li>
<li><p><strong>alltrue([...])</strong><br />  Ensures that <strong>every single item</strong> passes the condition. If even one fails, Terraform stops and shows our custom error.</p>
</li>
</ul>
<h2 id="heading-why-list-validation-is-useful"><strong>Why list validation is useful</strong></h2>
<ul>
<li><p>Prevents subtle mistakes across multiple entries</p>
</li>
<li><p>Makes infrastructure predictable</p>
</li>
<li><p>Saves time debugging deployment errors</p>
</li>
<li><p>Ensures <strong>human-friendly, consistent standards</strong></p>
</li>
</ul>
<h1 id="heading-type-conversion-functions-lists-sets-and-unique-values"><strong>Type Conversion Functions (Lists, Sets, and Unique Values)</strong></h1>
<p>In Terraform, sometimes we need to <strong>change the type of a variable</strong> to make it easier to work with.<br />This is especially common when dealing with <strong>lists</strong> and <strong>sets</strong>, or when we want to remove duplicate values.</p>
<h2 id="heading-why-this-matters-2"><strong>Why this matters</strong></h2>
<p>Imagine we have a list of regions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955555746/a25a5448-85dc-419e-b53e-4998bf98681b.png" alt class="image--center mx-auto" /></p>
<p>Notice how <strong>“us-east-1” appears twice</strong>.</p>
<ul>
<li><p>A <strong>list</strong> allows duplicates</p>
</li>
<li><p>A <strong>set</strong> only keeps <strong>unique values</strong></p>
</li>
</ul>
<p>Converting a list to a set is a simple way to automatically remove duplicates, making our infrastructure cleaner and more predictable.</p>
<h2 id="heading-terraform-example-converting-a-list-to-a-set"><strong>Terraform example: Converting a list to a set</strong></h2>
<p>Suppose we have two variables:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955616119/a8701e8e-d7ad-4d2c-95c2-290483287d34.png" alt class="image--center mx-auto" /></p>
<p>We can combine them with <code>concat</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955673962/f28449b0-13c8-4e2c-ba8b-6cb56e1be8e5.png" alt class="image--center mx-auto" /></p>
<p>This produces:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955701409/668268d8-917e-4952-a136-5dcb541af793.png" alt class="image--center mx-auto" /></p>
<p>Now, to get <strong>only unique locations</strong>, use <code>toset</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955735542/59ad26c7-fe61-40c2-846a-58acd47f25f3.png" alt class="image--center mx-auto" /></p>
<p>Result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955757872/0ce13451-219e-4678-bc14-691652e7ded8.png" alt class="image--center mx-auto" /></p>
<p>Duplicates are automatically removed, no extra effort needed.</p>
<h2 id="heading-why-type-conversion-is-useful"><strong>Why type conversion is useful</strong></h2>
<ul>
<li><p>Cleans up data automatically</p>
</li>
<li><p>Makes further operations easier</p>
</li>
<li><p>Prevents mistakes when working with lists that shouldn’t have duplicates</p>
</li>
<li><p>Bridges differences between Terraform’s <strong>list</strong> and <strong>set</strong> types</p>
</li>
</ul>
<h1 id="heading-number-functions-sum-max-min-absolute-and-average"><strong>Number Functions: Sum, Max, Min, Absolute, and Average</strong></h1>
<p>Terraform provides several handy <strong>number functions</strong> that help us perform calculations on variables, especially lists of numbers.'</p>
<p>These functions are useful for things like <strong>monthly costs</strong>, <strong>resource counts</strong>, or any scenario where we need math on our infrastructure data.</p>
<h2 id="heading-the-scenario"><strong>The scenario</strong></h2>
<p>Suppose we have a list of monthly costs (some are credits, so negative):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955830382/8d8a84cc-26a0-4673-a6be-af1a737abcb6.png" alt class="image--center mx-auto" /></p>
<p>We want to:</p>
<ol>
<li><p>Convert everything to <strong>positive numbers</strong></p>
</li>
<li><p>Find the <strong>maximum cost</strong></p>
</li>
<li><p>Find the <strong>minimum cost</strong></p>
</li>
<li><p>Calculate the <strong>total cost</strong></p>
</li>
<li><p>Calculate the <strong>average cost</strong></p>
</li>
</ol>
<h2 id="heading-step-1-convert-to-positive-values"><strong>Step 1: Convert to positive values</strong></h2>
<p>Terraform’s <code>abs()</code> function returns the absolute value of a number.<br />But because <code>abs()</code> doesn’t work on a list directly, we use a <strong>for loop</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955900386/2e6e897d-3c7c-4118-b293-200fff25e080.png" alt class="image--center mx-auto" /></p>
<p>Result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955932153/0cd368bd-4e89-429c-98f4-a939d82e9f75.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-2-find-max-and-min"><strong>Step 2: Find max and min</strong></h2>
<p>Terraform provides <code>max()</code> and <code>min()</code> functions.<br />Apply them on our positive list:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955965574/c30e737c-4529-4ba9-824f-9e05252fe1d0.png" alt class="image--center mx-auto" /></p>
<p>Result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764955991770/deb7747b-8828-4ee4-bd5e-2351b93a4b74.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-3-calculate-total-and-average"><strong>Step 3: Calculate total and average</strong></h2>
<p>To sum all costs:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764956027644/bb8d39d8-f0ee-4a7a-8dc2-1bbc01585031.png" alt class="image--center mx-auto" /></p>
<p>Result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764956049332/9e1b2703-ce07-4d2b-afdd-0d2f864b0d6b.png" alt class="image--center mx-auto" /></p>
<p>Average is simply total divided by the number of elements:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764956078508/11a9e05a-54ed-4251-917b-9d183af47142.png" alt class="image--center mx-auto" /></p>
<p>Result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764956106272/8d1a1930-2943-4e8d-ad29-db87e5344132.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-why-this-matters-3"><strong>Why this matters</strong></h2>
<ul>
<li><p>Handles negative/credit values easily</p>
</li>
<li><p>Calculates statistics across any list of numbers</p>
</li>
<li><p>Helps in budgeting, reporting, or monitoring infrastructure costs</p>
</li>
<li><p>Makes our Terraform configurations smarter and more automated</p>
</li>
</ul>
<h1 id="heading-timestamp-functions-current-time-and-formatting"><strong>Timestamp Functions: Current Time and Formatting</strong></h1>
<p>Terraform also provides <strong>timestamp functions</strong> to help us capture and format the current time.<br />This can be useful for logging, backups, or creating time-stamped resources automatically.</p>
<h2 id="heading-an-example"><strong>An example</strong></h2>
<p>Terraform’s <code>timestamp()</code> function doesn’t need any input:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764957102145/562daac1-caac-43e5-a71f-9844d79a64b1.png" alt class="image--center mx-auto" /></p>
<p>This produces a value like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764957133020/5cb11bb2-bac9-4284-b7ce-081a59525513.png" alt class="image--center mx-auto" /></p>
<p>This is the <strong>current UTC timestamp</strong> in ISO 8601 format.</p>
<h2 id="heading-formatting-the-timestamp"><strong>Formatting the timestamp</strong></h2>
<p>Sometimes, we may want a <strong>more human-friendly format</strong>.<br />Terraform provides <code>formatdate()</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764957177610/3796775a-d76c-4b66-8d77-017749101fc6.png" alt class="image--center mx-auto" /></p>
<p>Result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764957202917/fa5bcb6a-6873-4536-ba59-6cc4847e52ad.png" alt class="image--center mx-auto" /></p>
<p>Now we can use this formatted timestamp in <strong>backup names, logs, or resource tags</strong>.</p>
<h2 id="heading-why-timestamp-functions-matter"><strong>Why timestamp functions matter</strong></h2>
<ul>
<li><p>Helps with <strong>automation</strong> that relies on dates</p>
</li>
<li><p>Makes logs and outputs <strong>easier to interpret</strong></p>
</li>
<li><p>Supports <strong>versioning and auditing</strong> in our Terraform deployments</p>
</li>
</ul>
<h1 id="heading-file-handling-functions"><strong>File Handling Functions</strong></h1>
<p>Terraform provides <strong>file handling functions</strong> that let us read, decode, and use data from files directly in our configurations.</p>
<p>This is especially useful when working with JSON configuration files or other structured data.</p>
<h2 id="heading-the-scenario-1"><strong>The scenario</strong></h2>
<p>Suppose we have a JSON file <code>config.json</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764957276121/11655537-8da1-46e2-b62b-80806237b04a.png" alt class="image--center mx-auto" /></p>
<p>We want Terraform to:</p>
<ul>
<li><p>Check if the file exists</p>
</li>
<li><p>Read its contents</p>
</li>
<li><p>Decode the JSON into a usable map</p>
</li>
</ul>
<h2 id="heading-terraform-example-2"><strong>Terraform example</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764957579250/6e705512-83f4-4436-91b9-65fd7e973f1a.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-breaking-it-down-1"><strong>Breaking it down</strong></h3>
<ol>
<li><p><strong>fileexists("config.json")</strong><br /> Checks if the file exists. Returns <code>true</code> or <code>false</code>.</p>
</li>
<li><p><strong>file("config.json")</strong><br /> Reads the contents of the file.</p>
</li>
<li><p><strong>jsondecode(...)</strong><br /> Converts the JSON text into a Terraform map or object.</p>
</li>
<li><p><strong>Conditional operator (</strong><code>? :</code>)<br /> If the file exists → decode it<br /> If not → return an empty map <code>{}</code></p>
</li>
</ol>
<h2 id="heading-output-example"><strong>Output example</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764957634668/de380b7e-99b7-460c-9fe7-2899cf41a1c0.png" alt class="image--center mx-auto" /></p>
<p>Now Terraform can use the database host, API endpoint, or any other value from the JSON file in our infrastructure resources.</p>
<h2 id="heading-additional-notes"><strong>Additional notes</strong></h2>
<ul>
<li><p>Terraform also provides <strong>jsonencode()</strong> to convert data structures back into JSON</p>
</li>
<li><p>File handling functions let us <strong>reuse external data</strong> safely and efficiently</p>
</li>
<li><p>Great for keeping secrets, configurations, or templates <strong>outside our main</strong> <code>.tf</code> files</p>
</li>
</ul>
<h2 id="heading-why-this-matters-4"><strong>Why this matters</strong></h2>
<ul>
<li><p>Makes our Terraform configurations more <strong>dynamic and flexible</strong></p>
</li>
<li><p>Keeps sensitive or large configuration data <strong>outside our main code</strong></p>
</li>
<li><p>Supports <strong>automation, templating, and scaling</strong> your deployments</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And with this, we’ve reached the end of <strong>Day 12</strong> in our 30-day Terraform journey.<br />Today we closed the loop on the remaining Terraform functions, everything we couldn’t finish in Part 1 finally found its place here in Part 2.</p>
<p>We explored validations, sensitive values, type conversions, numeric functions, timestamps, and even dipped our toes into file handling.<br />Each of these shape the kind of Terraform code that <strong>behaves</strong> reliably, predictably, and in a way that protects us from surprises.</p>
<p>And I just want to say this: if you’ve been following along till Day 12, that’s not a small thing.<br />Learning something every day, especially something as deep and layered as Terraform, is not easy.</p>
<p>We’ve come this far, and there’s still a long, exciting road ahead. We’ll tackle it together!</p>
<p>And hey, if you still have doubts or something felt a bit fuzzy, here’s a helpful video by <strong>Piyush Sachdeva</strong> that might clear things up.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/ZYCCu9rZkU8?si=xK-sPuPGpggw8b14">https://youtu.be/ZYCCu9rZkU8?si=xK-sPuPGpggw8b14</a></div>
<p> </p>
<p>Never hesitate to revisit, rewatch, or relearn.</p>
<p>With that thought, I’ll leave you for today. We’ll catch up again tomorrow with a new topic tomorrow.</p>
<p>Peace!</p>
]]></content:encoded></item><item><title><![CDATA[DAY #36: [DAY-11] AWS Terraform Functions (Part-1)]]></title><description><![CDATA[Introduction
Hello and welcome back to our 30 Days of AWS Terraform journey! How’s everyone doing today? I hope life is treating you kindly and that you’re finding a little time to enjoy this learning adventure with me.
Last session, we dived into so...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-36-day-11-aws-terraform-functions-part-1</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-36-day-11-aws-terraform-functions-part-1</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[Devops]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Thu, 04 Dec 2025 18:53:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764874304349/e61a7098-5efb-437e-9483-3f61d98bd384.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction"><strong>Introduction</strong></h1>
<p>Hello and welcome back to our <strong>30 Days of AWS Terraform journey</strong>! How’s everyone doing today? I hope life is treating you kindly and that you’re finding a little time to enjoy this learning adventure with me.</p>
<p>Last session, we dived into some fascinating pieces of Terraform i.e. <strong>conditional expressions, splat expressions, and dynamic blocks</strong>. Each of these little gems helped us see how Terraform makes infrastructure descriptions more flexible and expressive.</p>
<p><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%2Fy8zrjll2k5p6btqqhbu4.png" alt="🚀 Crash Course: Terraform Basics for Freshers - DEV Community" class="image--center mx-auto" /></p>
<p>Today, we’re turning our attention to <strong>Terraform Functions: Part 1</strong>.</p>
<p>Now, if the word “function” makes you imagine complicated programming, don’t worry, that’s not what we’re doing here. Terraform isn’t a programming language. It’s a <strong>configuration language</strong>, which simply means we describe what we want our infrastructure to look like, rather than writing a program to compute it. And to make that description a bit smarter and more reusable, Terraform provides <strong>built-in helpers called functions</strong>.</p>
<p>Because there are so many functions, we’ll explore them in <strong>two parts</strong>. In <strong>Part 1</strong>, we’ll cover what a function really is, why Terraform only gives us inbuilt functions, and how these helpers can make our variables and values <strong>cleaner and easier to manage</strong>.</p>
<p>This journey isn’t about overwhelming us it’s about <strong>learning small, practical tools</strong> that help us express what we already know in a smarter, more organized way.</p>
<p>And here’s a little personal tidbit:</p>
<blockquote>
<p><strong>Fact #11:</strong> I prefer learning from <strong>YouTube tutorials</strong> instead of going through documentation. For me, I like to see things in action, that makes things click faster for me.</p>
</blockquote>
<h1 id="heading-understanding-functions-and-terraforms-inbuilt-functions"><strong>Understanding Functions and Terraform’s Inbuilt Functions</strong></h1>
<h2 id="heading-what-is-a-function-really"><strong>What is a Function, Really?</strong></h2>
<p>Before we dive into <strong>Terraform-specific functions</strong>, let’s take a moment to understand <strong>what a function really is</strong>, in the simplest way possible.</p>
<p>In plain language, a <strong>function is something that makes our life easier</strong>. How? By letting us <strong>reuse instructions</strong> instead of writing the same steps over and over again.</p>
<p>Imagine this: we want to add two numbers. In a typical programming language, we might write something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764870954639/4603fed8-6ee7-47c7-b37c-92f88da337f8.png" alt class="image--center mx-auto" /></p>
<p>Simple enough. But what if we had to do this <strong>10 different times</strong>, with different numbers each time? Sure, we could copy and paste those four lines again and again, but that’s <strong>inefficient and tedious</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765032296820/129236ea-a8a6-49ab-b17a-69f3efba0b63.png" alt class="image--center mx-auto" /></p>
<p>This is exactly where a <strong>function</strong> comes in. Instead of rewriting the same code multiple times, we <strong>wrap it once in a function</strong>, and then just call it whenever we need it, passing in the numbers we want to add. The <strong>logic is reused</strong>, our code stays clean, and our work becomes <strong>faster and more organized</strong>.</p>
<h2 id="heading-terraform-is-not-a-programming-language"><strong>Terraform is Not a Programming Language</strong></h2>
<p>Now, we might already know this, but it’s worth repeating: <strong>Terraform isn’t a full-fledged programming language</strong>. It’s a <strong>configuration language</strong>, officially called <strong>HCL i.e. HashiCorp Configuration Language</strong>.</p>
<p><img src="https://i.imgflip.com/6uapmk.jpg" alt="terraform Memes &amp; GIFs - Imgflip" class="image--center mx-auto" /></p>
<p>What does that mean?</p>
<ul>
<li><p>Terraform does <strong>not</strong> have classes, objects, or traditional OOP features.</p>
</li>
<li><p>We <strong>cannot</strong> create our own custom functions.</p>
</li>
</ul>
<p>Instead, Terraform provides a <strong>set of inbuilt functions</strong> that we can use anywhere in our configurations. These helpers cover a variety of tasks, including:</p>
<ul>
<li><p><strong>String manipulation</strong></p>
</li>
<li><p><strong>Numeric operations</strong></p>
</li>
<li><p><strong>Collections</strong> (lists and maps)</p>
</li>
<li><p><strong>Type conversion</strong></p>
</li>
<li><p><strong>Date and time handling</strong></p>
</li>
<li><p><strong>Validation and lookup</strong></p>
</li>
</ul>
<p>Even though Terraform can’t do everything a programming language can, these <strong>inbuilt functions give us the power to write clean, reusable, and dynamic configurations</strong> for our infrastructure.</p>
<h3 id="heading-categories-of-terraform-functions"><strong>Categories of Terraform Functions</strong></h3>
<p>Here’s a <strong>bird’s-eye view</strong> of the function categories we’ll explore:</p>
<ul>
<li><p><strong>String functions</strong>: manipulate and transform text</p>
</li>
<li><p><strong>Numeric functions</strong>: calculate max, min, absolute, etc.</p>
</li>
<li><p><strong>Collection functions</strong>: work with lists and maps</p>
</li>
<li><p><strong>Type conversion functions</strong>: convert between lists, sets, numbers, and strings</p>
</li>
<li><p><strong>Date and time functions</strong>:work with timestamps and formatted dates</p>
</li>
<li><p><strong>Validation and lookup function:</strong> check or select values dynamically</p>
</li>
</ul>
<h1 id="heading-terraform-string-functions"><strong>Terraform String Functions</strong></h1>
<p>Now that we understand <strong>what a function is</strong> and why Terraform provides <strong>inbuilt functions</strong>, let’s explore <strong>string functions</strong>, these are some of the most commonly used helpers in Terraform.</p>
<p>String functions let us <strong>manipulate text</strong>, making it easier to format, clean, or transform strings before using them in our infrastructure.</p>
<h2 id="heading-1-upper-and-lower-functions"><strong>1. Upper and Lower Functions</strong></h2>
<ul>
<li><p><code>upper()</code> converts a string to <strong>uppercase</strong>.</p>
</li>
<li><p><code>lower()</code> converts a string to <strong>lowercase</strong>.</p>
</li>
</ul>
<p>Example:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871170548/bf39658c-2b8d-4aeb-b84a-d08a8b24e541.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764864992989/36a8e833-a039-4398-9439-1ff032ed2479.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871228504/820fcb85-9e7e-4bc9-8d21-6e0a2f09cd08.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865034844/d3f44f9f-8b12-48a4-bc35-0e4f62ed68e1.png" alt class="image--center mx-auto" /></p>
<p>Notice how we <strong>call the function</strong> and pass the string as an argument. This is exactly how functions let us <strong>reuse logic</strong> without rewriting it.</p>
<h2 id="heading-2-trim-function"><strong>2. Trim Function</strong></h2>
<ul>
<li><code>trim(string, characters)</code> removes unwanted characters (like spaces) from the beginning and end of a string.</li>
</ul>
<p>Examples:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871345501/fe0c2393-18d2-4d47-8fc9-8b0760d20a02.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865248531/9de9783a-4a63-459d-afb2-d74e56148946.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871402629/679a69f1-fb65-4660-871b-40bd5b9635a2.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865437902/c5ed59f5-859a-45da-81cb-190424fd29c7.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>The <strong>first argument</strong> is the string to clean.</p>
</li>
<li><p>The <strong>second argument</strong> is the character to remove.</p>
</li>
</ul>
<h2 id="heading-3-replace-function"><strong>3. Replace Function</strong></h2>
<ul>
<li><code>replace(string, search, replace)</code> replaces occurrences of a character or substring with another.</li>
</ul>
<p>Example:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871458753/f23f2edc-390d-42e8-a5d2-b6a5b83ba5e2.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865571931/177d3f65-1386-489e-b555-c54c13b158a3.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>The first argument is the <strong>original string</strong>.</p>
</li>
<li><p>The second is the <strong>character to replace</strong>.</p>
</li>
<li><p>The third is the <strong>new character</strong>.</p>
</li>
</ul>
<h2 id="heading-4-substring-function"><strong>4. Substring Function</strong></h2>
<ul>
<li><code>substr(string, start, length)</code> extracts a portion of a string.</li>
</ul>
<p>Example:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871502560/72915835-ab42-423e-8b69-9187015995fc.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Starts at <strong>index 0</strong> (the first character) and goes up to <strong>length 3</strong>.</p>
</li>
<li><p>Perfect for trimming or formatting strings in Terraform.</p>
</li>
</ul>
<p>These functions are <strong>especially helpful</strong> when we need to clean inputs, format names, or prepare strings for resources like <strong>S3 bucket names</strong>, which have strict naming rules.</p>
<h1 id="heading-numeric-and-collection-functions-in-terraform"><strong>Numeric and Collection Functions in Terraform</strong></h1>
<h2 id="heading-1-numeric-functions"><strong>1. Numeric Functions</strong></h2>
<p>Numeric functions help perform <strong>simple calculations</strong> or <strong>analyze numbers</strong>.</p>
<p><code>max()</code>: returns the largest number in a set.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871579715/16257cda-221a-44c1-b874-0d6600289b7b.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865776361/6abbe6dd-2a3e-483c-ba6d-21d535a34bc3.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>min()</code>: returns the smallest number.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871633751/29315455-1fb0-4b99-84bb-61487ab56e1c.png" alt class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865829976/e26a120e-c9e6-4b52-bbdd-b96f9e6b7c3e.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><code>abs()</code>: returns the absolute value (removes the negative sign).</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871665003/5ccc90cd-205f-49b7-8b49-b8feac993b79.png" alt class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865866330/1371671b-cf5c-496d-a88f-6d45ba65eff2.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<p>These functions are <strong>helpful when working with variables</strong> where you might need to select limits, bounds, or ensure positive values.</p>
<h2 id="heading-2-collection-functions"><strong>2. Collection Functions</strong></h2>
<p>Collection functions let us <strong>manipulate lists and maps</strong>, which are fundamental in Terraform for storing multiple values.</p>
<ul>
<li><code>length()</code>: returns the number of elements in a list.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871728662/c581348c-d7e3-4248-8c4b-edaa3f0adbd3.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764865973310/3c195840-1d26-4fc8-8967-708f31dff39b.png" alt class="image--center mx-auto" /></p>
<ul>
<li><code>concat()</code>: combines multiple lists into one.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871772264/c5337f18-6614-454e-835c-3a7960313659.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764866056181/8283f351-b3f4-4363-9881-856240a309ef.png" alt class="image--center mx-auto" /></p>
<ul>
<li><code>merge()</code>: combines multiple maps (key-value pairs).</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871813007/af672010-2786-42ec-b220-bef7cb04297c.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764866184798/bc7fdb63-8c2a-4d7b-a7ac-68a14296c253.png" alt class="image--center mx-auto" /></p>
<p>Collections are very common in Terraform. For example, when managing <strong>tags</strong>, <strong>security group rules</strong>, or <strong>multiple ports</strong>, these functions let us combine, iterate, and manipulate values <strong>cleanly and dynamically</strong>.</p>
<h1 id="heading-type-conversion-and-datetime-functions-in-terraform"><strong>Type Conversion and Date/Time Functions in Terraform</strong></h1>
<p>In Terraform, sometimes we need to <strong>change the type of a value</strong> or <strong>work with time-based data</strong>. This is where <strong>type conversion</strong> and <strong>date/time functions</strong> come in handy.</p>
<h2 id="heading-1-type-conversion-functions"><strong>1. Type Conversion Functions</strong></h2>
<p>Type conversion functions let us <strong>change one type of value into another</strong>, ensuring Terraform can handle it correctly.</p>
<ul>
<li><code>toset()</code>: converts a list into a set.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764871912979/1b85a1e9-a961-4cd8-9892-f0587380ed28.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764866337873/b8b777b8-bc05-4c99-bbcc-04f256062109.png" alt class="image--center mx-auto" /></p>
<ul>
<li><code>tonumber()</code>: converts a string representing a number into an actual numeric value.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872002877/8dd33b86-5534-4fe2-812d-2d1eac748e27.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764866443169/a99ebd0d-e79c-4276-8196-80272b419b44.png" alt class="image--center mx-auto" /></p>
<p>Note: In Terraform, anything in <strong>double quotes</strong> is treated as a string, even if it looks like a number. So we use <code>tonumber()</code> to ensure numeric operations work correctly.</p>
<h2 id="heading-2-date-and-time-functions"><strong>2. Date and Time Functions</strong></h2>
<p>Terraform also allows us to <strong>work with timestamps</strong> or <strong>format dates</strong>:</p>
<ul>
<li><code>timestamp()</code>: returns the current time in a predefined format.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872069832/bd1890f3-e52e-46bf-b121-b78b01ebe514.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764866512399/23f8b5e4-08b5-4410-a796-37d04e11071e.png" alt class="image--center mx-auto" /></p>
<ul>
<li><code>formatdate()</code>: allows custom formatting of the timestamp.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872150382/849f026c-7c29-42a9-93dd-839426df36a3.png" alt class="image--center mx-auto" /></p>
<p>These functions are useful when we need <strong>timestamps for logs, resources, or versioning</strong>, or when a specific format is required for our infrastructure configurations.</p>
<h1 id="heading-using-terraform-functions-in-practical-aws-examples"><strong>Using Terraform Functions in Practical AWS Examples</strong></h1>
<p>Let’s see how some of the Terraform functions we discussed can be <strong>applied in real configurations</strong>.</p>
<h2 id="heading-1-lowercase-and-replace-for-variable-formatting"><strong>1. Lowercase and Replace for Variable Formatting</strong></h2>
<p>Imagine we have a variable for our project name:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872379537/b9c4e3b5-7c02-4587-bcf5-9e20bb53ad31.png" alt class="image--center mx-auto" /></p>
<p>We can use the <code>lower()</code> function to convert it to lowercase:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872411469/655ebd28-6b0d-4a5f-929f-2c8120ff1183.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Output will be: <code>"mission is terraform"</code></li>
</ul>
<p>Now, if we want to <strong>replace spaces with hyphens</strong>, we can combine functions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872451563/b91e73ac-cf81-43c8-8d69-38fcd3d3196b.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Output: <code>"mission-is-terraform"</code></li>
</ul>
<p>This ensures names are <strong>clean, lowercase, and safe</strong> for resources like S3 buckets.</p>
<h2 id="heading-2-merge-function-for-tags"><strong>2. Merge Function for Tags</strong></h2>
<p>Terraform often uses <strong>maps</strong> for tags. We might have <strong>default tags</strong> and <strong>environment-specific tags</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872518742/efb4d3fb-f0f9-4ba2-bfc8-4a8b5293245a.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>merge()</code> combines the two maps into one, ensuring <strong>all tags are applied</strong> without duplication.</p>
</li>
<li><p>This is a practical way to <strong>manage resource metadata</strong> consistently.</p>
</li>
</ul>
<h2 id="heading-3-substring-lower-replace-for-bucket-names"><strong>3. Substring, Lower, Replace for Bucket Names</strong></h2>
<p>S3 bucket names have <strong>restrictions</strong>: no capitals, no spaces, max 64 characters.</p>
<p>We can enforce this using <strong>multiple functions together</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872579960/e72e1e55-74f6-4959-810f-4c737e4e4622.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>lower()</code> → converts to lowercase</p>
</li>
<li><p><code>substr()</code> → limits length to 64 characters</p>
</li>
<li><p><code>replace()</code> → replaces spaces with hyphens</p>
</li>
</ul>
<p>Even if someone provides a bucket name with capitals or spaces, Terraform automatically <strong>formats it safely</strong>, preventing errors during <code>terraform apply</code>.</p>
<h2 id="heading-4-split-and-for-expression-for-multiple-ports"><strong>4. Split and For Expression for Multiple Ports</strong></h2>
<p>Suppose we have a variable with <strong>comma-separated ports</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872635222/51be440c-6715-48bb-bee4-a44c7000ba0e.png" alt class="image--center mx-auto" /></p>
<ul>
<li>First, <strong>split the string into a list</strong>:</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872666843/68895afb-eaa5-46ef-8eb3-a776af2d538c.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Then, <strong>iterate over the list</strong> to create structured data:</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872694875/49b1012f-7412-4fc5-9c9a-eb8de5edd8b2.png" alt class="image--center mx-auto" /></p>
<p>This is a simple way to convert a <strong>string input into multiple security group rules</strong>, automating repetitive work.</p>
<h2 id="heading-5-lookup-function-for-environment-specific-values"><strong>5. Lookup Function for Environment-Specific Values</strong></h2>
<p>Sometimes, we want Terraform to <strong>pick a value automatically</strong> based on the environment we’re working in. For example, maybe we want <strong>smaller instances for development</strong> and <strong>larger instances for production</strong>.</p>
<p>Here’s a simple example:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764872945752/b3f2c3b9-ad88-4a9a-81d7-6bacd49b027c.png" alt class="image--center mx-auto" /></p>
<p>Let’s break this down:</p>
<ol>
<li><p><code>var.instance_sizes</code> → This is a map that tells Terraform which instance to use for each environment.</p>
<ul>
<li><p>dev → t2.micro</p>
</li>
<li><p>staging → t3.small</p>
</li>
<li><p>prod → t3.large</p>
</li>
</ul>
</li>
<li><p><code>var.environment</code> → This tells Terraform which environment we are currently working in. Here, the default is <code>"dev"</code>.</p>
</li>
<li><p><code>lookup()</code> → This function looks at the map, finds the <strong>key</strong> that matches the environment, and returns the corresponding value.</p>
<ul>
<li><p>First argument: the map of values (<code>var.instance_sizes</code>)</p>
</li>
<li><p>Second argument: the key to look up (<code>var.environment</code>)</p>
</li>
<li><p>Third argument: the default value to use if the key is missing (<code>"t2.micro"</code>)</p>
</li>
</ul>
</li>
</ol>
<p>So in this example:</p>
<ul>
<li><p>Terraform sees that the environment is <code>dev</code>.</p>
</li>
<li><p>It looks in the map and finds <code>dev = t2.micro</code>.</p>
</li>
<li><p>It sets <code>local.instance_size</code> to <code>"t2.micro"</code>.</p>
</li>
</ul>
<p>This approach makes your configuration <strong>flexible and safe</strong>, because you can switch environments without manually changing values everywhere.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And just like that, we’ve reached the <strong>end of Day 11</strong> of our 30 Days of AWS Terraform journey.</p>
<p>Today, we explored <strong>Terraform Functions: Part 1</strong>. We saw how these functions can make our Terraform configurations cleaner, smarter, and more dynamic.</p>
<p>I know, at first glance, some of these functions might seem a little tricky. We might even wonder, <em>“Do I really need all these extra steps?”</em> Believe me, I’ve felt the same way.</p>
<p>But here’s the thing: if these functions exist, it’s because they are <strong>designed to make our lives easier</strong>. Instead of rewriting the same logic over and over, they help us <strong>reuse, automate, and avoid mistakes</strong>, so we can spend our energy on what truly matters i.e. <strong>building great infrastructure</strong>.</p>
<p>So, with that thought in mind, I’ll leave you here for today. Take a little time to practice what we covered, play around with the examples, and let the concepts sink in.</p>
<p>If any doubts remain, here’s a <strong>video by Piyush Sachdeva</strong> that explains these concepts in another friendly way, which might help clear up any lingering questions.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/-dKsmU4Z1hM?si=Rbo3fFsmMKVfMXOn">https://youtu.be/-dKsmU4Z1hM?si=Rbo3fFsmMKVfMXOn</a></div>
<p> </p>
<p>Tomorrow, we’ll continue with <strong>Terraform Functions Part 2</strong>, diving into more functions and practical examples to make your configurations even more powerful.</p>
<p>Until then, take care, stay curious, and <strong>peace out!</strong></p>
]]></content:encoded></item><item><title><![CDATA[Day #35: [DAY-10] AWS Terraform Conditional Expressions, Splat Expression & Dynamic Block]]></title><description><![CDATA[Introduction
Hello and welcome back to our 30 Days of AWS Terraform journey. How are you doing today? I hope you’re well!
We’ve made it to Day 10!

Seriously, showing up, learning a little each day, is the part that builds real skill. So let’s take a...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-35-day-10-aws-terraform-conditional-expressions-splat-expression-and-dynamic-block</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-35-day-10-aws-terraform-conditional-expressions-splat-expression-and-dynamic-block</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[#Iac #terraform #devops #aws]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Wed, 03 Dec 2025 17:55:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764784157500/e184702b-a7fc-4bc4-8459-d4b35544f712.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back to our <strong>30 Days of AWS Terraform</strong> journey. How are you doing today? I hope you’re well!</p>
<p>We’ve made it to <strong>Day 10</strong>!</p>
<p><img src="https://media.tenor.com/QzMeE8azunsAAAAM/judges-10.gif" alt="Ten GIFs | Tenor" class="image--center mx-auto" /></p>
<p>Seriously, showing up, learning a little each day, is the part that builds real skill. So let’s take a moment to feel good about that.</p>
<p>Yesterday, in <strong>Day 9</strong>, we spent time with <em>Terraform lifecycle rules</em>. We learned how lifecycle settings let us protect, control, or shape how resources behave during changes. It reminded us that Terraform is not just about creating infrastructure, but it’s about guiding how things change over time.</p>
<p>Today we continue that same careful, thoughtful approach, but with a different kind of tool.</p>
<p>We’re stepping into the world of <strong>Terraform Expressions</strong>. Think of expressions as tiny helpers inside your configuration: small bits of logic that let Terraform make simple decisions, repeat useful blocks for us, and gather values without extra fuss. They’re not full functions (we’ll cover those later), but they make our files cleaner and our work gentler.</p>
<p>In this post we’ll explore three friendly expressions:</p>
<ul>
<li><p><strong>Conditional Expressions</strong>: letting Terraform pick one value or another based on a simple condition.</p>
</li>
<li><p><strong>Dynamic Blocks</strong>: generating repeating nested blocks (like many ingress rules) without copy-paste.</p>
</li>
<li><p><strong>Splat Expressions</strong>: grabbing a single attribute from many resources in one tidy line.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764782725495/d341e70a-93f6-4523-a690-fb7574a9e42e.png" alt class="image--center mx-auto" /></p>
<p>As always, we’ll walk through demos so these ideas stop being abstract and start feeling practical. If any phrase sounds odd at first, that’s okay, it’ll click when we see it in action.</p>
<p>Here’s a little fun fact before we dive in:</p>
<blockquote>
<p>Fact #10: My phone is filled with random screenshots of quotes I love, and I just <strong>can’t bring myself to delete any of them</strong>. Every single one feels important, even the silly ones..and <strong>btw, I’m out of space</strong></p>
</blockquote>
<p>Ready? Let’s begin with <strong>Conditional Expressions</strong>.</p>
<h1 id="heading-conditional-expressions"><strong>Conditional Expressions</strong></h1>
<p>Before we touch any code today, let’s take a moment to understand <em>why</em> we’re learning this.</p>
<p>When we build infrastructure, we often face tiny decisions:</p>
<ul>
<li><p>Should this environment use a small instance or a bigger one?</p>
</li>
<li><p>Should this bucket be public or private?</p>
</li>
<li><p>Should this feature be ON or OFF?</p>
</li>
</ul>
<p>Doing this manually every single time becomes tiring, and honestly… it invites mistakes.</p>
<p>Terraform gives us a small tool to handle these decisions automatically i.e. <strong>conditional expressions</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764783020939/34ab6f97-05de-4868-a086-3bcc25e8bfe1.png" alt class="image--center mx-auto" /></p>
<p>And the good news?<br />If you’ve ever said “If this happens, do that,” or worked with if-else conditions, then you already understand the idea.</p>
<h2 id="heading-why-do-we-even-need-conditional-expressions">Why Do We Even Need Conditional Expressions?</h2>
<p>Imagine this:</p>
<p>We’re working on three environments: <strong>dev</strong>, <strong>staging</strong>, and <strong>production</strong>.</p>
<p>Dev needs a tiny instance.<br />Staging needs something a bit stronger.<br />Production? Definitely more power.</p>
<p>If we hard-code the instance type each time, we’ll constantly have to remember:</p>
<blockquote>
<p>“Wait, which size do we use for which environment?”</p>
</blockquote>
<p>That’s stressful.<br />Terraform helps us remove the stress by letting it <em>choose on our behalf</em>.</p>
<p>A conditional expression simply tells Terraform:</p>
<blockquote>
<p>“If this condition is true, pick value A. Otherwise, pick value B.”</p>
</blockquote>
<h2 id="heading-lets-build-it-together">Let’s Build It Together</h2>
<p>Let’s start with a basic EC2 instance:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764779712303/2299507e-f015-44eb-b45f-026470148963.png" alt class="image--center mx-auto" /></p>
<p>Right now, the instance type is fixed. But that’s the old way i.e. <em>manual, repetitive, easy to forget</em>.</p>
<p>Let’s guide Terraform to choose the instance type on its own.</p>
<h2 id="heading-the-conditional-pattern">The Conditional Pattern</h2>
<p>Terraform uses a simple and readable structure:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764779757151/242470ed-add6-4f05-9f6a-0caeabacbdf6.png" alt class="image--center mx-auto" /></p>
<p>Think of it like reading a sentence:</p>
<ul>
<li><p>“If this is true → pick this value.”</p>
</li>
<li><p>“Else → pick that value.”</p>
</li>
</ul>
<p>So let’s apply that to our environment.</p>
<h2 id="heading-our-first-real-example">Our First Real Example</h2>
<p>If the environment is <strong>dev</strong>, we want a small instance:</p>
<ul>
<li><code>"t2.micro"</code></li>
</ul>
<p>If it’s anything else (staging, prod, etc.), we want something stronger:</p>
<ul>
<li><code>"t3.micro"</code></li>
</ul>
<p>Here’s how we express that:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764779805708/0a534453-4ae9-43c3-b03b-f66a66ee1a66.png" alt class="image--center mx-auto" /></p>
<p>Terraform reads this the same way we would:</p>
<blockquote>
<p>“If environment equals dev, use t2.micro.<br />Otherwise, use t3.micro.”</p>
</blockquote>
<p>Now that we’re comfortable with this, let’s step into something that looks bigger at first i.e. <strong>dynamic blocks</strong>.<br />I promise, with the right example, they’ll feel just as friendly.</p>
<h1 id="heading-dynamic-blocks"><strong>Dynamic Blocks</strong></h1>
<p>When we start writing Terraform, most of us follow the same path:<br />we create a resource, it works, and we feel good.<br />But as we add more features, we often notice something frustrating:</p>
<blockquote>
<p>“Why am I writing the <strong>same kind of block</strong> again and again?”</p>
</blockquote>
<p>We’re not doing anything wrong, this is how everyone starts.<br />But after a few rules, a thought naturally appears:</p>
<blockquote>
<p>“There has to be a cleaner way.”</p>
</blockquote>
<p>And that thought is exactly what leads us to <strong>dynamic blocks</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764783384742/1a81844a-a717-4a90-afa7-9e68287b48e1.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-real-life-problem">The Real-Life Problem</h2>
<p>Imagine we’re setting up a security group.<br />We need:</p>
<ul>
<li><p>One rule for HTTP</p>
</li>
<li><p>One rule for HTTPS</p>
</li>
<li><p>Maybe more later</p>
</li>
</ul>
<p>And each rule looks almost identical, only one or two values change.</p>
<p>So we end up writing something like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764780271258/91091e89-0623-44d7-aaa1-89e40dd2fae6.png" alt class="image--center mx-auto" /></p>
<p>And after a few blocks, our file starts looking like a scroll.</p>
<p>And the moment we add or remove a rule, we have to rewrite or reorganize everything.</p>
<p>This is where terraform helps, using <strong>dynamic blocks</strong>.</p>
<h2 id="heading-a-way-to-look-at-dynamic-blocks">A Way to Look at Dynamic Blocks</h2>
<p>Think of dynamic blocks like a <strong>reusable mould</strong>.</p>
<p>If we were making chocolates, we wouldn’t shape each piece with your hands.<br />We’d use one mould and pour different fillings into it.<br />The mould stays the same.<br />The number of chocolates changes.</p>
<p>Dynamic blocks are exactly that:</p>
<ul>
<li><p>The <strong>mould</strong> is the block structure</p>
</li>
<li><p>The <strong>fillings</strong> are the values in your list</p>
</li>
<li><p>The <strong>result</strong> is however many blocks Terraform needs</p>
</li>
</ul>
<p>This analogy helps many beginners understand that we don’t need to create blocks ourselves, terraform will do it for us!</p>
<h2 id="heading-setting-up-the-input">Setting Up the Input</h2>
<p>Let’s say we have a variable called <code>ingress_rules</code>.</p>
<p>This is simply our <strong>list of configurations which we need,</strong> the ports, protocols, and descriptions we want.</p>
<p>We don’t have to understand everything in this block right now.<br />Just see it as “a collection of rules.”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764780556625/30371120-d960-44c6-b7d3-eefe7724ab50.png" alt class="image--center mx-auto" /></p>
<p>Let’s not worry about the details yet.<br />The important part is:</p>
<p>We now have a <strong>list of rules</strong> that look similar.</p>
<h2 id="heading-the-dynamic-block">The Dynamic Block</h2>
<p>Here is Terraform’s “dynamic block” or “the mould” if we go by the above analogy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764780664021/ba2ab95d-b810-4040-acac-3f40c3f7bc42.png" alt class="image--center mx-auto" /></p>
<p>Let’s break it down, one tiny piece at a time.</p>
<h3 id="heading-1-dynamic-ingress">1️⃣ <code>dynamic "ingress"</code></h3>
<p>This is where we tell Terraform:</p>
<blockquote>
<p>“I want you to create ingress blocks for me,<br />based on the list I give you.”</p>
</blockquote>
<p>We’re not creating the blocks manually anymore. Terraform is.</p>
<h3 id="heading-2-foreach-varingressrules">2️⃣ <code>for_each = var.ingress_rules</code></h3>
<p>This is Terraform’s way of saying:</p>
<blockquote>
<p>“I’ll repeat this block once for each rule in the list.”</p>
</blockquote>
<p>If there are 2 rules → it creates 2 blocks<br />If there are 5 rules → it creates 5 blocks<br />If we add a new rule tomorrow → Terraform adds one more automatically</p>
<h3 id="heading-3-content">3️⃣ <code>content { ... }</code></h3>
<p>This is the <strong>mould</strong> we talked about.</p>
<p>We’re simply describing what <em>one</em> ingress rule looks like.</p>
<p>Terraform will take this “shape” and fill it with:</p>
<ul>
<li><p>the first rule’s values</p>
</li>
<li><p>then the second rule’s values</p>
</li>
<li><p>and so on</p>
</li>
</ul>
<p>We don’t write the block repeatedly, Terraform clones it for us.</p>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>Dynamic blocks are helpful because they let us:</p>
<ul>
<li><p>Avoid writing repetitive blocks</p>
</li>
<li><p>Make our file cleaner and shorter</p>
</li>
<li><p>Add/remove rules by just editing the list</p>
</li>
<li><p>Trust Terraform to do the repetitive work</p>
</li>
<li><p>Reduce mistakes from copy-pasting</p>
</li>
</ul>
<p>And that’s the whole spirit behind IaC, write clean, reusable, predictable code.</p>
<h1 id="heading-splat-expressions"><strong>Splat Expressions</strong></h1>
<p>By the time we start working with multiple resources in Terraform, something interesting begins to happen:<br />we suddenly find yourself surrounded by <strong>lists</strong>.</p>
<p>Maybe we created 3 S3 buckets.<br />Or a few subnets.<br />Or several EC2 instances.</p>
<p>And at some point, a very simple need comes up:</p>
<ul>
<li><p>“Can I get all of their names?”</p>
</li>
<li><p>“Can I collect all their IDs?”</p>
</li>
<li><p>“Is there an easy way to gather all the ARNs?”</p>
</li>
</ul>
<p>If you’ve ever asked yourself these questions, you’re not alone.<br />Everyone who uses Terraform eventually reaches this moment i.e. a moment where getting <strong>one</strong> value is easy, but getting <strong>all</strong> of them feels like extra work.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764783940504/8b13d3fd-ab9a-4b82-89a2-e280c93935f4.png" alt class="image--center mx-auto" /></p>
<p>Terraform understands this pain, and it gives us a helper that makes things surprisingly easy:</p>
<h1 id="heading-the-splat-expression"><strong>The Splat Expression (</strong><code>*</code>)</h1>
<p>The splat expression is Terraform’s way of saying:</p>
<blockquote>
<p>“Tell me the attribute you want…<br />and I’ll collect it from <em>every</em> resource for you.”</p>
</blockquote>
<p>No loops.<br />No complex logic.<br />No extra locals.<br />Just a simple star.</p>
<h2 id="heading-a-simple-example"><strong>A Simple Example</strong></h2>
<p>Imagine we created <strong>three S3 buckets</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781156420/b6040282-701c-4a91-9325-c849354c4119.png" alt class="image--center mx-auto" /></p>
<p>Terraform now sees them like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781193355/c3186776-4eed-401d-91a1-e94ca1e1ba49.png" alt class="image--center mx-auto" /></p>
<p>Now imagine we want <em>all</em> their bucket names.</p>
<p>Without any shortcut, we’d probably think:</p>
<p>“Hmmm… do I need a loop?<br />Or a local variable?<br />Or do I write something long?”</p>
<p>Terraform’s answer is: “No… just use the star *”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781248083/50523f53-5828-4534-854e-022f40d7ad10.png" alt class="image--center mx-auto" /></p>
<p>And instantly, we get:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781281190/bf5efefd-6024-466b-bad5-b42b089c3185.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-using-splat-expressions-with-foreach"><strong>Using Splat Expressions With for_each</strong></h2>
<p>Now, suppose we created subnets using <code>for_each</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781323154/2a785dfc-bdb7-4a4b-8ee7-d25318b32898.png" alt class="image--center mx-auto" /></p>
<p>If we now want all subnet IDs, we don’t have to write anything fancy.</p>
<p>Just use the splat:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781361006/a7d30f73-e1ed-472d-906a-730fd7fecc4f.png" alt class="image--center mx-auto" /></p>
<p>Terraform gathers them and returns:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781400477/987e01a3-9f5d-4297-ad5a-debb609fd24a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-when-should-you-use-splat-expressions"><strong>When Should You Use Splat Expressions?</strong></h2>
<p>They shine the most when we:</p>
<ul>
<li><p>Create multiple resources</p>
</li>
<li><p>Want <strong>one specific attribute</strong> from all of them</p>
</li>
<li><p>Prefer cleaner, shorter code</p>
</li>
<li><p>Don’t want to create unnecessary loops or locals</p>
</li>
<li><p>Want our Terraform files to stay simple and readable</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Yaay! We’ve made it to the end of Day 10!</p>
<p>Today, we played with some of Terraform’s little helpers that make our life easier:</p>
<ul>
<li><p><strong>Conditional expressions</strong>: so Terraform can decide the right value for us.</p>
</li>
<li><p><strong>Dynamic blocks</strong>: so we don’t have to rewrite the same structure over and over.</p>
</li>
<li><p><strong>Splat expressions</strong>: so we can collect multiple values in one simple line.</p>
</li>
</ul>
<p>Each of these might seem small, but together they <strong>turn repetitive, boring code into something clean, readable, and intelligent</strong>. They’re the building blocks that help us think like Terraform, not just about Terraform.</p>
<p>I know sometimes it feels like there’s too much to remember, or we’re worried about making mistakes. That’s completely normal. Every learner feels that. The important thing is, we’re trying, experimenting, and actually building understanding step by step. That’s huge.</p>
<p>If you still have doubts or want a little visual walkthrough, here’s a <strong>video by Piyush Sachdeva</strong> that complements today’s blog beautifully and might make things click faster:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/R4ShnFDJwI8?si=qPjXt7A0KSIBhX8d">https://youtu.be/R4ShnFDJwI8?si=qPjXt7A0KSIBhX8d</a></div>
<p> </p>
<p>Look back at what we’ve done so far: 10 days of consistent learning!<br />And ahead? There’s so much more to explore, more Terraform, more ways to simplify our infrastructure.</p>
<p>Let’s keep going. We’ve got this.</p>
<p><img src="https://media.tenor.com/7GyHsInT8uoAAAAM/naruto.gif" alt="Dattebayo GIF - Dattebayo - Discover &amp; Share GIFs" class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Day #34: [DAY-09] AWS Terraform LifeCycle Rules]]></title><description><![CDATA[Introduction
Hello and welcome back!
We’re now on Day Nine of our 30 Days of AWS Terraform journey.

Over the past eight days, we’ve been building our understanding of Terraform step by step, from creating resources to seeing how Terraform keeps trac...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-34-day-09-aws-terraform-lifecycle-rules</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-34-day-09-aws-terraform-lifecycle-rules</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[#Iac #terraform #devops #aws]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Tue, 02 Dec 2025 18:14:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764698921714/68bf55f1-bb4e-42cb-8a5d-396f08ddd1a9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back!</p>
<p>We’re now on <strong>Day Nine</strong> of our <strong>30 Days of AWS Terraform</strong> journey.</p>
<p><img src="https://media3.giphy.com/media/v1.Y2lkPTZjMDliOTUybDQ3cnVqcms4bms5Z2J5d2V0NG1wbnRmN3Ewam83MTltbTBlb2F3eCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/4TaRqWJ4RbL41DaSmm/200.gif" alt="Nine GIFs - Find &amp; Share on GIPHY" class="image--center mx-auto" /></p>
<p>Over the past eight days, we’ve been building our understanding of Terraform <strong>step by step</strong>, from creating resources to seeing how Terraform keeps track of everything behind the scenes. Each day has added a new layer, and if you’ve been following along, you’ve already built a solid foundation.</p>
<p>Today, we’re taking a slightly deeper step. Our focus is on <strong>Terraform Lifecycle Rules</strong>.</p>
<p>Now, lifecycle rules may sound a little technical at first, but at their heart, they simply help Terraform decide <strong>how a resource should behave</strong> when it’s created, updated, or destroyed. Think of them as instructions we give Terraform so that our resources are handled safely, predictably, and without us turning into Joey!</p>
<p><img src="https://documentationwizard.com/wp-content/uploads/2022/01/surprise-gif-joey.gif" alt="No Surprise Act May Be a Surprise - Documentation Wizard, LLC" class="image--center mx-auto" /></p>
<p>In previous sessions, we’ve learned how Terraform provisions resources and manages their state. Today, we trace that thread forward:<br />How do we protect those resources? How do we avoid accidental deletions or unexpected downtime? How do we ensure updates happen the way we want?</p>
<p>That’s where lifecycle rules come in.</p>
<p>We’ll start with the basics, understand why they matter, and then explore each rule one by one, gently and clearly, just like we’ve done in earlier blogs.</p>
<p>Here’s a fact about me before we dive in:</p>
<blockquote>
<p>Fact #9: I’m a big fan of horror and thriller TV shows, but somehow <em>“The Office”</em> landed in my top three TV shows!</p>
</blockquote>
<h1 id="heading-what-are-terraform-lifecycle-rules">What Are Terraform Lifecycle Rules?</h1>
<p>Before we start looking at the lifecycle rules, let’s take a moment to understand what they really do.</p>
<p>Imagine we are using <strong>Terraform to manage AWS</strong>. We have an EC2 instance running our application, or an S3 bucket storing important files. Normally, Terraform knows what to do when we change something i.e. it will create, update, or delete resources. But sometimes, we need to tell it <strong>exactly how we want it to behave</strong>.</p>
<p>For example:</p>
<ul>
<li><p>Suppose we want to <strong>update the AMI</strong> of our EC2 instance. Terraform will need to create a new instance and remove the old one. But if it deletes the old one first, our app will go down.</p>
</li>
<li><p>Or imagine we have an <strong>S3 bucket</strong> storing backups. We don’t want it to be deleted by accident.</p>
</li>
<li><p>Maybe someone changed the <strong>desired capacity of our Auto Scaling Group</strong> outside of Terraform, and we don’t want Terraform to overwrite that change.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764697896948/65037e88-3f62-45a4-a1f2-9c7b1bd3fd83.png" alt class="image--center mx-auto" /></p>
<p>These are situations where <strong>lifecycle rules help us guide Terraform</strong>.</p>
<p>By using lifecycle rules, we can:</p>
<ul>
<li><p>Make updates safely with minimal downtime.</p>
</li>
<li><p>Protect important AWS resources from accidental deletion.</p>
</li>
<li><p>Respect changes made outside of Terraform when appropriate.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764698070133/00407880-ac33-4831-bbeb-f8ea14567aaf.png" alt class="image--center mx-auto" /></p>
<p>We’ll explore these rules <strong>one at a time</strong>, using <strong>real AWS examples</strong>, so we can see exactly how they work.</p>
<p>Let’s start slowly with the first rule: <code>create_before_destroy</code>, which helps ensure that updating an EC2 instance happens safely, without downtime.</p>
<h1 id="heading-lifecycle-rule-1-createbeforedestroy">Lifecycle Rule 1: create_before_destroy</h1>
<p>Let’s begin with one of the most practical and meaningful lifecycle rules:<br /><code>create_before_destroy</code></p>
<p>Now, at first glance, the name may look a bit long, but its purpose is beautifully straightforward:</p>
<blockquote>
<p><strong>Terraform should create the new resource <em>before</em> destroying the old one.</strong></p>
</blockquote>
<p>Why does this matter?</p>
<p>Imagine we have an EC2 instance running our application. One day, we decide to update it, maybe we want to use a new AMI. Terraform looks at this change and realizes it must <em>recreate</em> the resource.<br />But how should it do that?</p>
<ul>
<li><p>Should it destroy the old instance first and then create the new one?</p>
</li>
<li><p>Or should it create the new instance first and only then remove the old one?</p>
</li>
</ul>
<p>Well, if Terraform destroys the existing instance first, our application will go down until the new instance comes up. That’s downtime and we want to avoid that as much as we can.</p>
<p>And that’s exactly where <code>create_before_destroy</code> helps.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764698248362/1cca6379-54ea-46e0-90c7-60c4e26221d1.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-adding-it-to-our-resource">Adding It to Our Resource</h2>
<p>Here’s the EC2 resource we’re working with:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764694068010/6e9446a7-8a3b-4e31-a1be-4f49531e6629.png" alt class="image--center mx-auto" /></p>
<p>Now, let’s add the lifecycle rule:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764694111898/0aa5bc7d-3575-4dae-b440-1fcf6b40c2a1.png" alt class="image--center mx-auto" /></p>
<p>With this set to <strong>true</strong>, Terraform makes sure:</p>
<blockquote>
<p>It’ll prepare our new instance first.<br />Only after it’s safely created, I’tll remove the old one.”</p>
</blockquote>
<p>This reduces downtime and keeps things smooth.</p>
<h2 id="heading-what-happens-if-we-set-it-to-false">What Happens If We Set it to False?</h2>
<p>If we change it to:</p>
<pre><code class="lang-bash">create_before_destroy = <span class="hljs-literal">false</span>
</code></pre>
<p>Then Terraform behaves differently:</p>
<ul>
<li><p>It <strong>destroys</strong> the current resource immediately.</p>
</li>
<li><p>Only then does it try to create the new one.</p>
</li>
</ul>
<p>And here’s where things can go wrong.</p>
<p>Let’s say our new AMI isn’t authorized or there’s a misconfiguration. Terraform destroys the existing instance first, tries to create the new one… and fails.<br />Now our application is down, and nothing is running, all because the order got reversed.</p>
<p>This is why, in most real-world situations, <code>create_before_destroy</code> is kept <strong>true</strong>. It’s safer, more predictable, and much more friendly to applications that need high availability.<strong>ecycle Rule —</strong> <code>prevent_destroy</code>, written in the same warm, slow-paced, beginner-friendly style.</p>
<h1 id="heading-lifecycle-rule-2-preventdestroy">Lifecycle Rule 2: prevent_destroy</h1>
<p>Now that we’ve seen how <code>create_before_destroy</code> can protect our resources from downtime, let’s move on to another very important lifecycle rule: <code>prevent_destroy</code>.</p>
<p>At its heart, <code>prevent_destroy</code> is about <strong>safeguarding resources from accidental deletion</strong>.</p>
<p>Imagine we have an S3 bucket that stores critical data. What if someone accidentally runs a <code>terraform destroy</code> or makes a change that would remove the bucket? That could be disastrous.</p>
<p>By setting <code>prevent_destroy = true</code>, we’re telling Terraform:</p>
<blockquote>
<p>“No matter what, don’t destroy this resource unless I explicitly allow it.”</p>
</blockquote>
<p>It’s like putting a safety lock on your resource. Even if a plan includes a destructive change, Terraform will block it and warn us first.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764698462061/77af6300-825d-4649-b440-149a5bb8da0f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-how-to-use-preventdestroy">How to Use prevent_destroy</h2>
<p>Here’s how it looks in your EC2 example:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764694284633/ab6cea1e-1f40-4f2e-8b83-07b2503b85da.png" alt class="image--center mx-auto" /></p>
<p>Now, if someone tries to destroy the instance:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764694333725/050b1ab6-8567-4c0b-843c-fb3cd3fee465.png" alt class="image--center mx-auto" /></p>
<p>Terraform will first refresh the state and then <strong>throw an error</strong>, saying the resource cannot be destroyed.</p>
<p>Even changes that normally force a recreation (like changing the AMI) are blocked if <code>prevent_destroy</code> is enabled. To allow destruction, you would need to either <strong>set it to false</strong> or <strong>narrow the scope</strong> of the rule.</p>
<h2 id="heading-why-it-matters">Why It Matters</h2>
<p>Using <code>prevent_destroy</code> is a step that can save us from big mistakes. It ensures that important resources like databases, S3 buckets, or production EC2 instances are protected from accidental removal.</p>
<p>It’s a reminder that while Terraform gives us power to manage infrastructure, we also need safe guards to protect what matters most.dly style.</p>
<h1 id="heading-lifecycle-rule-3-ignorechanges">Lifecycle Rule 3: ignore_changes</h1>
<p>We’ve seen how <code>create_before_destroy</code> helps us avoid downtime, and <code>prevent_destroy</code> keeps our important resources safe from accidental deletions.</p>
<p>Now, let’s take a step forward to <code>ignore_changes</code>, a rule that helps Terraform <strong>coexist peacefully</strong> with changes made outside of it.</p>
<p>At its heart, <code>ignore_changes</code> says:</p>
<blockquote>
<p>“If certain properties of this resource change outside of Terraform, that’s okay. I won’t try to overwrite them.”</p>
</blockquote>
<p>This is particularly useful in real-world environments, where resources aren’t always managed only by Terraform. Sometimes, changes happen manually, through scripts, or by other tools. Without <code>ignore_changes</code>, Terraform would notice the difference and try to “correct” it, even if the change was intentional.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764698675047/3c8dfc9a-bea8-4dca-9462-06a7686cd781.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-an-example">An Example</h2>
<p>Let’s imagine a small scenario:</p>
<p>We have an <strong>Auto Scaling Group (ASG)</strong> in AWS. Terraform is managing it, and the desired number of instances is set to <strong>2</strong> in our configuration:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764695100976/38ae843e-306c-4867-aa2d-3f9f64619ae8.png" alt class="image--center mx-auto" /></p>
<p>One day, someone manually changes the desired capacity to <strong>3</strong> in the AWS console, maybe to handle extra load temporarily.</p>
<p>Without <code>ignore_changes</code>, if we run <code>terraform apply</code>, Terraform would notice that the desired capacity is <strong>3</strong> in the console, but <strong>2</strong> in your code, and would immediately try to change it back to <strong>2</strong>.</p>
<p>This could be confusing, especially if manual scaling was needed for a short time. It might also cause unnecessary instance creation or termination, disrupting your application.</p>
<h3 id="heading-how-ignorechanges-helps">How <code>ignore_changes</code> Helps</h3>
<p>By adding <code>ignore_changes</code>, you tell Terraform:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764695186903/40ea05f1-2813-4d99-bf09-d728575db95b.png" alt class="image--center mx-auto" /></p>
<p>Now:</p>
<ul>
<li><p>Terraform <strong>will no longer touch</strong> the desired capacity if it changes externally.</p>
</li>
<li><p>Other properties of the ASG are still fully managed by Terraform.</p>
</li>
<li><p>Manual or external adjustments are respected, avoiding conflicts and unnecessary resource churn.</p>
</li>
</ul>
<p>In other words, Terraform now <strong>trusts your intentional manual changes</strong>, while still keeping control of everything else.</p>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>This small rule keeps harmony between:</p>
<ul>
<li><p><strong>Terraform-managed configurations</strong></p>
</li>
<li><p><strong>Real-world operational adjustments</strong></p>
</li>
</ul>
<p>Without it, Terraform might constantly fight against legitimate external changes, causing confusion, repeated updates, or even disruptions in your infrastructure.</p>
<p>With <code>ignore_changes</code>, our resources remain flexible, predictable, and safer to manage, while still respecting Terraform’s authority where it matters most.</p>
<h1 id="heading-lifecycle-rule-4-replacetriggeredby">Lifecycle Rule 4: replace_triggered_by</h1>
<p>So far, we’ve seen how lifecycle rules can protect resources from downtime, accidental deletion, or external changes. Now, let’s explore a rule that helps <strong>resources stay in sync with related resources</strong>: <code>replace_triggered_by</code>.</p>
<p>Think of it like this:</p>
<p>Imagine we have an <strong>S3 bucket</strong> that stores logs, and we also have a <strong>logging configuration resource</strong> that sends logs to this bucket.<br />If the bucket changes, maybe we rename it or update its properties, the logging configuration might also need to be updated or recreated to keep sending logs correctly.</p>
<p>In Terraform, <code>replace_triggered_by</code> ensures that when one resource changes, <strong>dependent resources are automatically replaced</strong>, so everything continues to work smoothly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764698901459/5a4ceabe-3ceb-4659-8937-96c06956ac2e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-aws-example-s3-bucket-and-logging">AWS Example: S3 Bucket and Logging</h2>
<p>Let’s say we have:</p>
<ul>
<li><p>An <strong>S3 bucket</strong> called <code>log_bucket</code></p>
</li>
<li><p>A <strong>bucket logging configuration</strong> called <code>log_config</code> that sends logs to <code>log_bucket</code></p>
</li>
</ul>
<p>If the S3 bucket changes (for example, the name or key policy is updated), you want the logging configuration to be <strong>recreated automatically</strong> so it still points to the correct bucket.</p>
<p>Here’s how we define it in Terraform:</p>
<h3 id="heading-the-s3-bucket">The S3 bucket:</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764696057179/94ff6b3b-700d-40c0-843e-902a333c5213.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>resource "aws_s3_bucket" "log_bucket"</code>: This defines a new S3 bucket resource in Terraform named <code>log_bucket</code>.</p>
</li>
<li><p><code>bucket = "my-app-logs"</code>: This is the actual name of the bucket in AWS.</p>
</li>
<li><p><code>acl = "private"</code>: This sets the bucket’s access control to private, meaning only authorized users can access it.</p>
</li>
</ul>
<p><strong>In simple terms:</strong> This creates a bucket to store your application logs safely.</p>
<h3 id="heading-the-s3-bucket-logging-configuration">The S3 Bucket Logging Configuration</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764696154653/17071cad-0b43-4bc6-aa4b-9d53d362e2f8.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>resource "aws_s3_bucket_logging" "log_config"</code>: This defines a logging configuration resource. It tells AWS <strong>where logs should go</strong>.</p>
</li>
<li><p><code>bucket = aws_s3_bucket.log_</code><a target="_blank" href="http://bucket.id"><code>bucket.id</code></a>: This is the bucket that will <strong>receive the logs</strong> (our <code>log_bucket</code>).</p>
</li>
<li><p><code>target_bucket = aws_s3_bucket.log_</code><a target="_blank" href="http://bucket.id"><code>bucket.id</code></a>: This is the bucket where logs are stored — in this case, the same bucket.</p>
</li>
<li><p><code>target_prefix = "logs/"</code>: All logs will be stored under the <code>logs/</code> folder in the bucket.</p>
</li>
</ul>
<p><strong>In simple terms:</strong> This says, “Take the logs and put them into <code>my-app-logs/logs/</code>.”</p>
<h3 id="heading-the-lifecycle-rule-replacetriggeredby">The Lifecycle Rule: <code>replace_triggered_by</code></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764696219682/f75730db-1351-4dfd-8ac9-7082d7213289.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>lifecycle { ... }</code>: This block customizes how Terraform handles changes to the resource.</p>
</li>
<li><p><code>replace_triggered_by = [aws_s3_bucket.log_bucket.id]</code>: This tells Terraform:</p>
</li>
</ul>
<blockquote>
<p>“If the <code>log_bucket</code> changes in any way (like its name, ACL, or other important properties), automatically <strong>replace the logging configuration</strong> to keep it working properly.”</p>
</blockquote>
<p><strong>In simple terms:</strong> If the bucket changes, Terraform will recreate the logging setup automatically so logs keep flowing correctly. You don’t have to manually fix it.</p>
<h2 id="heading-why-it-matters-1">Why It Matters</h2>
<p>This lifecycle rule is helpful when resources are <strong>dependent on each other</strong>. It ensures:</p>
<ul>
<li><p>Dependent resources are updated automatically</p>
</li>
<li><p>No broken connections between resources</p>
</li>
<li><p>Your infrastructure remains consistent and predictable</p>
</li>
</ul>
<h1 id="heading-lifecycle-rules-5-amp-6-preconditions-and-postconditions">Lifecycle Rules 5 &amp; 6: Preconditions and Postconditions</h1>
<p>We’ve already seen how lifecycle rules can protect resources from downtime, prevent accidental deletions, and handle external changes.</p>
<p>Now, let’s take a step into something a little different i.e. <strong>preconditions and postconditions</strong>.</p>
<p>Think of them as <strong>guides</strong> for our infrastructure. They don’t change how resources are created, updated, or destroyed, but they <strong>make sure things happen the way they should</strong>.</p>
<ul>
<li><p><strong>Preconditions</strong> check before creating a resource: “Is everything okay to proceed?”</p>
</li>
<li><p><strong>Postconditions</strong> check after a resource is created: “Did everything turn out as expected?”</p>
</li>
</ul>
<h2 id="heading-preconditions">Preconditions</h2>
<p>A <strong>precondition</strong> is like a pause before Terraform starts building a resource. It asks:</p>
<blockquote>
<p>“Does this meet the rules we expect? If not, stop.”</p>
</blockquote>
<p>Imagine this scenario: we are creating an EC2 instance, but our company only allows resources in the <strong>US East (N. Virginia)</strong> region.</p>
<p>Without a precondition, Terraform will happily create the instance anywhere, and we might break organizational rules.</p>
<p>With a precondition, we can tell Terraform to <strong>pause and warn us</strong> if the region is not allowed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764696833148/945620ec-7230-464c-be69-557a06dbe5b4.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-how-it-works">How It Works</h3>
<ul>
<li><p>The <strong>condition</strong> checks if the EC2 instance’s <code>availability_zone</code> is in the list <code>["us-east-1", "us-west-2"]</code>.</p>
</li>
<li><p>If the condition is <strong>true</strong>, Terraform continues and creates the resource.</p>
</li>
<li><p>If the condition is <strong>false</strong>, Terraform stops and shows the <strong>error message</strong>.</p>
</li>
</ul>
<h2 id="heading-postconditions">Postconditions</h2>
<p>A <strong>postcondition</strong> works after Terraform has created a resource. It’s like a <strong>double-check</strong>:</p>
<blockquote>
<p>“Did it create the resource correctly? Are all the rules followed?”</p>
</blockquote>
<p>For example, imagine we’re creating an S3 bucket for storing important logs, and your organization requires every bucket to have a <strong>‘compliance’ tag</strong>.</p>
<p>We can add a postcondition to make sure this tag exists:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764696931708/27453b1f-b251-4288-9bc9-1baab256dcd4.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>If the bucket has the tag, all is good.</p>
</li>
<li><p>If the tag is missing, Terraform stops and alerts you, giving you a chance to fix it.</p>
</li>
</ul>
<h2 id="heading-why-they-matter">Why They Matter</h2>
<p>Preconditions and postconditions add <strong>peace of mind</strong>.</p>
<ul>
<li><p><strong>Preconditions</strong> prevent mistakes before resources are created.</p>
</li>
<li><p><strong>Postconditions</strong> verify compliance and correctness after creation.</p>
</li>
</ul>
<p>Together, they help you <strong>catch errors early</strong>, enforce rules automatically, and make Terraform safer and more predictable, all without adding stress or complexity.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p><strong>Add with that</strong>, we’ve explored <strong>Terraform lifecycle rules</strong> together today!</p>
<p>From basics to understanding lifecycle rules that make our infrastructure safer and smarter, we’ve come a long way! And there’s still so much more to explore!</p>
<p>Each of these rules thatw e learned today might seem small on its own, but together, they give us <strong>control, confidence, and safety</strong> in managing your infrastructure.</p>
<p>I know learning all of this can feel overwhelming at times, and that’s perfectly okay.</p>
<p>If you still have doubts or want to see these lifecycle rules in action, here’s a <strong>helpful video by Piyush Sachdeva</strong> that walks through them clearly:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/60tOSwpvldY?si=7odLJ4heXpgVLgR6">https://youtu.be/60tOSwpvldY?si=7odLJ4heXpgVLgR6</a></div>
<p> </p>
<p>Keep going, stay curious, and I’ll see you tomorrow for the next exciting step in our Terraform journey.</p>
]]></content:encoded></item><item><title><![CDATA[Day #33: [DAY-08]: AWS Terraform Meta Arguments]]></title><description><![CDATA[Introduction
Hello and welcome back! I hope you’re doing well today.
Can you believe it’s already 1st December? Time is flying, and here we are at Day Eight of our 30 Days of AWS Terraform journey. I’m genuinely happy you’ve taken the time to join me...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-33-day-08-aws-terraform-meta-arguments</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-33-day-08-aws-terraform-meta-arguments</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[IaC (Infrastructure as Code)]]></category><category><![CDATA[#Iac #terraform #devops #aws]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Mon, 01 Dec 2025 17:16:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764606504119/f34b5bae-4524-4ff2-9b4b-e6fb55c6dcee.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction"><strong>Introduction</strong></h1>
<p>Hello and welcome back! I hope you’re doing well today.</p>
<p>Can you believe it’s already <strong>1st December</strong>? Time is flying, and here we are at <strong>Day Eight</strong> of our 30 Days of AWS Terraform journey. I’m genuinely happy you’ve taken the time to join me today.</p>
<p><img src="https://images3.memedroid.com/images/UPLOADED246/5a217243b753b.jpeg" alt="December 1st - Meme by perridotti :) Memedroid" class="image--center mx-auto" /></p>
<p>Over the past week, we’ve been building our Terraform foundation, step by step. I know it can feel a little tricky at times and that’s completely normal! What matters most is that we’re comfortable, following along, and gradually gaining confidence with each concept.</p>
<p>Yesterday, we explored <strong>Terraform type constraints</strong>. We saw how lists, sets, and maps behave differently, and why that matters when we’re defining resources. It might have seemed a bit technical, but it’s going to be very useful for today’s topic, because meta arguments often interact with these types in subtle ways.</p>
<p>Today, we’re diving into <strong>Meta Arguments</strong>.</p>
<p>We’ll focus on three meta arguments: <code>depends_on</code>, <code>count</code>, and <code>for_each</code>. Each one plays a unique role, and by the end of today, we’ll see how they all fit together to make our Terraform code more predictable and easier to work with.</p>
<p>And, just to keep things personal as I like to do in my blogs:</p>
<blockquote>
<p>Fact #8: I’m <strong>not really fond of sweets</strong>, chocolate and pastries rarely tempt me. Give me a <strong>spicy, flavourful dish any day</strong>, and I’m in my happy place.</p>
</blockquote>
<h1 id="heading-what-are-arguments-vs-meta-arguments">What are Arguments vs Meta Arguments</h1>
<p>Before we jump into the specific meta arguments, let’s take a moment to understand the difference between <strong>regular arguments</strong> and <strong>meta arguments</strong> in Terraform.</p>
<p><img src="https://media.tenor.com/43yVPNqkeWkAAAAM/argument-arguments.gif" alt="Argument GIFs | Tenor" class="image--center mx-auto" /></p>
<p>When we create a resource in Terraform, like an AWS S3 bucket we provide <strong>arguments</strong>. These arguments describe the resource’s properties, such as its name, tags, or region. Terraform’s documentation lists all these arguments, showing which are required, which are optional, and what type of values they expect.</p>
<p>But Terraform also gives us something a little extra: <strong>meta arguments</strong>. These aren’t defined by AWS or any other provider, they are built into Terraform itself. Think of them as <strong>small helpers that make our lives easier</strong>. They let us add logic directly into our Terraform configurations <strong>without needing to write extra scripts</strong>.</p>
<p>Meta arguments help guide Terraform on how to manage resources. For example, they can tell Terraform <strong>in what order to create resources</strong>, <strong>how many copies of a resource to make</strong>, or <strong>how to loop over a collection of items</strong>. They don’t change what the resource is, they just help Terraform handle it more efficiently and cleanly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764607110150/20741019-a31e-4023-acfc-9496e547413a.png" alt class="image--center mx-auto" /></p>
<p>In the sections ahead, we’ll focus on three of the most useful meta arguments:</p>
<ol>
<li><p><code>depends_on</code> – to manage <strong>resource dependencies</strong></p>
</li>
<li><p><code>count</code> – to create <strong>multiple instances of a resource</strong></p>
</li>
<li><p><code>for_each</code> – to iterate over <strong>lists, sets, or maps</strong></p>
</li>
</ol>
<h1 id="heading-understanding-dependson"><strong>Understanding</strong> <code>depends_on</code></h1>
<p>Before we jump into the code, let’s pause for a moment and understand <strong>why this meta argument even exists</strong>.</p>
<p>When we write a Terraform file with multiple resources i.e. maybe a VPC, a subnet, an EC2 instance, and a security group, Terraform doesn’t automatically know the order in which you want things to be created. If all our resources are in a single file, Terraform simply sees a list of resources. Unless there’s a direct relationship between them, it can’t guess the sequence correctly.</p>
<p>We might think Terraform reads files from top to bottom, but it <strong>doesn’t</strong>.</p>
<h4 id="heading-how-terraform-decides-the-order">How Terraform Decides the Order</h4>
<ul>
<li><p>If we have multiple files, Terraform loads them <strong>alphabetically</strong>.</p>
</li>
<li><p>If we have multiple resources in a single file, Terraform <strong>does not follow the order we wrote them in</strong>.</p>
</li>
</ul>
<p>Instead, Terraform tries to detect <strong>implicit dependencies</strong>, which happen automatically when one resource references another (like <code>aws_vpc.main.id</code>). But if it can’t detect a relationship, it will create resources in whatever order it finds most efficient.</p>
<p>But sometimes, we need to make sure something is created first. For example:</p>
<ul>
<li><p>We want a <strong>VPC</strong> ready before creating an EC2 instance.</p>
</li>
<li><p>We want a <strong>security group</strong> available before attaching it.</p>
</li>
<li><p>We want an <strong>S3 bucket</strong> ready before something else references it.</p>
</li>
</ul>
<p>Terraform won’t know this unless we <strong>tell it explicitly</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764607777178/c8c143ec-6834-4353-b8ff-1f36dc96a3fd.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-what-is-dependson">What is <code>depends_on</code>?</h2>
<p>The <code>depends_on</code> meta argument is Terraform’s way of letting you say:</p>
<blockquote>
<p>“Terraform, please create Resource A before Resource B.”</p>
</blockquote>
<p>This is called an <strong>explicit dependency</strong>.</p>
<ul>
<li><p><strong>Implicit dependency</strong> happens automatically when a resource references another.</p>
</li>
<li><p><strong>Explicit dependency</strong> is when we manually declare the relationship.</p>
</li>
</ul>
<p>Terraform will then <strong>wait for Resource A to be fully provisioned</strong> before creating Resource B. This gives you <strong>complete control over the sequence</strong>, avoiding unexpected errors.</p>
<h2 id="heading-example">Example</h2>
<p>Let’s take two S3 buckets as an example:</p>
<ul>
<li><p><code>aws_s3_bucket.day_07_bucket</code></p>
</li>
<li><p><code>aws_s3_bucket.day_08_bucket</code></p>
</li>
</ul>
<p>If we want the <strong>Day 08 bucket</strong> to be created <strong>only after</strong> the Day 07 bucket, we write:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764603922516/ca1fe0bf-403b-4a2f-ba3e-60c035170673.png" alt class="image--center mx-auto" /></p>
<p>This tells Terraform:</p>
<ol>
<li><p>Create <code>day_07_bucket</code> first.</p>
</li>
<li><p>Only after that, create anything that depends on it.</p>
</li>
</ol>
<p>Even if Terraform thinks it could create them in parallel, the <code>depends_on</code> argument ensures it waits.</p>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>Using <code>depends_on</code> gives you <strong>predictable behavior</strong>.</p>
<ul>
<li><p>No resources being created out of order.</p>
</li>
<li><p>No debugging issues because something wasn’t ready yet.</p>
</li>
</ul>
<p>It’s a <strong>simple meta argument</strong>, but it can save you a lot of headaches and bring a lot of clarity to your Terraform workflow.</p>
<h1 id="heading-understanding-count"><strong>Understanding</strong> <code>count</code></h1>
<p>Now that we’ve understood <code>depends_on</code>, let’s move on to one of the most commonly used meta arguments in Terraform: <code>count</code>.</p>
<h2 id="heading-why-do-we-need-count">Why do we need <code>count</code>?</h2>
<p>Imagine we’re creating an <strong>S3 bucket</strong>. We write our resource block, give it a bucket name, maybe some tags, and Terraform creates <strong>one bucket</strong> for us.</p>
<p>That’s easy enough. But what happens if we need <strong>multiple buckets</strong> with the same configuration?</p>
<p>Without <strong>count</strong>, we would have to <strong>copy and paste</strong> the resource block multiple times, changing the bucket name each time. Not only is that tedious, but it also becomes harder to maintain as our infrastructure grows.</p>
<p>This is where <strong>count</strong> comes in.</p>
<p>It lets you tell Terraform:</p>
<blockquote>
<p>“Please create this resource <strong>N number of times</strong>.”</p>
</blockquote>
<p>Just one clean resource block that repeats itself automatically based on the number we provide.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764608060837/b954a58e-7817-4164-807c-a1474221f8d1.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-creating-one-bucket">Creating One Bucket</h2>
<p>Before we dive into <strong>count</strong>, let’s start with a simple S3 bucket, something familiar:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764604150756/d09f725a-59b5-44f4-9888-ba630f95e4d0.png" alt class="image--center mx-auto" /></p>
<p>Here’s a quick recap:</p>
<ul>
<li><p><code>aws_s3_bucket</code> → resource type</p>
</li>
<li><p><code>"day_08_bucket"</code> → Terraform’s internal name</p>
</li>
<li><p><code>bucket</code> → the actual bucket name in AWS</p>
</li>
<li><p><code>tags</code> → values passed from a variable</p>
</li>
</ul>
<p>If we run <code>terraform init</code> and <code>terraform plan</code>, Terraform will show that it plans to create <strong>one bucket</strong>.</p>
<h2 id="heading-what-if-we-want-multiple-buckets">What if we want multiple buckets?</h2>
<p>Let’s say we have a <strong>list of bucket names</strong> and we want Terraform to create all of them using <strong>one resource block</strong>.</p>
<p>First, we define a variable to hold those names:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764604217383/fcfd6570-e51d-4105-a7d0-1af59147e28a.png" alt class="image--center mx-auto" /></p>
<p>Now we have a <strong>list of strings</strong>, each representing a bucket name.</p>
<h2 id="heading-using-count-to-create-multiple-buckets">Using <code>count</code> to create multiple buckets</h2>
<p>Here’s how we can tell Terraform to loop through the list:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764604255885/2ebc79b6-9443-4ea3-8da6-e71cbfa0df08.png" alt class="image--center mx-auto" /></p>
<p>Let’s take this <strong>slowly</strong>, step by step:</p>
<p>✔ <code>count = length(var.bucket_names)</code></p>
<ul>
<li>This tells Terraform:</li>
</ul>
<blockquote>
<p>“Create one bucket for <strong>each element</strong> in this list.”</p>
</blockquote>
<ul>
<li><p>If the list has <strong>3 names</strong>, Terraform creates <strong>3 buckets</strong>.</p>
</li>
<li><p>If the list grows to <strong>10 names</strong> tomorrow, Terraform creates <strong>10 buckets</strong> automatically, no changes needed in the resource block.</p>
</li>
</ul>
<p>✔ <code>bucket = var.bucket_names[count.index]</code></p>
<ul>
<li><p><code>count.index</code> starts at <code>0</code>, then moves to <code>1</code>, <code>2</code>, and so on…</p>
</li>
<li><p>Terraform picks bucket names <strong>one by one</strong>:</p>
<ul>
<li><p>index 0 → <code>"my-unique-bucket-9876"</code></p>
</li>
<li><p>index 1 → <code>"my-unique-bucket-3112"</code></p>
</li>
<li><p>index 2 → <code>"my-unique-bucket-43242"</code></p>
</li>
</ul>
</li>
</ul>
<p>This is how a <strong>single resource block</strong> becomes multiple buckets effortlessly.</p>
<h2 id="heading-when-count-works-best">When <code>count</code> works best</h2>
<p><strong>Count</strong> works beautifully with <strong>lists</strong>, because:</p>
<ul>
<li><p>Lists have a <strong>fixed order</strong></p>
</li>
<li><p>Each element has an <strong>index</strong> (0, 1, 2…)</p>
</li>
</ul>
<p>But here’s an important question:</p>
<blockquote>
<p>What if the variable is not a <strong>list</strong>?<br />What if it’s a <strong>set</strong>?</p>
</blockquote>
<p>Sets <strong>do not have indexes</strong>, and that’s when <code>count.index</code> <strong>cannot be used directly</strong>. We’ll see how to handle that next.</p>
<h1 id="heading-understanding-foreach">Understanding <code>for_each</code></h1>
<p>Now that we’ve explored <code>count</code>, let’s talk about <code>for_each</code>, which is a little different but very powerful. I know this one can feel tricky at first, so we’ll take it step by step.</p>
<p>We might remember that <code>count</code> works perfectly when we have a <strong>list</strong> of items and we want Terraform to create multiple copies of the same resource. But <code>count</code> has limitations:</p>
<ul>
<li><p>It works only with <strong>lists</strong>.</p>
</li>
<li><p>It doesn’t work with <strong>sets</strong> (because sets are unordered).</p>
</li>
<li><p>It doesn’t give us easy access to <strong>key-value pairs</strong>, which we’ll often need with <strong>maps</strong>.</p>
</li>
</ul>
<p>That’s where <code>for_each</code> comes in. Think of <code>for_each</code> as a helper that <strong>goes through each item individually</strong> and lets us reference that item when creating resources.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764608500991/20dac79b-e3da-4fea-981a-f7e5ff5e0481.png" alt class="image--center mx-auto" /></p>
<p>It works with <strong>lists, sets, and maps</strong>, giving us much more flexibility than <code>count</code>.</p>
<h2 id="heading-example-1-using-foreach-with-a-set-of-bucket-names">Example 1: Using <code>for_each</code> with a set of bucket names</h2>
<p>Let’s start with a simple <strong>set of bucket names</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764605458472/6117170b-5c12-4ae8-8e31-61e4704435d1.png" alt class="image--center mx-auto" /></p>
<p>Here’s what’s happening:</p>
<ol>
<li><p><code>for_each</code> loops through each element in the set. Think of it like taking each bucket name from the set, one at a time.</p>
</li>
<li><p><code>each.key</code> gives us the current element during the iteration. <strong>In sets and lists,</strong> <code>each.key</code> <strong>and</strong> <code>each.value</code> <strong>are the same, because there’s no separate key-value structure.</strong></p>
</li>
<li><p>Terraform will create <strong>one bucket for each element</strong> in the set, without us having to write multiple resource blocks.</p>
</li>
</ol>
<p>So, if our set has three names, Terraform will create three buckets, one for each name.</p>
<h2 id="heading-example-2-using-foreach-with-a-map">Example 2: Using <code>for_each</code> with a map</h2>
<p>Sets are simple i.e. they only have values. But sometimes, we want both a <strong>name and some extra information</strong>, like a description. That’s where <strong>maps</strong> come in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764605894129/854b132f-fc71-4408-9ec4-25052418064d.png" alt class="image--center mx-auto" /></p>
<p><strong>Step by step explanation:</strong></p>
<ol>
<li><p><code>for_each</code> loops over each key-value pair in the map.</p>
<ul>
<li><p>Key = bucket name (<code>dev-bucket</code> or <code>prod-bucket</code>)</p>
</li>
<li><p>Value = description (<code>Development environment bucket</code> or <code>Production environment bucket</code>)</p>
</li>
</ul>
</li>
<li><p><code>each.key</code> → gives the bucket name</p>
</li>
<li><p><code>each.value</code> → gives the description</p>
</li>
<li><p>Terraform creates <strong>one bucket for each key-value pair</strong> and assigns the description as a tag</p>
</li>
</ol>
<p>This is something <code>count</code> cannot do easily, because <code>count</code> can only iterate over lists and doesn’t give you keys and values.</p>
<h2 id="heading-why-foreach-is-useful">Why <code>for_each</code> is useful</h2>
<ul>
<li><p>It <strong>works with sets and maps</strong>, not just lists.</p>
</li>
<li><p>We can access <strong>keys and values separately</strong>, which makes it great for more complex configurations.</p>
</li>
<li><p>It allows us to <strong>avoid repeating resource blocks</strong> and everything happens automatically for each element.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Here we are, at the end of another day in our 30 Days of AWS Terraform journey.</p>
<p>Sometimes, when I reflect on my own learning, I remember feeling a bit overwhelmed when I first encountered meta arguments like <code>count</code> and <code>for_each</code>. At first, the ideas seemed abstract, and I wasn’t sure how they would fit into my real projects. But slowly, by making mistakes, and observing what happened, it all started to make sense.</p>
<p>That’s exactly why I take the pace slow in these blogs, so we can see how these concepts actually play out, without feeling rushed or pressured.</p>
<p>We’ve come a long way already, but there’s still a lot ahead. Each day builds on the previous ones, and the journey is cumulative. The concepts we’ve learned now will be the foundation for more advanced features, like lifecycle rules, conditional creation, and modules. We’re developing intuition for how Terraform thinks, and that intuition is what will make your infrastructure work reliably.</p>
<p>If any of this still feels a bit fuzzy, here’s a <strong>video by Piyush Sachdeva</strong> that might help clarify things further:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/XMMsnkovNX4?si=X6DqGfMBJa9qWviW">https://youtu.be/XMMsnkovNX4?si=X6DqGfMBJa9qWviW</a></div>
<p> </p>
<p>Let’s take a moment to appreciate our progress. We’ve tackled eight days of learning, experiments, and new ideas and that’s no small feat.</p>
<p>Let’s keep going, stay curious, and remember: this is a journey, and every step forward matters.</p>
<p>Happy learning, and I can’t wait to continue exploring the next chapters with you!</p>
<p><img src="https://media.tenor.com/V8hTOutsUAcAAAAM/deadpool-logan.gif" alt="Bye Bye GIFs | Tenor" class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[DAY #32: [DAY-07]: AWS Terraform Type Constraints]]></title><description><![CDATA[Introduction
Hello and welcome back to our little corner of the Terraform journey!
I’m truly happy to see you here on Day Seven of our 30 Days of AWS Terraform adventure.

Every time we take a step into Terraform, open a new concept, and learn someth...]]></description><link>https://learning-out-loud-my-devops-journey.hashnode.dev/day-32-day-07-aws-terraform-type-constraints</link><guid isPermaLink="true">https://learning-out-loud-my-devops-journey.hashnode.dev/day-32-day-07-aws-terraform-type-constraints</guid><category><![CDATA[Terraform]]></category><category><![CDATA[AWS]]></category><category><![CDATA[#30DaysOfAWSTerraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[#Iac #terraform #devops #aws]]></category><dc:creator><![CDATA[Arnab]]></dc:creator><pubDate>Sun, 30 Nov 2025 16:34:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764506179959/fd5ce8b7-db55-48ee-b1ae-52bf3572a5df.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Hello and welcome back to our little corner of the Terraform journey!</p>
<p>I’m truly happy to see you here on <strong>Day Seven</strong> of our 30 Days of AWS Terraform adventure.</p>
<p><img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTjzQ4jSIbFwOg--iePCA0KS1vjiiVaBqoFWw&amp;s" alt="terraform Memes &amp; GIFs - Imgflip" class="image--center mx-auto" /></p>
<p>Every time we take a step into Terraform, open a new concept, and learn something at our own pace, we’re quietly building something meaningful. So, let’s take a moment to appreciate ourself. We’ve made it this far, and that consistency matters more than we might realize.</p>
<p>Yesterday, we explored <strong>Terraform file structure</strong>, understanding how to organize our resources and variables so that everything feels neat, clear, and manageable. That was all about giving our project a proper home i.e. a foundation for what comes next.</p>
<p>And today… we’re going to build on that foundation and step into a topic that is both powerful and essential: <strong>Terraform type constraints</strong>.</p>
<p>Type constraints might sound technical at first, but think of them as a guide that tell Terraform, <em>“This variable should behave in this specific way.”</em> They help prevent mistakes, keep our configurations predictable, and make our code easier to understand, especially as projects grow larger.</p>
<p>And, as always, a little personal tidbit to keep our journey connected:</p>
<blockquote>
<p><strong>Fact #7:</strong> I was naturally introverted during school and college, but somehow my close friends never saw me that way. They always made me feel included, confident and curious :P</p>
</blockquote>
<p>Now let’s ease into it together. In the next section, we’ll explore what type constraints are, why they matter, and how they help Terraform understand the kind of values each variable can hold.</p>
<h1 id="heading-primitive-type-numbers">Primitive Type: Numbers</h1>
<p>Alright, let’s begin with something simple but very important: <strong>primitive types</strong>. These are the simplest kinds of variables, and they form the foundation for everything else we’ll build. There are three main primitive types: <strong>string</strong>, <strong>number</strong>, and <strong>boolean</strong>. Today, we’ll start with <strong>numbers</strong> first.</p>
<p><img src="https://media.tenor.com/bZEUn3ywcQQAAAAM/stormcastle-count-down.gif" alt="Old Time Movie Countdown GIFs | Tenor" class="image--center mx-auto" /></p>
<p>A number is exactly what it sounds like i.e. a numeric value like <code>10</code>, <code>0</code>, or <code>500</code>. It seems simple, but defining it correctly in Terraform is important, because it tells Terraform what kind of value to expect.</p>
<p>Let’s imagine a simple scenario: we’re creating virtual machines for a small project. Maybe we want to start with <strong>two servers</strong> for our application. Instead of writing that number directly inside our resource block, we can create a variable. This makes our code <strong>flexible</strong> and <strong>easy to adjust</strong> later.</p>
<p>Here’s how we could define the variable in <code>variables.tf</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764514658496/d374ce3e-458e-4489-b74a-5825100d0ce1.png" alt class="image--center mx-auto" /></p>
<p>Notice a few things:</p>
<ul>
<li><p><code>type = number</code> tells Terraform that this variable should always hold a numeric value.</p>
</li>
<li><p><code>default = 2</code> means if we don’t provide any other value, Terraform will create <strong>two servers</strong>.</p>
</li>
</ul>
<p>Now, in our <code>main.tf</code> file, we can use this variable instead of hardcoding the number:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764514708368/c4b978ea-2a0d-4cf1-8f17-ffbaae6bf0b0.png" alt class="image--center mx-auto" /></p>
<p>If tomorrow we decide your application needs <strong>five servers</strong> instead of two, we don’t touch the resource block, we just change the variable’s value. Terraform handles the rest.</p>
<p>Numbers are simple, yes but using them thoughtfully sets the stage for everything else in Terraform.</p>
<h1 id="heading-primitive-types-strings">Primitive Types: Strings</h1>
<p>Now that we’ve spent some time with numbers, let’s move to another very common primitive type in Terraform: <strong>strings</strong>.</p>
<p><img src="https://i.imgflip.com/7zdck1.jpg" alt="No more strings - Imgflip" class="image--center mx-auto" /></p>
<p>A <em>string</em> is simply text. Anything inside quotes i.e. <code>"hello"</code>, <code>"production"</code>, <code>"ap-south-1"</code> is considered a string. It’s one of the most frequently used types in Terraform because so much of our infrastructure depends on names, IDs, regions, and descriptions… all of which are text.</p>
<p>Let’s understand this with a simple example:</p>
<p>Imagine we’re setting up an environment for a small project, and we want to store the <strong>environment name</strong> somewhere. Maybe today it’s <code>"development"</code>, but later it might change to <code>"staging"</code> or <code>"production"</code>. Instead of hunting for this word everywhere in your files, we can define it once as a variable.</p>
<p>Here’s how we might define a string variable in your <code>variables.tf</code> file:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764514860477/53d162b5-696b-47d0-9a43-100555ccab97.png" alt class="image--center mx-auto" /></p>
<p>A few things to notice:</p>
<ul>
<li><p>The value is wrapped in quotes, because strings always represent text.</p>
</li>
<li><p>Terraform now knows this variable should <strong>always</strong> hold a text value.</p>
</li>
<li><p>If we don’t provide anything else, <code>"development"</code> will be used by default.</p>
</li>
</ul>
<p>Then we can use this variable in our <code>main.tf</code> to name or tag our resources:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764514925376/ff708d43-7d76-49ca-a5d7-651d2f098cb5.png" alt class="image--center mx-auto" /></p>
<p>Here’s what’s happening:</p>
<ul>
<li><p>The bucket name uses the string variable to stay consistent.</p>
</li>
<li><p>The tag also uses the same variable, which means when we change <code>"development"</code> to <code>"production"</code>, our bucket names and tags update automatically.</p>
</li>
<li><p>No more searching and replacing text across files and Terraform takes care of it.</p>
</li>
</ul>
<p>Strings may feel simple, but they quietly hold together so many parts of our configuration. Getting comfortable with them makes the rest of our Terraform journey feel much smoother.</p>
<p>Absolutely — let’s slow this down, soften the tone, and make it feel more welcoming for someone who might be completely new. I’ll also switch to a simpler, more relatable example so it doesn’t feel technical right from the start.</p>
<h1 id="heading-primitive-types-booleans">Primitive Types: Booleans</h1>
<p>Now that we’ve explored numbers and strings, let’s take a moment to talk about something even simpler: <strong>booleans</strong>.</p>
<p>Booleans are one of those things that look almost <em>too</em> small to matter i.e. they can only ever be <strong>true</strong> or <strong>false</strong>. That’s it. Just two possibilities.</p>
<p><img src="https://i.programmerhumor.io/2025/07/5b855f23c60248e0f15e90ccbc46f4f5301bb530b8945948795b21d81ce4b20d.jpeg" alt="string Memes · ProgrammerHumor.io" class="image--center mx-auto" /></p>
<p>And yet, these tiny values quietly shape so many of the decisions our Terraform configuration makes.</p>
<p>If strings are like words and numbers are like quantities, then booleans are like the “yes” and “no” signals that help Terraform understand what we want.</p>
<p>Think of a boolean like a simple switch.<br />It’s either <strong>on</strong> or <strong>off</strong>.</p>
<p>To make this even more relatable, imagine we're defining <strong>Should the EC2 instance have a public IP?</strong></p>
<p>Let’s define that in Terraform:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764515306597/09ca7ec5-993e-4f15-8d40-570ab493f1d3.png" alt class="image--center mx-auto" /></p>
<p>Here, we’re simply telling Terraform:</p>
<p>“By default, please don’t give this EC2 instance a public IP.”</p>
<p>Later, in our Terraform configuration, you might use that value like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764515368584/fb61df65-d95a-4320-a078-648398e40298.png" alt class="image--center mx-auto" /></p>
<p>Now his tiny boolean has a real impact.<br />If someone sets it to <strong>true</strong>, Terraform will happily give the instance a public IP.<br />If it stays <strong>false</strong>, the instance stays private and quiet in the VPC.</p>
<p>Booleans don’t look powerful at first glance. But they help us toggle features on and off, shape the behavior of our infrastructure, and make our Terraform code feel more flexible and human.</p>
<p>They may feel tiny and almost too simple, but they enable many everyday decisions in Terraform. As we move forward to complex types, we’ll notice how booleans work beautifully alongside numbers, strings, lists, and maps to help shape real-world infrastructure.</p>
<p>Of course — thank you for telling me.<br />Let’s slow it down, make it gentler, more welcoming, and use a <strong>completely different example</strong> so it feels fresh and easier for newcomers.</p>
<h1 id="heading-complex-types-list">Complex Types: List</h1>
<p>So far, we’ve talked about primitive types. i.e. numbers, strings, and booleans. These are simple because they hold just one value at a time. But as we start working with real-world Terraform configurations, we’ll notice that many things come in groups. And when we want to store <em>multiple</em> values together in a clean, predictable way, Terraform gives us something called <strong>complex types</strong>.</p>
<p>Let’s begin with one of the most familiar complex types: <strong>the list</strong>.</p>
<p><img src="https://i.pinimg.com/736x/d8/a6/ab/d8a6abd2bdfa27e730859c35c40bcd43.jpg" alt="To-Do List meme" class="image--center mx-auto" /></p>
<p>A list is exactly what it sounds like i.e. a small collection of items arranged in a specific order. Think of it like writing down your goals for the week. Maybe we write:</p>
<ul>
<li><p>exercise</p>
</li>
<li><p>learn Terraform</p>
</li>
<li><p>cook something new</p>
</li>
</ul>
<p>Each item is written one after another, and the order matters because it helps us keep track of what comes first and what follows.</p>
<p>Terraform treats lists the same way. All the items inside a list must be of the <strong>same type</strong> i.e. all strings, or all numbers, or all booleans.</p>
<p>To make this feel simple, let’s look at a gentle example.</p>
<p>Imagine we’re defining the names of different availability zones we want to use in AWS. Instead of typing each one separately throughout your code, we might gather them into one tidy list in <code>variables.tf</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764515686860/f5e13db9-a6eb-4168-964a-4e4fa7669ec6.png" alt class="image--center mx-auto" /></p>
<p>Notice how the values sit inside square brackets, that’s Terraform’s way of saying “these belong together as a list.”</p>
<p>Later, in our <code>main.tf</code>, we can pick items from this list by using their position:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764515739531/921b7725-b58b-4613-a3d4-ffd065308fce.png" alt class="image--center mx-auto" /></p>
<p>Here, <code>[0]</code> means “give me the first item,” because Terraform starts counting from zero.</p>
<p>Lists are especially helpful when we have values that naturally belong together and need to appear in order. Whether it’s zones, instance names, or allowed IPs, a list keeps everything neat and predictable.</p>
<p>We’ll look at <strong>sets</strong> next, they feel a bit like lists at first glance, but they bring their own small personality with them, especially around uniqueness.</p>
<h1 id="heading-complex-types-sets">Complex Types: Sets</h1>
<p>Let’s take our time with this one, because sets can feel a little unusual when we first meet them.</p>
<p>Now that we’ve spent some time with <strong>lists</strong>, let’s move to something that looks similar at first glance, but behaves a bit differently: <strong>sets</strong>.</p>
<p>A good way to think about sets is to imagine a small bowl on your table.</p>
<p><img src="https://i.makeagif.com/media/9-15-2015/2xgMqU.gif" alt="How to make oven baked marbles &quot;fried marbles&quot; - with yoyomax12 on Make a  GIF" class="image--center mx-auto" /></p>
<p>We can drop a few marbles into it, maybe a red one, a blue one, and a green one. But here’s the special rule:<br />no matter how many times we try, the bowl will <strong>never</strong> let you put two identical marbles inside.</p>
<p>So even if we accidentally attempt to drop in another red marble, the bowl quietly ignores it.</p>
<p>That’s exactly how sets work in Terraform.<br />They hold multiple values, just like lists, but they always make sure each value is <strong>unique</strong>.</p>
<p>Here’s a simple example.<br />Let’s say we’re storing the names of different environments our team uses, like “dev”, “stage”, and “prod”. You want to make sure someone doesn’t add “dev” twice by mistake. A set is perfect for that:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764516057010/0b50ec9a-ae62-407b-8a3d-8a6474145800.png" alt class="image--center mx-auto" /></p>
<p>Even if someone changes the variable later and repeats a value, like adding “dev” again, Terraform will clean it up. That’s one of the nice little protections sets give you.</p>
<p>But there’s something important to remember, and I want to say it slowly so it sticks:</p>
<p><strong>Sets don’t care about order.</strong><br />They don’t remember which value came first, second, or third.<br />Because of that…<br />we cannot use indexing like <code>var.environments[0]</code>.</p>
<p>If, for any reason, we truly need to grab a value using an index, we can convert the set into a list first:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764516165360/0ceb7be1-7806-4134-8311-61c6e15d9bca.png" alt class="image--center mx-auto" /></p>
<p>Now <code>locals.environment_list</code> behaves like a list, and indexing works as expected.</p>
<p>So, in short:</p>
<ul>
<li><p>Use a <strong>list</strong> when order matters or when duplicates are allowed.</p>
</li>
<li><p>Use a <strong>set</strong> when order doesn’t matter and we want to protect ourself from duplicates.</p>
</li>
</ul>
<p>Take your time with this concept. Sets might feel new, but once we start using them, we’ll appreciate how they keep things clean and consistent in our Terraform projects.</p>
<h1 id="heading-complex-types-maps">Complex Types: Maps</h1>
<p>Up until now, we’ve been working with things like lists and sets i.e. places where values are stored one after another. Maps are a little different, and honestly, a lot friendlier once we get used to them.</p>
<p>Instead of remembering <em>positions</em> like “first value, second value…”, maps let us store things as <strong>key–value pairs</strong>.</p>
<p><img src="https://www.meme-arsenal.com/memes/f7b23865bf4fbaa9e2a9086bd15d4a57.jpg" alt="Сomics meme: &quot;id, name key, value&quot; - Comics - Meme-arsenal.com" class="image--center mx-auto" /></p>
<p>If you’re completely new to Terraform or even programming in general, don’t worry, maps are actually one of the easiest ideas to understand once we see them in action.</p>
<p>Think of a map like a small notebook where we write things down in <strong>pairs</strong>. On the left side we write <em>what</em> the thing is, and on the right side we write its <em>value</em>. Something like:</p>
<ul>
<li><p>Favorite_color → Blue</p>
</li>
<li><p>Country → India</p>
</li>
<li><p>Editor → VS Code</p>
</li>
</ul>
<p>This “something = something else” style is what a map is all about. It’s just a clean way to store information where the <strong>name</strong> of the value matters.</p>
<p>Let’s look at a Terraform example:</p>
<p>Imagine we are creating an S3 bucket, and we want to add a few tags to describe it, things like what the bucket is for, who owns it, and whether it’s used for dev or prod.</p>
<p>Instead of creating three separate variables, we can store all of these as a <strong>map</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764516515307/7836bb85-ab9d-438b-9ce3-2a8de4de07ce.png" alt class="image--center mx-auto" /></p>
<p>Here’s what each part means:</p>
<ul>
<li><p><code>purpose</code> is the name of the tag, and <code>"store-logs"</code> is its value</p>
</li>
<li><p><code>owner</code> is the tag name, <code>"arnab"</code> is the value</p>
</li>
<li><p><code>env</code> tells us this bucket belongs to the dev environment</p>
</li>
</ul>
<p>Everything stays neat and grouped together.</p>
<p>Now, in our AWS S3 bucket resource, you can simply pass the entire map:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764516548180/28338333-03ba-4c32-89ea-0c4fdfd9eda5.png" alt class="image--center mx-auto" /></p>
<p>Just like that, all our tags are applied and no need to write them one by one.<br />It keeps your Terraform file cleaner, and it also makes updates easier. For example, if the environment changes from dev to prod, we only update the value in one place.</p>
<p>Maps shine when you have settings that naturally belong together.</p>
<h1 id="heading-complex-types-tupple">Complex Types: Tupple</h1>
<p>A tuple is simply a way of storing <strong>multiple values of different types</strong>, all together, in a specific order.</p>
<p>Maybe the easiest way to picture it is this:</p>
<p>Imagine we’re packing a small lunchbox for the day.<br />Inside it, we might have:</p>
<ul>
<li><p>a sandwich (text)</p>
</li>
<li><p>a bottle of water (number for quantity or size)</p>
</li>
<li><p>and a note someone wrote for you (text again)</p>
</li>
</ul>
<p>Each item is different.<br />But they all belong together, in a certain order.<br />If we change the order or swap an item for something unexpected, the lunchbox suddenly doesn’t make sense the same way.</p>
<p>That’s exactly how tuples behave.</p>
<p>Here’s a simple Terraform example using a tuple.<br />Let’s say we want to describe a person in a very minimal way: their age, their name, and whether they like coffee.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764516850161/5b46d540-48cb-4ee4-85a8-274bae85caf6.png" alt class="image--center mx-auto" /></p>
<p>Let’s pause here and break it down:</p>
<ul>
<li><p>The first item <strong>must</strong> be a number (the person’s age).</p>
</li>
<li><p>The second item <strong>must</strong> be a string (their name).</p>
</li>
<li><p>The third item <strong>must</strong> be a boolean (true or false about coffee).</p>
</li>
</ul>
<p>If we change the order, Terraform will get confused, just like if you put the note at the bottom of your lunchbox and the sandwich on top of it.<br />The order matters because <strong>each position has a meaning</strong>.</p>
<p>Later, if we wanted to use these values inside our <code>main.tf</code>, it might look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764516910423/2c3122b3-167d-411b-a906-d5ce29d895ef.png" alt class="image--center mx-auto" /></p>
<p>Now Terraform clearly knows:</p>
<ul>
<li><p><code>var.person_info[0]</code> → 28</p>
</li>
<li><p><code>var.person_info[1]</code> → "Arnab"</p>
</li>
<li><p><code>var.person_info[2]</code> → true</p>
</li>
</ul>
<p>Tuples shine in moments where we want to group different kinds of information together, not too many, just a handful and each item has a clear place and purpose.</p>
<h1 id="heading-complex-types-objects">Complex Types: Objects</h1>
<p>We’ve reached the final type in our Terraform variables journey, and honestly… this one deserves a little smile.</p>
<p>Objects are powerful, yes but they don’t always have to feel serious or technical.<br />To make this easier to understand, let’s step away from servers, regions, and monitoring for a moment.</p>
<p>Imagine we’re standing at our favourite café.</p>
<p><img src="https://i.pinimg.com/originals/b9/76/3d/b9763d398d581a8a2916ea94682c8c4c.gif" alt="Cafe Gif Aesthetic" class="image--center mx-auto" /></p>
<p>We want to order a coffee.<br />Now, a coffee isn’t just <em>one</em> thing, it has several parts:</p>
<ul>
<li><p>the <strong>type</strong> (latte, cappuccino, espresso)</p>
</li>
<li><p>the <strong>size</strong> (small, medium, large)</p>
</li>
<li><p>whether we want it <strong>hot or iced</strong></p>
</li>
</ul>
<p>All these parts are different types of information…but they belong together as <strong>one coffee order</strong>.</p>
<p>That’s exactly what an <strong>object</strong> is in Terraform.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764517419014/ebb0b901-fcd9-4bd3-97c8-6c2acb8a09b1.png" alt class="image--center mx-auto" /></p>
<p>Breaking it down slowly:</p>
<ul>
<li><p><code>project</code> → string (like the coffee type)</p>
</li>
<li><p><code>environment</code> → string (like the size: small/medium/large)</p>
</li>
<li><p><code>owner</code> → string (who the coffee is for)</p>
</li>
<li><p><code>backup</code> → boolean (like “iced or hot”)</p>
</li>
</ul>
<p>Terraform will now know <strong>exactly what to expect</strong> for each field.<br />If a value is the wrong type, Terraform will alert us, keeping our infrastructure safe and predictable.</p>
<p>We can now use this object to tag our EC2 instance in AWS:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764517536411/5dbb61d7-49e5-420d-8d5e-e633800a6f6d.png" alt class="image--center mx-auto" /></p>
<p>See how neat and organized that is? All the metadata is grouped together in <strong>one object</strong>, just like all parts of our coffee order are grouped in one cup.</p>
<p>This approach keeps your Terraform configuration <strong>clean, readable, and easy to maintain</strong>, while also helping you manage multiple values together in a single, logical package.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Wow… we’ve come a long way today!</p>
<p>I know this blog might feel a bit long, and that’s completely understandable as there were quite a few concepts to cover. Terraform type constraints are one of those ideas that take a little patience to fully grasp, but the effort is worth it.</p>
<p>Understanding type constraints is important because it gives <strong>structure and safety</strong> to our Terraform code. It helps prevent mistakes, keeps your configurations neat, and makes it easier to manage as your projects grow. The more comfortable we get with these types, the more confident we’ll feel building real-world infrastructure.</p>
<p>If something still feels a little unclear, here’s a helpful video by <strong>Piyush Sachdeva</strong> that explains these concepts in another way, which might help clear any remaining doubts.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/gu2oCJ9DQiQ?si=chWks88QLMkMrcl9">https://youtu.be/gu2oCJ9DQiQ?si=chWks88QLMkMrcl9</a></div>
<p> </p>
<p>Take a moment to appreciate how far we’ve come. Seven days in, and we’re already building real infrastructure in AWS with Terraform. There’s still a lot ahead, but each day, each example is helping us grow into a more confident and capable Terraform practitioner.</p>
<p>Let’s keep going! Tomorrow, we’ll continue our exploration, and I promise it will feel even more natural as we build on what we’ve learned so far.</p>
<p><img src="https://media3.giphy.com/media/v1.Y2lkPTZjMDliOTUycTRiZmJqc2M0a2YwMnNzZGtpdWl6cjZjYWN5MXRva3I1bWt2azdhYiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/TFTF3AVOUGfyQglA1l/giphy.gif" alt="Good Luck With The Girl GIFs - Find &amp; Share on GIPHY" class="image--center mx-auto" /></p>
]]></content:encoded></item></channel></rss>