<?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[Implementations and Cloud]]></title><description><![CDATA[Implementanding comes from the Spanglish I use quite often. This is where I share my experiences and things I think about from time to time. If you find somethi]]></description><link>https://blog.mariano.cloud</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 07:29:25 GMT</lastBuildDate><atom:link href="https://blog.mariano.cloud/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Wing it]]></title><description><![CDATA[A month ago, I got my DevOpsWeekly (I highly recommend it) newsletter Sunday email (issue #661). I went through it, reading some interesting stuff and I saw one particular link that caught my eye: a brief introduction to a new cloud language called W...]]></description><link>https://blog.mariano.cloud/wing-it</link><guid isPermaLink="true">https://blog.mariano.cloud/wing-it</guid><category><![CDATA[#IaC]]></category><category><![CDATA[infrastructure]]></category><category><![CDATA[Devops]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[winglang]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Tue, 26 Sep 2023 16:34:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695745528498/3a54fd38-cc12-4beb-b3af-035ba7d2f77d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A month ago, I got my <a target="_blank" href="https://www.devopsweekly.com/">DevOpsWeekly</a> (I highly recommend it) newsletter Sunday email (issue #661). I went through it, reading some interesting stuff and I saw one particular link that caught my eye: <a target="_blank" href="https://www.youtube.com/watch?v=wzqCXrsKWbo&amp;ab_channel=WingProgrammingLanguage">a brief introduction to a new cloud language called Winglang</a>.</p>
<p>The idea behind it is very simple: how can we combine both Infrastructure and application code in the same codebase? Enter <a target="_blank" href="https://www.winglang.io/">Winglang</a>'s <strong>preflight</strong> and <strong>inflight</strong> concepts.</p>
<blockquote>
<p>New language, new syntax, <a target="_blank" href="https://www.winglang.io/docs/language-reference">some familiar, some new</a></p>
</blockquote>
<h1 id="heading-preflight-vs-inflight">Preflight vs Inflight</h1>
<p><strong>Preflight</strong> is the Infrastructure side mode. This code runs just once at creation time and will not only generate all required (Infra) resources but also wire all up using pertinent configurations, linkages, permissions and environment setup.</p>
<p><strong>Inflight</strong> is the application code per se. This is the application software that will be hosted in the Infrastructure resources (preflight).</p>
<blockquote>
<p>More on these and more Winglang concepts, <a target="_blank" href="https://www.winglang.io/docs/category/core-concepts">here</a>.</p>
</blockquote>
<p><img src="https://media.giphy.com/media/cXblnKXr2BQOaYnTni/giphy.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-features">Features</h1>
<p>Some of the cool things it offers include (just a couple of them):</p>
<ul>
<li><p>IaC ready (compiled code).</p>
</li>
<li><p>Web-based console.</p>
</li>
<li><p>Define classes to modify inflight built-in cloud libraries.</p>
</li>
<li><p>JS/TS and CDK support to import external resources/methods.</p>
</li>
</ul>
<h2 id="heading-iac-ready">IaC ready</h2>
<p>Another cool thing Winglang does is to compile your code into cloud-ready IaC like Terraform or CDK to be deployed right away.</p>
<pre><code class="lang-bash">$ wing compile --<span class="hljs-built_in">help</span>
Usage: wing compile [options] &lt;entrypoint&gt;

Compiles a Wing program

Arguments:
  entrypoint                 program .w entrypoint

Options:
  -h, --<span class="hljs-built_in">help</span>                 display <span class="hljs-built_in">help</span> <span class="hljs-keyword">for</span> <span class="hljs-built_in">command</span>
  -p, --plugins [plugin...]  Compiler plugins
  -r, --rootId &lt;rootId&gt;      App root id
  -t, --target &lt;target&gt;      Target platform (choices: <span class="hljs-string">"tf-aws"</span>, <span class="hljs-string">"tf-azure"</span>, <span class="hljs-string">"tf-gcp"</span>, <span class="hljs-string">"sim"</span>, <span class="hljs-string">"awscdk"</span>, default: <span class="hljs-string">"sim"</span>)
</code></pre>
<h2 id="heading-default-compile-mode-sim-the-wing-console">Default compile mode: sim = the Wing console</h2>
<p>By default, the local simulator is the target compiler - when you run it as:</p>
<pre><code class="lang-bash">wing it &lt;your_main_file&gt;.w
</code></pre>
<p>It will open a web browser with the Console for a visual representation (and interaction with your resources). It has an automatic reloader built in, so it monitors your changes and applies them on the fly, just save it and see it in the browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695723596029/bda58039-1ae1-4e7d-a08b-5643d2b36ddb.png" alt class="image--center mx-auto" /></p>
<p>If you select one of the resources, like the <code>check</code> function, you can make use of the interactive console on the right to invoke it, pass parameters to it and get feedback (logs) from the execution. Same with a bucket, you can see the objects in it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695723658702/d12a9383-622e-4544-90a8-72155d5abe28.png" alt class="image--center mx-auto" /></p>
<p>Yeah, live debugger FTW.</p>
<h2 id="heading-custom-classes">Custom classes</h2>
<p>You can also define classes to modify inflight default modes for a given object - much like a TF module. You create a wrapper around native methods and create your own composable object with custom methods, outputs, etc.</p>
<h1 id="heading-sample-app">Sample app</h1>
<p>Since Winglang is based on JS and Typescript, it is still a learning curve for me - but I built a simple working app to play around and apply some of its core concepts.</p>
<blockquote>
<p>This sample app is <a target="_blank" href="https://github.com/marianogg9/winging">here</a> as well.</p>
</blockquote>
<p>As a prerequisite, <a target="_blank" href="https://www.winglang.io/docs/start-here/installation">install</a> Winglang:</p>
<pre><code class="lang-bash">npm install -g winglang
</code></pre>
<p>(And the <a target="_blank" href="https://www.winglang.io/docs/start-here/installation#wing-vscode-extension">VSCode extension</a> if you want, but it's not required).</p>
<p>I wrote all the code in <code>main.w</code>, as follows.</p>
<pre><code class="lang-typescript">bring cloud;
bring aws;
bring <span class="hljs-string">"./classes.w"</span> <span class="hljs-keyword">as</span> customThings;

<span class="hljs-keyword">let</span> b = <span class="hljs-keyword">new</span> cloud.Bucket() <span class="hljs-keyword">as</span> <span class="hljs-string">"the_bucket"</span>; <span class="hljs-comment">// create a bucket</span>

<span class="hljs-keyword">let</span> bucket_funct = <span class="hljs-keyword">new</span> cloud.Function(inflight (data: str) =&gt; { <span class="hljs-comment">// create a sample function</span>
    b.put(<span class="hljs-string">"some-file.txt"</span>,<span class="hljs-string">"some text inside"</span>);

    log(<span class="hljs-string">"added ${data}"</span>);
}) <span class="hljs-keyword">as</span> <span class="hljs-string">"bucket_function"</span>;

<span class="hljs-keyword">let</span> s = <span class="hljs-keyword">new</span> cloud.Secret(name: <span class="hljs-string">"username1"</span>) <span class="hljs-keyword">as</span> <span class="hljs-string">"the_secret"</span>;

<span class="hljs-keyword">let</span> secret_funct = <span class="hljs-keyword">new</span> cloud.Function(inflight () =&gt; {
    <span class="hljs-keyword">let</span> sVal = s.value();
    b.put(<span class="hljs-string">"${sVal}.txt"</span>,sVal);
    log(<span class="hljs-string">"added secret"</span>);
}) <span class="hljs-keyword">as</span> <span class="hljs-string">"secret_function"</span>;

<span class="hljs-keyword">let</span> custom_bucket: customThings.CustomBucket = <span class="hljs-keyword">new</span> customThings.CustomStorage() <span class="hljs-keyword">as</span> <span class="hljs-string">"CustomBucket"</span>; <span class="hljs-comment">// create a bucket object from the CustomStorage class</span>

<span class="hljs-keyword">let</span> fput = <span class="hljs-keyword">new</span> cloud.Function(inflight () =&gt; {
    custom_bucket.store(<span class="hljs-string">"It works!"</span>);
}) <span class="hljs-keyword">as</span> <span class="hljs-string">"put"</span>;

<span class="hljs-comment">// next policy is not required (Winglang will populate policies by itself), but I had to dig a bit to find how to, so I'm adding it here for future reference</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> putFn = aws.Function.from(fput) { 
    putFn.addPolicyStatements(
        aws.PolicyStatement {
            actions: [<span class="hljs-string">"s3:PutObject*"</span>],
            effect: aws.Effect.ALLOW,
            resources: [<span class="hljs-string">"*"</span>] <span class="hljs-comment">// could not yet find a way of referencing the target bucket ARN</span>
        }
    );
}

<span class="hljs-keyword">let</span> fcheck = <span class="hljs-keyword">new</span> cloud.Function(inflight () =&gt; { <span class="hljs-comment">// declare the "check" function</span>
    custom_bucket.check(<span class="hljs-string">"upload.txt"</span>);
    custom_bucket.check(<span class="hljs-string">"upload.json"</span>);
    custom_bucket.check(<span class="hljs-string">"unexistent.file"</span>);
}) <span class="hljs-keyword">as</span> <span class="hljs-string">"check"</span>;
</code></pre>
<p>And a custom class definition in <code>classes.w</code>:</p>
<pre><code class="lang-typescript">bring cloud;
bring aws;

<span class="hljs-keyword">interface</span> CustomBucket <span class="hljs-keyword">extends</span> std.IResource { 
  inflight store(data: str): <span class="hljs-built_in">void</span>;
  inflight check(data: str): bool;
}

<span class="hljs-keyword">class</span> CustomStorage impl CustomBucket {
    bucket: cloud.Bucket;

    init() { <span class="hljs-comment">// Create a (cloud) bucket</span>
      <span class="hljs-built_in">this</span>.bucket = <span class="hljs-keyword">new</span> cloud.Bucket() <span class="hljs-keyword">as</span> <span class="hljs-string">"custom-bucket"</span>;
    }

    pub inflight store(data: str): <span class="hljs-built_in">void</span> { <span class="hljs-comment">// create a custom store method to upload a couple example files to the bucket</span>
      <span class="hljs-keyword">let</span> file = <span class="hljs-string">"upload"</span>;

      <span class="hljs-built_in">this</span>.bucket.put(<span class="hljs-string">"${file}.txt"</span>, data);
      <span class="hljs-built_in">this</span>.bucket.putJson(<span class="hljs-string">"${file}.json"</span>, Json { <span class="hljs-string">"data"</span>: data});
    }

    pub inflight check(data:str): bool { <span class="hljs-comment">// another custom method to check the content of a given file(s)</span>

        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.bucket.exists(data)) { <span class="hljs-comment">// check if the file exists in the bucket</span>

            <span class="hljs-keyword">let</span> fileData = <span class="hljs-string">""</span>;

            <span class="hljs-keyword">try</span> {
                <span class="hljs-keyword">let</span> fileData = <span class="hljs-built_in">this</span>.bucket.getJson(data);
                assert(fileData.get(<span class="hljs-string">"data"</span>) == <span class="hljs-string">"It works!"</span>);
                log(<span class="hljs-string">"a JSON file"</span>);
            } <span class="hljs-keyword">catch</span> e {
                <span class="hljs-keyword">if</span> e.contains(<span class="hljs-string">"is not a valid JSON"</span>) {
                    <span class="hljs-keyword">let</span> fileData = <span class="hljs-built_in">this</span>.bucket.get(data);
                    assert(fileData == <span class="hljs-string">"It works!"</span>);
                    log(<span class="hljs-string">"a TXT file"</span>);
                } <span class="hljs-keyword">else</span> {
                    log(e);
                }
            }

        } <span class="hljs-keyword">else</span> { <span class="hljs-comment">// if it doesn't exist, log an error</span>
            log(<span class="hljs-string">"File ${data} not found"</span>);
        }
    }
}
</code></pre>
<h3 id="heading-preflight">Preflight</h3>
<p>These are the Infrastructure resources defined:</p>
<ul>
<li><p>A standalone bucket.</p>
</li>
<li><p>Two standalone functions.</p>
</li>
<li><p>A <code>CustomStorage</code> class containing two functions and a bucket. These functions are new interfaces using native methods like <code>put</code> and <code>get</code>.</p>
</li>
</ul>
<h3 id="heading-inflight">Inflight</h3>
<p>For the app code:</p>
<ul>
<li><p>A function uploads a sample .txt file to the bucket.</p>
</li>
<li><p>The other function uploads a (local) secret to that same bucket.</p>
</li>
<li><p>In the <code>CustomStorage</code> class, a new bucket is created and two functions will: upload some files to the new bucket and run a simple check for the file names/contents in it.</p>
</li>
</ul>
<h3 id="heading-wing-it-locally">Wing it, locally</h3>
<pre><code class="lang-typescript">wing it main.w
</code></pre>
<p>It opens a web browser showing all the components and relations, ready to be <em>inflight</em> run.</p>
<h3 id="heading-the-compilation-into-tf-aws">The compilation into TF (AWS)</h3>
<p>Now let's compile this into Terraform-ready code, in my case for AWS.</p>
<pre><code class="lang-bash">wing compile --target tf-aws main.w
</code></pre>
<p>This command will create a <code>target</code> directory with the following schema:</p>
<pre><code class="lang-bash">└── target
    └── main.tfaws
        ├── assets
        │   ├── bucket_function_Asset_859DBBF7
        │   │   └── F4D309B0C55317888FECA9461027AD14
        │   │       └── archive.zip
        │   ├── check_Asset_BAE7D2BE
        │   │   └── 22B6A7822E4F7722DF50A1AA6A5ED16C
        │   │       └── archive.zip
        │   ├── put_Asset_3BF5C371
        │   │   └── DD164E414B5BA274E67C872B892F3B19
        │   │       └── archive.zip
        │   └── secret_function_Asset_729CDAD8
        │       └── CF5A69D348E2C2D58B2C6799DC8BD025
        │           └── archive.zip
        ├── connections.json
        ├── main.tf.json
        └── tree.json
</code></pre>
<p>As you can see in there, the inflight code (for each of the 4 defined functions) is zipped, ready to be referenced from the TF code in <code>main.tf.json</code>.</p>
<p>You can have a look at the <code>main.tf.json</code> contents (Terraform resources definitions) either in plain text or by having a look at the <code>Outline</code> section in VS Code (sorry for the newbie excitement, this was new to me!).</p>
<p>In there, you can see for example the pre-populated IAM roles and policies for the Lambda functions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695725025577/ad7b3864-fca4-43d9-8192-8f3d0296a063.png" alt class="image--center mx-auto" /></p>
<p>Now, to see what's to be created, let's run Terraform within the <code>target/main.tfaws</code> directory. Of course, you will have to have your AWS credentials already set up.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> target/main.tfaws
terraform init
terraform plan

<span class="hljs-comment"># Plan: 23 to add, 0 to change, 0 to destroy.</span>
</code></pre>
<h3 id="heading-creating-the-aws-resources">Creating the AWS resources</h3>
<p>Now if we wanted to see those resources deployed in AWS, we will need to first create the <code>secret</code> in AWS Secrets Manager. In the local simulation mode, the <code>secret</code> resource is created from a local FS directory, but in the cloud context, it expects that to be created in AWS Secrets Manager as Terraform will treat it as a <code>data</code> resource to be fetched internally.</p>
<blockquote>
<p>More about <a target="_blank" href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret">secret data resource</a> in Terraform. And <a target="_blank" href="https://developer.hashicorp.com/terraform/language/data-sources">datasources</a> in general.</p>
</blockquote>
<p>Then, apply:</p>
<pre><code class="lang-bash">terraform apply
</code></pre>
<p>This will end up creating:</p>
<ul>
<li><p>4 Lambda functions.</p>
</li>
<li><p>3 buckets:</p>
<ul>
<li><p><code>code</code> for the Lambda(s) code (4 zip files).</p>
</li>
<li><p>The <code>the_bucket</code> standalone.</p>
</li>
<li><p>The <code>custom_bucket</code> (from the <code>CustomBucket</code> class we created).</p>
</li>
</ul>
</li>
<li><p>3 IAM roles.</p>
</li>
<li><p>3 IAM policies.</p>
</li>
</ul>
<p>You can then test the Lambda functions that will upload files to the buckets. Same as in the Web Console.</p>
<h3 id="heading-clean-up">Clean up</h3>
<p>As always, don't forget to clean up whatever you created. Remember to first delete manually any uploaded file(s) from the buckets. Then run:</p>
<pre><code class="lang-bash">terraform destroy
</code></pre>
<h1 id="heading-conclusion">Conclusion</h1>
<p><a target="_blank" href="https://www.winglang.io/docs/#this-is-a-pre-release-">Winglang is in pre-release status</a>, so there's a lot to be added. There is still a minimal library toolset available but it looks <strong>very promising</strong>.</p>
<p>One thing to notice is the amount of assumptions it makes. Let's see a (AWS) Lambda function. Winglang will populate all the IAM roles and their policies to create a code repository bucket, upload the zip code and then access it, perform any action the function needs to do (like fetch the secret content or put objects to a given bucket) and give this role <code>assume</code> permission for the AWS Lambda service.</p>
<p>I mean it all makes sense as the language was created with "all batteries included" so there has to be a way of abstracting all these configurations and details out of the code somehow. But, if you are curious or concerned about low-level Infrastructure management, then you should keep an eye on this. You can always attach new policies - <a target="_blank" href="https://github.com/marianogg9/winging/blob/main/main.w#L27">I added an example implementation</a>.</p>
<p>And if you are coming from a TS/JS/Java background, Winglang should be a piece of cake for you - for me...still a long way to go.</p>
<p><img src="https://media.giphy.com/media/WUPYLhJGEwREt4rcMv/giphy.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://www.winglang.io/">Winglang</a>.</p>
</li>
<li><p><a target="_blank" href="https://developer.hashicorp.com/terraform/language/data-sources">TF datasources</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/marianogg9/winging">My sample app</a> in Github.</p>
</li>
<li><p><a target="_blank" href="https://github.com/winglang/examples">A lot of Winglang examples</a> in Github.</p>
</li>
<li><p><a target="_blank" href="https://winglang.slack.com/">Winglang</a> on Slack.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by!</p>
]]></content:encoded></item><item><title><![CDATA[The 3 Rs: Reduce, Recycle, Repeat]]></title><description><![CDATA[Why
One of (Software) Development's best practices is to make code reusable - meaning we want to make sure to reutilise pieces and bits of code so as not to write repetitive lines in our program.
What
A module in Terraform is a grouping of resources,...]]></description><link>https://blog.mariano.cloud/the-3-rs-reduce-recycle-repeat</link><guid isPermaLink="true">https://blog.mariano.cloud/the-3-rs-reduce-recycle-repeat</guid><category><![CDATA[Terraform]]></category><category><![CDATA[modules]]></category><category><![CDATA[reusablility]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Tue, 19 Sep 2023 18:28:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695147603537/d71e8eee-ea37-4543-b690-1342fd579a29.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-why">Why</h1>
<p>One of (Software) Development's best practices is to make code reusable - meaning we want to make sure to reutilise pieces and bits of code so as not to write repetitive lines in our program.</p>
<h1 id="heading-what">What</h1>
<p>A <a target="_blank" href="https://developer.hashicorp.com/terraform/language/modules">module in Terraform</a> is a grouping of resources, an object that allows us to create a bunch of resources from a given set of input parameters used to process and further manage the output infrastructure.</p>
<h1 id="heading-how">How</h1>
<p>There are two ways of using a module: referencing a published one or writing your own.</p>
<p>In the <a target="_blank" href="https://registry.terraform.io/">Terraform registry</a>, a lot of modules are published and available for you to use in your code. These are free, community-maintained (in most cases), tested and documented, which makes them a very good resource to use and reuse code.</p>
<p>Or (AND) you could write your own, based either on a published one or from scratch by declaring native resources.</p>
<h2 id="heading-lets-see-why-you-would-want-to-use-a-module">Let's see why you would want to use a module</h2>
<p><mark>The following example(s) assumes you have already used Terraform to manage Infrastructure resources and you know the basics. Also, this guide is based on a preexisting AWS EKS cluster.</mark></p>
<p>Say you have 3 services (apps) hosted in Kubernetes (EKS) that upload files to a (AWS) S3 bucket and you want to enable these EKS services to interact with S3 using dynamic credentials (IRSA).</p>
<blockquote>
<p>Psst, here is an <a target="_blank" href="https://blog.mariano.cloud/irsa-in-eks-a-kubernetes-aws-bridge">IRSA implementation</a>. Also, all these examples are <a target="_blank" href="https://github.com/marianogg9/tf-moduling">here</a>.</p>
</blockquote>
<h3 id="heading-what-do-you-need-to-create">What do you need to create?</h3>
<p>An S3 bucket with its base ACLs, then an IAM role with its policies to both allow the application in EKS to assume the role via <a target="_blank" href="https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html">AssumeRoleWithWebIdentity</a> and the permissions required to upload files to the bucket. Those are in total 4 different Terraform resources (per application) and some additional code to get information/parameters.</p>
<p><strong>Using plain resources</strong></p>
<p><img src="https://media.giphy.com/media/FZ7lPdteeiChZutNRv/giphy.gif" alt class="image--center mx-auto" /></p>
<p>You would need to do something like:</p>
<pre><code class="lang-bash">locals {
  apps = {
    app-1 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:PutObject"</span>] },
    app-2 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObject"</span>, <span class="hljs-string">"s3:PutObject"</span>] },
    app-3 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObjectVersion"</span>] }
  }
}

<span class="hljs-comment"># S3 resources</span>
resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"the_bucket"</span> {
  for_each = local.apps

  bucket_prefix = join(<span class="hljs-string">""</span>, [each.key, <span class="hljs-string">"-"</span>])
}

resource <span class="hljs-string">"aws_s3_bucket_ownership_controls"</span> <span class="hljs-string">"the_bucket_oc"</span> {
  for_each = {
    app-1 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:PutObject"</span>] },
    app-2 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObject"</span>, <span class="hljs-string">"s3:PutObject"</span>] },
    app-3 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObjectVersion"</span>] }
  }

  bucket = aws_s3_bucket.the_bucket[each.key].id

  rule {
    object_ownership = <span class="hljs-string">"BucketOwnerPreferred"</span>
  }
}

resource <span class="hljs-string">"aws_s3_bucket_acl"</span> <span class="hljs-string">"the_bucket_acl"</span> {
  depends_on = [aws_s3_bucket_ownership_controls.the_bucket_oc]

  for_each = local.apps

  bucket = aws_s3_bucket.the_bucket[each.key].id

  acl = <span class="hljs-string">"private"</span>
}

resource <span class="hljs-string">"aws_s3_bucket_public_access_block"</span> <span class="hljs-string">"the_bucket_ab"</span> {
  for_each = local.apps

  bucket = aws_s3_bucket.the_bucket[each.key].id

  block_public_acls       = <span class="hljs-literal">true</span>
  block_public_policy     = <span class="hljs-literal">true</span>
  ignore_public_acls      = <span class="hljs-literal">true</span>
  restrict_public_buckets = <span class="hljs-literal">true</span>
}

<span class="hljs-comment"># IAM resources</span>

<span class="hljs-comment">## Some prerequisites</span>
data <span class="hljs-string">"aws_eks_cluster"</span> <span class="hljs-string">"this"</span> { <span class="hljs-comment"># get EKS cluster attributes to use later on.</span>
  name = <span class="hljs-string">"test-eks-cluster"</span>
}

data <span class="hljs-string">"aws_caller_identity"</span> <span class="hljs-string">"current"</span> {} <span class="hljs-comment"># get current account ID</span>

data <span class="hljs-string">"aws_iam_policy_document"</span> <span class="hljs-string">"assume-policy"</span> { <span class="hljs-comment"># create an assume policy for STS</span>
  statement {
    actions = [<span class="hljs-string">"sts:AssumeRoleWithWebIdentity"</span>]
    principals {
      <span class="hljs-built_in">type</span> = <span class="hljs-string">"Federated"</span>
      identifiers = [
        replace(
          data.aws_eks_cluster.this.identity[0].oidc[0].issuer,
          <span class="hljs-string">"https://"</span>,
          join(<span class="hljs-string">""</span>, [<span class="hljs-string">"arn:aws:iam::"</span>, data.aws_caller_identity.current.account_id, <span class="hljs-string">":oidc-provider/"</span>])
        )
      ]
    }
  }
}

<span class="hljs-comment">## Role definition</span>
resource <span class="hljs-string">"aws_iam_role"</span> <span class="hljs-string">"the_role"</span> { <span class="hljs-comment"># create the IAM role and attach both assume and inline identity based policies.</span>
  for_each = local.apps

  name = each.key
  path               = <span class="hljs-string">"/"</span>
  assume_role_policy = data.aws_iam_policy_document.assume-policy.json

  inline_policy {
    name = <span class="hljs-string">"s3-put"</span>
    policy = jsonencode({
      Version = <span class="hljs-string">"2012-10-17"</span>
      Statement = [{
        Action = [<span class="hljs-keyword">for</span> permission <span class="hljs-keyword">in</span> each.value[<span class="hljs-string">"permissions"</span>] : permission]
        Effect = <span class="hljs-string">"Allow"</span>
        Resource = aws_s3_bucket.the_bucket[each.key].arn
      }]
    })
  }

  tags = {
    <span class="hljs-comment"># always add some tags!</span>
  }
}

resource <span class="hljs-string">"aws_iam_policy"</span> <span class="hljs-string">"the_policy"</span> {
  for_each = local.apps

  name_prefix = join(<span class="hljs-string">""</span>,[each.key,<span class="hljs-string">"-"</span>])
  path        = <span class="hljs-string">"/"</span>

  policy      = jsonencode({
    Version   = <span class="hljs-string">"2012-10-17"</span>
    Statement = [
      {
        Action   = [ <span class="hljs-keyword">for</span> permission <span class="hljs-keyword">in</span> each.value[<span class="hljs-string">"permissions"</span>]: permission ]
        Effect   = <span class="hljs-string">"Allow"</span>
        Resource = join(<span class="hljs-string">""</span>,[aws_s3_bucket.the_bucket[each.key].arn,<span class="hljs-string">"/*"</span>])
      },
    ]
  })
}

resource <span class="hljs-string">"aws_iam_role_policy_attachment"</span> <span class="hljs-string">"the_policy_attachment"</span> {
  for_each = local.apps

  role       = aws_iam_role.the_role[each.key].name
  policy_arn = aws_iam_policy.the_policy[each.key].arn
}

output <span class="hljs-string">"the_bucket"</span> {
  value = values(aws_s3_bucket.the_bucket).*.arn
}
output <span class="hljs-string">"the_role"</span> {
  value = values(aws_iam_role.the_role).*.arn
}
</code></pre>
<p>Total: 131 lines of code.</p>
<p><strong>Now let's do the same using upstream (registry) modules</strong></p>
<blockquote>
<p>There are published modules for <a target="_blank" href="https://registry.terraform.io/modules/terraform-aws-modules/s3-bucket/aws/latest">S3</a> and <a target="_blank" href="https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest">IAM</a> roles with IRSA support.</p>
</blockquote>
<pre><code class="lang-bash">locals {
  apps = {
    app-1 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:PutObject"</span>] },
    app-2 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObject"</span>, <span class="hljs-string">"s3:PutObject"</span>] },
    app-3 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObjectVersion"</span>] }
  }
}

data <span class="hljs-string">"aws_eks_cluster"</span> <span class="hljs-string">"this"</span> { <span class="hljs-comment"># get EKS cluster attributes to use later on.</span>
  name = <span class="hljs-string">"test-eks-cluster"</span>
}

data <span class="hljs-string">"aws_caller_identity"</span> <span class="hljs-string">"current"</span> {} <span class="hljs-comment"># get current account ID</span>

module <span class="hljs-string">"s3_bucket"</span> {
  for_each = local.apps

  <span class="hljs-built_in">source</span> = <span class="hljs-string">"terraform-aws-modules/s3-bucket/aws"</span>

  bucket_prefix    = join(<span class="hljs-string">""</span>,[each.key,<span class="hljs-string">"-"</span>])
  acl              = <span class="hljs-string">"private"</span>

  control_object_ownership = <span class="hljs-literal">true</span>
  object_ownership         = <span class="hljs-string">"BucketOwnerPreferred"</span>
}

module <span class="hljs-string">"iam_assumable_role_with_oidc"</span> {
    for_each = local.apps

  <span class="hljs-built_in">source</span>      = <span class="hljs-string">"terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"</span>

  create_role = <span class="hljs-literal">true</span>
  role_name   = each.key

  tags = {
    <span class="hljs-comment"># add some tags!</span>
  }
  provider_url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer

  role_policy_arns = [
    aws_iam_policy.the_policy[each.key].arn,
  ]
  number_of_role_policy_arns = 1
}

resource <span class="hljs-string">"aws_iam_policy"</span> <span class="hljs-string">"the_policy"</span> {
    for_each = local.apps

  name_prefix = join(<span class="hljs-string">""</span>,[each.key,<span class="hljs-string">"-"</span>])
  path        = <span class="hljs-string">"/"</span>

  policy      = jsonencode({
    Version   = <span class="hljs-string">"2012-10-17"</span>
    Statement = [
      {
        Action   = [ <span class="hljs-keyword">for</span> permission <span class="hljs-keyword">in</span> each.value[<span class="hljs-string">"permissions"</span>]: permission ]
        Effect   = <span class="hljs-string">"Allow"</span>
        Resource = join(<span class="hljs-string">""</span>,[module.s3_bucket[each.key].s3_bucket_arn,<span class="hljs-string">"/*"</span>])
      },
    ]
  })
}

output <span class="hljs-string">"the_bucket"</span> {
    value = values(module.s3_bucket).*.s3_bucket_arn
}
output <span class="hljs-string">"the_role"</span> {
    value = values(module.iam_assumable_role_with_oidc).*.iam_role_arn
}
</code></pre>
<p>Total: 69 lines.</p>
<p>With less (half) code, you will end up with the same set of resources as before (21).</p>
<h2 id="heading-a-module-using-modules">A module using modules</h2>
<p><img src="https://media.giphy.com/media/YvX6r2p41Ej0A/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Now, let's go a bit further and write our own module using two published ones, so it makes more sense to our use case and avoids code repetition.</p>
<p><strong>To achieve reusability you need to create code that:</strong></p>
<ul>
<li><p>Accepts a set of names/IDs for each bucket and role to be created.</p>
</li>
<li><p>Loops through that given set.</p>
</li>
<li><p>Calls the upstream modules the less amount of times.</p>
</li>
<li><p>Is flexible enough to be reused with as many inputs as needed.</p>
<blockquote>
<p>For this topic, let's think about what attributes all apps share and what is specific for each of them.</p>
<ul>
<li><p>Whatever is common will be set statically in the module and only use a name or identifier to be managed (e.g. a bucket ACL or a role assume policy).</p>
</li>
<li><p>But if we had a parameter that may vary per app, we would populate it dynamically in the module (e.g. the role permissions over the bucket objects).</p>
</li>
</ul>
</blockquote>
</li>
</ul>
<p>Also as a general guideline, we will be referencing a locally written module.</p>
<p>The directory schema will look like the following:</p>
<pre><code class="lang-bash">.
├── my_awesome_module
│   ├── iam_role.tf
│   ├── outputs.tf
│   ├── s3_bucket.tf
│   └── variables.tf
└── with_local_module
    └── the_apps_local_module.tf
</code></pre>
<p>Where:</p>
<ul>
<li><p><code>the_apps_local_module.tf</code> is the main Terraform file, where we call our module with a set of input applications' parameters.</p>
</li>
<li><p><code>my_awesome_module</code> is the local module directory.</p>
<ul>
<li><p><code>s3_bucket.tf</code> calls the upstream S3 bucket module.</p>
</li>
<li><p><code>iam_role.tf</code> references the upstream IAM role module and defines a standalone role policy.</p>
</li>
<li><p><code>variables.tf</code> where we define the input variables expected by our local module.</p>
</li>
<li><p><code>outputs.tf</code> defining a couple of return values from the created resources, which I highly recommend but it's optional.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-the-module-and-its-files">The module and its files</h3>
<p><code>s3_bucket.tf</code> (required)</p>
<pre><code class="lang-bash">module <span class="hljs-string">"s3_bucket"</span> {
  <span class="hljs-built_in">source</span> = <span class="hljs-string">"terraform-aws-modules/s3-bucket/aws"</span>

  bucket_prefix    = join(<span class="hljs-string">""</span>,[var.bucket_name,<span class="hljs-string">"-"</span>])
  acl              = <span class="hljs-string">"private"</span>

  control_object_ownership = <span class="hljs-literal">true</span>
  object_ownership         = <span class="hljs-string">"BucketOwnerPreferred"</span>
}
</code></pre>
<p><code>iam_role.tf</code> (required)</p>
<pre><code class="lang-bash">data <span class="hljs-string">"aws_eks_cluster"</span> <span class="hljs-string">"this"</span> { <span class="hljs-comment"># get EKS cluster attributes to use later on.</span>
  name = <span class="hljs-string">"test-eks-cluster"</span>
}

data <span class="hljs-string">"aws_caller_identity"</span> <span class="hljs-string">"current"</span> {} <span class="hljs-comment"># get current account ID</span>

module <span class="hljs-string">"iam_assumable_role_with_oidc"</span> {
  <span class="hljs-built_in">source</span>      = <span class="hljs-string">"terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"</span>

  create_role = <span class="hljs-literal">true</span>
  role_name   = var.role_name

  tags = {
    <span class="hljs-comment"># add some tags!</span>
  }
  provider_url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer

  role_policy_arns = [
    aws_iam_policy.the_policy.arn,
  ]
  number_of_role_policy_arns = 1
}

resource <span class="hljs-string">"aws_iam_policy"</span> <span class="hljs-string">"the_policy"</span> {
  name_prefix = join(<span class="hljs-string">""</span>,[var.role_name,<span class="hljs-string">"-"</span>])
  path        = <span class="hljs-string">"/"</span>

  policy      = jsonencode({
    Version   = <span class="hljs-string">"2012-10-17"</span>
    Statement = [
      {
        Action   = [ <span class="hljs-keyword">for</span> permission <span class="hljs-keyword">in</span> var.permissions: permission ]
        Effect   = <span class="hljs-string">"Allow"</span>
        Resource = join(<span class="hljs-string">""</span>,[module.s3_bucket.s3_bucket_arn,<span class="hljs-string">"/*"</span>])
      },
    ]
  })
}
</code></pre>
<p><code>variables.tf</code> (required)</p>
<pre><code class="lang-bash">variable <span class="hljs-string">"bucket_name"</span> {
    description = <span class="hljs-string">"S3 bucket name"</span>
    <span class="hljs-built_in">type</span>        = string
    default     = <span class="hljs-string">""</span>
}
variable <span class="hljs-string">"role_name"</span> {
    description = <span class="hljs-string">"IAM role name"</span>
    <span class="hljs-built_in">type</span>        = string
    default     = <span class="hljs-string">""</span>
}
variable <span class="hljs-string">"permissions"</span> {
    description = <span class="hljs-string">"List of permissions to attach to the IAM role"</span>
    <span class="hljs-built_in">type</span>        = list
    default     = []
}
</code></pre>
<p><code>outputs.tf</code> (optional)</p>
<pre><code class="lang-bash">output <span class="hljs-string">"the_bucket"</span> {
    value = module.s3_bucket.s3_bucket_arn
}
output <span class="hljs-string">"the_role"</span> {
    value = module.iam_assumable_role_with_oidc.iam_role_arn
}
</code></pre>
<h3 id="heading-the-call">The call</h3>
<p>Now that we have our module, we can reference it by passing all the apps we need with their corresponding input (variable) values.</p>
<p><code>the_apps_local_module.tf</code></p>
<pre><code class="lang-bash">module <span class="hljs-string">"the_apps"</span> {
  for_each = {
    app-1 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:PutObject"</span>] },
    app-2 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObject"</span>, <span class="hljs-string">"s3:PutObject"</span>] },
    app-3 = { <span class="hljs-string">"permissions"</span> : [<span class="hljs-string">"s3:GetObjectVersion"</span>] }
  }

  <span class="hljs-built_in">source</span> = <span class="hljs-string">"../my_awesome_module"</span> <span class="hljs-comment"># reference to our local module</span>

  bucket_name = each.key
  role_name   = each.key
  permissions = each.value[<span class="hljs-string">"permissions"</span>]
}

output <span class="hljs-string">"the_apps"</span> {
  value = module.the_apps
}
</code></pre>
<p>Total: 85 lines for the same 21 resources as before.</p>
<h2 id="heading-testing-all-up">Testing all up</h2>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/marianogg9/tf-moduling
<span class="hljs-built_in">cd</span> tf-moduling
<span class="hljs-built_in">cd</span> with_standalone_resources
terraform init
terraform plan
<span class="hljs-comment"># Plan: 21 to add, 0 to change, 0 to destroy.</span>

<span class="hljs-built_in">cd</span> ../with_upstream_modules/
terraform init
terraform plan
<span class="hljs-comment"># Plan: 21 to add, 0 to change, 0 to destroy.</span>

<span class="hljs-built_in">cd</span> ../with_local_module/
terraform init
terraform plan
<span class="hljs-comment"># Plan: 21 to add, 0 to change, 0 to destroy.</span>
</code></pre>
<h3 id="heading-cleanup">Cleanup</h3>
<p><img src="https://media.giphy.com/media/73j8OT8DqHGyQ/giphy.gif" alt class="image--center mx-auto" /></p>
<p>If you went ahead and created any resources, please don't forget about this, your future self will thank you.</p>
<ul>
<li><p>Delete any file uploaded to the S3 bucket(s).</p>
</li>
<li><p>Then delete TF-managed resources by running <code>terraform destroy</code> from the corresponding directory.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>As always, everything depends on your use case. While it is easier to use a well-documented mainstream solution, it may not make sense to you, your team or even your apps.</p>
<p>If you have a specific Infrastructure need to allocate custom resources, you can write your module by either reusing upstream Terraform registry modules or plain native resources.</p>
<p>And it is also true that you do not have to use modules at all, some applications' Infrastructure don't need complicated IaC code bases, just define plain resources and make it as simple as possible for you.</p>
<p>Whatever makes sense to you: try it out, fail often, iterate and you will for sure create extensible, maintainable, reusable and overall better code.</p>
<p><img src="https://media.giphy.com/media/6FrujVG4mafRETQTal/giphy.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest">IAM module(s) in Terraform registry</a>.</p>
</li>
<li><p><a target="_blank" href="https://registry.terraform.io/modules/terraform-aws-modules/s3-bucket/aws/latest">S3 module in Terraform registry</a>.</p>
</li>
<li><p><a target="_blank" href="https://developer.hashicorp.com/terraform/language/expressions/for">For expressions in Terraform</a>.</p>
</li>
<li><p><a target="_blank" href="https://developer.hashicorp.com/terraform/language/meta-arguments/for_each">For_each expression in Terraform</a>.</p>
</li>
<li><p><a target="_blank" href="https://developer.hashicorp.com/terraform/language/modules">Modules in Terraform</a>.</p>
</li>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/Code_reuse">Code reusing</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/irsa-in-eks-a-kubernetes-aws-bridge">IRSA implementation</a>.</p>
</li>
<li><p>The examples in this blog are <a target="_blank" href="https://github.com/marianogg9/tf-moduling">here</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[The Contra questions]]></title><description><![CDATA[I participated in a few interview processes during my career and also conducted some more. It is, to me, a very exciting experience where I get to measure my knowledge and a way of gamifying the learning process. In my experience, most of the intervi...]]></description><link>https://blog.mariano.cloud/the-contra-questions</link><guid isPermaLink="true">https://blog.mariano.cloud/the-contra-questions</guid><category><![CDATA[interview]]></category><category><![CDATA[infrastructure]]></category><category><![CDATA[Solution]]></category><category><![CDATA[questions]]></category><category><![CDATA[Experience ]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Sun, 10 Sep 2023 15:56:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694360887342/48616519-29ac-4923-8512-c81ddc438661.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I participated in a few interview processes during my career and also conducted some more. It is, to me, a very exciting experience where I get to measure my knowledge and a way of gamifying the learning process. In my experience, most of the interviews where I was the answering party, were truly a trigger to either research some new tool or go deeper into what I already had experience in.</p>
<p>But the best interviews I did were the ones I got asked about why NOT using X platform.</p>
<p><em>I am a Cloud Engineer, so most of my examples and quotes come from an Infrastructure background. But this article applies to any field to be fair.</em></p>
<h1 id="heading-traditionally">Traditionally</h1>
<blockquote>
<p>I have this setup, with these constraints and this expected performance; where do you recommend hosting this? Why?</p>
</blockquote>
<p>So one would go over the pros of a certain platform and why it is the best suited for the job. And that has value in itself, given a set of requirements, being able to identify and match strong arguments with the expected outcome is, at the end of the day, what makes a good solution.</p>
<h2 id="heading-but-what-if-you-started-with-cons-instead">But what if you started with cons instead?</h2>
<blockquote>
<p>I have this setup, with these constraints and this expected performance. Where do you NOT recommend hosting this? Why is that? Can you tell me a couple of deal breakers of X platform?</p>
</blockquote>
<p>Every solution, platform, tool or any involved process depends on a specific use case. You wouldn't overcomplicate a solution just because it involves a buzzword.</p>
<p>Right? Right?</p>
<p><img src="https://media.giphy.com/media/dXFKDUolyLLi8gq6Cl/giphy.gif" alt class="image--center mx-auto" /></p>
<p>So maybe a good way of understanding how a platform would actually fit a given problem is what <strong>not</strong> to choose. It will show a couple of good traits.</p>
<h3 id="heading-the-pros-of-the-cons">The Pros of the Cons</h3>
<ul>
<li><p>Deep analysis of the problem.</p>
</li>
<li><p>Weighing in the possible outcomes.</p>
</li>
<li><p>Trade-off discussion and affordable compromises.</p>
</li>
</ul>
<h2 id="heading-what-makes-this-way-valuable">What makes this way valuable?</h2>
<p>If you can articulate against a few solutions, it shows you have the experience to compare different platforms, which not only demonstrates actual practical hands-on but also that you can discuss approaches and disadvantages to get closer to the best solution.</p>
<h1 id="heading-example-kubernetes">Example: Kubernetes</h1>
<p><img src="https://media.giphy.com/media/WsNbxuFkLi3IuGI9NU/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Kubernetes is arguably the de facto platform to host and orchestrate most (containerised) applications. I'm not discovering anything.</p>
<h2 id="heading-is-it-technically-applicable-to-anything">Is it technically applicable to anything?</h2>
<p>Sure, you can use it for 99% of modern use cases and it will just work. Even for a simple API serving a JSON response for a given product metadata? Of course, you can make it happen using a simple setup of required objects (a deployment, <a target="_blank" href="https://hashnode.com/post/clheoq55700010akydtvd1073">secrets</a>, and a couple more).</p>
<blockquote>
<p><a target="_blank" href="https://blog.mariano.cloud/helm-declaration-environments-helmfile">Here</a>'s an example of a Kubernetes implementation using Helmfile, if you are feeling curious...</p>
</blockquote>
<h2 id="heading-but-is-it-worth-the-overhead">But is it worth the overhead?</h2>
<p>For <strong>every</strong> use case? Certainly not.</p>
<p>On the one hand, Kubernetes is excellent in a distributed environment, HA, autoscaling, resource management, security and 3rd party tools integration. Lots of documentation out there, everyone is using it, battle-tested, reliable and mature.</p>
<p>On the other, it also introduces some questions that you will have to deal with and think about, naming a few:</p>
<ul>
<li><p>Will you self-host it? Are you using any orchestrator, like Kind? Are you maintaining it yourself? Are you doing it in a cloud provider instead?</p>
</li>
<li><p>Have you considered the (additional) costs involved?</p>
</li>
<li><p>Have you planned a new deployment method? Are you willing to change your current one?</p>
</li>
<li><p>Is the practical knowledge already available in your team?</p>
</li>
</ul>
<h2 id="heading-then-why-would-you-not-use-it">Then why would you not use it?</h2>
<p>Well, if these sounds like your application:</p>
<ul>
<li><p>Single instance, no need for high availability.</p>
</li>
<li><p>It serves atomic actions, with no interdependencies or moving parts.</p>
</li>
<li><p>Short-lived processing, no intensive computing requirements.</p>
</li>
</ul>
<p>And/or your team:</p>
<ul>
<li><p>Does not have Kubernetes experience.</p>
</li>
<li><p>Has a somewhat short budget.</p>
</li>
</ul>
<p>Maybe you can consider other options to host your app. Serverless might be a good <em>candidate</em>.</p>
<blockquote>
<p>Everything is a matter of your use case, completely constrained to your app and business requirements.</p>
</blockquote>
<h1 id="heading-conclusion">Conclusion</h1>
<p>If you have the chance to conduct an interview, maybe explore this approach. I am sure it will give you much richer information about someone else's experience.</p>
<p>But you know, this is not only applicable to interviewing.</p>
<p>And as a final takeout, do interviews. It keeps you up to date with the market and pushes your learning forward. It is also a great opportunity to discover new ways of doing things, a new technology you haven't used or new features of a tool you use daily and did not know (because every use case is different, remember?).</p>
<blockquote>
<p>But also if you do, be prepared to consider an eventual offer - interview processes are ($$) costly and time-consuming for both parties, so please be mindful of others' work too.</p>
</blockquote>
<h1 id="heading-references">References</h1>
<ul>
<li><p>The <a target="_blank" href="https://capd.mit.edu/resources/the-star-method-for-behavioral-interviews/">STAR</a> method.</p>
</li>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/Socratic_method">The Socratic method</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[Do you even JSONPath?]]></title><description><![CDATA[I like the CLI and a black background console when it comes to checking on things and debugging, it makes me feel more confident in what I'm seeing and closer to the basics.

A big chunk of my recent years has been all about Kubernetes. Kubernetes th...]]></description><link>https://blog.mariano.cloud/do-you-even-jsonpath</link><guid isPermaLink="true">https://blog.mariano.cloud/do-you-even-jsonpath</guid><category><![CDATA[jsonpath]]></category><category><![CDATA[json]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[kubectl]]></category><category><![CDATA[cli]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Wed, 24 May 2023 09:28:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684864854284/ae8f7cb6-5804-4dca-8c84-22cde8eb228b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I like the CLI and a black background console when it comes to checking on <em>things</em> and debugging, it makes me feel more confident in what I'm seeing and closer to the basics.</p>
<p><img src="https://media.giphy.com/media/5SKwQMGTR1umLrKC7N/giphy.gif" alt class="image--center mx-auto" /></p>
<p>A big chunk of my recent years has been all about Kubernetes. Kubernetes this, Kubernetes that, mostly EKS professionally, here and there playing around GKS and of course, Minikube is my bro on local.</p>
<p>And <strong>kubectl</strong> is also a close friend of mine, the first tool kinda everyone learns when starting with Kubernetes, but perhaps the easiest and cleanest too.</p>
<h2 id="heading-jsonpath">JSONPath</h2>
<p>But do you know <a target="_blank" href="https://kubernetes.io/docs/reference/kubectl/jsonpath/">kubectl offers JSONPath support natively</a>? yeah, JSONPath as in a JSON query language that enables you to interact with a JSON structured data set.</p>
<p>Using a <a target="_blank" href="https://goessner.net/articles/JsonPath/index.html#e2">set of JSONPath expressions</a>, one could query, parse and format a JSON structure easily to get whatever information needed in a readable and clear way.</p>
<h2 id="heading-enough-with-the-written-words">Enough with the written words</h2>
<p><img src="https://media.giphy.com/media/fXwd1rBNMmtJyfYFU9/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Let's query some Kubernetes thingy(s).</p>
<h3 id="heading-the-usual-stuff">The usual stuff</h3>
<ul>
<li><p>Minikube (check <a target="_blank" href="https://blog.mariano.cloud/helm-declaration-environments-helmfile#heading-installation">this article</a> on how to install it in your local).</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/#install-with-homebrew-on-macos">Kubectl</a>.</p>
</li>
</ul>
<p>Once both are installed, let's deploy a sample pod + service + service account (for this example, I will be using the one to rule them all: <a target="_blank" href="https://httpbin.org/">httpbin</a>).</p>
<pre><code class="lang-bash"><span class="hljs-comment"># create a new namespace</span>
kubectl create ns jsonpath-playground

<span class="hljs-comment"># get httpbin's YAML (from Istio examples)</span>
curl -s https://raw.githubusercontent.com/istio/istio/master/samples/httpbin/httpbin.yaml -O

<span class="hljs-comment"># deploy httpbin resources</span>
kubectl apply -f httpbin.yaml -n jsonpath-playground
</code></pre>
<p>Now we have a set of resources created in our local cluster:</p>
<ul>
<li><p>A pod.</p>
</li>
<li><p>A service.</p>
</li>
<li><p>A service account.</p>
</li>
</ul>
<p>Check them out with:</p>
<pre><code class="lang-bash">$ kubectl get pod,svc,sa -n jsonpath-playground

NAME                          READY   STATUS    RESTARTS   AGE
pod/httpbin-bf5fc5d74-p9h2g   1/1     Running   0          65s

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/httpbin   ClusterIP   10.100.156.45   &lt;none&gt;        8000/TCP   65s

NAME                     SECRETS   AGE
serviceaccount/default   1         83s
serviceaccount/httpbin   1         65s
</code></pre>
<p><a target="_blank" href="https://kubernetes.io/docs/reference/kubectl/#formatting-output">By default</a>, <strong>kubectl</strong> outputs its data in plain-text columns, nice and readable, with a limited amount of information that gives an overview of what we are querying. This is indeed intended to give a quick look at the resources, but not much in-depth.</p>
<p>If we wanted to go beyond that, we could use the <code>-o wide</code> flag, making <strong>kubectl</strong> output a bit more information.</p>
<p>But let's go deeper. We want to see the service account's raw JSON output. Let's use <code>-o json</code> flag now.</p>
<pre><code class="lang-bash">kubectl get sa -n jsonpath-playground httpbin -o json
</code></pre>
<p>We get something like this:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"apiVersion"</span>: <span class="hljs-string">"v1"</span>,
    <span class="hljs-attr">"kind"</span>: <span class="hljs-string">"ServiceAccount"</span>,
    <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"annotations"</span>: {
            <span class="hljs-attr">"kubectl.kubernetes.io/last-applied-configuration"</span>: <span class="hljs-string">"{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"annotations\":{},\"name\":\"httpbin\",\"namespace\":\"jsonpath-playground\"}}\n"</span>
        },
        <span class="hljs-attr">"creationTimestamp"</span>: <span class="hljs-string">"2023-05-22T11:08:34Z"</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"httpbin"</span>,
        <span class="hljs-attr">"namespace"</span>: <span class="hljs-string">"jsonpath-playground"</span>,
        <span class="hljs-attr">"resourceVersion"</span>: <span class="hljs-string">"859"</span>,
        <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"uid"</span>
    },
    <span class="hljs-attr">"secrets"</span>: [
        {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"httpbin-token-uid"</span>
        }
    ]
}
</code></pre>
<p>This is a very simple object, which does not have that many attributes. Let's make it a bit richer by adding an AWS IRSA annotation (<a target="_blank" href="https://blog.mariano.cloud/irsa-in-eks-a-kubernetes-aws-bridge">see my IRSA article if you are curious about it</a>).</p>
<pre><code class="lang-bash">$ kubectl annotate sa -n jsonpath-playground httpbin eks.amazonaws.com/role-arn=arn:aws:iam::123456:role/irsa-role

serviceaccount/httpbin annotated
</code></pre>
<p>This will add an annotation to the <code>httpbin</code> service account, <a target="_blank" href="https://blog.mariano.cloud/irsa-in-eks-a-kubernetes-aws-bridge#heading-create-a-kubernetes-job">mapping it to a dummy AWS IAM role</a>. But fear not my fellow reader, it can be any annotation for this example purpose.</p>
<p>Let's check how it looks now, with:</p>
<pre><code class="lang-bash">kubectl get sa -n jsonpath-playground httpbin -o json
</code></pre>
<p>Getting something like:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"apiVersion"</span>: <span class="hljs-string">"v1"</span>,
    <span class="hljs-attr">"kind"</span>: <span class="hljs-string">"ServiceAccount"</span>,
    <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"annotations"</span>: {
            <span class="hljs-attr">"eks.amazonaws.com/role-arn"</span>: <span class="hljs-string">"arn:aws:iam::123456:role/irsa-role"</span>,
            <span class="hljs-attr">"kubectl.kubernetes.io/last-applied-configuration"</span>: <span class="hljs-string">"{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"annotations\":{},\"name\":\"httpbin\",\"namespace\":\"jsonpath-playground\"}}\n"</span>
        },
        <span class="hljs-attr">"creationTimestamp"</span>: <span class="hljs-string">"2023-05-22T11:08:34Z"</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"httpbin"</span>,
        <span class="hljs-attr">"namespace"</span>: <span class="hljs-string">"jsonpath-playground"</span>,
        <span class="hljs-attr">"resourceVersion"</span>: <span class="hljs-string">"1501"</span>,
        <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"uid"</span>
    },
    <span class="hljs-attr">"secrets"</span>: [
        {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"httpbin-token-uid"</span>
        }
    ]
}
</code></pre>
<p>What if we wanted to see just the service account's annotations? Enter JSONPath.</p>
<p><img src="https://media.giphy.com/media/6o0jq0G2tfv7W/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Let's modify the above command using a (JSONPath) query expression:</p>
<pre><code class="lang-bash">kubectl get sa -n jsonpath-playground httpbin -o jsonpath=<span class="hljs-string">'{.metadata.annotations}'</span>
</code></pre>
<p>Obtaining:</p>
<pre><code class="lang-json">{<span class="hljs-attr">"eks.amazonaws.com/role-arn"</span>:<span class="hljs-string">"arn:aws:iam::123456:role/irsa-role"</span>,<span class="hljs-attr">"kubectl.kubernetes.io/last-applied-configuration"</span>:<span class="hljs-string">"{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"annotations\":{},\"name\":\"httpbin\",\"namespace\":\"jsonpath-playground\"}}\n"</span>}
</code></pre>
<p>Not the prettiest, but still way more condensed than the first version.</p>
<p>Let's get just one of them. Remember you can get into any JSON structure by specifying an index (or a key in this case since <code>annotations</code> attribute is a dictionary).</p>
<p>Here you also need to escape <code>.</code> and <code>/</code> as they are interpreted JSONPath expressions.</p>
<pre><code class="lang-bash">$ kubectl get sa -n jsonpath-playground httpbin -o jsonpath=<span class="hljs-string">'{.metadata.annotations.eks\.amazonaws\.com\/role-arn}'</span>

arn:aws:iam::123456:role/irsa-role
</code></pre>
<p>See? Nice and clear. I use this, for example, whenever I want to check if a specific value is set, and matches whatever I need it to be. It's a very quick way to do so.</p>
<blockquote>
<p>Also, you could use this in a script or an automated test.</p>
</blockquote>
<h4 id="heading-above-and-beyond">Above and beyond</h4>
<p>Let's do something else, like recursive querying and parsing the <code>httpbin</code> pod structure for its <code>tolerations</code>. Given the pod's <code>tolerations</code> attribute is a list, we can traverse it and extract its elements. Then we can also add some external information to make it more readable.</p>
<p>Let's first check the pod name:</p>
<pre><code class="lang-bash">$ kubectl get pod -n jsonpath-playground

NAME                      READY   STATUS    RESTARTS   AGE
httpbin-bf5fc5d74-zgvtl   1/1     Running   0          34m
</code></pre>
<p>And then use its name for the next query:</p>
<pre><code class="lang-bash">$ kubectl get pod -n jsonpath-playground httpbin-bf5fc5d74-zgvtl -o jsonpath=<span class="hljs-string">'{range .spec.tolerations[*]}{"key name: "}{.key}{"\n"}{end}'</span>

key name: node.kubernetes.io/not-ready
key name: node.kubernetes.io/unreachable
</code></pre>
<p>Feeling the power of the CLI on your side already? I am.</p>
<p>We have done three things:</p>
<ul>
<li><p>Traversed the <code>.spec.tolerations</code> list and extracted only the key named <code>key</code> from all items, with <code>{range .items}{.key_name}{end}</code>.</p>
</li>
<li><p>Added the prefix <code>key name:</code> to make it more readable, by adding a <code>{"key name: "}</code> in between query parameters.</p>
</li>
<li><p>Printed the output in different lines to make it even more readable, adding <code>{"\n"}</code>.</p>
</li>
</ul>
<p><img src="https://media.giphy.com/media/DoPnrkasVpMCZIpzD9/giphy.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-cleaning-up">Cleaning up</h3>
<p>Don't forget to delete Minikube's cluster from your local with:</p>
<pre><code class="lang-bash">minikube delete
</code></pre>
<h3 id="heading-also-online-because-its-not-always-about-kubernetes">Also online, because it's not always about Kubernetes</h3>
<p>And, if you don't want to spin up a local cluster or you are already tired of <strong>kubectl</strong> and Kubernetes, you can play around with one of the many online debuggers, like <a target="_blank" href="https://jsonpath.com/">this one</a>. There is a sample JSON input provided, but feel free to use the ones above as well.</p>
<blockquote>
<p>Keep in mind the syntax is slightly different than the one we saw before.</p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I really like <strong>kubectl</strong>. That's pretty obvious at this point.</p>
<p>Though I checked other tools, like <a target="_blank" href="https://k9scli.io/">k9s</a>, I found myself always coming back to it.</p>
<p>When I discovered it has JSONPath support, at first it was not that usable for me as I was not using <strong>kubectl</strong> in that depth. But after a while, it became clear that it is a pretty neat tool to use if you like JSON better than YAML.</p>
<blockquote>
<p>Kubectl also allows YAML output with -o yaml flag.</p>
</blockquote>
<p>Of course, <a target="_blank" href="https://goessner.net/articles/JsonPath/index.html#e2">there are way more options</a> than the ones we used in this example overview, and you can combine them to create much simpler or richer outputs.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://kubernetes.io/docs/reference/kubectl/jsonpath/">JSONPath support in Kubernetes (kubectl) documentation</a>.</p>
</li>
<li><p><a target="_blank" href="https://jsonpath.com/">Online JSONPath evaluator</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/istio/istio/blob/master/samples/httpbin/httpbin.yaml">Httpbin</a> YAML.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[All right then, keep your secrets in Git with SOPS]]></title><description><![CDATA[Wait, what? Don't panic, SOPS will encrypt them for you.
Big NO NO

Secrets (a.k.a. sensitive values), such as passwords or any other potentially harmful information must be protected, we all know that.
That's why you should avoid sharing those or pu...]]></description><link>https://blog.mariano.cloud/all-right-then-keep-your-secrets-in-git-with-sops</link><guid isPermaLink="true">https://blog.mariano.cloud/all-right-then-keep-your-secrets-in-git-with-sops</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[secrets]]></category><category><![CDATA[Git]]></category><category><![CDATA[KMS]]></category><category><![CDATA[SOPS]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Mon, 08 May 2023 10:13:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1683373798272/ae315606-83b7-4600-9816-066f2f12fdb1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Wait, what? Don't panic, SOPS will encrypt them for you.</p>
<h1 id="heading-big-no-no">Big NO NO</h1>
<p><img src="https://media.giphy.com/media/8vUEXZA2me7vnuUvrs/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Secrets (a.k.a. sensitive values), such as passwords or any other potentially harmful information must be protected, we all know that.</p>
<p>That's why you should avoid sharing those or pushing them to a public (or even private) code repository. Right? Well, yes and no.</p>
<h3 id="heading-but-where-do-you-store-them">But where do you store them?</h3>
<p>There are many vault-like services such as <a target="_blank" href="https://www.vaultproject.io/">Hashicorp Vault</a>, <a target="_blank" href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html">AWS Secrets Manager</a>, <a target="_blank" href="https://cloud.google.com/security-key-management">GCP KMS</a>, <a target="_blank" href="https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts">Azure Key Vault</a> and the well-known credential storages out there like <a target="_blank" href="https://1password.com/">1Password</a>.</p>
<p>All of the above offer a separate secure location to isolate the secrets from the code referencing them, but also all of them need an extra fetch process; how do you get the actual value? you add a call to the secrets store using the corresponding method and you will have the secret value exported to your app.</p>
<p>Wanna save a step? Enter SOPS.</p>
<h1 id="heading-why-sops">Why SOPS?</h1>
<p>Because it offers a simplified version of keeping sensitive information. You can directly store the values in your code and use one of AWS KMS, PGP, GCP KMS, Azure Key Vault, Hashicorp Key Vault and <a target="_blank" href="https://github.com/FiloSottile/age">age</a> (I did not know this one) to encrypt and decrypt. No need to push/pull or store the secrets somewhere else.</p>
<h1 id="heading-example">Example</h1>
<p><img src="https://media.giphy.com/media/BpGWitbFZflfSUYuZ9/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Let's sample this in a Kubernetes local scenario, using:</p>
<ul>
<li><p>Helm (Helmfile) to deploy a sample chart + Minikube hosting a simple Kubernetes cluster in your local.</p>
<ul>
<li>Please refer to <a target="_blank" href="https://blog.mariano.cloud/helm-declaration-environments-helmfile">this article about Helmfile + Minikube for a full installation walkthrough</a>. For this tutorial, I will assume both are installed in your environment.</li>
</ul>
</li>
<li><p>AWS KMS key.</p>
</li>
<li><p>SOPS.</p>
</li>
</ul>
<blockquote>
<p><a target="_blank" href="https://github.com/marianogg9/playing-with-sops">Here</a> you can find the reference code and a few Terraform templates to create KMS and IAM resources. I will be using the AWS console for this tutorial, but I added a walkthrough for Terraform <a target="_blank" href="https://github.com/marianogg9/playing-with-sops/tree/main/terraform#via-terraform">here</a>.</p>
</blockquote>
<h2 id="heading-kms">KMS</h2>
<ul>
<li><p>Manually in the AWS console:</p>
<ol>
<li><p>Access AWS KMS service.</p>
</li>
<li><p>Go to <strong>Customer-managed</strong> keys and click on <strong>Create key</strong>.</p>
</li>
<li><p>Select <strong>Symmetric</strong> + <strong>Encrypt and decrypt</strong> options, then <strong>Next</strong>.</p>
</li>
<li><p>Give it an alias and <strong>Next</strong>.</p>
</li>
<li><p>Select a <strong>Key administrator</strong> and <strong>Next</strong>.</p>
</li>
<li><p>Select a <strong>Key user</strong> (this step can be done later, after creating the IAM user), <strong>Next</strong>.</p>
</li>
<li><p><strong>Finish</strong>.</p>
</li>
</ol>
</li>
</ul>
<h2 id="heading-iam">IAM</h2>
<ul>
<li><p>Manually in the AWS console:</p>
<ol>
<li><p>Access IAM service &gt; <strong>Users</strong>.</p>
</li>
<li><p><strong>Add Users</strong>.</p>
</li>
<li><p>Give it a name &gt; <strong>Next</strong>.</p>
</li>
<li><p>Select <strong>Attach policies directly</strong> &gt; and then <strong>Create policy</strong> (this will open the new policy wizard).</p>
<ol>
<li><p>In the new policy wizard, select <strong>JSON</strong> and then paste the following JSON policy definition &gt; <strong>Next</strong>:</p>
<pre><code class="lang-json"> {
     <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
     <span class="hljs-attr">"Statement"</span>: [
         {
             <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"VisualEditor0"</span>,
             <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
             <span class="hljs-attr">"Action"</span>: [
                 <span class="hljs-string">"kms:Decrypt"</span>,
                 <span class="hljs-string">"kms:Encrypt"</span>,
                 <span class="hljs-string">"kms:DescribeKey"</span>
             ],
             <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:of:your:kms:key"</span>
         }
     ]
 }
</code></pre>
</li>
<li><p>Give the new policy a name and then click on <strong>Create policy</strong>.</p>
</li>
</ol>
</li>
<li><p>Now go back to the IAM user creation wizard (previous tab) and click the refresh icon to update the available policies list. Then enter the new policy name in the search field and select it &gt; <strong>Next</strong> &gt; <strong>Create user</strong>.</p>
</li>
<li><p>Now go back to the KMS service, select the key you created previously and select <strong>Add</strong> in the <strong>Key Users</strong> section of the key.</p>
<ol>
<li>Select the IAM user created before &gt; <strong>Add</strong>.</li>
</ol>
</li>
</ol>
</li>
</ul>
<h2 id="heading-baseline-deployment-with-helmfile">Baseline deployment with Helmfile</h2>
<p>First, clone the repo:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/marianogg9/playing-with-sops.git
</code></pre>
<p><a target="_blank" href="https://blog.mariano.cloud/helm-declaration-environments-helmfile#heading-installation">Once you have Minikube running and Helmfile installed</a>, let's deploy the sample <code>httpbin</code> app in the cluster:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> playing-with-sops/helmfile/
helmfile -e example apply
</code></pre>
<p>This will deploy <code>httpbin</code> app in your local Kubernetes environment, including a pod, a service, a service account and a secret (we will use it later).</p>
<p>Let's see how environment variables look in the freshly created pod (we will use it to compare with a later step, trust me):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683372610085/27a01790-ac37-47c2-957e-f7b38faafe42.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-and-now-the-secrets">And now: THE SECRETS</h2>
<p><img src="https://media.giphy.com/media/YQk8nXloVftzW/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Helmfile supports secrets by using <code>helm secret</code> plugin to manage and inject sensitive information into a given release values.</p>
<blockquote>
<p>The caveat here is those secret values are going to be shown in plain text in the <code>helmfile diff</code> or <code>helmfile apply</code> outputs, so be careful!</p>
<p><strong>Keep reading, there is a workaround for that.</strong></p>
</blockquote>
<h3 id="heading-helm-secrets-plugin">Helm-secrets plugin</h3>
<p>You will need to <a target="_blank" href="https://helmfile.readthedocs.io/en/latest/#secrets">install</a> <code>helm-secrets</code> plugin:</p>
<pre><code class="lang-bash">helm plugin install https://github.com/jkroepke/helm-secrets
</code></pre>
<h3 id="heading-sops">SOPS</h3>
<p>Installing:</p>
<pre><code class="lang-bash">brew install sops
</code></pre>
<p>Let's use it now! First, we need to create a <code>.sops.yaml</code> with the following content:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">creation_rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">path_regex:</span> <span class="hljs-string">\.yaml$</span>
    <span class="hljs-attr">kms:</span> <span class="hljs-string">'arn:of:your:kms:key'</span>
</code></pre>
<p>This is a global configuration file that SOPS will use as a default when encrypting/decrypting files. You can set a regex for filenames and locations and a KMS ARN to use. That KMS is the one we created before.</p>
<p>The above example will accept any names anywhere ending in <code>.yaml</code> so it doesn't matter where you create this specific config file.</p>
<blockquote>
<p>Remember always to configure your local AWS CLI to use the KMS Key User we created before. See <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html">this article</a> on how to set up and use it.</p>
</blockquote>
<p>Next, create <code>secrets.yaml</code> file within <code>helmfile/</code> directory, by running the following:</p>
<pre><code class="lang-bash">sops secrets.yaml
</code></pre>
<p>This will open a text editor with prefilled sample values. Replace the content with:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">my_var:</span> <span class="hljs-string">a</span> <span class="hljs-string">top</span> <span class="hljs-string">secret</span> <span class="hljs-string">value</span>
<span class="hljs-attr">my_other_var:</span> <span class="hljs-string">not</span> <span class="hljs-string">that</span> <span class="hljs-string">sensitive,</span> <span class="hljs-string">but</span> <span class="hljs-string">still</span> <span class="hljs-string">please</span> <span class="hljs-string">don't</span> <span class="hljs-string">tell</span> <span class="hljs-string">anyone!</span>
</code></pre>
<p>When you save and close this file, SOPS will encrypt it automatically. You can have a look at its content now, SOPS added metadata referencing the KMS (ARN) used, a timestamp and some more.</p>
<h3 id="heading-helmfile-takes-over">Helmfile takes over</h3>
<p>Now for the Helmfile part, let's add <code>secrets:</code> section to the release in <code>helmfile.yaml</code>, as follows:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="a372b99b85f76175ab363a44477750b7"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/marianogg9/a372b99b85f76175ab363a44477750b7" class="embed-card">https://gist.github.com/marianogg9/a372b99b85f76175ab363a44477750b7</a></div><p> </p>
<p>As a result, Helmfile will merge <code>values.yaml</code> and <code>secrets.yaml</code> as one and then use it to populate the charts templates to be deployed (in <code>charts/httpbin/templates/</code>).</p>
<p>Let's modify the deployment template to make use of the new secrets, by replacing the environment variables values with references to secrets in <code>secrets.yaml</code>:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="60498790876f7835e61bd5993f8c7ec9"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/marianogg9/60498790876f7835e61bd5993f8c7ec9" class="embed-card">https://gist.github.com/marianogg9/60498790876f7835e61bd5993f8c7ec9</a></div><p> </p>
<p>Now let's see what Helmfile tries to do (<code>helmfile diff</code>):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683370219796/e65e661b-0fbb-4876-99ef-2959afde5028.png" alt class="image--center mx-auto" /></p>
<p>As you can see in the output, it is first decrypting the secrets file we specified: <code>Decrypting secret /your_local_path/playing-with-sops/helmfile/secrets.yaml</code>.</p>
<p>Ok, let's apply (<code>helmfile apply</code>) and then see what changed in the deployment:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683370354036/2a9a6bfd-387a-4d46-b5ba-18168381bcea.png" alt class="image--center mx-auto" /></p>
<p>Now both environment vars values were changed to the secrets (encrypted) values.</p>
<p><img src="https://media.giphy.com/media/tlGD7PDy1w8fK/giphy.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-a-step-further-for-cicd">A step further for CI/CD</h3>
<p>What if you are trying to implement this in a CI/CD pipeline where logs are printed on stdout and so are your secrets?</p>
<p>Well, then you might want to use a Kubernetes secret definition. In this case, <code>helm secrets</code> will avoid showing secret values in plain text.</p>
<blockquote>
<p>Use a Kubernetes secret object to manage a sensitive value that you do not want to be printed in plain text in a CI/CD pipeline log.</p>
</blockquote>
<p>You can create a new <code>secret.yaml</code> template definition in <code>helmfile/charts/httpbin/templates/</code> (I included an example in <a target="_blank" href="https://github.com/marianogg9/playing-with-sops/blob/main/helmfile/secrets.yaml">the repo</a> as well):</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="643d844ed1f557b0e5270667f1f72156"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/marianogg9/643d844ed1f557b0e5270667f1f72156" class="embed-card">https://gist.github.com/marianogg9/643d844ed1f557b0e5270667f1f72156</a></div><p> </p>
<p>The <code>data</code> section contains both secrets (from <code>secrets.yaml</code>) and encrypts them to <a target="_blank" href="https://kubernetes.io/docs/concepts/configuration/secret/#restriction-names-data">allow Kubernetes API to accept their format</a>.</p>
<p>Then you can reference these secret keys in <code>deployment.yaml</code> template:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="fbbfecf845f0c6a3f1ddbe76d41fc474"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/marianogg9/fbbfecf845f0c6a3f1ddbe76d41fc474" class="embed-card">https://gist.github.com/marianogg9/fbbfecf845f0c6a3f1ddbe76d41fc474</a></div><p> </p>
<p>If we check what Helmfile does:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683370820865/621c645d-d77b-4c9f-a53a-3073ac1724a9.png" alt class="image--center mx-auto" /></p>
<p>Now the values are effectively taken from a Kubernetes secret instead of directly from secrets defined in the Helmfile release.</p>
<p>Go ahead and run <code>helmfile -e example apply</code> to deploy the changes.</p>
<p>Let's now modify <code>secrets.yaml</code> values</p>
<pre><code class="lang-bash">sops secrets.yaml
</code></pre>
<p>I removed the last character from both <code>my_var</code> and <code>my_other_var</code> in <code>secrets.yaml</code> - it doesn't really matter the change, it is just an example. Save and close the file to have SOPS re-encrypt it.</p>
<p>And now let's see what Helmfile tries to do:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683371114420/09073ff0-c7e6-4691-92c6-c04c56874f7e.png" alt class="image--center mx-auto" /></p>
<p>There are no secrets shown in plain text anymore!</p>
<p><img src="https://media.giphy.com/media/TNnyxINX87VAKbNYmZ/giphy.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>You can manage secret values directly in Git now, without extra steps to fetch from remote. It is a simple concept and it is practical if you don't have that many sensitive values to justify an external platform setup for them. Of course, it is a possible tool for a given use case, it all depends on the context.</p>
<p>I found out about this way of managing secrets when I started working with Helmfile, it is pretty well integrated and just works. Simplicity, I like it.</p>
<p><a target="_blank" href="https://github.com/mozilla/sops#sops-secrets-operations">Please have a look at all SOPS features</a>.</p>
<blockquote>
<p>Helmfile also offers secrets <a target="_blank" href="https://github.com/roboll/helmfile/blob/master/docs/remote-secrets.md">remote fetch</a> natively as well.</p>
</blockquote>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://github.com/mozilla/sops">SOPS</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/helm-declaration-environments-helmfile">Helmfile + Minikube in action</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/marianogg9/playing-with-sops">Full code example on Github</a>.</p>
</li>
<li><p><a target="_blank" href="https://helm.sh/docs/topics/plugins/">Helm plugin</a>.</p>
</li>
<li><p><a target="_blank" href="https://helmfile.readthedocs.io/en/latest/">Helmfile</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[A non-green view of (tech) Conferences]]></title><description><![CDATA[I attended KubeCon EU 2023 in Amsterdam and I have been having this recurring thought of how it looked like from my not-so-interactive side.
Money, money, money
Even though I am a huge fan of listening to and seeing new stuff, more so if they are abo...]]></description><link>https://blog.mariano.cloud/a-non-green-view-of-tech-conferences</link><guid isPermaLink="true">https://blog.mariano.cloud/a-non-green-view-of-tech-conferences</guid><category><![CDATA[Kubecon]]></category><category><![CDATA[conference]]></category><category><![CDATA[technology]]></category><category><![CDATA[money]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Sat, 22 Apr 2023 13:45:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1682170225097/67207c0f-5551-4bce-9277-a6cf272cbbd6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I attended <a target="_blank" href="https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/">KubeCon EU 2023 in Amsterdam</a> and I have been having this recurring thought of how it looked like from my not-so-interactive side.</p>
<h1 id="heading-money-money-money">Money, money, money</h1>
<p>Even though I am a huge fan of listening to and seeing new stuff, more so if they are about IT and Cloud, I often feel awkward with sales pitches. Mostly because I never consider myself as the person who calls the shots moneywise in a company - I love playing with tools, implementations, fixing problems, give me a pain point and I will make it better eventually.</p>
<p>(big) Conferences are all about sales pitching and badge scanning to send an email afterwards or have your information in a vendor/3rd DB - which is expected and completely ok, I mean, don't get me wrong, it is a business after all. Open source is backed up by big and no so big corporations that spend <strong>a lot</strong> of money to set up these events and sponsor other big Organizations like the Linux Foundation or CNCF.</p>
<p>So, all in all, anyone that puts a booth at a Conference is paying a fee, hoping to get something out of it. How do they do it? well, it is basically with your information and, in <strong>many</strong> cases, a sales pitch that <strong>eventually</strong> leads to a sales meeting and in <strong>some cases</strong> to a product demo/purchase. They will talk about how their product could help your software lifecycle, make it more secure or completely take over your infrastructure overhead.</p>
<p>And then you get a sticker. Who doesn't love stickers, right?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682094528215/143e2856-d281-4d60-aea5-4253bb00534e.jpeg" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Hint: they put up a sticker wall for a lot of Open Source projects☝️</p>
</blockquote>
<h1 id="heading-colour-system">Colour system</h1>
<p>But what happens with people who are not so into this (or any) interaction? well, in this particular case, the CNCF implemented a colour-based system of pins that you could stick to your badge and would be quick info to anyone about what kind of interaction you are comfortable with:</p>
<ul>
<li><p>Green is "talk to me".</p>
</li>
<li><p>Yellow means "only whomever I know".</p>
</li>
<li><p>Red "please, don't".</p>
</li>
</ul>
<p>Pretty good and well thought in my opinion.</p>
<h1 id="heading-my-experience"><strong>My experience</strong></h1>
<p>How did I like it? It was great, got to spend some time with my teammates out of the office, met and had a drink with the guys from the <a target="_blank" href="https://www.cncf.io/blog/2022/08/19/cloud-native-glossary-the-spanish-version-is-live/">CNCF Glossary Spanish</a> translation group (to which I recently started contributing), attended very interesting talks and got my phone Chrome tabs full of new terms and tools to try out.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682167642106/05c301a2-a0ab-4a9c-b919-d52f0974bfe7.jpeg" alt class="image--center mx-auto" /></p>
<p>I also shortlisted 4 new things, some of which I heard for the first time:</p>
<ul>
<li><p><a target="_blank" href="https://linkerd.io/">Linkerd</a>.</p>
</li>
<li><p><a target="_blank" href="https://k3s.io/">k3s/k3d</a>.</p>
</li>
<li><p><a target="_blank" href="https://www.cncf.io/blog/2022/11/17/better-together-a-kubernetes-and-wasm-case-study/">Web Assembly</a>.</p>
</li>
<li><p><a target="_blank" href="https://sched.co/1HyZ9">How to treat a maliciously hijacked pod and apply forensics to it</a>.</p>
</li>
</ul>
<p>And lastly, I (re)confirmed that I have A LOT to learn. There are tons of tools and technologies I do not know about (remember <a target="_blank" href="https://en.wikipedia.org/wiki/Impostor_syndrome">impostor syndrome</a>?), and it's OK, that's where the fun is - you will probably never get bored working in IT.</p>
<h3 id="heading-happy-surprises">Happy surprise(s)</h3>
<p>One of the speakers at a WASM workshop overheard a comment a colleague of mine said and came by our table to talk about it. This guy was so passionate about the topic that it was fantastic to hear the arguments, and not only the evident reduced overhead but the reasoning based on low level OS and containerisation architecture processing times.</p>
<blockquote>
<p>"Forget about &lt;company this person works at&gt;, I want you to implement WASM because it will reduce your deployment and operational times by 10x and...". It was a delight to be there hearing.</p>
</blockquote>
<p>My point is: you never know what you will see or who you will meet. And I was not even an active participant in the conversation 😂.</p>
<h1 id="heading-my-suggestions-for-non-green-people">My suggestions for non-green people</h1>
<p>If you, like me, are between green and yellow, how can you enjoy the most? (At least this is what I did).</p>
<ul>
<li><p>Go to a talk where the topic is something you never heard of or maybe did hear about and never worked with.</p>
</li>
<li><p>Even though we are all kinda doing the same thing, it is never the same. You can always find someone doing something different and you can learn from that.</p>
</li>
<li><p>It is always better if you attend with someone you know, but if that's not the case, it is still worth the experience.</p>
</li>
</ul>
<h1 id="heading-some-takeout-links">Some takeout links</h1>
<ul>
<li><p><a target="_blank" href="https://www.cncf.io/blog/2022/08/19/cloud-native-glossary-the-spanish-version-is-live/">CNCF glossary</a> (<a target="_blank" href="https://glossary.cncf.io/contribute/">get involved!</a>).</p>
</li>
<li><p><a target="_blank" href="https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/">KubeCon EU</a>.</p>
</li>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/Impostor_syndrome">Impostor syndrome</a>.</p>
</li>
<li><p><a target="_blank" href="https://www.cncf.io/projects/">CNCF Open Source projects landscape</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Have you attended tech Conferences? Please let me know in the comments, I am always up to reading about different experiences.</p>
]]></content:encoded></item><item><title><![CDATA[Helm + declaration + environments = Helmfile]]></title><description><![CDATA[There's this lame Argentinian saying: "What's the difference between Flores and Floresta?" it's kind of a dad joke and it feels like I'm now closer to that humour.

Know when you read a lot about a tool that is kind of the standard but for some reaso...]]></description><link>https://blog.mariano.cloud/helm-declaration-environments-helmfile</link><guid isPermaLink="true">https://blog.mariano.cloud/helm-declaration-environments-helmfile</guid><category><![CDATA[Helm]]></category><category><![CDATA[helmfile]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[minikube]]></category><category><![CDATA[Declarative]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Mon, 10 Apr 2023 11:58:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681124975475/bae8d2c6-f886-4f30-9152-20087991c7da.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>There's this lame Argentinian saying: "What's the difference between Flores and Floresta?" it's kind of a dad joke and it feels like I'm now closer to that humour.</p>
</blockquote>
<p>Know when you read a lot about a tool that is kind of the standard but for some reason you never used it? That was me a few years ago right after I got my <a target="_blank" href="https://training.linuxfoundation.org/certification/certified-kubernetes-administrator-cka/">CKA</a>, ready to start working on real production Kubernetes implementations and Helm kept popping up wherever I looked.</p>
<p><img src="https://media.giphy.com/media/3o7WIQ4FARJdpmUni8/giphy-downsized-large.gif" alt class="image--center mx-auto" /></p>
<p>But I had not had the chance to use it in my <a target="_blank" href="https://minikube.sigs.k8s.io/docs/start/">playground</a> yet, it was mostly about declaring resources in YAML, no templating... it was very newbie stuff.</p>
<p>A few months forward, I joined a new company where we used AWS and got to work on a super exciting migration to EKS from on-premises. We had multiple environments and a few applications with customised configurations to move, which required some kind of automation not only to deploy but also to make it easier to manage, support and further extend.</p>
<h1 id="heading-here-comes-helmfile">Here comes Helmfile</h1>
<p><img src="https://media.giphy.com/media/Nn5iedMSx14F4ZEb1M/giphy.gif" alt class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://helm.sh/">Helm</a> is a Kubernetes release management tool built in Go, that facilitates the creation and management of objects (grouped in charts) via templating and creation of Kubernetes manifests.</p>
<p>And <a target="_blank" href="https://github.com/helmfile/helmfile">Helmfile</a> is a declarative implementation of Helm. It offers a way of defining a set of (Helm) charts or standalone objects deployment along with their dependencies, secrets and values file separation.</p>
<p>It also offers a <code>diff</code> feature to see the eventual changes to be applied, <code>template</code> to render a given release, <code>write-values</code> writing out specific environment-bound values and <a target="_blank" href="https://helmfile.readthedocs.io/en/latest/#cli-reference">a lot more</a> (many based on Helm's native methods and plugins).</p>
<h1 id="heading-quick-walkthrough">Quick walkthrough</h1>
<p>As always, the theory is nice and all but we are here to see it working. So let's do that.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>A Kubernetes cluster.</p>
<ul>
<li>Can be anywhere, for this overview I will use <a target="_blank" href="https://minikube.sigs.k8s.io/docs/start/">Minikube</a> deployed locally.</li>
</ul>
</li>
</ul>
<h2 id="heading-installation">Installation</h2>
<p>I am running this on a Mac, so the below commands are scoped to that. If you are using a different one, all of them have their versions for other OSs.</p>
<ul>
<li><p>Clone the <a target="_blank" href="https://github.com/marianogg9/helmfiling.git">examples</a> repository.</p>
<pre><code class="lang-bash">  git <span class="hljs-built_in">clone</span> https://github.com/marianogg9/helmfiling.git
  <span class="hljs-built_in">cd</span> helmfiling
</code></pre>
</li>
<li><p><a target="_blank" href="https://minikube.sigs.k8s.io/docs/start/"><strong>Minikube</strong></a><strong>.</strong></p>
<pre><code class="lang-bash">  brew install minikube
</code></pre>
</li>
<li><p>And start a Minikube cluster.</p>
<pre><code class="lang-bash">  minikube start
</code></pre>
<p>  You can customise it a bit (e.g. by specifying a Kubernetes version) but defaults are enough (at the moment <code>v1.22.3</code>).</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/"><strong>Kubectl</strong></a><strong>.</strong></p>
<pre><code class="lang-bash">  curl -LO <span class="hljs-string">"https://dl.k8s.io/release/<span class="hljs-subst">$(curl -L -s https://dl.k8s.io/release/stable.txt)</span>/bin/darwin/amd64/kubectl"</span>
</code></pre>
</li>
<li><p><a target="_blank" href="https://helmfile.readthedocs.io/en/latest/"><strong>Helmfile</strong></a><strong>.</strong></p>
<pre><code class="lang-bash">  brew install helmfile
</code></pre>
</li>
</ul>
<h2 id="heading-general-overview">General overview</h2>
<p>I added an MVP installation <a target="_blank" href="https://github.com/marianogg9/helmfiling">here</a>.</p>
<pre><code class="lang-bash">.
├── charts
│   └── httpbin
│       ├── Chart.yaml
│       └── templates
│           ├── deployment.yaml
│           ├── service.yaml
│           └── serviceaccount.yaml
├── default-values.yaml
├── environments.yaml
├── example-environment-values.yaml
├── helmfile.yaml
└── values.yaml.gotmpl
</code></pre>
<ul>
<li><p><code>charts/</code> contains a minimal <code>httpbin</code> Helm chart definition and all the resources' templates.</p>
</li>
<li><p><code>default-values.yaml</code> is the <code>default</code> environment set of values.</p>
</li>
<li><p><code>environments.taml</code> defines both <code>default</code> and <code>example</code> environments with their respective values files.</p>
</li>
<li><p><code>example-environment-values.yaml</code> is the <code>example</code> environment set of values.</p>
</li>
<li><p><code>helmfile.yaml</code> is where all releases are defined.</p>
</li>
<li><p><code>values.yaml.gotmpl</code> uses GO templating to reference different values. This template will be used as a centralised definition to be used by different environments.</p>
</li>
</ul>
<h2 id="heading-creating-a-release">Creating a release</h2>
<p>A release is where we declare metadata for the deployment. In a very minimal version:</p>
<ul>
<li><p>Name.</p>
</li>
<li><p>Values (list of single values or files).</p>
</li>
<li><p>Chart (path to a valid local or remote chart).</p>
</li>
<li><p>Namespace (where to deploy the resources defined in the above chart).</p>
</li>
<li><p>Labels (optional, as a way of targeting a specific release object).</p>
</li>
</ul>
<p>Please have a look at <a target="_blank" href="https://github.com/marianogg9/helmfiling/blob/main/helmfile.yaml">this</a> example.</p>
<h2 id="heading-adding-a-values-file">Adding a value(s) file</h2>
<p>There are many ways of passing values to a release, one of them being a values file. This contains a set of parameters that will be referenced as <code>{{ .Values.parameterX }}</code> in a given chart resource template definition.</p>
<p>We could also set values in a standalone way as in:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">values:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">parameter_1:</span> <span class="hljs-string">"something meaningful"</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">parameter_2:</span> <span class="hljs-number">13</span>
</code></pre>
<h3 id="heading-using-a-custom-environment">Using a custom environment</h3>
<p>By default, Helmfile will assume a <code>default</code> environment. So if we do not specify one, Helmfile will use a set of values defined under a <code>default</code> environment.</p>
<p>But what if we had more than one environment or logic separation or cluster and wanted to reuse as much as possible? we can make use of GO templating and a template values file.</p>
<p>Let's use one of those bad boys for this example then, creating a <code>values.yaml.gotmpl</code> file, referencing values by <code>{{ .Values.parameterX }}</code> notation (same as within a chart resource template).</p>
<p>Once we run Helmfile specifying a different environment, it will pick up that custom environment's set of values and populate the <code>values.yaml.gotmpl</code> with them, instead of using the <code>default</code> ones.</p>
<p>We use an <code>environments.yaml</code> file to tell Helmfile what values we want to use for that specific environment; see an example <a target="_blank" href="https://github.com/marianogg9/helmfiling/blob/main/environments.yaml">here</a>.</p>
<h2 id="heading-running-helmfile">Running Helmfile</h2>
<p>The custom environment I will be using is called <code>example</code>, as defined <a target="_blank" href="https://github.com/marianogg9/helmfiling/blob/main/environments.yaml">here</a>. If you want to use a different one, declare it in <code>environments.yaml</code> and use it in the following steps.</p>
<p>The <code>-e</code> flag tells Helmfile which environment to use. If none is specified, it will assume <code>default</code>.</p>
<h3 id="heading-dry-run">Dry-run</h3>
<pre><code class="lang-bash">$ helmfile -e example diff

Building dependency release=example-release, chart=charts/httpbin
Comparing release=example-release, chart=charts/httpbin
********************

    Release was not present <span class="hljs-keyword">in</span> Helm.  Diff will show entire contents as new.

********************
example-ns, example-httpbin, Deployment (apps) has been added:
-
+ <span class="hljs-comment"># Source: example-httpbin/templates/deployment.yaml</span>
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+   name: example-httpbin
+ spec:
+   replicas: 1
+   selector:
+     matchLabels:
+       app: example-httpbin
+       version: v1
+   template:
+     metadata:
+       labels:
+         app: example-httpbin
+         version: v1
+     spec:
+       serviceAccountName: example-httpbin
+       containers:
+       - image: docker.io/kong/httpbin
+         imagePullPolicy: IfNotPresent
+         name: example-httpbin
+         ports:
+         - containerPort: 80
example-ns, example-httpbin, Service (v1) has been added:
-
+ <span class="hljs-comment"># Source: example-httpbin/templates/service.yaml</span>
+ apiVersion: v1
+ kind: Service
+ metadata:
+   name: example-httpbin
+   labels:
+     app: example-httpbin
+     service: example-httpbin
+ spec:
+   ports:
+   - name: http
+     port: 8080
+     targetPort: 80
+   selector:
+     app: example-httpbin
example-ns, example-httpbin, ServiceAccount (v1) has been added:
-
+ <span class="hljs-comment"># Source: example-httpbin/templates/serviceaccount.yaml</span>
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+   name: example-httpbin
</code></pre>
<h3 id="heading-applying">Applying</h3>
<pre><code class="lang-bash">helmfile -e example apply
</code></pre>
<p>You can also check the status of the release deployment by:</p>
<pre><code class="lang-bash">$ helmfile status

Getting status example-release
NAME: example-release
LAST DEPLOYED: Fri Apr  7 13:58:58 2023
NAMESPACE: example-ns
STATUS: deployed
REVISION: 1
TEST SUITE: None
</code></pre>
<p>Let's port forward port 8080 from the service we just deployed to our localhost.</p>
<p>First, check the deployed service:</p>
<pre><code class="lang-bash">$ kubectl get svc -n example-ns

NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
example-httpbin   ClusterIP   10.96.41.153   &lt;none&gt;        8080/TCP   4m46s
</code></pre>
<p>Port forward to <code>localhost:7000</code>:</p>
<pre><code class="lang-bash">$ kubectl port-forward -n example-ns svc/example-httpbin 7000:8080

Forwarding from 127.0.0.1:7000 -&gt; 80
Forwarding from [::1]:7000 -&gt; 80
</code></pre>
<p>Now access your <code>localhost:7000</code> in a browser and you will see <code>httpbin</code> UI:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680869233884/617cfc7c-b497-4183-b2ce-7e05e5c987ae.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-updating-and-deleting">Updating and deleting</h3>
<p>Let's say we want to update the default service port.</p>
<p>First, modify its value in <a target="_blank" href="https://github.com/marianogg9/helmfiling/blob/main/example-environment-values.yaml#L2">the values file</a> to <code>8081</code>.</p>
<p>Then we can see the modification to be applied by running:</p>
<pre><code class="lang-bash">$ helmfile -e example diff

Building dependency release=example-release, chart=charts/httpbin
Comparing release=example-release, chart=charts/httpbin
example-ns, example-httpbin, Service (v1) has changed:
  <span class="hljs-comment"># Source: example-httpbin/templates/service.yaml</span>
  apiVersion: v1
  kind: Service
  metadata:
    name: example-httpbin
    labels:
      app: example-httpbin
      service: example-httpbin
  spec:
    ports:
    - name: http
-     port: 8080
+     port: 8081
      targetPort: 80
    selector:
      app: example-httpbin
</code></pre>
<p>Now let's apply that and verify port-forwarding again:</p>
<pre><code class="lang-bash">$ helmfile -e example apply

Building dependency release=example-release, chart=charts/httpbin
Comparing release=example-release, chart=charts/httpbin
example-ns, example-httpbin, Service (v1) has changed:
  <span class="hljs-comment"># Source: example-httpbin/templates/service.yaml</span>
  apiVersion: v1
  kind: Service
  metadata:
    name: example-httpbin
    labels:
      app: example-httpbin
      service: example-httpbin
  spec:
    ports:
    - name: http
-     port: 8080
+     port: 8081
      targetPort: 80
    selector:
      app: example-httpbin

Upgrading release=example-release, chart=charts/httpbin
Release <span class="hljs-string">"example-release"</span> has been upgraded. Happy Helming!
NAME: example-release
LAST DEPLOYED: Fri Apr  7 14:09:33 2023
NAMESPACE: example-ns
STATUS: deployed
REVISION: 2
TEST SUITE: None

Listing releases matching ^example-release$
example-release    example-ns    2           2023-04-07 14:09:33.374019 +0200 CEST    deployed    example-httpbin-1.0.0


UPDATED RELEASES:
NAME              CHART            VERSION
example-release   charts/httpbin
</code></pre>
<p>Rechecking the service, we can see its port changed to <code>8081</code>:</p>
<pre><code class="lang-bash">$ kubectl get svc -n example-ns

NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
example-httpbin   ClusterIP   10.96.41.153   &lt;none&gt;        8081/TCP   11m
</code></pre>
<p>And finally, let's port forward using the new port:</p>
<pre><code class="lang-bash">$ kubectl port-forward -n example-ns svc/example-httpbin 7000:8081
Forwarding from 127.0.0.1:7000 -&gt; 80
Forwarding from [::1]:7000 -&gt; 80
</code></pre>
<p>Which will enable us to see the UI in the same localhost port as before (<code>7000</code>).</p>
<p><img src="https://media.giphy.com/media/Rl9Yqavfj2Ula/giphy.gif" alt class="image--center mx-auto" /></p>
<p><strong>This also will reconcile deleted values/resources</strong>, meaning <code>apply</code> will delete any values/resources that are no longer defined (present) in the release YAML.</p>
<p>E.g. let's delete (or comment out all content in) the <a target="_blank" href="https://github.com/marianogg9/helmfiling/blob/main/charts/httpbin/templates/deployment.yaml">deployment template</a>.</p>
<p>If we run <code>apply</code>, it will delete the deployment from the cluster:</p>
<pre><code class="lang-bash">$ helmfile -e example apply

Building dependency release=example-release, chart=charts/httpbin
Comparing release=example-release, chart=charts/httpbin
example-ns, example-httpbin, Deployment (apps) has been removed:
- <span class="hljs-comment"># Source: example-httpbin/templates/deployment.yaml</span>
- apiVersion: apps/v1
- kind: Deployment
- metadata:
-   name: example-httpbin
- spec:
-   replicas: 1
-   selector:
-     matchLabels:
-       app: example-httpbin
-       version: v1
-   template:
-     metadata:
-       labels:
-         app: example-httpbin
-         version: v1
-     spec:
-       serviceAccountName: example-httpbin
-       containers:
-       - image: docker.io/kong/httpbin
-         imagePullPolicy: IfNotPresent
-         name: example-httpbin
-         ports:
-         - containerPort: 80
+

Upgrading release=example-release, chart=charts/httpbin
Release <span class="hljs-string">"example-release"</span> has been upgraded. Happy Helming!
NAME: example-release
LAST DEPLOYED: Fri Apr  7 14:17:01 2023
NAMESPACE: example-ns
STATUS: deployed
REVISION: 2
TEST SUITE: None

Listing releases matching ^example-release$
example-release    example-ns    2           2023-04-07 14:17:01.64583 +0200 CEST    deployed    example-httpbin-1.0.0


UPDATED RELEASES:
NAME              CHART            VERSION
example-release   charts/httpbin
</code></pre>
<p>Or we could also run <code>destroy</code> to <strong>delete all resources</strong> defined in a release YAML.</p>
<pre><code class="lang-bash">helmfile -e &lt;name_of_the_environment&gt; destroy
</code></pre>
<blockquote>
<p>Keep in mind the above destroy command <strong>will NOT</strong> ask for confirmation!</p>
</blockquote>
<h2 id="heading-cleaning-up">Cleaning up</h2>
<p>As usual, don't forget to clean up!</p>
<p><img src="https://media.giphy.com/media/l0MYOw8BXSIoeXVFC/giphy.gif" alt class="image--center mx-auto" /></p>
<p>First Helmfile release(s):</p>
<pre><code class="lang-bash">$ helmfile -e example destroy

Building dependency release=example-release, chart=charts/httpbin
Listing releases matching ^example-release$
example-release    example-ns    2           2023-04-07 14:17:01.64583 +0200 CEST    deployed    example-httpbin-1.0.0

Deleting example-release
release <span class="hljs-string">"example-release"</span> uninstalled


DELETED RELEASES:
NAME
example-release
</code></pre>
<p>Then Minikube cluster(s).</p>
<pre><code class="lang-bash">$ minikube delete --all

🔥  Deleting <span class="hljs-string">"minikube"</span> <span class="hljs-keyword">in</span> hyperkit ...
💀  Removed all traces of the <span class="hljs-string">"minikube"</span> cluster.
🔥  Successfully deleted all profiles
</code></pre>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Much like IaC, Helm gives you a way of declaring a desired state of things. You define your Kubernetes objects, their dependencies, how they should behave and the values you need them to have - and every time you need to modify or recreate your stack, just run Helmfile.</p>
<p>Helmfile is an improved version of Helm, offering a declarative approach where you visualize all involved parties and their dependencies.</p>
<p>Reproducibility, versioning, templating, easy environment separation and a toolset of functions. A lot to play around with. Helmfile also implements <code>secrets</code> definitions from a variety of sources, <a target="_blank" href="https://helmfile.readthedocs.io/en/latest/#environment-secrets">have a look</a>!</p>
<p>There are also other similar tools, <a target="_blank" href="https://kustomize.io/">Kustomize</a> being probably the one I mostly read about but did not use yet.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://helm.sh/">Helm</a>.</p>
</li>
<li><p><a target="_blank" href="https://helmfile.readthedocs.io/en/latest/">Helmfile</a>.</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/">Kubectl</a>.</p>
</li>
<li><p><a target="_blank" href="https://minikube.sigs.k8s.io/docs/start/">Minikube</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/marianogg9/helmfiling">My example implementation</a>.</p>
</li>
<li><p><a target="_blank" href="https://www.cncf.io/certification/cka/">CKA</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[Troposphere: make CloudFormation legible again]]></title><description><![CDATA[Let's go back a few years. I'd just joined a new team in a new company, all the infrastructure was in AWS, and I had very little IaC experience at the time.

Some background
CloudFormation is AWS's Infrastructure as Code service by default and many o...]]></description><link>https://blog.mariano.cloud/troposphere-make-cloudformation-legible-again</link><guid isPermaLink="true">https://blog.mariano.cloud/troposphere-make-cloudformation-legible-again</guid><category><![CDATA[cloudformation]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Python]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[troposphere]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Sat, 18 Mar 2023 11:20:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678651291807/ab243932-2ecd-43e7-88ac-546be22945a4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's go back a few years. I'd just joined a new team in a new company, all the infrastructure was in AWS, and I had very little <a target="_blank" href="https://glossary.cncf.io/infrastructure-as-code/">IaC</a> experience at the time.</p>
<p><img src="https://media.giphy.com/media/oaRG0HAau2X1SU7BTw/giphy.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-some-background">Some background</h2>
<p><a target="_blank" href="https://aws.amazon.com/cloudformation/">CloudFormation</a> is AWS's Infrastructure as Code service by default and many other internal services use it behind curtains to perform certain configuration tasks and deploy their own components. It is a very powerful tool, it is fully integrated with a lot of AWS APIs and services, it is a no-brainer if you are (and you really should) managing your infrastructure via code, not using 3rd party tools.</p>
<p>But CloudFormation can be a bit too much when it comes to creating big stacks (as in infrastructure resources grouped in a logical way that makes sense application-wise. E.g. a load balancer, security groups, EC2 autoscaling groups, RDS databases and S3 buckets all serving a single application).</p>
<p>As CloudFormation defines the resources either in a JSON or YAML file, the template file will grow as you add more AWS objects. This, as you may imagine, will get messy and prone to time-wasting, eventual errors and most of all: difficult to read.</p>
<p>Of course, one could argue, why not decouple resources into function stacks? Sure, valid point - but everything depends on the use case. Even though you have the perfect tool for 98% of infrastructure topologies and ways of managing almost all examples you read about, there is always something a bit different. And that's where the fun begins.</p>
<blockquote>
<p>It is worth mentioning, these were <a target="_blank" href="https://aws.amazon.com/blogs/developer/introducing-the-aws-cdk-public-roadmap/">pre-CDK</a> times and we had not fully implemented Terraform because..reasons.</p>
</blockquote>
<p>So how could we define resources in a programmatic, readable (short!) and maintainable way? Yes, maybe you already guessed from the article title, <a target="_blank" href="https://github.com/cloudtools/troposphere">Troposphere</a>.</p>
<h2 id="heading-troposphere">Troposphere</h2>
<p>This is a Python library that creates AWS CloudFormation definitions (templates). These will then be used as descriptors sent to CloudFormation API to create and manage resources.</p>
<p>In short, you define a resource using a given class and it creates a definition (template) out of it that you can then use to pass to a CloudFormation API method (like <code>create stack</code>). Sound pretty familiar huh? CDK vibes?</p>
<p><img src="https://media.giphy.com/media/L3ERvA6jWCd0qO4NdX/giphy.gif" alt class="image--center mx-auto" /></p>
<p>A couple of great things about Troposphere:</p>
<ul>
<li><p>Active community.</p>
</li>
<li><p><a target="_blank" href="https://troposphere.readthedocs.io/en/latest/index.html#examples-of-the-error-checking-full-tracebacks-removed-for-clarity">Property and type check built in</a> (meaning it will output errors if a resource is malformed or badly initialized).</p>
</li>
<li><p>It is written in (and uses) Python.</p>
</li>
<li><p>There are a lot of implementation <a target="_blank" href="https://github.com/cloudtools/troposphere/tree/main/examples">examples</a>.</p>
</li>
</ul>
<p>But not everything is roses: this is not an official AWS tool, which means it depends on community contribution to support any AWS service updates - so it can get behind, as expected.</p>
<h2 id="heading-quick-walkthrough">Quick walkthrough</h2>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li><p>An AWS account with at least a VPC in any region.</p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html">AWS CLI</a> configured with IAM credentials allowing to create/delete EC2 resources (security groups, subnets and instances).</p>
</li>
<li><p>(option) An existing <a target="_blank" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html">EC2 KeyPair</a>, if you want to test out a SSH connection to the created instances.</p>
</li>
<li><p>Python &gt;= 3.11.2 (that's the one I tested this with).</p>
</li>
</ul>
<h3 id="heading-installation">Installation</h3>
<p>I always recommend using a (Python) <a target="_blank" href="https://docs.python.org/3/library/venv.html">virtual environment</a> to avoid messing around with any other locally installed version and dependencies you may have..but yeah, it's a personal preference, not required.</p>
<pre><code class="lang-bash">python3 -m venv new_env
<span class="hljs-built_in">source</span> new_env/bin/activate
</code></pre>
<pre><code class="lang-bash">pip install troposphere
</code></pre>
<h3 id="heading-example-templates">Example template(s)</h3>
<p>I added a couple of <a target="_blank" href="https://github.com/marianogg9/troposphering">example scripts in this repo</a> to showcase the functionalities.</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/marianogg9/troposphering.git
</code></pre>
<p>There are two folders:</p>
<pre><code class="lang-bash">.
├── README.md
├── instances
│   ├── instances.py
│   └── instances_input
└── subnets
    ├── subnets.py
    └── subnets_input
</code></pre>
<p>Each folder contains a script and an input file, where you will add your current AWS account values, such as:</p>
<ul>
<li><p>VPC id.</p>
</li>
<li><p>Resources names.</p>
</li>
<li><p>CIDRs.</p>
</li>
<li><p>Region.</p>
</li>
<li><p>(optional) Common tags.</p>
</li>
<li><p>EC2 KeyPair name.</p>
</li>
<li><p>(optional) Your local IP CIDR (if you want to test out the SSH connection).</p>
</li>
<li><p>Etc.</p>
</li>
</ul>
<p>Once you added the corresponding values, change the input files names to be: <code>subnets_input.json</code> and <code>instances_input.json</code>.</p>
<blockquote>
<p>One important detail: "instances" stack is dependent on "subnets" stack as it references the subnet names from the latter.</p>
</blockquote>
<p>Run the scripts to generate both templates:</p>
<pre><code class="lang-python">cd subnets
python3 subnets.py

cd instances
python3 instance.py
</code></pre>
<p>The first script will create a template along these lines (defining a set of subnets in a given input VPC + exporting the values to be used by the second template later on):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Outputs:</span>
  <span class="hljs-attr">a:</span>
    <span class="hljs-attr">Export:</span>
      <span class="hljs-attr">Name:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">'${AWS::StackName}-a'</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">'a'</span>
  <span class="hljs-attr">b:</span>
    <span class="hljs-attr">Export:</span>
      <span class="hljs-attr">Name:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">'${AWS::StackName}-b'</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">'b'</span>
<span class="hljs-attr">Resources:</span>
  <span class="hljs-attr">a:</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-attr">AvailabilityZone:</span> <span class="hljs-string">a</span>
      <span class="hljs-attr">CidrBlock:</span> <span class="hljs-string">some-cidr</span>
      <span class="hljs-attr">MapPublicIpOnLaunch:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">Tags:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">subnet-a</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Description</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Playing</span> <span class="hljs-string">around</span> <span class="hljs-string">with</span> <span class="hljs-string">CloudFormation</span> <span class="hljs-string">and</span> <span class="hljs-string">Troposphere</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">CommongTag2</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Just</span> <span class="hljs-string">to</span> <span class="hljs-string">add</span> <span class="hljs-string">one</span> <span class="hljs-string">more</span>
      <span class="hljs-attr">VpcId:</span> <span class="hljs-string">vpc-abcdefgh</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Subnet</span>
  <span class="hljs-attr">b:</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-attr">AvailabilityZone:</span> <span class="hljs-string">b</span>
      <span class="hljs-attr">CidrBlock:</span> <span class="hljs-number">172.30</span><span class="hljs-number">.124</span><span class="hljs-number">.80</span><span class="hljs-string">/28</span>
      <span class="hljs-attr">MapPublicIpOnLaunch:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">Tags:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">subnet-b</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Description</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Playing</span> <span class="hljs-string">around</span> <span class="hljs-string">with</span> <span class="hljs-string">CloudFormation</span> <span class="hljs-string">and</span> <span class="hljs-string">Troposphere</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">CommongTag2</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Just</span> <span class="hljs-string">to</span> <span class="hljs-string">add</span> <span class="hljs-string">one</span> <span class="hljs-string">more</span>
      <span class="hljs-attr">VpcId:</span> <span class="hljs-string">vpc-abcdefgh</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Subnet</span>
</code></pre>
<p>And this will be the second generated file (creating an EC2 instance, in one of the above to-be-created subnets, and a set of security groups):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Outputs:</span>
  <span class="hljs-attr">firstIntanceInstanceID:</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">'firstIntance'</span>
  <span class="hljs-attr">firstIntancePrivateIP:</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">'firstIntance.PrivateIp'</span>
  <span class="hljs-attr">firstIntancePublicIP:</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">'firstIntance.PublicIp'</span> <span class="hljs-comment"># we will use this value to test SSH connection</span>
  <span class="hljs-attr">instanceNumberTwoInstanceID:</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">'instanceNumberTwo'</span>
  <span class="hljs-attr">instanceNumberTwoPrivateIP:</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">'instanceNumberTwo.PrivateIp'</span>
  <span class="hljs-attr">instanceNumberTwoPublicIP:</span>
    <span class="hljs-attr">Value:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">'instanceNumberTwo.PublicIp'</span> <span class="hljs-comment"># we will use this value to test SSH connection</span>
<span class="hljs-attr">Resources:</span>
  <span class="hljs-attr">defaultSG:</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-attr">GroupDescription:</span> <span class="hljs-string">This</span> <span class="hljs-string">is</span> <span class="hljs-string">the</span> <span class="hljs-string">default</span> <span class="hljs-string">Security</span> <span class="hljs-string">Group</span>
      <span class="hljs-attr">SecurityGroupEgress:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">CidrIp:</span> <span class="hljs-string">someOtherCidr</span>
          <span class="hljs-attr">FromPort:</span> <span class="hljs-number">80</span>
          <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
          <span class="hljs-attr">ToPort:</span> <span class="hljs-number">80</span>
      <span class="hljs-attr">SecurityGroupIngress:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">CidrIp:</span> <span class="hljs-string">someCidr</span> <span class="hljs-comment"># You can add your local public IP CIDR to test the SSH connection</span>
          <span class="hljs-attr">FromPort:</span> <span class="hljs-number">22</span>
          <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
          <span class="hljs-attr">ToPort:</span> <span class="hljs-number">22</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">CidrIp:</span> <span class="hljs-string">someOtherCidr</span>
          <span class="hljs-attr">FromPort:</span> <span class="hljs-number">123</span>
          <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
          <span class="hljs-attr">ToPort:</span> <span class="hljs-number">123</span>
      <span class="hljs-attr">Tags:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">defaultSG</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Description</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Playing</span> <span class="hljs-string">around</span> <span class="hljs-string">with</span> <span class="hljs-string">CloudFormation</span> <span class="hljs-string">and</span> <span class="hljs-string">Troposphere</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">CommongTag2</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Just</span> <span class="hljs-string">to</span> <span class="hljs-string">add</span> <span class="hljs-string">one</span> <span class="hljs-string">more</span>
      <span class="hljs-attr">VpcId:</span> <span class="hljs-string">vpc-abcdefgh</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SecurityGroup</span>
  <span class="hljs-attr">firstIntance:</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-attr">ImageId:</span> <span class="hljs-string">ami-123</span>
      <span class="hljs-attr">InstanceType:</span> <span class="hljs-string">t2.micro</span>
      <span class="hljs-attr">KeyName:</span> <span class="hljs-string">YourExistingKeyPair</span>
      <span class="hljs-attr">SecurityGroupIds:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">'defaultSG'</span>
      <span class="hljs-attr">SubnetId:</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">'AddingSubnetsWithTroposphere-someregiona'</span>
      <span class="hljs-attr">Tags:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">firstIntance</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Description</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Playing</span> <span class="hljs-string">around</span> <span class="hljs-string">with</span> <span class="hljs-string">CloudFormation</span> <span class="hljs-string">and</span> <span class="hljs-string">Troposphere</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">CommongTag2</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Just</span> <span class="hljs-string">to</span> <span class="hljs-string">add</span> <span class="hljs-string">one</span> <span class="hljs-string">more</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Instance</span>
  <span class="hljs-attr">instanceNumberTwo:</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-attr">ImageId:</span> <span class="hljs-string">ami-123</span>
      <span class="hljs-attr">InstanceType:</span> <span class="hljs-string">t2.micro</span>
      <span class="hljs-attr">KeyName:</span> <span class="hljs-string">YourExistingKeyPair</span>
      <span class="hljs-attr">SecurityGroupIds:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">'defaultSG'</span>
      <span class="hljs-attr">SubnetId:</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">'AddingSubnetsWithTroposphere-someregionb'</span>
      <span class="hljs-attr">Tags:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">instanceNumberTwo</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Description</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Playing</span> <span class="hljs-string">around</span> <span class="hljs-string">with</span> <span class="hljs-string">CloudFormation</span> <span class="hljs-string">and</span> <span class="hljs-string">Troposphere</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">CommongTag2</span>
          <span class="hljs-attr">Value:</span> <span class="hljs-string">Just</span> <span class="hljs-string">to</span> <span class="hljs-string">add</span> <span class="hljs-string">one</span> <span class="hljs-string">more</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Instance</span>
</code></pre>
<p>These YAML templates use CloudFormation <code>ImportValue</code>, <code>GetAtt</code>, <code>Ref</code> <a target="_blank" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html">intrinsic functions</a> to fetch and reference values either defined externally (like Subnet Names from the first stack) or locally.</p>
<p>Now use those templates to create the resources:</p>
<pre><code class="lang-bash">$ <span class="hljs-built_in">cd</span> subnets
$ python3 subnets.py <span class="hljs-comment"># to create the YAML template</span>
$ aws cloudformation create-stack --stack-name AddingSubnetsWithTroposphere --template-body file://subnets_template.yaml
</code></pre>
<p>The stack name <code>AddingSubnetsWithTroposphere</code> is part of the <code>instances_input.json</code>, so if you want to use a different stack name, please remember to update it in the values before running:</p>
<pre><code class="lang-bash">$ <span class="hljs-built_in">cd</span> instances
$ python3 instances.py <span class="hljs-comment"># to create the YAML template</span>
$ aws cloudformation create-stack --stack-name AddingInstancesWithTroposphere --template-body file://instances_template.yaml
</code></pre>
<p>Each of the above will output the CFN stack id, something like:</p>
<pre><code class="lang-bash">{
    <span class="hljs-string">"StackId"</span>: <span class="hljs-string">"arn:aws:cloudformation:&lt;your_region&gt;:&lt;your_aws_account_id&gt;:stack/AddingInstancesWithTroposphere/&lt;CFN_stack_id&gt;"</span>
}
</code></pre>
<p><strong>(optional)</strong> After a few minutes (to give some time for the instances to be ready), you can test the SSH connection by:</p>
<pre><code class="lang-bash">ssh -i your_key_pair ec2-user@&lt;InstanceXPublicIP&gt;
</code></pre>
<p>Where <code>&lt;InstanceXPublicIP&gt;</code> is an output value available in the <code>AddingInstancesWithTroposphere</code> CFN stack. You can check all output values in CloudFormation console &gt; <code>AddingInstancesWithTroposphere</code> stack &gt; <strong>Outputs</strong> tab.</p>
<p><img src="https://media.giphy.com/media/MBVemoHuyw9Ik/giphy.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-dont-forget-to-clean-up">Don't forget to clean up!</h3>
<p>Delete both CloudFormation stacks, either via the console or using the CLI:</p>
<ul>
<li><p><code>aws cloudformation delete-stack --stack-name &lt;Stack Name&gt;</code></p>
<ul>
<li>It does not print any output, but you can always check in the console.</li>
</ul>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This was my first proper IaC tool deep dive experience, and although my teammates were the ones who implemented it from scratch, it was a great first step.</p>
<p>Looking back I think Terraform and mostly CDK have taken over this approach, but again this is a very simple concept, pretty easy to use once you get a hold of it. The Community is super active and they are open to getting help. If you know Python and like the tool, don't think it twice and <a target="_blank" href="https://github.com/cloudtools/troposphere#community">open an issue</a>!</p>
<p>This implementation can use a couple more iterations like automating CFN stacks creation directly from a parent script and adding some more code reusing into the scripts regarding mapping regions or AMIs and instance types. I will be updating <a target="_blank" href="https://github.com/marianogg9/troposphering">the repo</a>, stay tuned!</p>
<p>Last but not least as a suggestion, if you are working with CloudFormation or IaC on AWS resources, remember to always check the CloudFormation reference documentation on a given resource, e.g. for an <a target="_blank" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html">EC2 instance</a>. It explains which attribute can be updated on the fly without interruption (replacement) - it has saved me more than once.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://troposphere.readthedocs.io/en/latest/index.html">Troposphere</a> official docs.</p>
<ul>
<li>And its <a target="_blank" href="https://github.com/cloudtools/troposphere">repository</a>.</li>
</ul>
</li>
<li><p>CNCF glossary: <a target="_blank" href="https://www.cncf.io/blog/2020/06/29/infrastructure-as-code-a-non-boring-guide-for-the-clueless/">Infrastructure as Code</a>.</p>
</li>
<li><p>AWS <a target="_blank" href="https://aws.amazon.com/cdk/">CDK</a>.</p>
</li>
<li><p><a target="_blank" href="https://boto.cloudhackers.com/en/latest/">Boto</a> documentation.</p>
</li>
<li><p>AWS CloudFormation <a target="_blank" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html">intrinsic functions</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/marianogg9/troposphering">Example implementation repo</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[AWS estimated charges - get notified in a few steps!]]></title><description><![CDATA[Whenever we deploy resources on AWS, we need to keep in mind that costs are something to monitor closely.

AWS offers a Free Tier, with the most used services for you to start exploring. And some of them are always free (within a given quota)!

If yo...]]></description><link>https://blog.mariano.cloud/aws-estimated-charges-get-notified-in-a-few-steps</link><guid isPermaLink="true">https://blog.mariano.cloud/aws-estimated-charges-get-notified-in-a-few-steps</guid><category><![CDATA[shorts]]></category><category><![CDATA[AWS]]></category><category><![CDATA[costs]]></category><category><![CDATA[billing]]></category><category><![CDATA[#CloudWatch]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Sun, 12 Mar 2023 16:19:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678651182545/8a07749d-c282-423d-95ef-bffa800cb3f8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Whenever we deploy resources on AWS, we need to keep in mind that costs are something to monitor closely.</p>
<p><img src="https://media.giphy.com/media/3oKIPm3BynUpUysTHW/giphy-downsized-large.gif" alt class="image--center mx-auto" /></p>
<p>AWS offers a Free Tier, with the most used services for you to start exploring. And some of them are always free (within a given quota)!</p>
<blockquote>
<p>If you are curious, here is the <a target="_blank" href="https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&amp;all-free-tier.sort-order=asc&amp;awsf.Free%20Tier%20Types=*all&amp;awsf.Free%20Tier%20Categories=*all">free tier offering vs quotas vs services</a>.</p>
</blockquote>
<p>AWS also offers a quick way of monitoring your usage and costs, which consists in <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html">enabling an alert whenever your forecasted costs reach a certain threshold</a> using CloudWatch alarms and AWS SNS service (to send email notifications).</p>
<p>To add this feature, you will need to have access to your <a target="_blank" href="https://console.aws.amazon.com/billing/">AWS Billing console</a> and logged as a user with proper permissions or use the root account (not recommended).</p>
<h2 id="heading-enabling-notifications">Enabling notifications</h2>
<p>In <strong>Billing Preferences</strong> &gt; under <strong>Cost Management Preferences</strong> tick the box <strong>Receive Billing Alerts</strong> and save preferences. Also, tick <strong>Receive Free Tier Usage Alerts</strong> if you want to be notified about Free Tier usage.</p>
<h2 id="heading-creating-an-alarm">Creating an alarm</h2>
<blockquote>
<p>Before starting, change your current region to <strong>us-east-1 (N. Virginia)</strong>. This is where the Billing information is stored and represents worldwide charges.</p>
</blockquote>
<p>In <a target="_blank" href="https://console.aws.amazon.com/cloudwatch/">CloudWatch console</a>, go to <strong>Alarms</strong> in the left pane and choose <strong>All Alarms</strong>.</p>
<ul>
<li><p><strong>Create alarm</strong> (top right).</p>
</li>
<li><p>Then <strong>Select metric</strong>.</p>
</li>
<li><p>In <strong>Browse</strong>, select <strong>Billing</strong> and then <strong>Total Estimated Charge</strong>.</p>
</li>
<li><p>Select the <strong>EstimatedCharges</strong> metric row and then <strong>Select metric</strong>.</p>
</li>
<li><p>For <strong>Statistics</strong>, select <strong>Maximum</strong>.</p>
</li>
<li><p>For <strong>Period</strong>, select 6 hours.</p>
</li>
<li><p>Next, for <strong>Threshold type</strong>, choose <strong>Static.</strong></p>
</li>
<li><p>For <strong>Whenever EstimatedCharges is...</strong>, choose <strong>Greater</strong>.</p>
</li>
<li><p>For <strong>than...</strong> enter a threshold that will trigger this alarm. For this example, I used 2 USD as the value threshold.</p>
<ul>
<li>For reference, I have mine set to 5 USD and it is more than enough to play around with my implementations.</li>
</ul>
</li>
<li><p>In <strong>Additional configuration</strong>, leave <strong>Datapoints to alarm</strong> default 1 out of 1 and <strong>Treat missing data as missing.</strong></p>
</li>
<li><p>Next.</p>
</li>
<li><p>In <strong>Notification</strong> section:</p>
<ul>
<li><p>under <strong>Alarm</strong> state trigger, select <strong>In Alarm</strong>.</p>
</li>
<li><p><strong>Send a notification to the following SNS topic</strong>, you can select an existing topic or create a new one (or use a topic ARN in a different account).</p>
<ul>
<li><p>(In my case, I created a new topic).</p>
</li>
<li><p>Give it a descriptive name.</p>
</li>
<li><p>Add an email account to send emails to.</p>
</li>
<li><p><strong>Create topic</strong>.</p>
<ul>
<li><p>You will get an email asking to confirm the subscription. If you don't receive it in 5 minutes, check your Spam folder (mine got filtered by Gmail).</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678198021145/6565f91a-b6bc-43d8-ab89-b50baebf1b5a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>If for some reason you cannot confirm it via email, you can do it manually in the SNS console using the link generated after you click on <strong>Create topic</strong>.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Next.</p>
</li>
<li><p>Add a name for the alarm, and an optional description.</p>
</li>
<li><p>Preview the alarm and click <strong>Create alarm</strong>.</p>
</li>
</ul>
<p>Then you will be able to see the alarm.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678197763862/ed35e8f1-2471-414f-9c9e-15e68b20ec94.png" alt class="image--center mx-auto" /></p>
<p>First in an <strong>Insufficient data</strong> status, then <strong>OK</strong> in a few minutes.</p>
<p>If you click on the alarm name you will be able to see its metrics over time. Keep in mind it is fetching data every 6 hours.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678197889842/0b885704-dc5a-4da8-a20c-7fc464f8fa60.png" alt class="image--center mx-auto" /></p>
<p><strong>FIN!</strong> You now have a reliable alarm checking every 6 hours for your estimated charges.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Keep in mind the threshold you set for the alarm is a forecast value, based on current and past usage in your account. AWS will calculate this value as an estimation of what could be the final number on your current month's bill.</p>
<p>The actual charge could be way less. To check on the actual vs forecast, please visit your <a target="_blank" href="https://console.aws.amazon.com/billing/">Billing console</a>.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html">Create Billing alarms with CloudWatch</a> on estimated charges.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&amp;all-free-tier.sort-order=asc&amp;awsf.Free%20Tier%20Types=*all&amp;awsf.Free%20Tier%20Categories=*all">AWS Free Tier</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[Airflow in ECS with Redis - Part 3: docker compose]]></title><description><![CDATA[Previously in Deploying Airflow in ECS using S3 as DAG storage via Terraform, I described how to deploy all components in AWS ECS using a hybrid EC2/Fargate launch type and S3 as DAG storage.
Now let's do the same, but with three main differences:


...]]></description><link>https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-3-docker-compose</link><guid isPermaLink="true">https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-3-docker-compose</guid><category><![CDATA[AWS]]></category><category><![CDATA[ECS]]></category><category><![CDATA[airflow]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[fargate]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Fri, 03 Mar 2023 09:36:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677437508938/66d81f7c-4a8d-4dda-ba75-595210220fd0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously in <a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-2-hands-on">Deploying Airflow in ECS using S3 as DAG storage via Terraform</a>, I described how to deploy all components in AWS ECS using a hybrid EC2/Fargate launch type and S3 as DAG storage.</p>
<p>Now let's do the same, but with three main differences:</p>
<p><img src="https://media.giphy.com/media/C6JQPEUsZUyVq/giphy.gif" alt="James Franco same same" class="image--center mx-auto" /></p>
<ul>
<li><p>Using <code>docker compose</code> <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/">integration with AWS ECS</a>, which uses AWS CloudFormation behind curtains, instead of doing it via Terraform.</p>
<ul>
<li>Fewer hybrid tools, you just need <a target="_blank" href="https://docs.docker.com/desktop/">Docker Desktop</a> installed locally.</li>
</ul>
</li>
<li><p>All components running on Fargate ECS launch type.</p>
</li>
<li><p>AWS EFS as DAG storage instead of S3.</p>
<ul>
<li>No need to worry about S3 mount drivers, <code>docker compose</code> natively <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/#volumes">integrates with AWS EFS</a>.</li>
</ul>
</li>
</ul>
<h1 id="heading-components">Components</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677836137173/7ac0d1cb-affc-4930-b27b-2764aa7b6a7a.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-deploy">Deploy</h1>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><a target="_blank" href="https://docs.docker.com/desktop/">Docker Desktop</a>.</p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html">AWS CLI configured</a> in your local using a set of credentials with:</p>
<ul>
<li><p>Baseline <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/#requirements">required permissions</a>.</p>
</li>
<li><p>Additional permissions:</p>
<ul>
<li><p><code>ec2:DescribeVpcAttribute</code></p>
</li>
<li><p><code>elasticfilesystem:DescribeFileSystems</code></p>
</li>
<li><p><code>elasticfilesystem:CreateFileSystem</code></p>
</li>
<li><p><code>elasticfilesystem:DeleteFileSystem</code></p>
</li>
<li><p><code>elasticfilesystem:CreateAccessPoint</code></p>
</li>
<li><p><code>elasticfilesystem:DeleteAccessPoint</code></p>
</li>
<li><p><code>elasticfilesystem:CreateMountTarget</code></p>
</li>
<li><p><code>elasticfilesystem:DeleteMountTarget</code></p>
</li>
<li><p><code>elaticfilesystem:DescribeAccessPoints</code></p>
</li>
<li><p><code>elasticfilesystem:DescribeMountTargets</code></p>
</li>
<li><p><code>elasticfilesystem:DescribeFileSystemPolicy</code></p>
</li>
<li><p><code>elasticfilesystem:DescribeBackupPolicy</code></p>
</li>
<li><p><code>logs:TagResource</code></p>
</li>
<li><p><code>iam:PutRolePolicy</code></p>
</li>
<li><p><code>iam:DeleteRolePolicy</code></p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-getting-started.html">VPC</a> + <a target="_blank" href="https://docs.aws.amazon.com/vpc/latest/userguide/working-with-subnets.html#create-subnets">subnets</a>.</p>
</li>
<li><p>Security Group to be used in this deployment.</p>
<ul>
<li><p>Create the Security Group.</p>
<pre><code class="lang-bash">  aws ec2 create-security-group --group-name Airflow --description <span class="hljs-string">"Airflow traffic"</span> --vpc-id <span class="hljs-string">"&lt;your_vpc_id&gt;"</span>
</code></pre>
<p>  It creates a default egress rule to <code>0.0.0.0/0</code>. Take note of the output <code>GroupId</code>.</p>
<pre><code class="lang-bash">  {
      <span class="hljs-string">"GroupId"</span>: <span class="hljs-string">"take note of this value"</span>
  }
</code></pre>
</li>
<li><p>Add internal rules to it.</p>
<ul>
<li><p>Self traffic.</p>
<pre><code class="lang-bash">  aws ec2 authorize-security-group-ingress --group-id <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span> --protocol all --source-group <span class="hljs-string">"&lt;airflow_SG_above_created_id"</span>
</code></pre>
</li>
<li><p>Internal VPC traffic.</p>
<pre><code class="lang-bash">  aws ec2 authorize-security-group-ingress --group-id <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span> --ip-permissions IpProtocol=-1,FromPort=-1,ToPort=-1,IpRanges=<span class="hljs-string">"[{CidrIp=&lt;your_vpc_cidr&gt;,Description='Allow VPC internal traffic'}]"</span>
</code></pre>
</li>
<li><p>(optional) Add a rule for your public IP to access ports <code>5555</code> (Flower service) and <code>8080</code> (Webserver service).</p>
<pre><code class="lang-bash">  aws ec2 authorize-security-group-ingress --group-id <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span> --ip-permissions IpProtocol=tcp,FromPort=5555,ToPort=5555,IpRanges=<span class="hljs-string">"[{CidrIp=&lt;your_public_CIDR&gt;,Description='Allow Flower access'}]"</span> IpProtocol=tcp,FromPort=8080,ToPort=8080,IpRanges=<span class="hljs-string">"[{CidrIp=&lt;your_public_CIDR&gt;,Description='Allow Webserver access'}]"</span>
</code></pre>
<p>  <strong>NOTE:</strong> there is <a target="_blank" href="https://github.com/docker/compose-cli/issues/1783">currently no way</a> (natively) of avoiding CloudFormation to create a <code>0.0.0.0/0</code> rule in the SG for exposed ports in declared services. If you need to narrow down this access, you will have to delete the additional rules from the SG while <code>docker compose</code> creates the ECS services.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-steps">Steps</h2>
<ul>
<li><p>Clone the repo:</p>
<pre><code class="lang-bash">  $ git <span class="hljs-built_in">clone</span> https://github.com/marianogg9/airflow-in-ecs-with-compose local_dir
  $ <span class="hljs-built_in">cd</span> local_dir
</code></pre>
</li>
<li><p>Set required variables in <code>docker-compose.yaml</code>:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">x-aws-vpc:</span> <span class="hljs-string">"your VPC id"</span>
  <span class="hljs-attr">networks:</span>
    <span class="hljs-attr">back_tier:</span>
      <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span>
</code></pre>
</li>
<li><p>(optional) If you want to use a custom password for the Webserver admin user (default user <code>airflow</code>):</p>
<ul>
<li><p>This password will be created as an AWS Secrets Manager <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/#secrets">secret</a> and its ARN will be passed as an environment variable with the following format:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">secrets:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">_AIRFLOW_WWW_USER_PASSWORD</span>
    <span class="hljs-attr">valueFrom:</span> <span class="hljs-string">&lt;secrets_manager_secret_arn&gt;</span>
</code></pre>
<ul>
<li><p>Add a custom password in a local file:</p>
<pre><code class="lang-bash">  <span class="hljs-built_in">echo</span> <span class="hljs-string">'your_custom_password'</span> &gt; ui_admin_password
</code></pre>
</li>
<li><p>Add a <code>secrets</code> definition block in <code>docker-compose.yaml</code>:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">secrets:</span>
    <span class="hljs-attr">ui_admin_password:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">_AIRFLOW_WWW_USER_PASSWORD</span>
      <span class="hljs-attr">file:</span> <span class="hljs-string">./ui_admin_password.txt</span>
</code></pre>
</li>
<li><p>Add a <code>secrets</code> section in each service to mount to:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">secrets:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">_AIRFLOW_WWW_USER_PASSWORD</span>
</code></pre>
</li>
<li><p>Add the following required AWS Secrets Manager permissions to the IAM credentials you set <code>docker context</code> to use.</p>
<ul>
<li><p><code>secretsmanager:CreateSecret</code>.</p>
</li>
<li><p><code>secretsmanager:DeleteSecret</code>.</p>
</li>
<li><p><code>secretsmanager:GetSecretValue</code>.</p>
</li>
<li><p><code>secretsmanager:DescribeSecret</code>.</p>
</li>
<li><p><code>secretsmanager:TagResource</code>.</p>
<ul>
<li><p>and please narrow down the above permissions to the secret ARN:</p>
<p>  <code>arn:aws:secretsmanager:&lt;your_aws_region&gt;:&lt;your_aws_account_id&gt;:secret:AIRFLOWWWWUSERPASSWORD*</code></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Create a new <code>docker</code> context, selecting a preferred method of obtaining IAM credentials (either via environment variables, a named local profile or a set of <code>key:secret</code>):</p>
<pre><code class="lang-bash">  docker context create ecs new-context-name
</code></pre>
</li>
<li><p>Use the newly created context:</p>
<pre><code class="lang-bash">  docker context use new-context-name
</code></pre>
</li>
<li><p>(optional) Review CloudFormation template to be applied, via:</p>
<pre><code class="lang-bash">  docker compose convert
</code></pre>
</li>
<li><p>Deploy:</p>
<pre><code class="lang-bash">  docker compose up
</code></pre>
</li>
</ul>
<p>Once the deployment starts, <code>docker compose</code> will show updates on screen. You can also follow up on the resources creation in the (AWS) CloudFormation console.</p>
<h3 id="heading-web-access">Web access</h3>
<ul>
<li><p>Get NLB name:</p>
<pre><code class="lang-bash">  aws elbv2 describe-load-balancers | grep DNSName | awk <span class="hljs-string">'{print$2}'</span> | sed -e <span class="hljs-string">'s|,||g'</span>
</code></pre>
<p>  Or if you have <code>jq</code> installed:</p>
<pre><code class="lang-bash">  aws elbv2 describe-load-balancers | jq .LoadBalancers[].DNSName
</code></pre>
</li>
<li><p>Webserver: <code>http://NLBDNSName:8080</code>. Login with <code>airflow:airflow</code> or <code>airflow:your_custom_created_password</code>.</p>
</li>
<li><p>Flower: <code>http://NLBDNSName:5555</code>.</p>
</li>
</ul>
<h1 id="heading-running-an-example-pipeline">Running an example pipeline</h1>
<p>I have included an <a target="_blank" href="https://github.com/marianogg9/airflow-in-ecs-with-compose/blob/main/example-dag/process-employees.py">example DAG</a> (from <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/tutorial/pipeline.html">Airflow's examples</a>) in the repo. This DAG is also being fetched by <code>airflow-scheduler</code> task in startup time, so it will be available in Webserver UI.</p>
<p>As a prerequisite, create a PostgreSQL connection (to be then used by the DAG). In Webserver UI &gt; Admin &gt; Connections &gt; Add <code>+</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677755729394/6a0403a5-b50f-4495-9151-df48892e109f.png" alt class="image--center mx-auto" /></p>
<p>With the following parameters:</p>
<ul>
<li><p>Connection Id: <code>tutorial_pg_conn</code>.</p>
</li>
<li><p>Connection Type: <code>postgres</code>.</p>
</li>
<li><p>Host: <code>postgres</code>.</p>
</li>
<li><p>Schema: <code>airflow</code>.</p>
</li>
<li><p>Login: <code>airflow</code>.</p>
</li>
<li><p>Password: <code>airflow</code>.</p>
</li>
<li><p>Port: <code>5432</code>.</p>
</li>
</ul>
<p>Then you can Test the connection, and if it passes, you can Save it.</p>
<p>Back to DAGs list, unpause the DAG <code>process-employees</code> and it will automatically run:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677776807899/10b09f10-6bd8-4a1f-bfb1-8ec8af106782.png" alt class="image--center mx-auto" /></p>
<p>We can check the tasks being run (click in Last Run column link):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677776858558/96d71214-252c-4b23-bd11-93a8c5617840.png" alt class="image--center mx-auto" /></p>
<p>And have a look at Flower UI to followup on tasks vs workers:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677776894369/fef77f6c-8748-468f-9f7b-7b626b884fe2.png" alt class="image--center mx-auto" /></p>
<p><strong>FIN!</strong> Now you can start adding/fetching DAGs from other sources as well, by modifying the fetch step in <code>airflow-scheduler</code> startup script and running <code>docker compose up</code> again to apply changes.</p>
<h1 id="heading-clean-up">Clean up</h1>
<p>Once you are done, you can delete all created resources with:</p>
<pre><code class="lang-bash">docker compose down
</code></pre>
<blockquote>
<p><strong>Important: remember to delete EFS volumes manually!!</strong></p>
<p><code>docker compose</code> integration creates EFS volumes with <code>retain</code> policy, so that whenever a new deployment occurs, it can reuse them.</p>
<p>Please <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/#volumes">see the official documentation</a> for more info.</p>
</blockquote>
<p>Also, remember to delete the SG and custom rules.</p>
<ul>
<li><p>First the rules.</p>
<pre><code class="lang-bash">  aws ec2 revoke-security-group-ingress --group-id <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span> --security-group-rule-ids &lt;self_internal_SG_rule_above_created_id&gt;
</code></pre>
<pre><code class="lang-bash">  aws ec2 revoke-security-group-ingress --group-id <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span> --ip-permissions IpProtocol=tcp,FromPort=5555,ToPort=5555,IpRanges=<span class="hljs-string">"[{CidrIp=&lt;your_public_CIDR&gt;,Description='Allow Flower access'}]"</span> IpProtocol=tcp,FromPort=8080,ToPort=8080,IpRanges=<span class="hljs-string">"[{CidrIp=&lt;your_public_CIDR&gt;,Description='Allow Webserver access'}]"</span>
</code></pre>
<pre><code class="lang-bash">  aws ec2 revoke-security-group-ingress --group-id <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span> --ip-permissions IpProtocol=-1,FromPort=-1,ToPort=-1,IpRanges=<span class="hljs-string">"[{CidrIp=&lt;your_vpc_cidr&gt;,Description='Allow VPC internal traffic'}]"</span>
</code></pre>
</li>
<li><p>Then the SG.</p>
<pre><code class="lang-bash">  aws ec2 delete-security-group --group-id <span class="hljs-string">"&lt;airflow_SG_above_created_id&gt;"</span>
</code></pre>
</li>
</ul>
<h1 id="heading-gotchas-andamp-comments">Gotchas &amp; comments</h1>
<ul>
<li><p><code>docker compose</code> outputs are not very descriptive and only show one error at a time -&gt; to understand AWS access errors, I used CloudTrail (Warning! heavy S3 usage).</p>
</li>
<li><p><code>docker compose</code> sometimes fails silently using <code>ecs</code> context. If you happen to face this, go back to default context with <code>docker context use default</code>, try to debug the errors (now they will be shown on screen) and then go back to <code>ecs</code> context <code>docker context use new-context-name</code>. See <a target="_blank" href="https://github.com/docker/compose-cli/issues/2068">this issue</a> for more info.</p>
</li>
<li><p><code>servicediscovery:*</code> permissions refer to CloudMap.</p>
</li>
<li><p>Workers and Webserver <code>.25 vCPU | .5 GB</code> is too few. Get it up to 2GB.</p>
</li>
<li><p>Don't forget to delete EFS volumes manually!! This <code>docker compose</code> ECS integration will define Docker volumes with <code>retain</code> policy, so they <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/#volumes">will <strong>not</strong> be deleted automatically</a> with <code>docker compose down</code>.</p>
</li>
<li><p><code>service.deploy.restart_policy</code> is <a target="_blank" href="https://github.com/docker/compose-cli/issues/2153"><strong>not</strong> supported</a> even though the <a target="_blank" href="https://docs.docker.com/cloud/ecs-compose-features/">documentation says it is</a>.</p>
</li>
<li><p>You can configure POSIX permissions on the AWS EFS access points by adding <code>volumes.your-volume.driver_opts</code> as described in <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/#volumes">volumes section</a>. Also, trying to figure out how to mount the <code>/dags</code> directory from AWS EFS correctly on the containers, <a target="_blank" href="https://github.com/docker/compose-cli/issues/1459">setting rootdir and permissions</a> is also allowed and supported.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>This <code>docker compose</code> ECS integration allows rolling updates as well. Since it relies on AWS CloudFormation, you could get a working baseline version (e.g. PostgreSQL, Redis, Scheduler) and then add the Webserver or Worker services without having to start from scratch, or modify existing resources.</p>
<p>Simply updating your <code>docker-compose.yaml</code> locally and running <code>docker compose up</code> again will apply any new changes. This new run translates to a <a target="_blank" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html">CloudFormation Stack update</a>.</p>
<p>This integration gives a lot of flexibility and offers an abstraction layer if you don't want to deal with external dependencies. In doing so, it will assume and configure a lot of settings for you, which for some use cases will not be ideal. If this is your case, then it might make more sense to tweak the underlying CloudFormation template being generated.</p>
<p>Overall a great experience, with lots of unknowns and learnings. The documentation is not that extensive, it seems to be very practical as it won't go into deep details but gets the job done. Still some way to go, please check out this integration <a target="_blank" href="https://github.com/docker/compose-cli/issues">issues and enhancements</a>.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html">Run Airflow in Docker</a>.</p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/">Docker compose ECS integration</a>.</p>
</li>
<li><p>This implementation <a target="_blank" href="https://github.com/marianogg9/airflow-in-ecs-with-compose/blob/main/example-dag/process-employees.py">repository</a>.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/blogs/containers/running-airflow-on-aws-fargate/">Airflow on Fargate (AWS blog)</a>.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/blogs/containers/deploy-applications-on-amazon-ecs-using-docker-compose/">Run apps in ECS using docker compose (AWS blog)</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-1-overview">Part 1 of this series</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-2-hands-on">Part 2 of this series</a>.</p>
</li>
</ul>
<h1 id="heading-improvements"><strong>Improvements</strong></h1>
<ul>
<li><p>Add a reverse proxy <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/howto/run-behind-proxy.html"><strong>https://airflow.apache.org/docs/apache-airflow/stable/howto/run-behind-proxy.html</strong></a>.</p>
<ul>
<li>HTTPS support.</li>
</ul>
</li>
<li><p>Explore <a target="_blank" href="https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/ec2-and-fargate-spot.html">Fargate Spot</a>.</p>
</li>
<li><p>Deploy in Kubernetes.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[Airflow in ECS with Redis - Part 2: Hands On]]></title><description><![CDATA[Previously on How to set up a containerised Airflow installation in AWS ECS using Redis as its queue orchestrator, I gave an overview of the infrastructure and Airflow components.
Now let's deploy all that.

This deployment will incur charges!!

Base...]]></description><link>https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-2-hands-on</link><guid isPermaLink="true">https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-2-hands-on</guid><category><![CDATA[airflow]]></category><category><![CDATA[ECS]]></category><category><![CDATA[AWS]]></category><category><![CDATA[ETL]]></category><category><![CDATA[workflow]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Fri, 24 Feb 2023 17:09:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677154357750/57c09c74-0ced-48f3-b30a-968a6325fc2f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously on <a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-1-overview">How to set up a containerised Airflow installation in AWS ECS using Redis as its queue orchestrator</a>, I gave an overview of the infrastructure and Airflow components.</p>
<p>Now let's deploy all that.</p>
<blockquote>
<p>This deployment <strong>will incur charges</strong>!!</p>
</blockquote>
<h1 id="heading-baseline">Baseline</h1>
<ul>
<li><p>AWS ECS, with 6 services. Scheduler, Webserver, Workers and (Celery) Flower on EC2 launch type using a mix of on-demand (scheduler, webserver) and spot instances (workers, flower). PostgreSQL and Redis on Fargate launch type.</p>
</li>
<li><p>AWS S3 bucket as DAGs repository.</p>
</li>
<li><p>AWS EFS, as a backing storage service for PostgreSQL.</p>
</li>
<li><p>AWS NLB for Airflow Webserver and Flower services.</p>
</li>
<li><p>Terraform to create all resources.</p>
<ul>
<li>I did not use any modules for this version, but it's completely doable.</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677231789792/e067a43a-7fbc-4063-8527-9a33cf34f1f8.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-prerequisites">Prerequisites</h1>
<ul>
<li><p>Already created AWS resources:</p>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-getting-started.html">VPC</a> + subnets (at least one).</p>
</li>
<li><p>(optional) EC2 <a target="_blank" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html">key pair</a>.</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html">AWS CLI configured</a> in your local using a set of credentials with permissions to create (and delete) resources in your account.</p>
</li>
<li><p><a target="_blank" href="https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli">Terraform</a> (I'm using <code>1.3.0</code> at the time of writing this article).</p>
</li>
</ul>
<h1 id="heading-deploy">Deploy</h1>
<p>I have uploaded all the code I worked with to <a target="_blank" href="https://gitlab.com/marianogg9/airflow-in-ecs">this</a> repo.</p>
<blockquote>
<p><strong>Please keep in mind this will create resources in your AWS account that will consume credits and will be billed at the end of the month!</strong></p>
</blockquote>
<h3 id="heading-clone-to-your-local">Clone to your local</h3>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/marianogg9/airflow-in-ecs.git local_dir
</code></pre>
<h3 id="heading-fill-in-the-required-vars">Fill in the required vars</h3>
<p><strong>local_dir/locals.tf</strong></p>
<ul>
<li><p><code>instance-type</code> (for the EC2 ASGs).</p>
</li>
<li><p><code>ecs-ami</code> (also for the EC2 ASGs launch templates, taken from <a target="_blank" href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-ami-versions.html">AWS official documentation</a>).</p>
</li>
<li><p><code>default_vpc_id</code> (your existing VPC).</p>
</li>
<li><p><code>subnetsIDs</code> (the existing subnets you want to deploy to, at least 1).</p>
</li>
<li><p><code>custom_cidr</code> (your public IP, to access the Webserver and Flower services, and grants SSH access to - ECS - EC2 instances).</p>
</li>
<li><p><code>user_data_vars.region</code> (AWS region where the S3 bucket is being created, used for <code>s3fs-fuse</code> mount configuration).</p>
</li>
<li><p><code>log_configuration.options.region</code> (same as above, but for CloudWatch logs).</p>
</li>
<li><p><code>aws_key_pair_name</code> (an existing EC2 key pair name. If left empty, SSH traffic is not allowed from outside the default VPC).</p>
</li>
</ul>
<p><strong>local_dir/terraform-backend.tf</strong></p>
<ul>
<li>fill in your S3 Terraform bucket, state file name and DynamoDB table.</li>
</ul>
<p><strong>local_dir/provider.tf</strong></p>
<ul>
<li><code>provider["aws"].region</code>, the region you are deploying resources to.</li>
</ul>
<h3 id="heading-run-terraform">Run terraform</h3>
<blockquote>
<p><strong>Again, please check the resources being created and the billing details!</strong></p>
</blockquote>
<pre><code class="lang-bash">$ <span class="hljs-built_in">cd</span> local_dir
$ terraform init
$ terraform apply
</code></pre>
<p>Once all of the above is deployed, you can get the NLB DNS name from <code>nlb_cname</code> from Terraform outputs.</p>
<h2 id="heading-accessing-webserver-ui">Accessing Webserver UI</h2>
<pre><code class="lang-bash">http://nlb_cname:8080
</code></pre>
<p>Give it a couple of minutes for the health checks to pass and authenticate with:</p>
<ul>
<li><p>Username: <code>airflow</code>.</p>
</li>
<li><p>Password: you can check it in AWS Secrets Manager using Terraform output <code>airflow_ui_password_secret_arn</code>.</p>
</li>
</ul>
<h2 id="heading-accessing-flower-celery-queue-ui">Accessing Flower (Celery queue UI)</h2>
<pre><code class="lang-bash">http://nlb_cname:5555
</code></pre>
<h1 id="heading-running-an-example-dag">Running an example DAG</h1>
<p>Now, let's run an example DAG (from <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/tutorial/pipeline.html">Airflow's pipeline examples</a>), but instead of downloading the data from Github, we will create a local file with the input data to have the DAG import it into the DB. I included the <code>example-dag.py</code> DAG file <a target="_blank" href="https://raw.githubusercontent.com/marianogg9/airflow-in-ecs/main/example-dag/example-dag.py">here</a> as well.</p>
<p>Let's populate the input data first, running the following in your local console:</p>
<pre><code class="lang-bash">curl -s  https://raw.githubusercontent.com/apache/airflow/main/docs/apache-airflow/tutorial/pipeline_example.csv &gt; employees.csv
</code></pre>
<p>Then upload the data to the S3 bucket created before (use <code>s3_bucket</code> Terraform output):</p>
<pre><code class="lang-bash">aws s3 cp employees.csv s3://&lt;s3_bucket&gt;/files/employees.csv
</code></pre>
<p>Now upload the example DAG to the same S3 bucket, making it available to all Airflow components:</p>
<pre><code class="lang-bash">aws s3 cp example-dag/example-dag.py s3://&lt;s3_bucket&gt;/example-dag.py
</code></pre>
<p>After around <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#dag-dir-list-interval">5 minutes</a>, the DAG should be available in the UI. You might want to reload the browser to have it listed.</p>
<p>In the meantime, as a prerequisite, create a PostgreSQL connection (to be then used by the DAG). In Webserver UI &gt; Admin &gt; Connections &gt; Add <code>+</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677230286684/63368ad2-31f3-456c-bca8-0b4af30971fb.png" alt class="image--center mx-auto" /></p>
<p>With the following parameters:</p>
<ul>
<li><p>Connection Id: <code>tutorial_pg_conn</code>.</p>
</li>
<li><p>Connection Type: <code>postgres</code>.</p>
</li>
<li><p>Host: <code>postgres.airflow.local</code>.</p>
</li>
<li><p>Schema: <code>airflow</code>.</p>
</li>
<li><p>Login: <code>airflow</code>.</p>
</li>
<li><p>Password: <code>airflow</code>.</p>
</li>
<li><p>Port: <code>5432</code>.</p>
</li>
</ul>
<p>Then you can Test the connection, and if it passes, you can Save it.</p>
<p>Now we need to unpause the DAG in the UI (and it will automatically run):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677152968615/aa4e0c18-2ced-46ee-9548-52869f9543f8.png" alt class="image--center mx-auto" /></p>
<p>In flower UI, we will see the DAG task(s) being executed on the worker(s):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677154000980/123ac2ed-3505-49a6-a576-8c1f75651233.png" alt class="image--center mx-auto" /></p>
<p>Also, we can have a look at the DAG tasks running in Webserver UI (clicking on the Last Run column link):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677154053582/f8e36333-4db9-48d4-8e48-9ee577b9315c.png" alt class="image--center mx-auto" /></p>
<p><strong>FIN!</strong> Now you can play around with <a target="_blank" href="https://github.com/apache/airflow/tree/main/airflow/example_dags">other DAGs</a> or start running your own pipelines.</p>
<h1 id="heading-some-gotchas-and-comments">Some gotchas and comments</h1>
<p>While (re) implementing this solution, there were some forgotten details I (re) found on my way.</p>
<ul>
<li><p>Modify health check retries for Redis container definition to =&lt; 10.</p>
</li>
<li><p>Modify all containers environment variables to point to local service discovery entries (<code>service.airflow.local</code>).</p>
<ul>
<li>In the original Airflow <code>docker-compose</code>, there is a sidecar container running a service discovery resolution for local networking and DNS. I replaced this container with a hostname fix, from <code>service</code> to <code>service.airflow.local</code> in each container environment variables.</li>
</ul>
</li>
<li><p>Add internal traffic rule (VPC CIDR, all ports) to the default Security Group.</p>
<ul>
<li>Also a big one! In the beginning, the NLB health checks against webserver/flower services were not passing. After a while of debugging, I realized there were no internal traffic rules in the Security Group attached to the ECS service(s) allowing the NLB to connect to the tasks. Yeah, it happens.</li>
</ul>
</li>
<li><p>Give at least 512cpu + 1024 ram to Webserver task definition.</p>
<ul>
<li>Otherwise, it will start shutting down workers out of the blue, with no OOM errors (at least that I could see).</li>
</ul>
</li>
<li><p>Add <code>ECS_INSTANCE_ATTRIBUTES</code> environment variable in the ASG launch template UserData to set a custom value and differentiate core (on-demand) and worker (spot) instances.</p>
<ul>
<li>And then use that attribute as a <code>placement_constraint</code> filter in each ECS service definition.</li>
</ul>
</li>
<li><p>Ah, yes, <code>s3fs-fuse</code> driver. When trying to use <code>iam_role</code> flag, it only works by compiling from source using a specific version/commit: <a target="_blank" href="https://github.com/s3fs-fuse/s3fs-fuse/issues/1162#issuecomment-536864032">https://github.com/s3fs-fuse/s3fs-fuse/issues/1162#issuecomment-536864032</a> + <a target="_blank" href="https://github.com/s3fs-fuse/s3fs-fuse/wiki/Installation-Notes#amazon-linux">https://github.com/s3fs-fuse/s3fs-fuse/wiki/Installation-Notes#amazon-linux</a>.</p>
<ul>
<li><p>This one took a while. Turns out there is a bug (<a target="_blank" href="https://github.com/s3fs-fuse/s3fs-fuse/issues/1196">https://github.com/s3fs-fuse/s3fs-fuse/issues/1196</a> closed in Feb 2020) preventing the <code>-o iam_role</code> flag to work due to a recursive call to <code>CheckIAMCredentials</code> method.<br />  The problem is that running <code>s3fs-fuse</code> mount command with <code>-f -o curldbg</code> flags will just hang and not show any useful information. After a while of trying different flags, I gave it a try using a set of IAM credentials (<code>access:secret</code>); it worked instantly. Now this implementation is using version <code>1.84</code>.<br />  <code>v1.86</code> was released in Feb 2020, that's why I never faced this issue up until now.</p>
</li>
<li><p>The working set of steps is included in the UserData script for both core and workers ASG launch templates.</p>
</li>
</ul>
</li>
<li><p>In both core and worker ASG launch template UserData, I used <a target="_blank" href="https://developer.hashicorp.com/terraform/language/functions/templatefile">https://developer.hashicorp.com/terraform/language/functions/templatefile</a> to add external vars (like S3 bucket name and IAM instance profile name).</p>
</li>
<li><p>As done with the UI admin user, PostgreSQL connection credentials can be parametrized using AWS Secrets Manager secrets + env vars in the task definitions:</p>
<ul>
<li><p>For PostgreSQL tasks: <code>POSTGRES_USER</code> &amp; <code>POSTGRES_PASSWORD</code>.</p>
</li>
<li><p>All Airflow's container definitions: <code>AIRFLOW__CORE__SQL_ALCHEMY_CONN</code> by passing in a Secrets Manager secret ARN.</p>
</li>
</ul>
</li>
<li><p>Many more Airflow configurations can be overwritten using <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html">environment variables</a>.</p>
</li>
<li><p>To have a guideline for the resources and dependencies I had to create, I used the <code>docker compose</code> version of this implementation. <a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/">This native Docker Compose integration with AWS using CloudFormation</a> will give you a way of deploying directly in AWS from <code>docker compose</code> command line.<br />  It also comes with a dry-run (<code>docker compose convert</code>) type of feature where you can get a look at the actual CloudFormation template that is to be applied. I used this template to create all the Terraform resources.</p>
</li>
<li><p>The official <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/tutorial/pipeline.html">Airflow documentation example</a> uses a DAG fetching the CSV input data from a GitHub repo. When I was running this implementation, I faced an issue where every task run would be (rendered and) passed to workers replacing <code>airflow</code> with <code>***</code>. Since the <code>get_data</code> task uses an airflow repository to fetch data from, the used URL would get rendered with <code>***</code> instead of the word <code>airflow</code>, making it invalid, which then made the task hang and fail.<br />  In favour of practicality, I decided to replace that fetch with a local CSV as input.<br />  You can always check the task logs in <code>/opt/airflow/logs/</code> where all components will add specific run information.</p>
</li>
</ul>
<h1 id="heading-cleaning-up">Cleaning up</h1>
<p>When you are ready to clean this all up, you will need to delete any files and folders in the S3 bucket manually first (otherwise, Terraform will complain about the bucket not being empty and fail with <code>BucketNotEmpty: The bucket you tried to delete is not empty</code>).</p>
<p>Then run:</p>
<pre><code class="lang-bash">$ terraform destroy
</code></pre>
<p>It will take a while to finish up, mostly because of the connection drain times on the NLB target groups vs ECS services and the EFS volume/mountpoints.</p>
<h1 id="heading-improvements">Improvements</h1>
<ul>
<li><p>Add a reverse proxy <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/howto/run-behind-proxy.html">https://airflow.apache.org/docs/apache-airflow/stable/howto/run-behind-proxy.html</a>.</p>
<ul>
<li>HTTPS support.</li>
</ul>
</li>
<li><p>Deploy in Kubernetes.</p>
</li>
<li><p>Use EFS as DAG storage instead of S3.</p>
</li>
</ul>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html">Running Airflow with docker compose</a>.</p>
</li>
<li><p><a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html">Airflow configuration reference</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/s3fs-fuse/s3fs-fuse">S3fs-fuse</a>.</p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/cloud/ecs-integration/">docker compose ECS integration</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-1-overview">Airflow in ECS with Redis - Part 1: Overview</a>.</p>
</li>
<li><p>This implementation <a target="_blank" href="https://github.com/marianogg9/airflow-in-ecs">repository</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-3-docker-compose">Part 3 of this series</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-1-overview">Part 1 of this series</a>.</p>
</li>
</ul>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[Airflow in ECS with Redis - Part 1: Overview]]></title><description><![CDATA[How to set up a containerised Airflow installation in AWS ECS using Redis as its queue orchestrator.
A bit of background
A few years ago I joined a Data team where we processed a lot of analytics information coming from online search engines. This ET...]]></description><link>https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-1-overview</link><guid isPermaLink="true">https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-1-overview</guid><category><![CDATA[airflow]]></category><category><![CDATA[ECS]]></category><category><![CDATA[Redis]]></category><category><![CDATA[containers]]></category><category><![CDATA[workflow]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Sat, 28 Jan 2023 18:19:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674929376319/a300b72e-2377-4245-943f-b283c0d58238.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>How to set up a containerised Airflow installation in AWS ECS using Redis as its queue orchestrator.</p>
<h2 id="heading-a-bit-of-background">A bit of background</h2>
<p>A few years ago I joined a Data team where we processed a lot of analytics information coming from online search engines. This ETL process consisted of three main stages: fetch raw data from external APIs, transform it into something meaningful for our applications and load that data into a database.</p>
<p>All this was carried on with in-house developed DAG scripts that were orchestrated via Airflow. The problem was that this intake was so lengthy and unstable that it was getting really painful for our daily performance.</p>
<p>Having previous ECS experience, I thought we could try improving the process using two main changes:</p>
<ul>
<li><p>Decouple the Airflow controller from the runners, using ECS.</p>
</li>
<li><p>Add a queue orchestrator for improved parallelism that could shorten the processing times, using Airflow's native <code>Celery</code> executor (integration) with Redis.</p>
</li>
</ul>
<h2 id="heading-what-is-airflow">What is Airflow?</h2>
<p>Airflow is a DAG orchestrator, a platform that provides management for workflows.</p>
<p>You can schedule, monitor and even create workflows to execute a set of tasks in a predefined order and dependent on each other. Airflow will control these tasks execution while giving insights and a UI where this can all be monitored, among many other features.</p>
<blockquote>
<p>Have a look at <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/index.html#what-is-airflow">Airflow</a> documentation for more information.</p>
</blockquote>
<h2 id="heading-what-is-ecs">What is ECS?</h2>
<p>AWS ECS is a container orchestrator to deploy, manage and scale your containerised applications. It integrates with a lot of AWS services, which makes it very flexible and easy to use if your infrastructure is already in AWS.</p>
<blockquote>
<p>Check out a few ECS <a target="_blank" href="https://aws.amazon.com/ecs/">uses cases</a>.</p>
</blockquote>
<h2 id="heading-what-is-redis">What is Redis?</h2>
<p>Is an in-memory data store, used as a database, message broker, cache, etc.</p>
<p>AWS <a target="_blank" href="https://aws.amazon.com/elasticache/">ElastiCache</a> offers Redis as one of its available engines.</p>
<blockquote>
<p>A bit <a target="_blank" href="https://redis.io/docs/getting-started/">more</a> on Redis.</p>
</blockquote>
<h2 id="heading-how-everything-glues-together">How everything glues together?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674668908525/eba14cc6-ea23-4944-91f6-d26375d1c616.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-remote-execution-using-redis">Remote execution using Redis</h3>
<p>Airflow has many <code>executor</code> <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/executor/index.html">integrations</a>. An <code>executor</code> is the mechanism by which tasks get run, either locally or remotely. So you can configure Airflow to have it locally executed - for small, standalone machine installations - or remotely if you are planning to run a considerable amount of tasks per workflow and have access to a pool of resources, like in this case, ECS.</p>
<p>One of the remote executors is <code>Celery</code>, which allows you to span out to many workers and supports different backends, such as RabbitMQ and Redis.</p>
<h3 id="heading-dags-storage">DAGs storage</h3>
<p>In this decentralised pattern, every worker has to have access to the scripts (DAGs) to be able to run their assigned task. To do so, you can mount an S3 bucket - containing all the scripts - as a local volume to all ECS tasks, using drivers such as <code>s3fs</code> or <code>s3fs-fuse</code>, <code>rexray/s3fs</code>, etc.</p>
<p>This S3 access is possible by adding the required IAM permissions to the ECS tasks, allowing both scheduler and worker to interact with the mounted bucket.</p>
<h3 id="heading-decoupling-airflow-scheduler-and-the-workers">Decoupling Airflow scheduler and the workers</h3>
<p>The whole idea of this pattern is to separate the brain from the muscle. Using ECS services, you can create a service for the scheduler (controller) and another for the worker tasks, controlled by the executor.</p>
<p>The former would run with a minimum of 1 task to keep things running, along with access to its webserver (UI). The latter will scale up/down depending on the scheduler sent workload.</p>
<h3 id="heading-some-additional-comments">Some additional comments</h3>
<ul>
<li><p>The IAM credentials are fetched from AWS SecretsManager or Parameter Store and mounted as environment variables on each task.</p>
</li>
<li><p>The S3 mounting driver installation is added to each ECS EC2 instance <code>userData</code> so it gets installed, initialised and mounted.</p>
</li>
</ul>
<h2 id="heading-improvements">Improvements</h2>
<ul>
<li><p>Use a git repository as DAGs datastore and have each runner download that repo in startup time.</p>
</li>
<li><p>Docker added an ECS integration for its <code>docker compose</code>. This is done by previously configuring Docker <code>context</code> to ECS.</p>
<ul>
<li><p>With this integration, there is no need to create ECS tasks definitions, you can directly work with Airflow's <code>docker-compose.yaml</code> <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/2.5.1/docker-compose.yaml">file</a> and create all infrastructure resources in AWS (via native CloudFormation integration).</p>
<ul>
<li>One of the key differences is that it will use AWS EFS as filesystem instead of S3, so there is no need to mount buckets as tasks volumes - no need for external drivers either.</li>
</ul>
</li>
<li><p>This integration was added in 2020 (way after my experience), so I will also include this new deployment approach in the next posts of this series.</p>
</li>
</ul>
</li>
<li><p>Explore Airflow's <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/executor/kubernetes.html">Kubernetes executor</a>.</p>
</li>
</ul>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#running-airflow-in-docker">Airflow in Docker</a>.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/ecs/">ECS</a>.</p>
</li>
<li><p><a target="_blank" href="https://redis.io/docs/getting-started/">Redis</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-2-hands-on">Part 2 of this series</a>.</p>
</li>
<li><p><a target="_blank" href="https://blog.mariano.cloud/airflow-in-ecs-with-redis-part-3-docker-compose">Part 3 of this series</a>.</p>
</li>
</ul>
<h2 id="heading-whats-next">What's next?</h2>
<p>I will be sharing a working example of the original implementation and an improved version using <code>docker compose</code> ECS integration.</p>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item><item><title><![CDATA[Dev Retro 2022: a Cloud Engineer perspective]]></title><description><![CDATA[This year has been pretty intense in terms of learning, from moving countries to getting used to a new way of working with (almost) the same tools.
Join me in my 2022 journey, when I discovered you do not have to dive into some new technology to keep...]]></description><link>https://blog.mariano.cloud/dev-retro-2022-a-cloud-engineer-perspective</link><guid isPermaLink="true">https://blog.mariano.cloud/dev-retro-2022-a-cloud-engineer-perspective</guid><category><![CDATA[#DevRetro2022]]></category><category><![CDATA[Cloud Engineering ]]></category><category><![CDATA[journey]]></category><category><![CDATA[AWS]]></category><category><![CDATA[mentoring]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Thu, 29 Dec 2022 10:45:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/7cd9b3d82244b0657094e423589093fd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This year has been pretty intense in terms of learning, from moving countries to getting used to a new way of working with (almost) the same tools.</p>
<p>Join me in my 2022 journey, when I discovered you do not have to dive into some new technology to keep on learning.</p>
<h3 id="heading-jan-feb-march">Jan-Feb-March</h3>
<p>2022 started pretty high for me, I was living in Kraków, spent the holidays in Argentina and had the good news that the Kubernetes migration I worked on last year shipped successfully to Production. I was ready for the next adventure!</p>
<p>I am a person who really enjoys what I do professionally, and I feel privileged for that. I like it so much that I got a Big Tech Company round of interviews on my birthday (March) - those 5 in one day types of interviews. It felt pretty awesome, this is a company I am very curious about and want to work there someday. I got an offer from them, but I decided to decline in favour of moving to Amsterdam.</p>
<h3 id="heading-apr-may-jun">Apr-May-Jun</h3>
<p>So I started to look for an Amsterdam based job, I participated in some interview processes and finally decided to join <a target="_blank" href="https://quin.md/">Quin</a>. The offer was excellent, the people I met were super kind and professional, and the product/industry was new and attractive to me; so everything went perfectly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672307128438/50907fff-320f-44b9-9246-cbb573dbfb5f.jpeg" alt="Photo by &lt;a href=&quot;https://unsplash.com/@philipmyr?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Philip Myrtorp&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/iiqpxCg2GD4?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;   " class="image--center mx-auto" /></p>
<p>We moved in June with my family; it was a bit stressful at the beginning, naturally expected in such situations, with all the coordination, paperwork, flights, housing, etc. Fortunately, we had help from a group of fantastic professionals.</p>
<h3 id="heading-jul-aug-sept">Jul-Aug-Sept</h3>
<p>These were the highest learning months, a lot to take in, first three months in a new job. People to know, teams to get familiar with, the new product and the Infrastructure behind all that.</p>
<blockquote>
<p>I know it can be difficult to keep up and sometimes it becomes a stressful situation. If you feel it is too much, <strong>please relax for a bit</strong>. Take a walk, go grab a tea. Nobody (should) expects you to be 100% productive in your first months.</p>
</blockquote>
<p>The most challenging for me was getting used to the new approach to handling application deployments, specifically with ArgoCD. I never worked with it but it is a platform to manage Kubernetes deployments, among many other capabilities.</p>
<p>I was used to handling all deployments using Helm (<a target="_blank" href="https://helmfile.readthedocs.io/en/latest/">Helmfile</a>) with no other additional orchestrator. But ArgoCD offers an abstraction layer on top of Kubernetes and it structures declarative deployments and k8s resources, along with application lifecycles, resource pruning and a set of deployment policies to pick from. This gives a wide range of possibilities for you to adopt, depending on your use case.</p>
<blockquote>
<p>If you want to know more, please check out <a target="_blank" href="https://argo-cd.readthedocs.io/en/stable/getting_started/">ArgoCD documentation</a>.</p>
</blockquote>
<p>Along with that, I learned a new way of managing Terraform modules in a structured way. I plan to write about that as well!</p>
<p>I was also able to add value to the product by implementing security best practices for Kubernetes in AWS. That first contribution always feels awesome, it is like giving back a bit of the trust the company put in you.</p>
<blockquote>
<p>I wrote my first blog post on IRSA. <a target="_blank" href="https://blog.mariano.cloud/irsa-in-eks-a-kubernetes-aws-bridge">Check it out!</a></p>
</blockquote>
<h3 id="heading-oct-nov-dec">Oct-Nov-Dec</h3>
<p>I always had the curiosity to share content about what I do, mostly because I enjoy helping people, and I have been googling and reading a lot (as we all do) to solve stuff or implement fixes on my daily job. So, why not giving back?</p>
<p>I got this idea from my current Manager - the importance of having people around you that pushes you forward, and more so if they are people in charge. I found Hashnode while googling about IRSA, so I decided to create an account and started getting familiar with it.</p>
<p>I gathered a list of topics that I specifically worked on in the past, and things I wanted to share. I read a lot of advice from content creators in Hashnode, which is a really good starting point; I think one of the huge advantages of being on IT is the number of resources, information and people willing to help.</p>
<p>And finally started to write blog posts. It is a pretty good feeling to be able to share experiences with fellow people and more with someone with no contact with technology.</p>
<p>I started my <a target="_blank" href="https://mariano.cloud/">website</a> as well. I was looking to create one as a presentation way of what I do, who I am and how to contact me.</p>
<blockquote>
<p><a target="_blank" href="https://blog.mariano.cloud/your-website-in-minutes-gitlab-hugo-blowfish">This</a> is my blog post about creating a site with Hugo &amp; Blowfish on Gitlab Pages, check it out!</p>
</blockquote>
<p>I also started a <a target="_blank" href="https://www.udemy.com/course/learn-go-the-complete-bootcamp-course-golang/">Udemy course in GO</a> that I recommend. It is very well explained for beginners (in GO) like myself, and there are a lot of exercises and projects to go through.</p>
<h3 id="heading-whats-next">What's next?</h3>
<ul>
<li><p>I would like to explore public speaking. There is a lot of good advice and many people share their experiences with it, so I think I will start writing about topics I want to talk about and getting reviews from my peers. I think I can get a grasp of how my confidence level before committing and starting practising with my family 😁.</p>
</li>
<li><p>I would also like to get Certifications in AWS and GCP.</p>
</li>
<li><p>Keep on learning GO.</p>
</li>
<li><p>Start mentoring people willing to learn or start in Cloud Engineering. I have created a profile in CodingCoach. If you are interested in talking about Infrastructure <strong>for free</strong>, <a target="_blank" href="https://mentors.codingcoach.io/u/63ad5c864474770664cbfb18">please have a look!</a></p>
</li>
</ul>
<h3 id="heading-conclusion">Conclusion</h3>
<p>I am happy someone suggested blogging, it was something I did internally with documentation and presentations, but never publishing. It feels scary at first, but I am getting used to being out there.</p>
<p>Even though technology could not change from changing jobs, there are plenty of learning opportunities. From a new approach, a new way of managing secrets, a different authorisation method, or even a new way of peer review. There might always be an opportunity to learn stuff you already know.</p>
<p>I am always open to discussing and talking about technology, Cloud, football or even living abroad.</p>
<p>Thank you for reading. Reach out, I would love to help in your Cloud journey!</p>
]]></content:encoded></item><item><title><![CDATA[Your website in minutes: Gitlab, Hugo & Blowfish]]></title><description><![CDATA[I was looking for a way to create my first website as a way to tell a bit about myself and my IT journey when I stumbled first on Gitlab Pages templates, and then Blowfish - a Hugo theme that will give you a flexible running website in a couple of mi...]]></description><link>https://blog.mariano.cloud/your-website-in-minutes-gitlab-hugo-blowfish</link><guid isPermaLink="true">https://blog.mariano.cloud/your-website-in-minutes-gitlab-hugo-blowfish</guid><category><![CDATA[blowfish]]></category><category><![CDATA[GitLab]]></category><category><![CDATA[Hugo]]></category><category><![CDATA[ssg]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Tue, 27 Dec 2022 19:44:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1672161915133/852bf606-0930-4e80-88e5-4180b41e504a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was looking for a way to create my first website as a way to tell a bit about myself and my IT journey when I stumbled first on <a target="_blank" href="https://docs.gitlab.com/ee/user/project/pages/">Gitlab Pages</a> templates, and then <a target="_blank" href="https://nunocoracao.github.io/blowfish/">Blowfish</a> - a <a target="_blank" href="https://gohugo.io/">Hugo</a> theme that will give you a flexible running website in a couple of minutes.</p>
<p>Let's create a sample website that can be later customised. I will be using my <a target="_blank" href="https://mariano.cloud">site</a> as a guiding example for all the steps.</p>
<h1 id="heading-some-background">Some background</h1>
<h2 id="heading-what-is-hugo">What is Hugo?</h2>
<p>Hugo is an SSG, meaning static site generator, a free framework for creating static websites using CSS, JS and HTML. The best part is that you do not need to have any knowledge of those, just adding your content and configuring its appearance with themes is all you need.</p>
<p>Please have a look at the various <a target="_blank" href="https://gohugo.io/">features</a> Hugo offers.</p>
<h2 id="heading-what-is-blowfish">What is Blowfish?</h2>
<p>Blowfish is a (one of the many) theme for Hugo, built on Tailwind CSS, that will give you a range of customisations including hero view, background images, dark mode, pages tree, rich content and <strong>a lot</strong> more.</p>
<p>Have a look at <a target="_blank" href="https://nunocoracao.github.io/blowfish/">Blowfish</a> variations and possibilities.</p>
<h2 id="heading-gitlab-managed">Gitlab managed</h2>
<p>In my case I was looking for a Gitlab managed deployment, so I looked into <a target="_blank" href="https://about.gitlab.com/stages-devops-lifecycle/pages/">Gitlab Pages</a>, a way to deploy a static website using a simple <code>.gitlab-ci.yml</code> pipeline, including a custom <code>pages:</code> stage definition.</p>
<p>There are many possible ways of deploying a website using Gitlab Pages: you can either deploy it from scratch, you can use a template, or a forked project.</p>
<p>Here is a lot more on <a target="_blank" href="https://docs.gitlab.com/ee/user/project/pages/#getting-started">how you can deploy yours</a>.</p>
<h1 id="heading-tutorial">Tutorial</h1>
<p>I chose to use an existing project template, and there are a lot of them <a target="_blank" href="https://docs.gitlab.com/ee/user/project/pages/getting_started/pages_new_project_template.html">here</a>. I went with Hugo as it looked promising and I wanted to try it out, and I liked one of its <a target="_blank" href="https://themes.gohugo.io/">themes</a> (Blowfish).</p>
<h2 id="heading-create-using-a-project-template">Create using a project template</h2>
<p>From your Gitlab home page, select <code>New Project</code> and choose <code>Create from template</code> option. You can select any of the 31 available templates, let's use <code>Pages/Hugo</code> &gt; select <code>Use template</code>.</p>
<p>This will open a Gitlab new project screen where you can add your new project <strong>name</strong>, <strong>slug</strong> (URL friendly name) and its <strong>visibility level</strong> (given this is a standalone project - if this is part of a group, then it will inherit its group visibility).</p>
<p>It will take some time to import and once it is created, the project contains a copy of the original template files, and it is almost ready to use. Now it is time to set your project name and Hugo's default theme base URL (where all assets will be served from).</p>
<h3 id="heading-change-your-project-name">Change your project name</h3>
<p>In your project <code>Settings</code> &gt; <code>General</code> &gt; <code>Advanced</code> &gt; <code>Change path</code> you will need to change your project's path to match the <a target="_blank" href="https://docs.gitlab.com/ee/user/project/pages/getting_started_part_one.html#gitlab-pages-default-domain-names">Gitlab pages URL</a> convention. In this example, I created a project named <code>hugo-blowfish-website</code>. I will change its path to be <code>hugo-blowfish-website.gitlab.io</code> as I am using Gitlab.com to create it.</p>
<p>Keep in mind, if you are creating a group project (like me), then the modified path is going to look like this: <code>https://gitlab.com/your-group/your-project.gitlab.io</code>, and the final <strong>published URL</strong> will be <code>https://your-group.gitlab.io/your-project.gitlab.io</code>.</p>
<p>If you are creating a standalone project, then the project path will be <code>https://gitlab.com/your-username/your-project.gitlab.io</code> and the <strong>published URL</strong> <code>https://your-username.gitlab.io/your-proyect.gitlab.io</code>.</p>
<p>One of the above <strong>published URLs</strong> is the one to be set in the next step.</p>
<h3 id="heading-change-the-current-themes-base-url">Change the current theme's base url</h3>
<p>Once you changed the project path, update the <code>baseurl</code> parameter in your project root's <code>config.toml</code> with the published URL. By default, it is set to <a target="_blank" href="https://pages.gitlab.io/hugo/"><code>https://pages.gitlab.io/hugo/</code></a>, you need to change it to <code>https://your-group.gitlab.io/your-project.gitlab.io</code> or <code>https://your-username.gitlab.io/your-proyect.gitlab.io</code> depending on if this is a group/project or a standalone one.</p>
<h3 id="heading-ready-to-deploy">Ready to deploy</h3>
<p>The last step can be done either in your local workstation (previous cloning of the project) or directly in the web IDE in Gitlab. Either way, when you push the change, it will automatically start a CI/CD pipeline. To access it, in your project (left pane) <code>CI/CD</code> will open the CI/CD pipelines view.</p>
<p>If you click on the most recent run, you will see two stages: <code>test</code> that performs a quick test on the files to check for eventual errors and <code>deploy</code> Gitlab custom job, running <code>hugo</code> and using <code>public</code> folder as the content source. You can have a look at the pipeline definition in <code>.gitlab-ci.yml</code> file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672162514186/52e4ab49-cd62-4c54-8cb1-4516ea495cac.png" alt class="image--center mx-auto" /></p>
<p>If the pipeline finishes successfully, it means the site has been deployed and you can access it in the configured <code>baseurl</code>. If you go to your project (left pane) <code>Settings</code> &gt; <code>Pages</code> you will access your Pages menu, where you will be able to see the website link.</p>
<p>If this is the first time you are deploying a page in Gitlab, it will require a credit card to avoid abusive usage of this Pages functionality. Don't worry, it will not charge anything (actually it will, $1 is the default but they will revert the payment in a few minutes).</p>
<p>Now you can access your website! it is a vanilla theme, by default it uses <a target="_blank" href="https://themes.gohugo.io/themes/gohugo-theme-ananke/">Ananke</a>, but we will customise it later on.</p>
<h2 id="heading-use-a-custom-theme">Use a custom theme</h2>
<p>Let's say you (like me) would like to use a different theme, how can we do that?</p>
<p>Hugo has a lot of themes to pick and use, the one I chose is called <a target="_blank" href="https://nunocoracao.github.io/blowfish/">Blowfish</a>.</p>
<p>To change the default theme, you can follow the Blowfish theme <a target="_blank" href="https://nunocoracao.github.io/blowfish/">installation steps</a> in combination with the default instructions to <a target="_blank" href="https://gitlab.com/pages/hugo#use-a-custom-theme-using-a-hugo-module">use a custom theme</a> from Hugo's Gitlab Pages template, where you have a quick start tutorial. Let's go thru those steps.</p>
<p>My recommendation is to clone the project to your local so you can work easier.</p>
<ul>
<li><p>Change the <code>hugo mod get</code> command within <code>.gitlab-ci.yml</code> to be <code>hugo mod get -u github.com/nunocuracao/blowfish</code> .</p>
</li>
<li><p>Create <code>config/_default/module.toml</code> (new directory/file) in your project's root, containing the following:</p>
<pre><code class="lang-yaml">  [[<span class="hljs-string">imports</span>]] 
   <span class="hljs-string">path</span> <span class="hljs-string">=</span> <span class="hljs-string">"github.com/nunocoracao/blowfish"</span>
</code></pre>
</li>
<li><p>Commit and push these changes.</p>
</li>
</ul>
<p>This will create a new pipeline run, and when it finishes you can access your site again. It will show an updated Blowfish theme, but still with default content.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672142875139/45a82406-e47e-4464-aa63-b437662a6b40.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-add-your-content">Add your content</h2>
<p>Let's customise the theme and add some content.</p>
<h3 id="heading-basics">Basics</h3>
<p>First of all, we need to create a default set of configurations. We already created a <code>config/_default</code> directory, now we can start populating it.</p>
<p>Start by deleting <code>config.toml</code> from the root directory.</p>
<p>Then download a copy of the required files from Blowfish's <a target="_blank" href="https://github.com/nunocoracao/blowfish/tree/main/config/_default">repo</a> and copy all <code>*.toml</code> into your <code>config/_default/</code> directory.</p>
<blockquote>
<p>Remember not to override the <code>config/_default/module.toml</code> created before.</p>
</blockquote>
<p>Now update <code>config/_default/config.toml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-string">baseURL</span> <span class="hljs-string">=</span> <span class="hljs-string">"https://your_domain.com/"</span>
<span class="hljs-string">languageCode</span> <span class="hljs-string">=</span> <span class="hljs-string">"en"</span>
</code></pre>
<p>Where <code>baseURL</code> is the same as we configured before and <code>languageCode</code> is the default language we will be showing content.</p>
<p>Let's now configure the language settings in <code>config/_default/languages.en.toml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-string">title</span> <span class="hljs-string">=</span> <span class="hljs-string">"Your website"</span>

[<span class="hljs-string">author</span>]
 <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"Your name"</span>
 <span class="hljs-string">image</span> <span class="hljs-string">=</span> <span class="hljs-string">"img/author.jpg"</span>
 <span class="hljs-string">headline</span> <span class="hljs-string">=</span> <span class="hljs-string">"A generally awesome human"</span>
 <span class="hljs-string">bio</span> <span class="hljs-string">=</span> <span class="hljs-string">"Just someone sharing content"</span>
 <span class="hljs-string">links</span> <span class="hljs-string">=</span> [
   { <span class="hljs-string">twitter</span> <span class="hljs-string">=</span> <span class="hljs-string">"https://twitter.com/username"</span> }
 ]
</code></pre>
<p>These are going to be generally available across the whole site, in particular, the Home page - depending on the home layout we use. The <code>image</code> comes from files in <code>assets</code> directory. If left commented it will be picked up from a default in the theme. Also the <code>links</code> list is configurable, you can add more if you like (there are a lot to use in the default file).</p>
<p>In addition, if you would like to serve content in a different language, you can rename this file to match the language ISO code you would like to use.</p>
<h3 id="heading-content">Content</h3>
<p>Now for content, there is a <code>content</code> directory in the root of your project. That is the place where we will be creating pages (sections) to organise content into. It is as simple as creating subdirectories, e.g. <code>blog</code> or <code>about</code>. These will be new pages, accessible via <code>baseurl/page-name</code> (<code>baseurl/blog</code> or <code>baseurl/about</code>).</p>
<p>To add content, we will be adding an <code>index.md</code> within each subfolder and Hugo will render them as "articles" or our well known <code>README.md</code> type of documentation. It supports markdown, rich content and many other features you can check in the <a target="_blank" href="https://nunocoracao.github.io/blowfish/samples/">documentation</a>. Let's add two pages: <code>about</code> and <code>blog</code>.</p>
<p>There is a special type of file <code>_index.md</code> that will inject content as default whenever you place it. E.g. if we would like to add default content to the home page, it would be in <code>content/_index.md</code>. Same scenario for subdirectories. A deeper explanation and usages, you can find <a target="_blank" href="https://nunocoracao.github.io/blowfish/docs/front-matter/">here</a>.</p>
<h3 id="heading-menus">Menus</h3>
<p>Lastly for basic customisation, we have the menus (<code>menus.en.toml</code>). This is the file where we define the menu items for the header and footer. Here we can reference local pages (defined in <code>content/page-name</code>) or external links:</p>
<pre><code class="lang-yaml">[[<span class="hljs-string">main</span>]]
 <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"Blog"</span>
 <span class="hljs-string">pageRef</span> <span class="hljs-string">=</span> <span class="hljs-string">"blog"</span>
 <span class="hljs-string">weight</span> <span class="hljs-string">=</span> <span class="hljs-number">10</span>

[[<span class="hljs-string">main</span>]]
 <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"External"</span>
 <span class="hljs-string">url</span> <span class="hljs-string">=</span> <span class="hljs-string">"https://github.com/nunocoracao/blowfish"</span>
 <span class="hljs-string">weight</span> <span class="hljs-string">=</span> <span class="hljs-number">20</span>

[[<span class="hljs-string">footer</span>]]
 <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"About"</span>
 <span class="hljs-string">pageRef</span> <span class="hljs-string">=</span> <span class="hljs-string">"about"</span>
 <span class="hljs-string">weight</span> <span class="hljs-string">=</span> <span class="hljs-number">10</span>
</code></pre>
<p>Finally, remove default (Ananke theme) language content directories: <code>content/en</code> and <code>content/fr</code>.</p>
<p>Now we are ready to deploy a more personalised site, so let's commit and push our changes. After the pipeline run, it will look something like this.</p>
<p>Home page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672149423585/bf16bd8e-6b49-4228-8d95-e07fd71fef94.png" alt="HomePage" class="image--center mx-auto" /></p>
<p>Blog page (<code>baseurl/blog</code>):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672149492149/6aa54bae-cdea-45bf-867b-2902c80a7c48.png" alt class="image--center mx-auto" /></p>
<p>We can see both header and footer menus, accessing any of those will take us to the page and its content. The <code>author</code> section is added by default on every page (but Home) and <code>External</code> header menu section will take us to whatever link we added in its menu <code>url</code> parameter.</p>
<h3 id="heading-further-customisations">Further customisations</h3>
<p>There is <strong>a lot</strong> more that can be done by customising Blowfish config files.</p>
<p>The main ones:</p>
<ul>
<li><p><code>config/_default/params.toml</code> to control pretty much everything that is shown on each page, along with the theme hero feature, the sections, taxonomy, background, etc.</p>
</li>
<li><p><code>config/_default/config.toml</code> general customisation and behaviour.</p>
</li>
<li><p><code>config/_default/menus.&lt;lang_iso_code&gt;.toml</code> menu customisation.</p>
</li>
<li><p><code>config/_default/language.&lt;lang_iso_code&gt;.toml</code> language and content settings.</p>
</li>
</ul>
<p>And much more. Please refer to <a target="_blank" href="https://nunocoracao.github.io/blowfish/docs/configuration/">Blowfish documentation</a> for more.</p>
<h2 id="heading-visibility">Visibility</h2>
<p>Remember this page is still under its project default visibility settings, meaning that if it is set to private, then nobody can access it unless they are members of your project/group.</p>
<p>To make it publicly accessible, there is an option in your project <code>Settings</code> &gt; <code>General</code> &gt; <code>Visibility, project features, permissions</code> &gt; <code>Pages</code>. Setting that to <code>Everyone</code> will do.</p>
<h2 id="heading-optional-build-locally">(optional) Build locally</h2>
<p>For debugging purposes, I find it very helpful to build the website locally and then push changes to deploy in Gitlab.</p>
<p>To do so, and after you cloned your Gitlab project, you will need:</p>
<ul>
<li><p><a target="_blank" href="https://gohugo.io/installation/">Hugo</a>.</p>
</li>
<li><p>Initialise Hugo:</p>
<pre><code class="lang-bash">  hugo mod init gitlab.com/pages/hugo
</code></pre>
</li>
<li><p>Add Blowfish theme:</p>
<pre><code class="lang-bash">  hugo mod get -u github.com/nunocuracao/blowfish
</code></pre>
</li>
<li><p>Add <code>config/_default</code> directories and files (as above described).</p>
</li>
<li><p>Comment out <code>baseurl</code> from <code>config/_default/config.toml</code>.</p>
</li>
</ul>
<p>Then run:</p>
<pre><code class="lang-bash">hugo server
</code></pre>
<p>It will build all static files (compiled from any <code>assets</code> added and <code>content</code>) - it will create <code>public</code> and <code>resources</code> directories - and start a server (defaults to <code>localhost:1313</code>) where you can access the built site. You can then modify/add/delete content/configs and it will auto-reload the site.</p>
<p>Whenever you are ok with the changes, remember to add a <code>.gitignore</code> to avoid pushing to Gitlab <code>resources</code>, <code>public</code> or any files/directories Hugo created additionally.</p>
<h2 id="heading-bonus-tls-dns-and-custom-domain">Bonus: TLS, DNS and custom domain</h2>
<p>Gitlab offers the possibility to set up a custom domain for your website. E.g. let's say you want to have this Gitlab Page accessible under your domain <code>your.domain</code> - it is doable.</p>
<p><em>You will need a custom domain you own and control (meaning, you can modify its DNS records).</em></p>
<p>Gitlab offers TLS out of the box, so you won't need to worry about getting a certificate for HTTPS traffic either.</p>
<h3 id="heading-how-to-set-it-all-up">How to set it all up?</h3>
<p>First, let's create a custom domain in the Gitlab Pages section of your project <code>Settings</code> &gt; <code>Pages</code> &gt; <code>New Domain</code>.</p>
<p>Here you will add your domain, leave the <code>Certificate</code> feature enabled and click on <code>Create New Domain</code>.</p>
<p>Gitlab will then ask for your domain to be verified. Gitlab generates a <code>TXT</code> record that you will have to add to your DNS in your domain. This is how Gitlab checks you rightly own the domain you are trying to add as custom.</p>
<p>When you add the <code>TXT</code> to your DNS, it will become active in the Pages section and a certificate will be generated. Default HTTPS redirection is enabled for this Page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672155871001/5b1a57a2-3aa5-4666-879a-6528076f407c.png" alt class="image--center mx-auto" /></p>
<p>Finally, you will need to add the domain <code>ALIAS</code> DNS record, also provided by Gitlab in the same Pages section.</p>
<blockquote>
<p>Remember to update <code>baseurl</code> in <code>config/_default/config.toml</code> with your newly added custom domain. If not, the site will not be accessible as it will be expected to be accessible in an outdated URL (former gitlab.io).</p>
</blockquote>
<h2 id="heading-conclusion-amp-references">Conclusion &amp; references</h2>
<p>Now you have your website, running on Gitlab Pages, built using Hugo + Blowfish, populated with your content, in a custom domain you own. Mostly for free (depending on DNS service).</p>
<p>It is a straightforward process involving a couple of moving parts, that hopefully can be done fairly quickly.</p>
<h3 id="heading-references">References</h3>
<ul>
<li><p><a target="_blank" href="https://nunocoracao.github.io/blowfish/">Blowfish</a>.</p>
</li>
<li><p><a target="_blank" href="https://gohugo.io/">Hugo</a>.</p>
</li>
<li><p><a target="_blank" href="https://docs.gitlab.com/ee/user/project/pages/">Gitlab Pages</a>.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[IRSA in EKS: a Kubernetes - AWS bridge]]></title><description><![CDATA[This guide assumes you manage your infrastructure with Terraform.
Here is an example IRSA implementation using Terraform and kubectl.
Background
Let's say you want to allow your EKS-hosted app to access an AWS service. You have a couple of options to...]]></description><link>https://blog.mariano.cloud/irsa-in-eks-a-kubernetes-aws-bridge</link><guid isPermaLink="true">https://blog.mariano.cloud/irsa-in-eks-a-kubernetes-aws-bridge</guid><category><![CDATA[irsa]]></category><category><![CDATA[AWS]]></category><category><![CDATA[OIDC]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[EKS]]></category><dc:creator><![CDATA[Mariano González]]></dc:creator><pubDate>Sat, 17 Dec 2022 17:37:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1671294070436/_BCz1XYMm.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>This guide assumes you manage your infrastructure with Terraform.</strong></p>
<p>Here is an example IRSA implementation using Terraform and kubectl.</p>
<h2 id="heading-background">Background</h2>
<p>Let's say you want to allow your EKS-hosted app to access an AWS service. You have a couple of options to do so, depending on the application:</p>
<ul>
<li><p>Using AWS IAM credentials (key/secret) injected into your pods as Kubernetes secrets or via environment variables.</p>
</li>
<li><p>Have your pods use the AWS IAM EC2 instance profile (EKS nodes).</p>
</li>
</ul>
<p>But how can you achieve the same in a secure and scalable way? IRSA is your friend.</p>
<h3 id="heading-what-is-irsa">What is IRSA?</h3>
<p>IRSA stands for IAM Roles for Service Accounts. It is the method of linking an AWS IAM role with a Kubernetes <strong>service account</strong> attached to a pod. This method offers some advantages:</p>
<ul>
<li><p>You specify the Kubernetes <strong>service account</strong> (and namespace) that has access and trust to assume the corresponding IAM role. No other <strong>service account</strong> will be able to do so.</p>
</li>
<li><p>You can easily track the access events in AWS CloudTrail for a specific combination of IAM role and <strong>service account</strong>.</p>
</li>
<li><p>You can make the access as specific and granular as you need via IAM (role) policies.</p>
</li>
</ul>
<p>Every EKS cluster natively hosts a public OIDC discovery (unique) endpoint for your workloads to authenticate and access other AWS services, via AWS STS.</p>
<h3 id="heading-ok-cool-what-is-oidc-then">Ok cool, what is OIDC then?</h3>
<p>OIDC is an authentication protocol based on OAuth 2.0. It is designed to offer a handover layer to allow authenticating users/services without having to maintain credentials.</p>
<p><a target="_blank" href="https://openid.net/connect/">Here</a> you can find more info about OIDC.</p>
<p>Kubernetes supports various <a target="_blank" href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#authentication-strategies">authentication strategies</a>, and <a target="_blank" href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens">OpenID Connect</a> is one of them.</p>
<h3 id="heading-what-does-the-workflow-look-like">What does the workflow look like?</h3>
<h4 id="heading-involved-parties">Involved parties</h4>
<p>The authentication is done using three main components:</p>
<ul>
<li><p>SDK (client app) running in a pod.</p>
</li>
<li><p>OIDC discovery endpoint (hosted by EKS cluster).</p>
</li>
<li><p>(Kubernetes) <strong>service account</strong>.</p>
<ul>
<li><p>When a <strong>service account</strong> is created with an IRSA annotation, a JWT token is generated as a Kubernetes secret.</p>
<ul>
<li>This annotation is the linkage with the IAM role it needs to assume: <code>annotations: eks.amazonaws.com/role-arn: arn:aws:iam::&lt;aws_account_id&gt;:role/your-role</code></li>
</ul>
</li>
<li><p>When that <strong>service account</strong> is attached to a pod, it will automatically inject two new environment variables:</p>
<ul>
<li><p><code>AWS_ROLE_ARN</code> - the IAM role ARN.</p>
</li>
<li><p><code>AWS_WEB_IDENTITY_TOKEN_FILE</code> - the path to the JWT token.</p>
<ul>
<li>and mount the JWT token as a volume to that pod.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="heading-the-process">The process</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671295547200/LfAaPha3y.png" alt /></p>
<ol>
<li><p>When a Kubernetes <strong>service account</strong> (with the IAM role ARN annotation) is attached to a pod, the control plane EKS mutating admission webhook injects two env variables: <code>AWS_ROLE_ARN</code> and <code>AWS_WEB_IDENTITY_TOKEN_FILE</code> and mounts a volume containing an OIDC-generated JWT token.</p>
</li>
<li><p>When the client SDK (running in a pod with the <strong>service account</strong> attached) performs the AWS API call (e.g. <code>eks:ListClusters</code>), it under the hood sends a <code>AssumeRoleWithWebIdentity</code> request to AWS STS service, including both the JWT token and the role ARN it wants to assume to do the AWS API call (e.g. <code>eks:ListClusters</code>) from the <strong>service account</strong> annotation.</p>
</li>
<li><p>AWS STS validates the IAM role trust (assume) policy condition, which contains both the OIDC discovery ID and a combination of the Kubernetes namespace and <strong>service account</strong> name where the original API call is coming from.</p>
</li>
<li><p>If all checks out, it sends back a set of temporary AWS IAM role credentials to the client SDK.</p>
</li>
<li><p>Now the client SDK is using temporary credentials for an AWS IAM role, which allows access to a set of AWS resources defined in its policy.</p>
</li>
</ol>
<h2 id="heading-implementation">Implementation</h2>
<h3 id="heading-you-will-need">You will need</h3>
<ul>
<li><p>EKS cluster.</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/#kubectl">Kubectl</a> cli.</p>
</li>
<li><p><a target="_blank" href="https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli">Terraform</a>.</p>
</li>
</ul>
<h3 id="heading-what-we-will-create">What we will create</h3>
<ul>
<li><p>IAM side.</p>
<ul>
<li><p>role.</p>
</li>
<li><p>assume policy.</p>
</li>
<li><p>identity-based policy.</p>
</li>
</ul>
</li>
<li><p>Kubernetes side.</p>
<ul>
<li><p>namespace.</p>
</li>
<li><p>service account.</p>
</li>
<li><p>job.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-create-an-iam-role-policies">Create an IAM role + policies</h3>
<p>Add the following Terraform code and run <code>terraform apply</code>. It will create an IAM role with two policies attached.</p>
<pre><code class="lang-plaintext">locals {
    namespace = "irsa-sample-ns" # this is the namespace we will create within the EKS cluster.
    serviceaccount = "irsa-test" # this will be the service account the job will use.
}

data "aws_eks_cluster" "this" { # get EKS cluster attributes to use later on.
    name = "your-eks-cluster-name"
}

data "aws_iam_policy_document" "assume-policy" { # create an assume policy for STS + a combination of namespace + service account.
    statement {
      actions = ["sts:AssumeRoleWithWebIdentity"]
      condition {
        test = "StringEquals"
        variable = "${replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:sub"
        values = [
            join(":",["system:serviceaccount",local.namespace,local.serviceaccount])
        ]
      }
      condition {
        test = "StringEquals"
        variable = "${replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:aud"
        values = ["sts.amazonaws.com"]
      }
      principals {
        type        = "Federated"
        identifiers = [data.aws_eks_cluster.this.identity[0].oidc[0].issuer]
      }
    }
}

resource "aws_iam_role" "irsa-role" { # create the IAM role and attach both assume and inline identity based policies.
    name = "irsa-role"
    path = "/"
    assume_role_policy = data.aws_iam_policy_document.assume-policy.json
    inline_policy {
      name = "eks-list"
      policy = jsonencode({
        Version = "2012-10-17"
        Statement = [{
            Action = ["eks:ListClusters"]
            Effect = "Allow"
            Resource = "*"
        }]
      })
    }
}
</code></pre>
<p>As described, the <code>assume</code> policy will contain two conditions:</p>
<ul>
<li><p><code>data.aws_eks_cluster.this.identity[0].oidc[0].issuer:aud: sts.amazonaws.com</code> allowing AWS STS interaction.</p>
</li>
<li><p><code>data.aws_eks_cluster.this.identity[0].oidc[0].issuer:sub: system:serviceaccount:irsa-namespace:irsa-sa</code> matching with the correct namespace + <strong>service account</strong> name.</p>
</li>
</ul>
<p>And a <code>federated principal</code> pointing to the EKS OIDC provider. Those three combined are the logic involved to facilitate the final <code>assume</code> action.</p>
<p>The second policy (identity-based) is to allow the role to perform a <code>eks:ListClusters</code> API call.</p>
<h3 id="heading-create-a-test-namespace">Create a test namespace</h3>
<pre><code class="lang-bash">kubectl create ns irsa-sample-ns
</code></pre>
<h3 id="heading-create-a-kubernetes-service-account">Create a kubernetes service account</h3>
<pre><code class="lang-bash">cat &lt;&lt;EoF&gt; serviceAccount-eks.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: irsa-test
  namespace: irsa-sample-ns
EoF

kubectl apply -f serviceAccount-eks.yaml
</code></pre>
<h3 id="heading-create-a-kubernetes-job">Create a Kubernetes job</h3>
<pre><code class="lang-bash">cat &lt;&lt;EoF&gt; job-eks.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: eks-iam-test-eks
  namespace: irsa-sample-ns
spec:
  template:
    metadata:
      labels:
        app: eks-iam-test-eks
    spec:
      serviceAccountName: irsa-test
      containers:
      - name: eks-iam-test
        image: amazon/aws-cli:latest
        args: [<span class="hljs-string">"eks"</span>, <span class="hljs-string">"list-clusters"</span>]
      restartPolicy: Never
  backoffLimit: 0
EoF

kubectl apply -f job-eks.yaml
</code></pre>
<p>This job will create a pod using the official aws-cli image, in <code>irsa-sample-ns</code> namespace, with an <code>app=eks-iam-test-eks</code> label.</p>
<p>It will then run <code>$ aws eks list-clusters</code> and finish. In this first iteration, the job should fail as there is no explicit policy or permission allowing this job (pod) to run any AWS API calls.</p>
<p>Let's check its logs to see the error:</p>
<pre><code class="lang-bash">$ kubectl logs -n irsa-sample-ns -l app=eks-iam-test-eks

An error occurred (AccessDeniedException) when calling the ListClusters operation: User: arn:aws:sts::&lt;your_aws_account_id&gt;:assumed-role/&lt;your_eks_node_group_id&gt;/&lt;ec2_instance_id&gt; is not authorized to perform: eks:ListClusters on resource: arn:aws:eks:&lt;your_aws_account_region&gt;:&lt;your_aws_account_id&gt;:cluster/*
</code></pre>
<p><em>I have redacted the AWS account, region, EKS node group and EC2 instance IDs.</em></p>
<p>The aws-cli session running in the pod (SDK, in this case, Python <code>botocore</code>) does not have a linked IAM role giving permissions for the requested API call. It is using the <code>irsa-test</code> <strong>service account</strong> we created in this namespace, which does not have any IRSA annotations, hence it is using the EKS node (instance profile) IAM role.</p>
<p>Now let's annotate the <code>irsa-test</code> <strong>service account</strong> we created, with the IAM role that has the permission(s) the job needs.</p>
<pre><code class="lang-bash">kubectl annotate sa -n irsa-sample-ns irsa-test eks.amazonaws.com/role-arn=arn:aws:iam::&lt;your_aws_account_id&gt;:role/irsa-role
</code></pre>
<p>This annotation is the linkage between the <strong>service account</strong> and the IAM role we created before.</p>
<p>Now, let's delete the job:</p>
<pre><code class="lang-bash">kubectl delete -f job-eks.yaml
</code></pre>
<p>And redeploy:</p>
<pre><code class="lang-bash">cat &lt;&lt;EoF&gt; job-eks.yaml 
apiVersion: batch/v1
kind: Job
metadata:
  name: eks-iam-test-eks
  namespace: irsa-sample-ns
spec:
  template:
    metadata:
      labels:
        app: eks-iam-test-eks
    spec:
      serviceAccountName: irsa-test
      containers:
      - name: eks-iam-test
        image: amazon/aws-cli:latest
        args: [<span class="hljs-string">"eks"</span>, <span class="hljs-string">"list-clusters"</span>]
      restartPolicy: Never
  backoffLimit: 0
EoF

kubectl apply -f job-eks.yaml
</code></pre>
<p>Let's check its logs to see what happened:</p>
<pre><code class="lang-bash">$ kubectl logs -n irsa-sample-ns -l app=eks-iam-test-eks
{
    <span class="hljs-string">"clusters"</span>: [
        <span class="hljs-string">"&lt;your_eks_cluster_name&gt;"</span>
    ]
}
</code></pre>
<p>Now that we annotated the <code>irsa-test</code> <strong>service account</strong>, the aws-cli running in the pod successfully assumed the <code>irsa-role</code> IAM Role that has the proper permissions to perform the required API call <code>eks:ListClusters</code>.</p>
<h3 id="heading-optional-see-related-events-in-cloudtrail">(optional) See related events in CloudTrail</h3>
<p>If you have CloudTrail enabled, you can also see these in Event history &gt; search for <code>User Name = system:serviceaccount:irsa-sample-ns:irsa-test</code> - you will find an entry for this <strong>service account</strong> performing the required <code>sts:AssumeRoleWithWebIdentity</code> API call.</p>
<p>And searching for <code>Event name = ListClusters</code> you will find:</p>
<ul>
<li><p>A first entry for the EKS node (EC2 instance ID) trying to do <code>eks:ListClusters</code> via <code>arn:aws:sts::&lt;your_aws_account_id&gt;:assumed-role/&lt;your_eks_node_group_id&gt;/&lt;ec2_instance_id&gt;</code> with an <code>AccessDenied</code> error code.</p>
</li>
<li><p>And then, a second entry for the aws-cli session (botocore) being able to successfully do <code>eks:ListClusters</code> by assuming <code>arn:aws:sts::&lt;your_aws_account_id&gt;:assumed-role/irsa-role/botocore-session-&lt;id&gt;</code> .</p>
</li>
</ul>
<h3 id="heading-cleanup">Cleanup</h3>
<ul>
<li><p>Delete the job</p>
<pre><code class="lang-bash">  kubectl delete -f job-eks.yaml
</code></pre>
</li>
<li><p>Delete the <strong>service account</strong></p>
<pre><code class="lang-bash">  kubectl delete -f serviceAccount-eks.yaml
</code></pre>
</li>
<li><p>Delete the namespace</p>
<pre><code class="lang-bash">  kubectl delete ns irsa-sample-ns
</code></pre>
</li>
<li><p>Delete the IAM role by removing the Terraform code and running <code>terraform apply</code>.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>IRSA helps secure your workloads by allowing them to use temporary credentials instead of using static IAM keys created for users or roles.</p>
<p>Also if you were to use the default EKS node IAM role (EC2 instance profile), then you would have to include every single service permission for every eventual app (pod) running on that specific EKS node - each application running on EKS might need to access a different AWS service, ergo different permissions. This will make accesses difficult to track down and it will get harder to maintain.</p>
<p>If you do not want to worry about mixed services policies or maintaining IAM credentials, consider implementing IRSA.</p>
<hr />
<p>Thank you for stopping by! Do you know other ways to do this? Please let me know in the comments, I always like to learn how to do things differently.</p>
]]></content:encoded></item></channel></rss>