<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ervin Szilagyi</title>
    <description>The latest articles on Forem by Ervin Szilagyi (@ervin_szilagyi).</description>
    <link>https://forem.com/ervin_szilagyi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F485678%2Fbaece0e0-a37e-4abe-9d77-66728db1fe95.jpeg</url>
      <title>Forem: Ervin Szilagyi</title>
      <link>https://forem.com/ervin_szilagyi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ervin_szilagyi"/>
    <language>en</language>
    <item>
      <title>my latest blog post</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Mon, 28 Jul 2025 06:15:35 +0000</pubDate>
      <link>https://forem.com/ervin_szilagyi/my-latest-blog-post-4j7a</link>
      <guid>https://forem.com/ervin_szilagyi/my-latest-blog-post-4j7a</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/aws-builders/lessons-learned-from-building-an-mcp-client-30if" class="crayons-story__hidden-navigation-link"&gt;Lessons Learned from Building an MCP Client&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/aws-builders"&gt;
            &lt;img alt="AWS Community Builders  logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2794%2F88da75b6-aadd-4ea1-8083-ae2dfca8be94.png" class="crayons-logo__image"&gt;
          &lt;/a&gt;

          &lt;a href="/ervin_szilagyi" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F485678%2Fbaece0e0-a37e-4abe-9d77-66728db1fe95.jpeg" alt="ervin_szilagyi profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ervin_szilagyi" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Ervin Szilagyi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Ervin Szilagyi
                
              
              &lt;div id="story-author-preview-content-2723244" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ervin_szilagyi" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F485678%2Fbaece0e0-a37e-4abe-9d77-66728db1fe95.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Ervin Szilagyi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/aws-builders" class="crayons-story__secondary fw-medium"&gt;AWS Community Builders &lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/aws-builders/lessons-learned-from-building-an-mcp-client-30if" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 25 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/aws-builders/lessons-learned-from-building-an-mcp-client-30if" id="article-link-2723244"&gt;
          Lessons Learned from Building an MCP Client
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/java"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;java&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/langchain"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;langchain&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/aws-builders/lessons-learned-from-building-an-mcp-client-30if" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;9&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/aws-builders/lessons-learned-from-building-an-mcp-client-30if#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            15 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>mcp</category>
      <category>java</category>
      <category>langchain</category>
    </item>
    <item>
      <title>Lessons Learned from Building an MCP Client</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Fri, 25 Jul 2025 20:05:43 +0000</pubDate>
      <link>https://forem.com/aws-builders/lessons-learned-from-building-an-mcp-client-30if</link>
      <guid>https://forem.com/aws-builders/lessons-learned-from-building-an-mcp-client-30if</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;It is &lt;del&gt;May&lt;/del&gt; July 2025. By now, everyone and their mother has heard about AI agents, MCP, and all these fancy words being thrown around. According to some, MCP will change the world, while others consider it a marginal improvement over existing solutions. Having a skeptical mind, I decided to fiddle around and figure out what all the fuss is about with this shiny new technology.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP exactly?
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) is an open protocol that standardizes how applications provide context to LLMs&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. It is a set of rules and concepts defining how additional information can be provided to an LLM to achieve better results from prompts or how to augment LLMs with tools, such as the ability to search the web, use a calculator, or control a computer. The MCP protocol defines other concepts besides tool calling and resources, and it is likely we will see further additions in the future. Currently, tool calling is by far the most widely implemented concept from the specification.&lt;/p&gt;

&lt;p&gt;The protocol defines the notions of MCP Servers and MCP Clients. While these resemble the classical client-server architecture, there are certain important differences, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The majority of MCP Server implementations run locally on the same machine as the MCP Client. Although the protocol provides the option for a remote server, it currently feels somewhat like an afterthought. I am confident this will be improved in the future.&lt;/li&gt;
&lt;li&gt;Many MCP servers are implemented to serve one user at a time. This is somewhat unheard of in the traditional server landscape. However, since most of these servers are intended to be local-first, this is to be expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The concepts brought together by MCP are not entirely new. Tool/function calling, for instance, was introduced by OpenAI back in 2023, after which others, such as Anthropic, followed suit with their own support and APIs. The aim of the MCP protocol is to standardize these efforts and provide a common way of implementing agentic workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Project of Choice
&lt;/h2&gt;

&lt;p&gt;Back to my project: I decided to build an MCP client. My idea was to create something similar to &lt;a href="https://github.com/block/goose" rel="noopener noreferrer"&gt;Goose CLI&lt;/a&gt;. The initial goal was to develop a CLI application that can be executed from a terminal. I aimed to support multiple large language models and enable the configuration of one or more MCP servers.&lt;/p&gt;

&lt;p&gt;My programming language of choice for this project was Java. While most online tutorials focus on JavaScript or Python, I wanted to approach this task differently. Java might not be the trendiest option, and it is certainly not always the most recommended choice for a CLI application, but it is not an inherently worse one. Several libraries, such as &lt;a href="https://picocli.info/" rel="noopener noreferrer"&gt;picocli&lt;/a&gt; and &lt;a href="https://jline.org/docs/intro" rel="noopener noreferrer"&gt;jline3&lt;/a&gt;, are aimed at helping with the development of text-based CLI applications. Additionally, &lt;a href="https://docs.langchain4j.dev/get-started" rel="noopener noreferrer"&gt;LangChain4J&lt;/a&gt; provides an implementation for many MCP features, although it is still in an experimental phase.&lt;/p&gt;

&lt;p&gt;Ultimately, my goal was to get a better understanding of the current state of working with LLMs and MCP using Java. I was not looking to reinvent the wheel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Here are a few lessons I learned during development. Some may be obvious to anyone who has spent more than a few minutes with an LLM model, while others might be new.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It is really easy to build the main loop of the application
&lt;/h3&gt;

&lt;p&gt;With the help of LangChain4J, it can be implemented in a few lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Read something from the user &lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readLine&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;gt;&amp;gt; "&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MaskingCallback&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Some boilerplate code to handle commands such as /exit from the user&lt;/span&gt;
        &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SessionCommand&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toCommand&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isPresent&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;SessionCommand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EXIT&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
                &lt;span class="c1"&gt;// .. other commands&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// This is the gist of our app. This makes an API call to the LLM, it automatically handles MCP tools calls and sends back the responses to the LLM.&lt;/span&gt;
        &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llmClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="c1"&gt;// Print the final answer to the user&lt;/span&gt;
        &lt;span class="n"&gt;stylizedPrinter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printMarkDown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RateLimitException&lt;/span&gt; &lt;span class="n"&gt;rateLimitException&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since it is Java, it is not as concise as it could be, but nevertheless, the main interaction with the LLM can be boiled down to this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llmClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The &lt;code&gt;llmClient&lt;/code&gt; itself is an interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;LlmClient&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@SystemMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""
                    You are a general-purpose AI agent.

                    The current date is: {{currentDateTime}}.

                    ...
                    """&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@UserMessage&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@V&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"currentDateTime"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;currentDateTime&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don't have to provide an implementation for this interface. All of this is handled under the hood by LangChain. It uses reflection to provide an implementation for this interface; we just have to specify that we want to use that interface for "AI" kind of purposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AiServices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LlmClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toolProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolProvider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I personally don't like this pattern. It makes our life harder whenever we need to debug something; it can be challenging to place a breakpoint inside something that exists only at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use a capable model for better outcomes
&lt;/h3&gt;

&lt;p&gt;I'm aware that what I'm saying with this point is obvious to any reader. However, in the case of MCP, it is really important to have a good and powerful model.&lt;/p&gt;

&lt;p&gt;First of all, the model should be able to support tool calling/function calling. Nowadays, most LLM models support tool calling, so this is not something we have to worry about.&lt;/p&gt;

&lt;p&gt;During development, I had the chance to test out the tool calling capabilities of a few models. To get straight to the most well-known LLM providers, at the time of writing this article, the latest models from Anthropic—Claude Sonnet 4, Claude 3.7, and even Claude 3.5—are, in my opinion, perfectly fine for "any" MCP tool calls. I'm using quotes because, obviously, there can be some counter-examples. Again, from my experience, these models are pretty good for this purpose. For most of my development, I used Claude Sonnet 3.5. It is faster than 3.7 most of the time, and it is good enough to focus on the things that matter when providing responses.&lt;/p&gt;

&lt;p&gt;That being said, I had a bad experience with Haiku 3.5. As a concrete example, I built a small MCP server to be able to query AWS cost usage based on a specific time period. Whenever I asked something like what are my costs for the last month, Haiku most of the time forgot what last month was. This information was explicitly provided in the system prompt &lt;code&gt;The current date is: {{currentDateTime}}.&lt;/code&gt;. On the other hand, Sonnet 3.5 was entirely fine remembering this piece of information, and it managed to query the correct information each and every time.&lt;/p&gt;

&lt;p&gt;Moving on to OpenAI models, the latest O1 model, GPT-4.1, and even GPT-4.5 are also good for anything. From my experience, O1 specifically gives more natural answers compared to any Anthropic model. Claude likes to be as chatty as possible and many times answers with a lot of fluff, while O1 usually tries to stick to the topic. With GPT-4.1, I noticed something interesting. In some cases, it simply avoids calling a tool and answers the question from its own knowledge base. This might be good; it will use fewer tokens, but it can also be a bad thing since it can come up with some nonsense.&lt;/p&gt;

&lt;p&gt;From Google, I tried the Gemini 2.5 Pro preview model. This model is also perfectly fine for an MCP client, especially for tool usage.&lt;/p&gt;

&lt;p&gt;I also tried Amazon Nova models. I had a really bad experience with Amazon Titan models&lt;sup id="fnref2"&gt;2&lt;/sup&gt; in the past, and unfortunately, I was still struggling with the latest AWS Nova models. For tool usage, everything other than Nova Pro is just borderline usable. Nova Pro itself also managed to hallucinate tools on the fly. This happened only a few times; in most cases, it is able to navigate between tools at hand. Nevertheless, I do not prefer the answers I get from it. More often than not, it feels like something is off. Even Haiku manages to feel more "natural," although Haiku can simply forget things, as I've mentioned before.&lt;/p&gt;

&lt;p&gt;What is important is that there are many options. I prefer Sonnet models; I did most of my development using Sonnet 3.5. Probably I would recommend Gemini 2.5 Pro; at the time of writing this article, it is the cheapest as far as I checked, and it is powerful enough for everything we might need.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Dealing with all kinds of limits
&lt;/h3&gt;

&lt;p&gt;Here is an exception I managed to receive many times while I was developing my MCP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error: Rate limited by the LLM provider: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"error"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"message"&lt;/span&gt;: &lt;span class="s2"&gt;"Request too large for gpt-4o in organization org-gqgox1u3NjYa2JiUAHl3HrMn on tokens per min (TPM): Limit 30000, Requested 87968. The input or output tokens must be reduced in order to run successfully. Visit https://platform.openai.com/account/rate-limits to learn more."&lt;/span&gt;,
        &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"tokens"&lt;/span&gt;,
        &lt;span class="s2"&gt;"param"&lt;/span&gt;: null,
        &lt;span class="s2"&gt;"code"&lt;/span&gt;: &lt;span class="s2"&gt;"rate_limit_exceeded"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is another one from Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;:&lt;span class="s2"&gt;"error"&lt;/span&gt;,&lt;span class="s2"&gt;"error"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;:&lt;span class="s2"&gt;"rate_limit_error"&lt;/span&gt;,&lt;span class="s2"&gt;"message"&lt;/span&gt;:&lt;span class="s2"&gt;"This request would exceed the rate limit for your organization (874899f0-c2de-4906-8802-cc478416bee6) of 20,000 input tokens per minute. For details, refer to: https://docs.anthropic.com/en/api/rate-limits. You can see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase."&lt;/span&gt;&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem we may face while working with LLM models, especially if we do tool calls, is that we cannot really control the size of our messages and also the rate of our messages. What do I mean by this?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Depending on the MCP servers we use, we might have just a few available tools that return short responses. On the other hand, we might be working with a server that exposes many tools, some of which can generate lengthy responses (looking at you, GitHub MCP). All of these things—tool definitions, requests for tool usage, and responses from tools—become part of the chat context, which ultimately translates to tokens.&lt;/li&gt;
&lt;li&gt;Depending on what the model decides, after a response from an MCP tool call, it can request further tool calls instead of providing a final answer to the initial user query. This will result in more back-and-forth communication between our client and the LLM model, racking up even more token usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge with this is that you, as the MCP client developer, cannot really intervene here in any of those cases, especially if you develop a general-purpose MCP client that is meant to be used with any kind of MCP server.&lt;/p&gt;

&lt;p&gt;Possible solutions I could think of to deal with the challenge would be the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try to somehow shorten/compress large MCP server responses. If your client somehow knows what to expect from the MCP tool call and also knows that some parts of the response are irrelevant, you might want to extract only the meaningful part from the response.&lt;/li&gt;
&lt;li&gt;Try to use another model (probably from another vendor) to do a summary of the response from the MCP. This might introduce a bunch of other issues, like information loss, even more token usage (more $), and way more waiting for a final answer to the initial query.&lt;/li&gt;
&lt;li&gt;Apply some rate limiting. Aside from the fact that this could make the user experience way worse, it could be challenging to implement in case we want to support several models from different providers. Limits can be different for each provider; each of them is using a different tokenizer; limits can and will change over time, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Let's Talk about $
&lt;/h3&gt;

&lt;p&gt;The limits we talked about in the previous section are there for several reasons: to save you from racking up a huge bill and also for the LLM providers to save themselves from bad actors. Running an LLM model requires a lot of computing power, hence a lot of energy.&lt;/p&gt;

&lt;p&gt;I figured that this would be the time to talk about money from the perspective of a client developer. While developing this client, I spent around $30 on Claude, around $10 on OpenAI, and a few bucks on both Gemini and Bedrock. Models from Bedrock are barely usable because of their aggressive rate limiting, so I could not rack up a huge cost there even if I wanted to.&lt;/p&gt;

&lt;p&gt;LLM providers are mainly charging based on the number of tokens used. As I mentioned before, if we add MCP tools to the mix, we kind of lose control over how many tokens we eat up with each query. This, I think, is a problem. In my client, I display the number of tokens used after each user query, but I have no way to estimate either the token usage or the cost beforehand. In the end, it is a good thing to have a limit imposed because I can imagine that we could pretty easily go haywire with a simple query.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Overcoming limitations imposed by the Context Windows
&lt;/h3&gt;

&lt;p&gt;Words and phrases we send to LLM models via API calls are transformed into tokens. This action is called tokenization and it is performed by a tokenizer&lt;sup id="fnref3"&gt;3&lt;/sup&gt;. There are different tokenization algorithms, the output of each being a set of tokens. Most of the time, we can call these tokens sub-words - they are groups of characters that represent part of the words from the original text.&lt;/p&gt;

&lt;p&gt;Why is it important to be aware of this tokenization process?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;LLM providers charge based on the number of tokens.&lt;/li&gt;
&lt;li&gt;Each LLM model has a context window, that is, the amount of input in tokens that the model can accept at any time&lt;sup id="fnref4"&gt;4&lt;/sup&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In case a query from a user cannot fit in the context window of the model, then the outcome will be a huge error response from the model. Nowadays, most of the models have a context window above 128k tokens, a number that is significantly larger than what we had around a year ago (for example, Amazon Titan, a model that was decommissioned at the start of this year, had a context window of 8k tokens). Even with a large context window, MCP usage, especially with tool calling, can quickly become problematic. To illustrate this, let's break down what a context window will contain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;System prompt: MCP clients need a system prompt. This can be shorter or lengthier depending on what we consider a good system prompt.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Definition of each available tool with a description of what it does, description of each argument, and other relevant information. As an example, this is a snippet that will be sent to the model in case we are using &lt;a href="https://github.com/github/github-mcp-server" rel="noopener noreferrer"&gt;GitHub's MCP server&lt;/a&gt;. Emphasis on the part that this is a snippet; the tool definition goes on and on.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create_pull_request_review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Create a review on a pull request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"input_schema"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Review comment text"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"comments"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Line-specific comments array of objects to place comments on pull request changes. Requires path and body. For line comments use line or position. For multi-line comments use start_line and line with optional side parameters."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comment body"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"line number in the file to comment on. For multi-line comments, the end of the line range"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path to the file"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"position of the comment in the diff"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"side"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. (LEFT or RIGHT)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"start_line"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The first line of the range to which the comment refers. Required for multi-line comments."&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"start_side"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The side of the diff on which the start line resides for multi-line comments. (LEFT or RIGHT)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commitId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SHA of commit to review"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT')"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Repository owner"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pullNumber"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pull request number"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Repository name"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pullNumber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User message(s): obviously, user requests will also be included in the context window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI messages: the responses from the model itself will also be part of the context window, in case there is an ongoing conversation, of course.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Requests from the model for calling tools and responses from tools: as you may have guessed, the whole procedure of executing a tool will also be part of the context window at some point. This is important because, depending on which MCP server we are communicating with, the responses from tools can be short or really, really large. For example, when using GitHub's MCP server, if I ask what kind of repositories I have, I will get a very lengthy response back, since I have a lot of repositories published to GitHub. To make matters worse, the LLM can request further tool calls after it gets back the response from the one it called. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI message summarizing the response from the tool: this happens each time a tool invocation occurs. The model will do its best to either summarize the tool response or to extract the necessary information from the tool response to answer the initial question from the user.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The point of all this was to illustrate that we can fill the available context window quickly. From a user's perspective, it can be really annoying if, after one or two interactions, the whole communication stream falls apart.&lt;/p&gt;

&lt;p&gt;The challenge is: how can we limit the growth of the content sent back and forth between the client and the model? Personally, I don't think there is a silver bullet to solve this challenge. What I did in my CLI application was try to squash tool messages. Requests and responses from tool calls can be eliminated without losing much information. The goal of the model is to give an answer to the user's question; tool calls essentially act as additional context (or RAG&lt;sup id="fnref5"&gt;5&lt;/sup&gt;) to provide a more accurate answer. What we can do, to be able to continue the conversation for longer, is remove certain "tool messages," essentially reducing the size of the context we are sending back and forth. In case the model finishes triggering new tool calls and decides to return an answer of its own, we can go back from the client and remove all the tool messages that are not visible to the user anyway.&lt;/p&gt;

&lt;p&gt;Again, this is probably a naive solution to the problem at hand, and it has downsides, such as information loss; or, in many cases, it simply does not work if the context window gets filled before the LLM provides a final answer. Nevertheless, it was an attempt to avoid crashing the program as often.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Lack of examples/best practices
&lt;/h3&gt;

&lt;p&gt;While scouring the internet for anything meaningful in my quest to build an MCP client application, I learned one thing: there is no clear way of doing it right. Everybody does tool calling; that is easy to understand and to implement. For anything else, well... &lt;/p&gt;

&lt;p&gt;My client also supports tool calls. It can also list available resources and prompts, but at this point, these cannot really be used for anything meaningful. MCP offers a lot more concepts. Aside from tools, resources, and prompts, there are concepts such as sampling and elicitation. My client does not use them in any way. My client is essentially a CLI app that can do tool calling. That's it. Admittedly, there are certain limitations to what is possible to accomplish with a text-based CLI app, but still, my app is nothing special. To be fair, most of the available open-source MCP clients are mainly based on tool calling.&lt;/p&gt;

&lt;p&gt;Why mainly tool calling? It is easy to understand, simple to implement, and can be genuinely useful to enhance the capabilities of an LLM model. &lt;/p&gt;

&lt;p&gt;In contrast, implementation for MCP resources is more challenging than expected. According to the protocol, resources can be anything&lt;sup id="fnref6"&gt;6&lt;/sup&gt;. It is literally impossible to implement a client that can use any kind of resource. I understand that specialized clients, such as code editors with MCP client features, would be able to support text-based resources of git repositories (GitHub MCP servers define them with the protocol &lt;code&gt;repo://&lt;/code&gt;), and the notion of resources was essentially invented for this kind of purpose. My main problem with resources is that the protocol definition is really vague for them. And it is similarly vague for other concepts as well.&lt;/p&gt;

&lt;p&gt;That being said, I'm pretty sure this will improve in the future as long as the protocol is adopted by many more people and organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I started writing this article in May 2025. Then life happened, and I had to put it aside to work on other things. Now, at the end of July, a lot has happened in the world of MCP. The protocol definition has improved significantly in the area of security, elicitation was introduced, and HTTP/SE is already considered legacy.&lt;/p&gt;

&lt;p&gt;The protocol is still changing, which is good. There are still a lot of rough edges to be ironed out.&lt;/p&gt;

&lt;p&gt;From a client development perspective, libraries are getting better. Langchain4j has evolved a lot in the last few months, but it lags behind in implementing everything from the MCP protocol definition. We are getting there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Links
&lt;/h2&gt;

&lt;p&gt;The source code for the CLI application I developed can be found on GitHub: &lt;a href="https://github.com/Ernyoke/jmcpx" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/jmcpx&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;MCP - Introduction: &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;link&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;How to Build a BlueSky RSS-like Bot with AWS Lambda and Terraform: &lt;a href="//how-to-build-a-bluesky-rss-like-bot-with-aws-lambda-and-terraform.md"&gt;link&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;The Technical User's Introduction to LLM Tokenization: &lt;a href="https://christophergs.com/blog/understanding-llm-tokenization" rel="noopener noreferrer"&gt;link&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;What is a context window?: &lt;a href="https://www.ibm.com/think/topics/context-window" rel="noopener noreferrer"&gt;link&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;What is RAG (Retrieval-Augmented Generation)?: &lt;a href="https://aws.amazon.com/what-is/retrieval-augmented-generation/" rel="noopener noreferrer"&gt;link&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;From the MCP protocol documentation as of version &lt;a href="https://modelcontextprotocol.io/docs/concepts/resources" rel="noopener noreferrer"&gt;2025-06-18&lt;/a&gt;: "Resources represent any kind of data that an MCP server wants to make available to clients." ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>mcp</category>
      <category>java</category>
      <category>langchain</category>
    </item>
    <item>
      <title>Building Super Slim Containerized Lambdas on AWS - Revisited</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Wed, 02 Apr 2025 14:11:50 +0000</pubDate>
      <link>https://forem.com/aws-builders/building-super-slim-containerized-lambdas-on-aws-revisited-8m7</link>
      <guid>https://forem.com/aws-builders/building-super-slim-containerized-lambdas-on-aws-revisited-8m7</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Recently, I was reading through some AWS blogs when I stumbled upon this article, &lt;a href="https://aws.amazon.com/blogs/containers/optimize-your-container-workloads-for-sustainability/" rel="noopener noreferrer"&gt;Optimize your container workloads for sustainability&lt;/a&gt; by Karthik Rajendran and Isha Dua. Among other topics, the article discusses reducing the size of your Lambda container images to achieve better sustainability. Some of the main points discussed are &lt;strong&gt;how&lt;/strong&gt; and &lt;strong&gt;why&lt;/strong&gt; to reduce the size of Lambda containers - the idea being that smaller containers require less bandwidth to transfer over the internet, take up less storage on disk, and are usually faster to build, thereby consuming less energy.&lt;/p&gt;

&lt;p&gt;At first glance, this might seem like a minor optimization, but when you consider that AWS has millions of customers building Docker images, it suddenly makes sense to recommend working with slimmer images. Furthermore, having smaller images is considered a best practice overall.&lt;/p&gt;

&lt;p&gt;Around three years ago, I wrote an article about the &lt;strong&gt;"how"&lt;/strong&gt;, I was discussing ways to reduce the size of Lambda containers. My article was titled &lt;a href="https://dev.to/aws-builders/building-super-slim-containerized-lambdas-on-aws-3kpe"&gt;Building Super Slim Containerized Lambdas on AWS&lt;/a&gt; and it primarily focused on Lambda functions written in Rust. Reading the AWS blog article reminded me that I should probably revisit the topic of creating slim Lambda images and provide a more informed perspective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short Recap
&lt;/h2&gt;

&lt;p&gt;As I mentioned, in my old article, I used Rust to build a Lambda function. Code written in Rust is compiled into a binary. To execute this binary as a Lambda function, you can either upload the binary directly to AWS Lambda or package it in a Docker image, push the image to Amazon ECR, and configure a Lambda function to use that image.&lt;/p&gt;

&lt;p&gt;The size of this Docker image can vary significantly. If you use the default image recommended by AWS (&lt;code&gt;public.ecr.aws/lambda/provided&lt;/code&gt;), the container size can be a few hundred megabytes. However, if you go with a minimal approach, such as a Distroless image, you can get containers down to just a few dozen megabytes, depending mostly on the size of the compiled binary.&lt;/p&gt;

&lt;p&gt;When it comes to Lambda execution time, image size has little impact. Whether the image is large or small, the function's execution time remains roughly the same - though I haven’t tested exceptionally large images that might push Lambda’s limits.&lt;/p&gt;

&lt;p&gt;Ultimately, the takeaway from the article was that while Distroless allows you to build smaller images, it might not be all that beneficial since it doesn’t improve performance. I also pointed out that a smaller image can speed up the build pipeline. Admittedly, at that time, I didn’t even consider the sustainability angle. Even after all these years, I can only speculate about its impact. As a solo AWS user, it’s difficult to quantify how much of a difference using smaller images would actually make.&lt;/p&gt;

&lt;p&gt;On the other hand, I’m really excited to dive into the nifty details of building tiny images. &lt;/p&gt;

&lt;h2&gt;
  
  
  Steps to Build a Slim Container
&lt;/h2&gt;

&lt;p&gt;In order to build slim containers, you would want to leverage the following steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Use tiny base images
&lt;/h3&gt;

&lt;p&gt;This is probably the most important step in reducing the image size. In most cases, this step allows us to achieve the greatest size reduction without modifying our application's source code.&lt;/p&gt;

&lt;p&gt;The general idea is that instead of base images such as &lt;code&gt;public.ecr.aws/lambda/provided:al2&lt;/code&gt; (the AWS-recommended image for compiled executables like Rust applications) or images based on Ubuntu you can use images such as Alpine, Distroless or Chainguard. These alternatives have a significantly smaller footprint. &lt;/p&gt;

&lt;p&gt;Distroless and Chainguard images are optimized to include only the essentials needed for execution. They lack a shell, package manager, and most standard Linux binaries, making them extremely small. Alpine-based images include more utilities, such as a shell and basic Linux binaries, yet they are still designed to occupy only a few megabytes on disk.&lt;/p&gt;

&lt;p&gt;The downside of using these small images is that they can be challenging to work with during development or debugging. For example, you cannot directly open a shell in a Distroless image since it does not include one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Note&lt;/strong&gt;:&lt;br&gt;
To start a shell inside a Distroless image, you can rebuild your image using the &lt;code&gt;:debug&lt;/code&gt; tag. For example, instead of &lt;code&gt;FROM gcr.io/distroless/cc:latest-amd64&lt;/code&gt; you can have &lt;code&gt;FROM gcr.io/distroless/cc:debug&lt;/code&gt;. "Debug" images include busybox, which provides a minimal set of common Linux binaries..&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ℹ️ Note&lt;/strong&gt;:&lt;br&gt;
Similarly, with Chainguard images, you can append the &lt;code&gt;-dev&lt;/code&gt; keyword to the tag of the image. For example, if you have an image built on &lt;code&gt;FROM cgr.dev/chainguard/static:latest&lt;/code&gt;, you can rebuild it with &lt;code&gt;FROM cgr.dev/chainguard/static:latest-dev&lt;/code&gt; to have access to a shell and other debug tools.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Add strictly what you need to the image
&lt;/h3&gt;

&lt;p&gt;This point is similar to the previous one, the difference being that even if you choose the smallest base image possible, you might found yourself having a bunch of unnecessary stuff added back to your image at build time. To avoid this, you can do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Use multistage builds&lt;sup id="fnref1"&gt;1&lt;/sup&gt;: Most likely, your application is built alongside the Docker image you publish. In many cases, developers copy the source code into the image and then run commands to create the application package. As a result, the final image may contain unnecessary development dependencies that are not required for execution. You can either remove these manually, or just simply use multistage builds. In case of multistage builds, you can use different images for build and execution. In fact, you can use a fully fledged, "large" development image for the build step, after which you copy the artifacts to a stripped down image used for execution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;.dockerignore&lt;/code&gt; to copy only what you need to your image: similarly to &lt;code&gt;.gitignore&lt;/code&gt;, .&lt;code&gt;dockerignore&lt;/code&gt; let's you specifies files and folders which you should not copy over to your images at build time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Use a compiled language to build a small executable
&lt;/h3&gt;

&lt;p&gt;Nowadays, it is becoming increasingly challenging to distinguish between interpreted and compiled languages. Many interpreted (or so-called "scripting") languages use just-in-time (JIT) compilers, meaning the code is compiled to machine code at execution time.&lt;/p&gt;

&lt;p&gt;The key takeaway here is that, to optimize the size of your container, you may want to avoid writing your Lambda functions in languages that require an interpreter or JIT compiler. Instead, you should choose something that compiles to machine code and is compressed into an executable.&lt;/p&gt;

&lt;p&gt;To be more specific, rather than using Python or JavaScript, you might consider Rust, Go, or C++. Of course, I fully understand that this may not always be feasible, and my intention is not to discredit Python, JavaScript, or any other language. However, it is important to recognize that if we prioritize minimizing container size, eliminating the interpreter can free up tens-if not hundreds—of megabytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Static vs dynamic linking
&lt;/h3&gt;

&lt;p&gt;In case you followed previous points, your image should be really small right now. At this point, probably your main goal is to reduce the size of the executable. One thing you may encounter at this point is the presence of libc (usually &lt;code&gt;glibc&lt;/code&gt;) in your Docker image. Both Distroless and Chainguard present the option to chose a base image the has &lt;code&gt;glibc&lt;/code&gt; and an equivalent image that does not have it. Obviously, the image that has glibc is larger in size.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;glibc&lt;/code&gt;, or the GNU C Library, is the standard C library (libc) implementation used on Linux systems. It offers a wide range of functions that allow programs to interact with the operating system, such as handling input/output, managing memory, and manipulating strings. Rust, relies on &lt;code&gt;glibc&lt;/code&gt; for interacting with the operating system. On Linux, this typically means linking against &lt;code&gt;glibc&lt;/code&gt;, as Rust's standard library, &lt;code&gt;libstd&lt;/code&gt;, uses it for system calls and other operations. On the other hand, Go offers more flexibility by allowing compilation without relying on the system's C standard library. By setting &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;, Go programs use their own implementations for system interactions, avoiding &lt;code&gt;glibc&lt;/code&gt; dependency. This means that if you build a Lambda function with Go, you can just disable linking against &lt;code&gt;glibc&lt;/code&gt; and you can put our executable in an image that does not have the library. In case of Rust, you can build an executable that statically links libc by using musl&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Building an image from Scratch
&lt;/h3&gt;

&lt;p&gt;The most minimal base image you can use is &lt;code&gt;scratch&lt;/code&gt;&lt;sup id="fnref4"&gt;4&lt;/sup&gt;. You can simply copy your binary executable to it the image and it will be executed as PID 1. This can work for AWS Lambda functions as well, but you may encounter issues depending on what your Lambda function attempts to accomplish. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CA (Certificate Authority) certificates will be missing, you wont even have a &lt;code&gt;/etc/ssl/certs/&lt;/code&gt; folder. This will cause HTTPS connections to fail. To use any AWS service such as DynamoDB or S3, HTTPS must work.&lt;/li&gt;
&lt;li&gt;Standard directories such as &lt;code&gt;/var&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, and &lt;code&gt;/root&lt;/code&gt; will be missing. The exception is the &lt;code&gt;/tmp&lt;/code&gt; directory, which will be mounted by AWS, allowing us to write to a temporary folder if needed.&lt;/li&gt;
&lt;li&gt;Time zone data may cause issues, as the &lt;code&gt;/usr/share/zoneinfo&lt;/code&gt; directory will be missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, you can overcome these issues by adding the necessary files and folders at build time, but that defeats the purpose of using the &lt;code&gt;scratch&lt;/code&gt; base image. Instead, you would rather choose Distroless or Chainguard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Slimmest Image Possible for a Rust Lambda
&lt;/h2&gt;

&lt;p&gt;Following these steps let's try to build a slim but usable containerized image for a Lambda function developed in Rust.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ARG &lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/function"&lt;/span&gt;

FROM rust:1.84-bullseye AS builder

WORKDIR /build

ADD Cargo.toml Cargo.toml
ADD Cargo.lock Cargo.lock
ADD src src

&lt;span class="c"&gt;# Cache build folders, see: https://stackoverflow.com/a/60590697/7661119&lt;/span&gt;
&lt;span class="c"&gt;# Docker Buildkit required&lt;/span&gt;
RUN &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/cargo/registry &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/home/root/app/target &lt;span class="se"&gt;\&lt;/span&gt;
    rustup target add x86_64-unknown-linux-musl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; x86_64-unknown-linux-musl

&lt;span class="c"&gt;# copy artifacts to a clean image&lt;/span&gt;
FROM cgr.dev/chainguard/static:latest

COPY &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;builder /build/target/x86_64-unknown-linux-musl/release/bootstrap bootstrap

ENTRYPOINT &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"./bootstrap"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The size of this image is 4.03 MB, of which the final base image itself (&lt;code&gt;chainguard/static&lt;/code&gt;) accounts for approximately 1.33 MB, while the remaining 2.7 MB is the executable. Admittedly, my Lambda function does not do a lot and has only a few dependencies (the code for the function can be found here: &lt;a href="https://github.com/Ernyoke/aws-lambda-benchmarks/tree/main/aws-lambda-compute-pi-rs/src" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). What I would want to point out is that this Docker image follows the steps outlined above to achieve the reduced size:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It is using a builder image to compile the executable.&lt;/li&gt;
&lt;li&gt;Only the necessary files a copied to the final image - specifically, the executable named &lt;code&gt;bootstrap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It is using &lt;code&gt;chainguard/static&lt;/code&gt; to run the Lambda function. Distroless could have been an option as well, but that would result in a slightly larger image size (4.68 MB).&lt;/li&gt;
&lt;li&gt;It is using &lt;code&gt;x86_64-unknown-linux-musl&lt;/code&gt; toolchain to build the executable, ensuring that libc is statically linked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Additionally, the target architecture is &lt;code&gt;x86_64&lt;/code&gt;. However, with a few modifications, you could build the same image for arm64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ARG &lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/function"&lt;/span&gt;

FROM rust:1.84-bullseye AS builder

WORKDIR /build

ADD Cargo.toml Cargo.toml
ADD Cargo.lock Cargo.lock
ADD src src

&lt;span class="c"&gt;# Cache build folders, see: https://stackoverflow.com/a/60590697/7661119&lt;/span&gt;
&lt;span class="c"&gt;# Docker Buildkit required&lt;/span&gt;
RUN &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/cargo/registry &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/home/root/app/target &lt;span class="se"&gt;\&lt;/span&gt;
    rustup target add aarch64-unknown-linux-musl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; aarch64-unknown-linux-musl

&lt;span class="c"&gt;# copy artifacts to a clean image&lt;/span&gt;
FROM gcr.io/distroless/static:latest-arm64

COPY &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;builder /build/target/aarch64-unknown-linux-musl/release/bootstrap bootstrap

ENTRYPOINT &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"./bootstrap"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The size of this image will be roughly the same - I measured 4.06 MB on my computer. There are minor variations in size depending on the target architecture, with &lt;code&gt;x86_64&lt;/code&gt; typically being a few KB smaller. However, this difference is negligible.&lt;/p&gt;

&lt;p&gt;You can still get the image slimmer by using &lt;code&gt;scratch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ARG &lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/function"&lt;/span&gt;

FROM rust:1.84-bullseye AS builder

WORKDIR /build

ADD Cargo.toml Cargo.toml
ADD Cargo.lock Cargo.lock
ADD src src

&lt;span class="c"&gt;# Cache build folders, see: https://stackoverflow.com/a/60590697/7661119&lt;/span&gt;
&lt;span class="c"&gt;# Docker Buildkit required&lt;/span&gt;
RUN &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/cargo/registry &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/home/root/app/target &lt;span class="se"&gt;\&lt;/span&gt;
    rustup target add x86_64-unknown-linux-musl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; x86_64-unknown-linux-musl

&lt;span class="c"&gt;# copy artifacts to a clean image&lt;/span&gt;
FROM scratch

COPY &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;builder /build/target/x86_64-unknown-linux-musl/release/bootstrap bootstrap

ENTRYPOINT &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"./bootstrap"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case, this Lambda function works - it executes successfully and produces the expected output. However, all it does is calculate the value of PI using the Unbounded Spigot Algorithm for the Digits of PI&lt;sup id="fnref5"&gt;5&lt;/sup&gt;. It is a "toy" function, serving as proof that &lt;code&gt;scratch&lt;/code&gt; can work for Lambda functions. Nevertheless, I do not recommend using this base image. The size of this image is 2.69 MB.&lt;/p&gt;

&lt;p&gt;Before closing this section, here is a comparison between base images with Rust executables:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Build the Slimmest Image Possible for a Python Lambda
&lt;/h2&gt;

&lt;p&gt;Understandably, there can be several reasons not to use a compiled language for your Lambda function. In this section of the article, we will try to build a slim image for a Lambda function written in Python.&lt;/p&gt;

&lt;p&gt;For this example, I will use a Python image based on Alpine Linux. Alpine-based images are widely known as slim images, but they are not the slimmest possible options. They come with a package manager (&lt;code&gt;apk&lt;/code&gt;), a shell, and all the well-known Linux utilities, so they are not as "clean" as Distroless or Chainguard-based images.&lt;/p&gt;

&lt;p&gt;I tried to build an image for a Python Lambda using a Distroless base, but I failed miserably. The AWS Lambda Python Runtime Interface Client relies on C++ modules using CPython&lt;sup id="fnref6"&gt;6&lt;/sup&gt;. These C++ modules have to be built at installation time and require a bunch of dependencies. Besides, they rely on dynamically linking several dependencies (musl vs. glibc, remember?). I tried to add all the necessary dependencies to the final runtime image, but in the end, whatever I did, it didn’t work out. So I gave up. This might be another project of mine for a later time—to try to make it work.&lt;/p&gt;

&lt;p&gt;Nevertheless, here is how a containerized Lambda function for Python would look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ARG &lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/home/app/"&lt;/span&gt;
ARG &lt;span class="nv"&gt;RUNTIME_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"3.11"&lt;/span&gt;
ARG &lt;span class="nv"&gt;DISTRO_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"3.21"&lt;/span&gt;

&lt;span class="c"&gt;# Stage 1 - bundle base image + runtime&lt;/span&gt;
&lt;span class="c"&gt;# Grab a fresh copy of the image and install GCC&lt;/span&gt;
FROM python:&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;-alpine&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DISTRO_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; AS python-alpine
&lt;span class="c"&gt;# Install GCC (Alpine uses musl but we compile and link dependencies with GCC)&lt;/span&gt;
RUN apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; libstdc++

&lt;span class="c"&gt;# Stage 2 - build function and dependencies&lt;/span&gt;
FROM python-alpine AS build-image

&lt;span class="c"&gt;# Needed for libexecinfo-dev. Alternatives such as libunwind may build awslambdaric, but the function wont execute in the final runtime image.&lt;/span&gt;
&lt;span class="c"&gt;# Tanks https://stackoverflow.com/questions/77518311/dockerfile-for-node16-alpine-in-aws-lambda&lt;/span&gt;
RUN apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--repository&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://dl-cdn.alpinelinux.org/alpine/v3.16/main/ libexecinfo-dev

&lt;span class="c"&gt;# Install aws-lambda-cpp build dependencies&lt;/span&gt;
RUN apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    build-base &lt;span class="se"&gt;\&lt;/span&gt;
    libtool &lt;span class="se"&gt;\&lt;/span&gt;
    autoconf &lt;span class="se"&gt;\&lt;/span&gt;
    automake &lt;span class="se"&gt;\&lt;/span&gt;
    libexecinfo-dev &lt;span class="se"&gt;\&lt;/span&gt;
    make &lt;span class="se"&gt;\&lt;/span&gt;
    cmake &lt;span class="se"&gt;\&lt;/span&gt;
    libcurl

ARG FUNCTION_DIR
ARG RUNTIME_VERSION

RUN &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

COPY &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

RUN python&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;awslambdaric &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
RUN python&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;requirements.txt &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Stage 3 - final runtime image&lt;/span&gt;
&lt;span class="c"&gt;# Grab a fresh copy of the Python image&lt;/span&gt;
FROM python-alpine

ARG FUNCTION_DIR

WORKDIR &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

COPY &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;build-image &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FUNCTION_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

ENTRYPOINT &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"python"&lt;/span&gt;, &lt;span class="s2"&gt;"-m"&lt;/span&gt;, &lt;span class="s2"&gt;"awslambdaric"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

CMD &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"main.handler"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Admittedly, I didn’t come up with all of this by myself. The image is based on &lt;a href="https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support" rel="noopener noreferrer"&gt;this&lt;/a&gt; blogpost&lt;sup id="fnref7"&gt;7&lt;/sup&gt; from &lt;a href="https://bsky.app/profile/danilop.bsky.social" rel="noopener noreferrer"&gt;Danilo Poccia&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The final image size is 81.6 MB, which is significantly smaller than the 717 MB base image (&lt;code&gt;public.ecr.aws/lambda/python&lt;/code&gt;) recommended in the AWS documentation&lt;sup id="fnref8"&gt;8&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;To put into perspective what we built, here is a comparison of the Rust x86-64 images and the Python images. The size of the Alpine image is more than three times larger than the largest Distroless image. But all in all, it is still relatively small compared to the base images provided by AWS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdkvuao4e8q5dlgri5o1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdkvuao4e8q5dlgri5o1z.png" alt="Rust (x86-64) and Python Container Image Sizes by Base Images" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Revisiting this topic made me realize that building smaller Docker images can be a lot of fun, but it can also be quite challenging. Returning to the original point of this article, it is true that smaller containers can contribute to sustainability. However, in order to build and work with them as developers, we must invest a significant amount of time and effort.&lt;/p&gt;

&lt;p&gt;Should you go in tomorrow and try to reduce the size of all your Lambda images running in production? Probably not. There is no such thing as a free lunch. You trade ease of use for bandwidth and storage savings. It is up to you to decide if it’s worth it.&lt;/p&gt;

&lt;p&gt;As always, the code referenced in this article can be found on Github: &lt;a href="https://github.com/Ernyoke/aws-lambda-benchmarks" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/aws-lambda-benchmarks&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References:
&lt;/h2&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://docs.docker.com/build/building/multi-stage/" rel="noopener noreferrer"&gt;Multistage Builds&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://doc.rust-lang.org/1.15.0/book/advanced-linking.html#static-linking" rel="noopener noreferrer"&gt;Rust - Static Linking&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://doc.rust-lang.org/reference/linkage.html#static-and-dynamic-c-runtimes" rel="noopener noreferrer"&gt;The Rust Reference - Static and dynamic C runtimes&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch" rel="noopener noreferrer"&gt;Docker Docs - Create a minimal base image using scratch&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;a href="https://www.cs.ox.ac.uk/jeremy.gibbons/publications/spigot.pdf" rel="noopener noreferrer"&gt;Unbounded Spigot Algorithms for the Digits of Pi - Jeremy Gibbons&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;a href="https://github.com/aws/aws-lambda-python-runtime-interface-client" rel="noopener noreferrer"&gt;GitHub Repository for AWS Lambda Python Runtime Interface Client&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/" rel="noopener noreferrer"&gt;New for AWS Lambda – Container Image Support&lt;/a&gt; - in the blogpost Danilo gives an example for Python 3.9. Based on his Dockerfile, I updated mine to use Python 3.11. At first glance, this should have been pretty easy to do, but I still had to track down dependencies (&lt;code&gt;libexecinfo-dev&lt;/code&gt;) that got removed from newer version of Alpine images. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/python-image.html#python-image-instructions" rel="noopener noreferrer"&gt;Deploy Python Lambda functions with container images&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>containers</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to Build a BlueSky RSS-like Bot with AWS Lambda and Terraform</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Wed, 20 Nov 2024 14:53:42 +0000</pubDate>
      <link>https://forem.com/aws-builders/how-to-build-a-bluesky-rss-like-bot-with-aws-lambda-and-terraform-2f0</link>
      <guid>https://forem.com/aws-builders/how-to-build-a-bluesky-rss-like-bot-with-aws-lambda-and-terraform-2f0</guid>
      <description>&lt;p&gt;BlueSky, an alternative social media platform to the well-known X (or commonly known as Twitter), is currently experiencing a surge of new users. There are multiple reasons why many people, especially from Twitter, are migrating to BlueSky, but this blog post is not about that. We want to talk about bots, useful bots, not spam/scam bots, obviously.&lt;/p&gt;

&lt;p&gt;With the influx of the new user base, I also decided to create a new account there. I'm not a social butterfly, I tend to post once in a while. Seeing the activity on BlueSky, I decided to get involved in the way I can the best, which is building something useful (or at least I want to believe that is useful) for a part of the community.&lt;/p&gt;

&lt;p&gt;As a disclaimer before going into the technicalities of building a bot:&lt;/p&gt;

&lt;p&gt;Social media bots, especially in the context of Twitter, have a negative connotation. That is because many people are abusing them. In this article, I don't want to promote that. A bot, a client that automatically can share/re-share content on social media, can be useful. Many organizations rely on bots and/or external clients for automatically sharing information on multiple social media sites at once. In the other hand, I strongly condemn bots that have malicious intent in the messaging they spread, spam bots whose reason is to create as many posts as possible regardless of whether the content they share is meaningful or not, and scam bots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea for a Bot
&lt;/h2&gt;

&lt;p&gt;I became an AWS Community Builder in 2022. Since then, I've authored a few blog posts and have read many more by other authors. My initial idea for a BlueSky bot was to create one that shares blog posts written by fellow builders as part of the &lt;a href="https://dev.to/aws-builders"&gt;DEV.to AWS Community Builders&lt;/a&gt; organization.&lt;/p&gt;

&lt;p&gt;This idea is not entirely new. A similar bot was created some time ago by another fellow Community Builder &lt;a href="https://x.com/jreijn" rel="noopener noreferrer"&gt;Jeroen Reijn&lt;/a&gt;. If you use Twitter and are interested in a feed of Community Builder blog posts, please follow &lt;a href="https://x.com/aws_cb_blogs" rel="noopener noreferrer"&gt;@aws_cb_blogs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How would this bot work? Pretty simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the latest blog posts from DEV.to&lt;/li&gt;
&lt;li&gt;Make a nice post on BlueSky: add tags, a card with the link to the origin post, mention the author, etc.&lt;/li&gt;
&lt;li&gt;Wait for N minutes&lt;/li&gt;
&lt;li&gt;Go to step 1. and repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation of the "AWS Community Builder Blog Posts" Bot
&lt;/h2&gt;

&lt;p&gt;As you can imagine, the idea is pretty simple, so the implementation would be also straightforward. That's partially true, although there might be some edge cases to handle.&lt;/p&gt;

&lt;p&gt;First, let's take a look at the API provided by DEV.to. DEV.to is powered by Forem, an open-source platform for blogging. The API &lt;a href="https://developers.forem.com/api/v1#tag/organizations/operation/getOrgArticles" rel="noopener noreferrer"&gt;provided by Forem&lt;/a&gt; for fetching articles from an organization is as simple as it gets. We need to specify an identifier for the organization we’re interested in and provide pagination details. Articles are sorted by their publishing timestamp, from the most recent to the least recent. For pagination, we need to specify the page number (the default is 1, which contains the most recent articles) and the number of articles per page.&lt;/p&gt;

&lt;p&gt;As far as I know, there is no way to specify a timestamp in the past and retrieve all more recent articles. Therefore, we need to rely on pagination to create our own solution for identifying which articles we have already shared on BlueSky and which ones have not yet been posted.&lt;/p&gt;

&lt;p&gt;My solution is illustrated in the following state diagram:&lt;/p&gt;

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

&lt;p&gt;In short, what I'm doing is the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In a loop get the the last 10 articles from the first page;&lt;/li&gt;
&lt;li&gt;For all of the articles, check if they exist in a DynamoDB table;&lt;/li&gt;
&lt;li&gt;Drop all the articles which already exist in the table;&lt;/li&gt;
&lt;li&gt;The remaining articles are considered "new" or "recently published" articles. Save them in the table and publish them to BlueSky;&lt;/li&gt;
&lt;li&gt;In case all the 10 articles are considered "new" fetch the second page as well and repeat the actions from step 1. In case there is at least one article that has already been published OR we reached page 3, stop and go to the next step;&lt;/li&gt;
&lt;li&gt;Wait for 5 minutes and re-do everything from the beginning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You might ask, "How did I come up with all of these numbers? Why do I fetch 10 articles, and why do I do this 3 times?" To answer briefly, I'm just guessing that they are safe, even for periods with higher activity, like when re:Invent happens.&lt;/p&gt;

&lt;p&gt;To make a more educated guess, here is the number of posts published monthly by all Community Builders under the DEV.to organization (thank you, Jeroen Reijn, for the chart):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpjrl01gl1zrk9di3p3v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpjrl01gl1zrk9di3p3v.jpg" alt="Number of DEV.to posts by month" width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
(source: &lt;a href="https://bsky.app/profile/jeroenreijn.com/post/3l7uzeyay3r2u" rel="noopener noreferrer"&gt;https://bsky.app/profile/jeroenreijn.com/post/3l7uzeyay3r2u&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The number of posts fluctuates, but the periods we are most interested in are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;March: This is when a new cohort of Community Builders joins. At this point, everyone is enthusiastic about posting something;&lt;/li&gt;
&lt;li&gt;December: This is usually when re:Invent takes place. Many new things are introduced during this event, providing plenty of exciting topics to write about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over the past year, the highest number of articles was posted in March, slightly exceeding 250. With this in mind, we can conclude that the values I chose are likely much higher than necessary. Realistically, we don’t expect all 250 articles to be posted within a 5-minute window. If that were the case, I’d have other concerns, such as BlueSky’s rate limits, especially for blob content like images.&lt;/p&gt;
&lt;h3&gt;
  
  
  Technology Stack
&lt;/h3&gt;

&lt;p&gt;For the technology stack, I went with an AWS Lambda using TypeScript. For detecting which articles I should re-share, I'm using a DynamoDB. I'm using CloudWatch scheduled events to trigger the Lambda every 5 minutes. At this point, I don't have any special error-handling mechanism for the Lambda (other than the obvious exception checking and logging), but I would most likely create a dead-letter queue for missed events.&lt;/p&gt;

&lt;p&gt;For the infrastructure, I'm using Terraform. The reason is that I'm mostly familiar with it, and it took the shortest time for me to set everything up and get running.&lt;/p&gt;

&lt;p&gt;The whole codebase can be found on GitHub: &lt;a href="https://github.com/Ernyoke/bsky-aws-community-builder-blogposts" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/bsky-aws-community-builder-blogposts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have a BlueSky account and you want to see recently published blog posts by AWS community builders, you can follow this account: &lt;a href="https://bsky.app/profile/awscmblogposts.bsky.social" rel="noopener noreferrer"&gt;https://bsky.app/profile/awscmblogposts.bsky.social&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Giving Another Try and Implementing an RSS Feed Bot for AWS News
&lt;/h2&gt;

&lt;p&gt;After finishing with the "AWS Community Builder Blog Posts" bot, I wanted to move on to another topic that interests me: AWS News. AWS has an &lt;a href="https://aws.amazon.com/about-aws/whats-new/recent/feed/" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; for any important short announcement they make. These announcements are succinct and to the point. I think it would make sense to re-share them also on BlueSky, since there is a &lt;a href="https://x.com/awswhatsnew" rel="noopener noreferrer"&gt;similar account doing the same thing on Twitter&lt;/a&gt; with a considerable amount of followers.&lt;/p&gt;

&lt;p&gt;The approach to implementing this kind of bot is similar to the one for the Community Builder blog posts. One thing that differs is that in this case, we have to parse an RSS feed. Usually, there are only a few announcements made by AWS on a daily basis. We can extend the fetching schedule to 30 minutes (or even more). For the DynamoDB table, we can lower the provisioned READ/WRITE capacity to a small amount, since there won't be a huge number of reads and writes.&lt;/p&gt;

&lt;p&gt;Again, the implementation for this bot can also be found on GitHub: &lt;a href="https://github.com/Ernyoke/bsky-aws-news-feed" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/bsky-aws-news-feed&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In case you would like to follow the BlueSky account with RSS news feed from AWS, you can do it here: &lt;a href="https://bsky.app/profile/awsrecentnews.bsky.social" rel="noopener noreferrer"&gt;https://bsky.app/profile/awsrecentnews.bsky.social&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After I finished developing this bot, I noticed that a fellow community builder, &lt;a href="https://bsky.app/profile/thulasirajkomminar.com" rel="noopener noreferrer"&gt;Thulasiraj Komminar&lt;/a&gt; developed his &lt;a href="https://bsky.app/profile/awsnews.bsky.social" rel="noopener noreferrer"&gt;own variant&lt;/a&gt;. You can follow whichever account you like, the more important thing is to stay updated.&lt;/p&gt;
&lt;h2&gt;
  
  
  Taking it a Step Further: Re-sharing Content from Official AWS Blogs and Detecting Deprecations
&lt;/h2&gt;

&lt;p&gt;In case you want to be as up-to-date as possible with all things AWS, you most likely stumbled into the &lt;a href="https://aws-news.com/" rel="noopener noreferrer"&gt;AWS News Feed&lt;/a&gt; page. This page is created and maintained by fellow AWS Hero, &lt;a href="https://bsky.app/profile/lucvandonkersgoed.com" rel="noopener noreferrer"&gt;Luc van Donkersgoed&lt;/a&gt;. The purpose of this page is to aggregate blog posts from all the official AWS blogs. Moreover, it can detect blog posts talking about service and feature deprecations, which unfortunately, are getting more frequent lately. It is a pretty impressive piece of work, and I absolutely recommend bookmarking and following this page.&lt;/p&gt;

&lt;p&gt;My idea was to bring both of those functionalities to my BlueSky feed. I wanted to create a bot that simply re-shares and tags all the articles from different kinds of official AWS blogs, and I also wanted to create a bot that shares posts talking about AWS service deprecations.&lt;/p&gt;

&lt;p&gt;Long story short, I came up with the following event-based architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrtlp6szcl7yaje5lu3c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrtlp6szcl7yaje5lu3c.png" alt="Event based architecture for AWS blogs and AWS deprecations Bots" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I split the so-called business logic into three parts (three Lambda Functions):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetcher Lambda: Works very similarly to the previous RSS-like bots I presented. It queries the AWS API for blog posts, uses a DynamoDB table to detect posts that have not yet been shared on BlueSky, and publishes those to an SNS topic..&lt;/li&gt;
&lt;li&gt;Blogs Publisher Lambda: Uses an SQS standard queue to listen to the topic. From this queue, it polls the messages and simply re-shares them to BlueSky.&lt;/li&gt;
&lt;li&gt;Deprecations Lambda: The "fun" part of this architecture. It also has its own queue from which it retrieves newly published blog posts. Before re-sharing them to BlueSky, it will use an LLM Model from Bedrock to detect if the article is about any kind of service deprecation. If it is, it will proceed to post the article to BlueSky.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What we have here is called a fan-out architecture. We have a producer Lambda (Fetcher) which produces events for multiple consumers.&lt;/p&gt;
&lt;h3&gt;
  
  
  Event Sourcing with Lambda
&lt;/h3&gt;

&lt;p&gt;Having an SQS queue gives a lot of flexibility and control over how are our functions invoked. Using a Lambda event source mapping we get access to features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batching: we can group messages from a queue together and have our function invoked once for multiple messages;&lt;/li&gt;
&lt;li&gt;Error handling and partial batch processing: in case something fails while dealing with a message from the queue, we can have a number of retry attempts. Event source mapping allows partial batch processing. In case there is an error from one of the messages from the batch, we don't necessarily have to reprocess everything. We can reprocess only those for which the execution failed;&lt;/li&gt;
&lt;li&gt;Parallel concurrence invocation: we can limit how many function invocations should we have in parallel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these features have to be configured when defining the event source mapper. Since, I'm using Terraform, in my case I have the following configuration for the Blogs Publisher Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_event_source_mapping"&lt;/span&gt; &lt;span class="s2"&gt;"blogs_event_source_mapping"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;event_source_arn&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blogs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# ARN of the source SQS queue&lt;/span&gt;
 &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# Flag used mainly for debugging&lt;/span&gt;
 &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blogs_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="c1"&gt;# Lambda ARN&lt;/span&gt;
 &lt;span class="nx"&gt;batch_size&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# Accept a batch of max 10 messages&lt;/span&gt;
 &lt;span class="nx"&gt;maximum_batching_window_in_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# Time to wait for messages to arrive to be able to be gathered in a batch&lt;/span&gt;
 &lt;span class="nx"&gt;function_response_types&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ReportBatchItemFailures"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Used for partial error handling of a batch&lt;/span&gt;

 &lt;span class="nx"&gt;scaling_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;maximum_concurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="c1"&gt;## Limit the number of instances of the function that can be invoked at the same time&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the case of the Deprecation Lambda, I have to deal with other limitations. Since AWS decided to limit my account to 20 invocations per minute of a base model from Bedrock, I decided to use the poor man's approach to Lambda rate limiting: setting the reserved concurrency at 1. This will allow only one instance of my function to be executed at the same time. I'm aware that with this I still can hit the rate limit imposed by Bedrock, but I feel like at this point there is not much I can do. Also, important to notice, that in this case &lt;code&gt;maximum_concurrency&lt;/code&gt; has to be disabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working with AI
&lt;/h3&gt;

&lt;p&gt;I'm using AI to detect if an article is about any AWS service deprecation. This works, most of the time, but in many cases, it can decide to be as disciplined as a badly behaved toddler.&lt;/p&gt;

&lt;p&gt;What I'm doing is extracting the text content of each article. This text is provided to the bot. As a response, I expect answers to the following questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the article mention any deprecation of any AWS service?&lt;/li&gt;
&lt;li&gt;If yes, what is the name of those services?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Moreover, I expect to get the response in JSON format.&lt;/p&gt;

&lt;p&gt;At first, I was under impression that this should not be a big challenge for any available models Boy, I was wrong!&lt;/p&gt;

&lt;p&gt;Both of my questions require summarization. LLM models should be pretty good at summarization. The second part of my challenge to the model is to provide the answer in structured format. &lt;/p&gt;

&lt;p&gt;I tried different models for this, with different degrees of success:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Bedrock Titan&lt;/strong&gt;: it can do summarization really well. It could answer both of my questions. However, when trying to get the answers in a JSON format, it was a challenge. I was unable to get a valid JSON no matter how much I tried. I used LangChain's &lt;a href="https://python.langchain.com/v0.1/docs/modules/model_io/chat/structured_output/" rel="noopener noreferrer"&gt;&lt;code&gt;Structured Output&lt;/code&gt;&lt;/a&gt; and explicitly provided the format instructions to the bot as part of the prompt. Titan managed to generate something similar to a valid JSON, but every time something was off. Ultimately, I decided to drop it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Instant&lt;/strong&gt;: I decided to try one of the cheapest offerings from the Anthropic models. Summarization works well enough, and it can build JSON most of the time. When I don't get a valid JSON response, the solution is to retry. Considering that I have 20 invocations per minute (thanks to AWS), this will quickly consume those invocations. So, I decided to try another model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Haiku&lt;/strong&gt;: I'm currently using Haiku, which provides a correctly formatted JSON 99% of the time. When it doesn't, I simply retry the request. It seems stable enough for my purposes, and I can work within the strong rate limiting imposed by AWS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Aside from that, I still need to refine the prompt to avoid having false positive detections. Sometimes AI struggles with identifying AWS services, or I might simply be bad at prompting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technology Stack
&lt;/h3&gt;

&lt;p&gt;In short, I'am using TypeScript for the Lambda Functions, DynamoDB for knowing what article to re-share, SNS with SQS standard for fan-out and LangChain with Claude Haiku from Bedrock. For the infrastructure I'm using Terraform.&lt;/p&gt;

&lt;p&gt;The whole codebase can be found on GitHub: &lt;a href="https://github.com/Ernyoke/bsky-aws-blogs" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/bsky-aws-blogs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to see the deprecations warning in your BlueSky feed, you can follow the handle: &lt;a href="https://bsky.app/profile/deprecatedbyaws.bsky.social" rel="noopener noreferrer"&gt;https://bsky.app/profile/deprecatedbyaws.bsky.social&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Words
&lt;/h2&gt;

&lt;p&gt;In conclusion, I had a lot of fun developing these bots. It was just about time for me to revisit all the features we have for streaming and event sourcing. Working with AI, although it is a lot of fun, can be challenging sometimes.&lt;/p&gt;

&lt;p&gt;BlueSky also has the concept of starter packs. A starter pack makes it easier to follow multiple accounts at the same time with the push of a button. If you have a lot of AWS-related blog posts/articles/news in your feed, I created a starter pack for all of these bots. You can simply follow them from here: &lt;a href="https://go.bsky.app/EdJArRR" rel="noopener noreferrer"&gt;https://go.bsky.app/EdJArRR&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://developers.forem.com/api/v1#tag/organizations/operation/getOrgArticles" rel="noopener noreferrer"&gt;Forem API for Organizations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://python.langchain.com/v0.1/docs/modules/model_io/chat/structured_output/" rel="noopener noreferrer"&gt;LangChain - Structured Output&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>serverless</category>
      <category>bluesky</category>
    </item>
    <item>
      <title>About AWS AI Practitioner (Beta) Exam</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Sun, 29 Sep 2024 10:11:31 +0000</pubDate>
      <link>https://forem.com/aws-builders/about-aws-ai-practitioner-beta-exam-21e4</link>
      <guid>https://forem.com/aws-builders/about-aws-ai-practitioner-beta-exam-21e4</guid>
      <description>&lt;p&gt;In June this year, AWS announced two new certification exams: the AI Practitioner exam and the Machine Learning Engineer Associate exams. At the same time, AWS has the Machine Learning Specialty (MLS-C01) certification. You can still take this exam, and at the point of writing this article, there is no news of being retired. However, we can assume that the Machine Learning Engineer Associate will take its place.&lt;/p&gt;

&lt;p&gt;On the flip side, the AI Practitioner certification is an entirely new offering, marking the second foundational level certification alongside the AWS Cloud Practitioner exam. Foundational level certifications are considered to be entry level certifications. They can be attempted by people who do not necessarily have an in-depth technical knowledge of cloud concepts.&lt;/p&gt;

&lt;p&gt;From pure curiosity, I decided to attempt this certification. In my day-to-day job, I had the chance to work with AWS technologies and with LLM models lately, so I thought it would be an interesting challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exam Overview
&lt;/h2&gt;

&lt;p&gt;The exam is intended not only for IT professionals, but also for people working as business analysts, sales and marketing professionals, and IT managers. It relies mainly on theoretical knowledge, it does not require as much hands-on experience as an associate or professional-level exam. This does not mean that it does not have its challenges.&lt;/p&gt;

&lt;p&gt;The exam consists of 85 questions which should be answered in 120 minutes (though I had 130 minutes through Pearson's online testing). You can opt for accommodation of an extra 30 minutes, in case English is not your mother tongue. I did not opt for that, but I recommend considering it if you are in a similar situation as myself.&lt;/p&gt;

&lt;p&gt;Aside from the multiple choice, multiple selection types of questions, AWS recently introduced new question types: ordering, matching, and case studies. At this point, I believe, AWS is A/B testing these new types of questions, since I did not encounter any of them during my session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need to Know for the Exam?
&lt;/h2&gt;

&lt;p&gt;Considering that this is my 8th AWS certification, I can affirm that the exam was more challenging than expected. Regardless of your level of AWS cloud and AI/machine learning knowledge, I strongly suggest taking the time to do some meaningful preparation. To aid with that, I will present my learning plan and what I think you should know to take the exam successfully. However, I suggest considering a fully-fledged course.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bear in mind that this is not a comprehensive guide. If you are seriously considering attempting this certification, you may want to enroll in training provided by AWS or by a third-party trainer. See the next section for my recommendations for courses and learning material.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My training guide does not specifically respect the order of the domains enumerated in the &lt;a href="https://d1.awsstatic.com/training-and-certification/docs-ai-practitioner/AWS-Certified-AI-Practitioner_Exam-Guide.pdf" rel="noopener noreferrer"&gt;official exam guide&lt;/a&gt;, which I strongly suggest that you read. That being noted, these are the topics I learned about during my preparation:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cloud Computing Basics
&lt;/h3&gt;

&lt;p&gt;For the exam, you need to have a clear understanding of some basic concepts related to cloud computing, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud vs on-prem: what are the advantages, what are the drawbacks of both&lt;/li&gt;
&lt;li&gt;Public vs private cloud&lt;/li&gt;
&lt;li&gt;Pricing of cloud services: one important keyword that you will encounter is &lt;strong&gt;on-demand&lt;/strong&gt;. Everything in the cloud is pay-per-use, you only pay for what you use. Whenever you see a question talking about pricing, as a rule of thumb, you can default to the answer that mentions the &lt;strong&gt;on-demand&lt;/strong&gt; keyword. Of course, there might be exceptions, so use your best judgment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Basic Machine Learning Concepts
&lt;/h3&gt;

&lt;p&gt;For the exam, you will need to have surface-level knowledge of machine learning concepts and algorithms. In-depth understanding of these topics is not required, but you should be able to recognize them and understand their applications. The key concepts are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI/Machine learning&lt;/strong&gt;: What is AI and what is machine learning? What use cases are solved by AI systems?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI application components&lt;/strong&gt;: Data layer, ML algorithms, model layer, application layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neural Networks&lt;/strong&gt;: What are neural networks? Also, you should know about their components: neurons, layers, activation functions, and loss functions.&lt;/li&gt;
&lt;li&gt;You should understand what &lt;strong&gt;backpropagation&lt;/strong&gt; is: the process of updating the weights in the network to minimize the loss function&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Learning&lt;/strong&gt;: Know what deep learning is and what it is used for. Remember the keyword convolution, as it refers to a type of deep learning network (convolutional networks), primarily used for computer vision and image recognition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenAI&lt;/strong&gt;: What is generative AI and what is it used for?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformer Models/Diffusion Models/Multi-Modal Models&lt;/strong&gt;: These are types of GenAI models. There is a high likelihood that you will encounter questions regarding the properties of these models, so it's recommended to have a basic understanding of them. You likely won't need to go into detail about their inner workings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supervised vs. Unsupervised learning&lt;/strong&gt;: Understand the difference between these two approaches and know when to use one over the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reinforcement Learning (RL)&lt;/strong&gt;: How does it work, and what is it used for? Be familiar with some key use cases.&lt;/li&gt;
&lt;li&gt;Machine learning model training and evaluation:

&lt;ul&gt;
&lt;li&gt;Model fit: The exam covers some more technical topics, such as &lt;strong&gt;underfitting&lt;/strong&gt; and &lt;strong&gt;overfitting&lt;/strong&gt;. These concepts are essential when training a model. A model is considered to &lt;strong&gt;underfit&lt;/strong&gt; if it does not perform well on the training data. Conversely, it is &lt;strong&gt;overfitting&lt;/strong&gt; if it performs well on the training data but poorly on evaluation or real-world data. If neither of these applies to your model, you can assume you have a &lt;strong&gt;balanced&lt;/strong&gt; model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bias&lt;/strong&gt; and &lt;strong&gt;variance&lt;/strong&gt;: Both of these refer to errors introduced by your model. A model is &lt;strong&gt;biased&lt;/strong&gt; when it consistently makes the same error in its predictions, often due to erroneous assumptions during training. &lt;strong&gt;Variance&lt;/strong&gt;, on the other hand, reflects a model's sensitivity to small fluctuations in the input data. For the exam, you should be able to identify whether a model is highly biased or has high variance. Additionally, you should understand how bias and variance relate to model fit. For example, a model with high variance will likely overfit, while a model with high bias will probably underfit.&lt;/li&gt;
&lt;li&gt;Model evaluation metrics: it is expected that you are familiar with concepts of how to evaluate a model. It is not required to know the math behind those concepts, but it expects you to know when you should use one metric versus another. These metrics are the following:

&lt;ul&gt;
&lt;li&gt;Metrics used for classification models:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confusion Matrix&lt;/strong&gt;: used to evaluate the performance of classification models. It is usually structured as a square matrix, where rows represent the actual classes, and columns represent the predicted classes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy&lt;/strong&gt;: measures the fraction of correct prediction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall (or Sensitivity)&lt;/strong&gt;: measures the true positive rates of the predictions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precision&lt;/strong&gt;: measures the correct positive rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specificity&lt;/strong&gt;: measures the true negative rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F1 score&lt;/strong&gt;: combines precision and recall into a final score. Use it when both precision and recall are considered important for evaluating your model.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Metrics used for regression models: similar to the metrics from the classification models, you don't need to know the formulas and mathematics behind these metrics. What you should know for the exam is to recognize them and know if they can be used with a presented model or not:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MAE (Mean Absolute Error)&lt;/strong&gt;: measures the average magnitude of errors between the predicted values and the actual values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MAPE (Mean Absolute Percentage Error)&lt;/strong&gt;: used to assess the accuracy of a predictive model by calculating the average percentage error between the predicted values and the actual values. MAPE expresses the error as a percentage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RMSE (Root Mean Squared Error)&lt;/strong&gt;: measures the average magnitude of the error between the predicted values and the actual values, with a higher emphasis on larger errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R Squared&lt;/strong&gt;: explains variance in our model. R2 close to 1 means predictions are good.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Metrics used to evaluate LLMs - the exam might ask you about evaluating the performance of a large language model. In this case, you would want to know about these:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perplexity loss&lt;/strong&gt;: measures how well the model can predict the next word in a sequence of text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall-Oriented Understudy for Gisting Evaluation (ROUGE)&lt;/strong&gt;: a set of metrics used in the field of natural language processing to evaluate the quality of machine-generated text.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Generative AI and AWS Services for GenAI
&lt;/h3&gt;

&lt;p&gt;The exam expects you to be familiar with GenAI models. While it does not require an understanding of their inner workings, you should have experience using them. For example, you might be asked whether, for a certain problem, you would prefer to use a GenAI-based model or a more "legacy" ML model.&lt;/p&gt;

&lt;p&gt;Probably the most important AWS service for this exam is AWS Bedrock. You can expect around 15 to 20 questions involving Bedrock in one way or another. Bedrock is essentially a one-stop shop for GenAI models. It provides access to a variety of Foundation Models, such as Amazon Titan, several Anthropic models (Claude), and models from AI21 Labs, Cohere, Stability AI, Meta (LLaMA), Mistral, and others. In addition to allowing you to build on these models, Bedrock offers several other features. The following are relevant for the exam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge Bases&lt;/strong&gt;: A solution for provisioning and using &lt;strong&gt;vector databases&lt;/strong&gt;. You may want to use vector databases if you are building a system based on &lt;strong&gt;RAG (Retrieval-Augmented Generation)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents&lt;/strong&gt;: BBedrock's method for function calling. You can integrate your model with an "action," allowing the model to perform tasks in addition to responding to messages, such as executing a Lambda function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt;: You can use Guardrails to limit the model's responses in terms of what it should be able to answer. Additionally, Guardrails offer features such as detecting personally identifiable information (PII) and removing it from the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Evaluation&lt;/strong&gt;: You can evaluate a model using AWS-provided metrics or by employing human reviewers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aside from these, there are other features of Bedrock the exam might ask about (Bedrock Studio, Watermark Detection). I strongly recommend doing some hands-on practice with Bedrock and experiencing what it has to offer.&lt;/p&gt;

&lt;p&gt;Another GenAI-based AWS service is &lt;strong&gt;Amazon Q&lt;/strong&gt;, which is a fully managed GenAI assistant for enterprise usage (whatever that means). It combines a lot of stuff into one service. It has a few flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Q Business&lt;/strong&gt;: it is a question-answer-based chatbot built on Amazon Bedrock. It can ingest your company-owned internal data, and it will be able to answer your questions based on that data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Q Developer&lt;/strong&gt;: it servers two completely different use cases:

&lt;ul&gt;
&lt;li&gt;It is a chatbot built on AWS documentation, so it can answer your questions related to AWS services.&lt;/li&gt;
&lt;li&gt;It is a code companion (previously known as CodeWhisperer) similar to GitHub Copilot. It can generate source code. It can integrate with a bunch of IDEs and it can help you write code.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;I personally would not worry too much about Amazon Q for the exam. In my experience, there aren’t many questions about this service. On the other hand, it’s important to ensure you don’t confuse Amazon Q with Kendra, another service built for similar purposes. The exam may present them side by side, but you should be able to determine which one is more appropriate for your scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Prompt Engineering
&lt;/h3&gt;

&lt;p&gt;Before embarking on my learning journey for the AI Practitioner certification, I considered prompt engineering to be a pseudo-science. My rule of thumb was (and still is) that if you want better answers from a model, provide it with as much information as possible. During my preparation and while building AI chatbots at my workplace, I learned that there are useful prompting techniques that can yield significantly better answers compared to what I was accustomed to before.&lt;/p&gt;

&lt;p&gt;For the AI practitioner certification you should be aware of the following prompting techniques:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Shot Prompting&lt;/strong&gt;: This is a more "scientific" definition of what I was doing before adopting any prompt engineering techniques. You simply present a query to the model without specific wording, formatting, or examples of expected output. The model's output may vary. It could be useful or it might be complete garbage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Few-Shots Prompting&lt;/strong&gt;: In this approach, you provide a prompt with examples of what you would expect as output from the model. Surprisingly, this technique works better than I had imagined. In terms of the exam, you should choose this technique when asked for a low-cost solution with the highest precision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain of Thought Prompting&lt;/strong&gt;: This involves dividing your queries into a sequence of reasoning steps, using phrases like "Think step by step." Interestingly, the GPT-3 model utilizes chain of thought prompting under the hood, which has contributed to its recent popularity. Therefore, expect some questions about it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt;: RAG is also considered a prompting technique. It relies on using a vector database to fetch content related to the user query. This content is then injected into the user prompt as additional context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Related to the prompt engineering, the exam might ask you about hyperparameters you can set for a model to optimize its responses. Parameters you should be aware of are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temperature&lt;/strong&gt;: value between 0 and 1.0, defines the creativity of a model. The higher the value, the more creative responses you will get.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top P&lt;/strong&gt;: value between 0 and 1.0, defines the size of the set of available words when generating the parts of an answer. For example, for a value of 0.25, the model will use the 25% most likely next word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top K&lt;/strong&gt;: similar to Top P, but it is an integer value. By setting a Top K value, we tell our model that it only should use words from the next Top K available options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general rule for the hyperparameters is that setting lower values will make your model more conservative and give more coherent responses while setting a parameter to a higher value will result in more creative and less coherent responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Amazon SageMaker
&lt;/h3&gt;

&lt;p&gt;Another important AWS service for the exam is Amazon SageMaker. SageMaker is a managed service used by data and machine learning engineers to build, train, and deploy machine learning models. Like other AWS services, it offers a variety of features. I will focus on those that may appear in the exam, although I encountered some unexpectedly challenging questions during my session. These questions felt as if they were taken from the Machine Learning Specialty exam question set.&lt;/p&gt;

&lt;p&gt;One of the most important offerings of Sagemaker is &lt;strong&gt;SageMaker Studio&lt;/strong&gt;. At first glance, this looks like a managed Jupyter Notebook, where a machine learning engineer can write Python code. It is way more than that. Part of SageMaker Studio is &lt;strong&gt;Data Wrangler&lt;/strong&gt;, used for feature engineering and data preparation before training. From Data Wrangler we can publish data into &lt;strong&gt;SageMaker Feature Store&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Part of SageMaker Studio is &lt;strong&gt;SageMaker Clarify&lt;/strong&gt;. It is used to evaluate foundation models against AWS-provided metrics or metrics provided by you. It even lets you leverage human intervention (have your employee evaluate the model, or use Ground Truth). SageMaker Clarify has a specific feature you should be aware of for the exam, this is &lt;strong&gt;Model Explainability&lt;/strong&gt;. This is used to explain why you get certain outputs from a model and what kind of feature influenced the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SageMaker Ground Truth&lt;/strong&gt; is another sub-service the exam expects you to know about. It is based on &lt;strong&gt;Reinforcement Learning from Human Feedback (RLHF)&lt;/strong&gt;, whenever you see this keyword, think of Ground Truth and vice-versa. Ground Truth is used to review models, do customizations, and do evaluations based on human feedback. &lt;/p&gt;

&lt;p&gt;In terms of ML Governance, you should be aware of &lt;strong&gt;SageMaker Model Cards&lt;/strong&gt;, &lt;strong&gt;SageMaker Model Dashboards&lt;/strong&gt;, and &lt;strong&gt;SageMaker Role Manager&lt;/strong&gt;. Model Cards lets you create cards with essential information about a model. Model Dashboard is a centralized repository for ML models.  It displays insights for each model such as risk ratings, model quality, and data quality. Role Manager lets you create and define roles for AWS users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SageMaker Model Monitor&lt;/strong&gt; lets you monitor the quality of your models deployed in production. You can also create alerts for your models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SageMaker Pipelines&lt;/strong&gt; allows you to create pipelines for training and deploying models. Whenever the exam asks about MLOps-related services, most likely SageMaker Pipeline would be the correct answer.&lt;/p&gt;

&lt;p&gt;Model Fine-Tuning: in the exam, you might face questions about model fine-tuning. You may want to use fine-tuning when you want to take an existing model and do some additional training on it with your data. &lt;strong&gt;SageMaker JumpStart&lt;/strong&gt; is one of the places where you want to start fine-tuning a model. &lt;/p&gt;

&lt;p&gt;The exam also likes to compare fine-tuning with other preparation techniques of an LLM model. In case you are faced with a comparison based on price, you would want to keep in mind the following order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prompt engineering (excluding RAG): least expensive&lt;/li&gt;
&lt;li&gt;RAGs: they are more expensive than other prompt engineering techniques because they usually require the presence of a vector database&lt;/li&gt;
&lt;li&gt;Instruction-based fine-tuning: it is a fine-tuning approach that uses labeled data to modify the weights of a model. It requires model training, which demands specific hardware, so it is considered more expensive than RAGs&lt;/li&gt;
&lt;li&gt;Domain adaptation fine-tuning: uses unlabeled data for fine-tuning, it is the most expensive approach&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Other SageMaker sub-services you would want to look up are the following: &lt;strong&gt;SageMaker Canvas&lt;/strong&gt;, &lt;strong&gt;MLFlow for SageMaker&lt;/strong&gt;, &lt;strong&gt;Automatic Model Tuning&lt;/strong&gt;. Moreover, SageMaker provides a bunch of built-in machine-learning algorithms (for example: XGBoost, DeepAR, Seq2Seq, etc.). You may want to check them out, to at least recognize them if they pop up during the certification.&lt;/p&gt;

&lt;p&gt;SageMaker is an advanced topic. It’s somewhat surprising that AWS expects such a deep level of knowledge about it, especially considering the exam is recommended for individuals who may never use this product. If you're comfortable writing a few lines of code and are familiar with Jupyter Notebooks, I recommend doing some hands-on practice with SageMaker.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. AWS Managed AI Services
&lt;/h3&gt;

&lt;p&gt;AWS offers a comprehensive list of AI services that are managed and trained by them. The list of the services you should be aware of are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Comprehend and Amazon Comprehend Medical: extract relevant information from documents of all kinds.&lt;/li&gt;
&lt;li&gt;Amazon Translate: an on-demand translation service, think of it as an on-demand version of Google Translate.&lt;/li&gt;
&lt;li&gt;Amazon Transcribe and Amazon Transcribe Medical: speech-to-text service.&lt;/li&gt;
&lt;li&gt;Amazon Polly: text-to-speech service.&lt;/li&gt;
&lt;li&gt;Amazon Rekognition: used for image recognition and classification.&lt;/li&gt;
&lt;li&gt;Amazon Forecast: used with time series data to forecast stuff. Discontinued by AWS, but is still part of the exam.&lt;/li&gt;
&lt;li&gt;Amazon Lex: it is similar to Amazon Q, or an Amazon Bedrock agent. It is technically Alexa as a service.&lt;/li&gt;
&lt;li&gt;Amazon Personalize: recommendation service.&lt;/li&gt;
&lt;li&gt;Amazon Textract: used to extract text from images (OCR).&lt;/li&gt;
&lt;li&gt;Amazon Kendra: it is a document search service. It is somewhat similar to Amazon Q, but it is way more restricted and it cannot do summarization (good idea to keep this in mind!)&lt;/li&gt;
&lt;li&gt;Amazon Mechanical Turk: it is not necessarily an AI service. With Mechanical Turk you rely on a human workforce to carry out certain tasks for machine learning, such as labeling, classification, and data collection. &lt;/li&gt;
&lt;li&gt;Amazon Augmented AI (A2I): likewise Mechanical Turk, is not necessarily a managed AI service. It is a service that lets you conduct a human review of machine learning models. It can use Mechanical Turk under the hood.&lt;/li&gt;
&lt;li&gt;AWS DeepRacer: this is also an interesting thing to mention. It is a game, where you use reinforcement learning to drive a race car. While DeepRaces is still part of the exam, the service is discontinued by AWS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exam might present a task and it might ask which service would be able to solve that task. It also might put one of these services head-to-head with Bedrock or Amazon Q.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. AI Challenges and Responsibilities
&lt;/h3&gt;

&lt;p&gt;The exam will ask you about generative AI challenges and how to overcome them. A few challenges you should keep in mind are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regulatory violation&lt;/li&gt;
&lt;li&gt;Social risks&lt;/li&gt;
&lt;li&gt;Data security and privacy concerns&lt;/li&gt;
&lt;li&gt;Toxicity&lt;/li&gt;
&lt;li&gt;Hallucinations&lt;/li&gt;
&lt;li&gt;Nondeterminism&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should be able to identify the challenges of a given AI application. You might be asked to find solutions to overcome some of these challenges. For example, to address hallucinations, you can use Knowledge Bases and Retrieval-Augmented Generation (RAG) techniques. To mitigate toxicity, you can implement Guardrails. To reduce nondeterminism, you can adjust the model's hyperparameters (such as temperature, Top P, or Top K).&lt;/p&gt;

&lt;p&gt;Another important topic that may arise is governance. Governance refers to a set of practices that must be followed when developing AI products. For example, you should be able to address the ethical concerns of an AI-based solution, consider bias and fairness, adhere to regulatory and compliance requirements, and ensure data lineage and data quality. There are several AWS services you should be familiar with when discussing governance, including AWS Config, Amazon Inspector, AWS Audit Manager, AWS Artifact, AWS CloudTrail, and AWS Trusted Advisor. You should understand the purpose of each of these services and be able to recognize them in the context of governance-related questions.&lt;/p&gt;

&lt;p&gt;Generative AI Security Scoping Matrix: it is a framework designed to identify and manage security risks associated with deploying GenAI applications.&lt;br&gt;
It is used to classify your app in 5 defined GenAI scopes, from low to high ownership:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope 1: your app is using public GenAI services&lt;/li&gt;
&lt;li&gt;Scope 2: your app is using a SaaS with GenAI features&lt;/li&gt;
&lt;li&gt;Scope 3: your app is using a pre-trained model&lt;/li&gt;
&lt;li&gt;Scope 4: your app is using a fine-tuned model&lt;/li&gt;
&lt;li&gt;Scope 5: your app is using a self-trained model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. AWS Security Services and Other Services
&lt;/h3&gt;

&lt;p&gt;For the exam, you should be aware of a list of AWS services. As the themes go with other topics, you should have a surface-level knowledge about them. Most importantly, you should know when to use which.&lt;/p&gt;

&lt;p&gt;The most important service you will be asked about is &lt;strong&gt;AWS Identity and Access Management (IAM)&lt;/strong&gt;. It is used to define roles and access policies. Whenever you, as a user, want to perform any action in your AWS account, you need the necessary permissions. These permissions are granted through roles and policies. Similarly, when one AWS service needs to interact with another, it must have an assigned role that grants the required access. In some cases, this interaction can also be facilitated through service policies. The exam does not go into detail about when to use roles versus policies. The key point to remember is that whenever you are asked about security, think of IAM.&lt;/p&gt;

&lt;p&gt;Another important service that will pop-up is S3. S3 is an object storage service, think of it as Google Drive on steroids. Whenever the exam asks about storage for model input/out, you would want to default to S3.&lt;/p&gt;

&lt;p&gt;EC2 is also an important service. EC2 provides virtual machines. In the context of machine learning, you need EC2 instances for training and inference. There are several types of EC2 instances. For the exam, you may want to remember the following ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;P3, P4, P5, G3, G6 instances: These are instances with a GPU assigned to them, they can be used for training and for inference as well;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Trainium&lt;/strong&gt; and &lt;strong&gt;AWS Inferentia&lt;/strong&gt;: these are instances specifically built for training and inference. They provide a lower cost for either training or inference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exam might mention Spot Instances. Spot Instances are EC2 instances available at a lower cost. You can obtain a Spot Instance by placing a bid. They are cheaper than On-Demand Instances, but the trade-off is that they can be interrupted if someone bids a higher price or if AWS needs the capacity for other purposes.&lt;/p&gt;

&lt;p&gt;Networking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You should know what a VPC is. A VPC (Virtual Private Cloud) is a virtual private network, isolated by default from internet traffic or other AWS services. Certain resources, such as EC2 instances or most databases, need to be placed in a VPC to function properly. You can provide internet access to the VPC if desired.&lt;/li&gt;
&lt;li&gt;The exam often asks about VPC Endpoints in terms of security. VPC Endpoints allow communication with AWS services from a private VPC, one that does not have internet access. When using a VPC Endpoint, the traffic does not pass through the public internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudWatch: used for monitoring and logging&lt;/li&gt;
&lt;li&gt;CloudTrail: used for having a trail about any action in an AWS account&lt;/li&gt;
&lt;li&gt;AWS Config: used to enforce compliance in an account&lt;/li&gt;
&lt;li&gt;AWS Lambda Function: serverless functions that run only when needed. In the context of this exam, usually they are used for integration between 2 services&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Courses and Practice Exams that I Recommend
&lt;/h2&gt;

&lt;p&gt;The previous section aimed to present what you need to know to pass the exam. It is not comprehensive material, it might have missing topics or inaccuracies. If you want a more robust preparation plan, I recommend enrolling in a paid course.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://skillbuilder.aws/" rel="noopener noreferrer"&gt;AWS Skill Builder&lt;/a&gt; is the official learning portal run by AWS Training and Certification. They have a learning path for the AI practitioner exam. I personally did not use this because the video courses in the learning path do not strictly focus on exam topics. &lt;/p&gt;

&lt;p&gt;I recommend taking one of the Udemy courses from Stephane Maarek or Frank Kane. For my preparation, I used Stephane Maarek's course. I'm also familiar with Frank's teaching style, making his course a solid recommendation as well.&lt;/p&gt;

&lt;p&gt;Stephane's course was the first available on the market. I purchased it on the day of its release. Having taken his other AWS certification courses, I appreciate his organized content and clear presentation, which makes note-taking easy. Initially, the course had some gaps in the required material, but it was updated based on student feedback. I confidently recommend it.&lt;/p&gt;

&lt;p&gt;In addition to courses, taking a practice exam before the live exam is beneficial. For this, Skill Builder is an excellent option. It offers a free practice exam with 20 questions, which is essential for anyone preparing for this certification. If you're willing to pay, Skill Builder also provides a comprehensive practice exam with 85 questions and explanations.&lt;/p&gt;

&lt;p&gt;On Udemy, Stephane also offers a set of practice exams. I did not purchase these after completing everything on Skill Builder, so I cannot review them.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience Taking the Exam
&lt;/h2&gt;

&lt;p&gt;I took the exam on Thursday morning from home through Pearson. I had no issues this time, everything went smoothly.&lt;/p&gt;

&lt;p&gt;I went through all 85 questions within an hour, after which I spent the next 20 minutes reviewing the questions I flagged. As I hinted before, the exam was not easy and had its fair share of challenges. Most of the questions had the difficulty I expected, but many seemed to be taken directly from the Machine Learning Specialty question set, or at least that’s how it felt. Moreover, the spelling of some questions felt really awkward, making me guess what the author was looking for. I suppose this is a downside to taking a beta exam.&lt;/p&gt;

&lt;p&gt;I did not encounter any new types of questions, all of them were multiple choice and multiple selection. These types of questions are not a new innovation from AWS. Anyone who has taken a Microsoft Azure exam should be familiar with them. In my opinion, they do not affect the difficulty of the exam. Personally, I detest the ordering type of questions, while I prefer the case studies.&lt;/p&gt;

&lt;p&gt;That being said, I passed without issues. My score was lower than I expected, but at the end of the day, it’s not something I care that much about.&lt;/p&gt;

&lt;h2&gt;
  
  
  My List of Recommendations for You for the Exam
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep an eye on the clock. There are 85 questions, and you have less than 1.5 minutes per question. Don't spend too much time on any single question. Many of them can be answered quickly as long as you're well-prepared. Try to select the correct answer and move on.&lt;/li&gt;
&lt;li&gt;If you're unsure about a question, flag it and move on. You'll have time to return to it later and figure it out. It's important not to panic if you don't know something right away.&lt;/li&gt;
&lt;li&gt;You will encounter questions you don't know the answer to, and that's okay. The official exam guide is intentionally vague. Any course you take will have some gaps. Don't be discouraged if you come across something completely new during the exam. The important thing is to pass. Your final score doesn't matter much.&lt;/li&gt;
&lt;li&gt;The exam is challenging, but with adequate preparation, anyone should be able to pass it. You'll be fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;Ultimately, I enjoyed the process of preparation and taking the exam itself. You should enjoy yours too! Even though it may not have a significant impact on my career, I’m proud that I was able to pass it.&lt;/p&gt;

&lt;p&gt;I wish happy learning and good luck to anyone preparing to become an AI Practitioner!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>certification</category>
      <category>genai</category>
      <category>career</category>
    </item>
    <item>
      <title>Recertification and the Current State of AWS Exams</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Wed, 14 Aug 2024 14:30:22 +0000</pubDate>
      <link>https://forem.com/aws-builders/recertification-and-the-current-state-of-aws-exams-2872</link>
      <guid>https://forem.com/aws-builders/recertification-and-the-current-state-of-aws-exams-2872</guid>
      <description>&lt;p&gt;Three years ago I wrote one of my first blog posts titled "&lt;a href="https://ervinszilagyi.dev/articles/my-journey-to-become-5-times-aws-certified.html" rel="noopener noreferrer"&gt;My Journey to become 5 times AWS Certified&lt;/a&gt;". This was right after I received my badge for the Solutions Architect Professional certification. I was happy and relieved back then after I successfully passed one of the most challenging exams.&lt;/p&gt;

&lt;p&gt;The unfortunate thing about AWS exams is that they have an expiration date. They are valid for 3 years, after which you either have to recertify or you just accept that your certification has expired and you move on with your life. Recertification means you will have to sit through the same exams all over again. If you go for the professional exams and successfully pass them, the associate exams will be automatically renewed, hence it was enough for me to go for both the Devops Professional and Solutions Architect Professional exams to have all of my 5 badges active.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparation
&lt;/h2&gt;

&lt;p&gt;For the preparation, I used the same resources I was using 3 years ago. For the DevOps exam, back then I purchased Stephane Maarek's course from Udemy, based on which I wrote my notes back then, which I planned to reuse. The problem with this course is that it was entirely re-shot by Stephane. Admittedly, a lot of topics have changed. There is a new version of this exam, DOP-C02, which requires knowledge of a set of new AWS services such as Security Hub, Control Tower, Network Firewall, etc. At the same time, AWS tends to introduce updates and new features for most of its offerings. I decided to re-watch the whole course content again, while also skimming over certain lessons, and I did not do any practical exercise. I work with AWS daily, I did not feel the need to do any of those.&lt;/p&gt;

&lt;p&gt;For the Solutions Architect Professional exam back then I purchased the course from Adrian Cantril. In contrast, this course remained roughly the same as 3 years ago, with additional updates where it was needed. The Direct Connect portion, for example, received a major overhaul. Adrian added all the DX content from the Advanced Networking course to the Solutions Architect Professional course. Unfortunately, there is some legacy leftover content, which was required for the previous version of the exam, but it is simply not needed anymore. Services such as Server Migration Service or Simple Workflow Service are not in scope anymore for the exam.&lt;/p&gt;

&lt;p&gt;To put in contrast these courses, I find it difficult to recommend the course from Stephane as the sole source of preparation. Admittedly, Stephane himself recommends going through his courses from the associate exams before going for the professional ones. The DevOps exam really dropped the ball in terms of difficulty, and I think the course material is not detailed enough for the exam. Adrian's course, on the other hand, is very lengthy with lots of practice examples. While this course covers most of the necessary topics for the exam, it still has some things missing (and some unnecessary stuff, as stated before). I'm confident there will be updates and more with more topics. As guidance, for both of the exams, I strongly recommend downloading the exam guides (can be found &lt;a href="https://d1.awsstatic.com/training-and-certification/docs-sa-pro/AWS-Certified-Solutions-Architect-Professional_Exam-Guide.pdf" rel="noopener noreferrer"&gt;here&lt;/a&gt; for the SAP and &lt;a href="https://d1.awsstatic.com/training-and-certification/docs-devops-pro/AWS-Certified-DevOps-Engineer-Professional_Exam-Guide.pdf" rel="noopener noreferrer"&gt;here&lt;/a&gt; for the DOP) and going through the Appendix part with the list of services. You should at least be able to identify the purpose of each service in case you are seeing them in questions and answer choices. In-depth knowledge of some services is more important for each exam, but for many other things, it is enough to mainly know what is used.&lt;/p&gt;

&lt;p&gt;In terms of practice questions, I've used the ones from TutorialDojo and also the ones from Digital Cloud Training. As with the courses, I did my preparation with the same question sets 3 years ago, but I was not able to recall any of those questions from back then. These question sets are good, but unfortunately, they start to show their age, specifically those from TutorialDojo. &lt;/p&gt;

&lt;p&gt;The DevOps question set from either of these vendors does not reflect the difficulty that you will face in the exam. In my case, probably, I was unlucky or something, but the questions I got in the exam were way more challenging than expected. Many of them were mainly focused on AWS Organizations, Control Tower, Security Hub, and other newer features. These services are barely covered in the question sets from TutorialDojo and Digital Cloud Training. &lt;/p&gt;

&lt;p&gt;The Solutions Architect Professional question set is a bit better in the case of Tutorial Dojo, the one from Digital Cloud is fantastic, to the point that for certain questions in the live exam, you will have a deja-vu feeling.&lt;/p&gt;

&lt;p&gt;A more representative practice exam comes straight from AWS on their SkillBuilder site. Sadly, the question set contains only 20 questions, but at least it is for free. AWS offers these practice questions for each of their certifications, so I strongly recommend going through the ones for your exam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exam Experience
&lt;/h2&gt;

&lt;p&gt;I took the DevOps exam in May, after which I took the Solutions Architect Professional in August. To begin with my experience with the DevOps exam, I must confess, that I had a rough time. I was aware of the fact that there was a new version of this exam around a year ago, but I did not think it would be so much different than the previous one.&lt;/p&gt;

&lt;p&gt;In my opinion, the exam become way more challenging. For my test case, most of the questions were wordy and lengthy, many of which were multiple selections. I understand that this is what we should expect from a professional-level exam, but I felt that this exam was crossing a threshold and was testing my sanity instead of my knowledge. Usually, I don't even bother about the clock, I tend to be fast enough to finish in time, but in this case, I had to use all the time I had. &lt;/p&gt;

&lt;p&gt;For the types of questions, I had all the usual CICD and cloud automation-related problems, stuff you might expect. Aside from that, I had way too many questions about AWS Organizations and Control Tower. In fact, at some point, I was rolling my eyes while encountering another AWS Organizations question. Other significant services touched were the following: Security Hub, AWS Config, AWS Lambda, S3 (I had a question about S3 Object Lambda as well), Storage Gateway, and FSx (I got 2 questions which touched FSx for NetApp, topic which is not covered by any tutor at the point of writing this article), EventBridge, CloudWatch (Logs, Dashboards, Synthetics), and many other stuff I don't remember.&lt;/p&gt;

&lt;p&gt;Aside from this, there were some borderline infuriating questions. To provide an example, the exam expected me to know what kind of condition should (or should not) accept a certain action from a bucket policy or what exactly a certain AWS Config rule (more specifically it expected me to know if there is a Config rule for a certain thing). Memorizing such things is a waste of time in my opinion, and I think it proves basically nothing.&lt;/p&gt;

&lt;p&gt;Continuing with the Solutions Architect Professional exam, most of the things I was asked were in the realm of what I had expected. Even though this exam essentially throws every AWS product at you, you only need in-depth experience with a handful of services. &lt;/p&gt;

&lt;p&gt;Nevertheless, it is a challenging exam, and it can have some surprises. For example, I had a question involving Lambda SnapStart. SnapStart is a new feature used to improve cold starts for Lambdas developed in Java. I was closely monitoring this functionality when it was released by AWS since I am a Java developer at heart. Back then, I did not consider it as revolutionary as AWS was advertising it. As a tangent, in my opinion, if you want fast cold starts you either go for GraalVM or just simply drop Java and write your Lambda in anything else. A native option such as Rust (no fanboy, I'm just stating &lt;a href="https://maxday.github.io/lambda-perf/" rel="noopener noreferrer"&gt;facts&lt;/a&gt;) would offer way better cold starts.&lt;/p&gt;

&lt;p&gt;Anyway, aside from SnapStart, most of the questions were okay. In some cases, the wording was abysmal for either the questions or the answer options, but I'm not a native English speaker, so I won't complain. A few services I would like to mention here for which I encountered multiple questions: networking and DX, IoT (IoT Core, Greengrass, and some other obscure IoT service, you should know about each of them anyway), containers (ECS, Kubernetes - I had one question where Kubernetes made the most sense), streaming services (Kineses Streams and Firehose, AWS MSK in one question only), big data and data engineering (Redshift, Glue, EMR, Athena) and many other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it Worth to get (Re)Certified?
&lt;/h2&gt;

&lt;p&gt;Is it worth getting recertified? &lt;/p&gt;

&lt;p&gt;As cliche as it sounds, it depends. I tend to believe it was worth it for me. I wanted to maintain the status of having active certifications in my current workplace. Also, I wanted to be up to date with the latest changes in AWS.&lt;/p&gt;

&lt;p&gt;Will it be worth it for you? I don't know.&lt;/p&gt;

&lt;p&gt;In case you are reading this article before having any certification, a more important question for you would be if it is worth getting certified.&lt;/p&gt;

&lt;p&gt;If you stumbled on this blog post just before your exam, you must know that you did not waste your time and money. You acquired useful knowledge that you will be able to apply even outside of the AWS cloud.&lt;/p&gt;

&lt;p&gt;On the other hand, if you are still considering an AWS certification, any type of AWS certification, you may take into consideration the following as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You are being tested on a volatile technology. This means that there are a lot of changes happening for many of the AWS products while you are doing your preparation. AWS constantly releases upgrades to products, making them more usable with more features and this is a good thing. From a certification perspective, however, these new upgrades are not introduced right away in the required curriculum. What might happen is that your knowledge might be outdated at the moment you get your certification badge. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Admittedly, even if certain updates are rolled out and you do not learn about those, is not the end of the world. Similarly, in the ballpark of volatility, AWS can retire services. Unfortunately, this has happened more often lately. For example: &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa12uma335v8burwdjiv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa12uma335v8burwdjiv.png" alt="Jeff Bar Tweet about retiring AWS products" width="593" height="1077"&gt;&lt;/a&gt;&lt;br&gt;
Most of these AWS products are part of the currently required learning material for certifications. In fact, CodeCommit is a pivotal part of the DevOps exam. Aside from these services, other services were sunset such as OpsWorks, Server Migration Service, Snowmobile, and others which I cannot remember. The thing is that these are products about which the exam expects in-depth knowledge, but this knowledge becomes outdated right away. I understand that what you are learning might be carried over to alternative solutions, but many of those are not drop-in replacements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The next point might apply to some specialized exams focusing on certain areas of AWS, such as the DevOps exam. You may not benefit as much from what you are learning. Your workplace might use alternative tools, or your role might not correspond with how AWS envisions it. To give you an example, I worked as a DevOps/Platform Engineer in the past. We were using AWS for the infrastructure. We were using Terraform for IaC code, we did not touch CloudFormation at all. We were not using SAM for Lambda functions, Terraform could deploy Lambda functions more cleanly without all the baggage of a Serverless Application. We were working for a huge organization, meaning that account creation and managing the AWS Organizations was the role of an entirely different team. AWS Config rules were managed by the security team, an entirely different department. For CICD we were using GitHub with GitHub Actions. As you can see, we are diverging more and more compared to how AWS saw a DevOps engineer. They have all the right to ask questions about their services, and I strongly believe that all the CloudFormation and CodeCommit/CodeBuild/CodeDeploy questions have their place in the exam. Ultimately, my point is that I'm not the DevOps engineer that the exam portrays, and this certification might not be as beneficial for my short-term career. This might apply to you as well.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Will the Certifications Help Me Get a Job?
&lt;/h2&gt;

&lt;p&gt;During my career, I've seen a handful of job descriptions having a certification as a requirement. They do exist, but in most of the cases, the hiring company does not care about certification. Also, I did not see any job description explicitly mentioning that it needs an active certificate from a candidate. As long as your certificate is relatively recent, you probably should be fine regardless if it is still active or not. And probably, nobody will care anyway if you have hands-on experience.&lt;/p&gt;

&lt;p&gt;Will a certificate help me get a job?&lt;/p&gt;

&lt;p&gt;It might help you get noticed or jump the queue in certain cases. But that's all about it, or at least this is what I experienced. You will still have to pass the interview after that, no certification will give you an automatic pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;p&gt;I recently got recertified. I took both the &lt;strong&gt;AWS Certified DevOps Engineer - Professional (DOP-C02)&lt;/strong&gt; and the &lt;strong&gt;AWS Certified Solutions Architect - Professional (SAP-C02)&lt;/strong&gt; exams. Here are the resources I used:&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Certified DevOps Engineer - Professional (DOP-C02):
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Learning Material&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Free or Paid&lt;/th&gt;
&lt;th&gt;Additional Comments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.udemy.com/course/aws-certified-devops-engineer-professional-hands-on" rel="noopener noreferrer"&gt;Stephane Maarek - AWS Certified DevOps Engineer Professional - Udemy Course&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Video content&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;I recommend only if you did Stephane's associate level Udemy Courses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://portal.tutorialsdojo.com/courses/aws-certified-devops-engineer-professional-practice-exams" rel="noopener noreferrer"&gt;TutorialDojo practice exams&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Practice Questions&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;They are good, but they are starting to show their age&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://explore.skillbuilder.aws/learn/course/external/view/elearning/14673/aws-certified-devops-engineer-professional-official-practice-question-set-dop-c02-english" rel="noopener noreferrer"&gt;Exam Prep Official Practice Question Set&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Practice Questions&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Strongly recommend doing these questions, sadly the set contains only 20 of them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://learn.digitalcloud.training/course/aws-certified-devops-engineer-practice-exams" rel="noopener noreferrer"&gt;Digital Cloud Training practice exams&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Practice Questions&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;They are similar to TutorialDojo questions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My notes can be found here: &lt;a href="https://github.com/Ernyoke/certified-aws-devops-professional" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/certified-aws-devops-professional&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Certified Solutions Architect - Professional (SAP-C02):
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Learning Material&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Free or Paid&lt;/th&gt;
&lt;th&gt;Additional Comments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://learn.cantrill.io/" rel="noopener noreferrer"&gt;AWS Certified Solutions Architect - Professional - Adrian Cantrill&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Video content&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;It is a lengthy 60+ hours course. I recommend it if you can afford the higher price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://portal.tutorialsdojo.com/courses/aws-certified-solutions-architect-professional-practice-exams/" rel="noopener noreferrer"&gt;TutorialDojo practice exams&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Practice Questions&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;They are good, but they are starting to show their age&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://explore.skillbuilder.aws/learn/course/external/view/elearning/13270/aws-certified-solutions-architect-professional-official-practice-question-set-sap-c02-english" rel="noopener noreferrer"&gt;Exam Prep Official Practice Question Set&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Practice Questions&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Strongly recommend doing these questions, sadly the set contains only 20 of them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://learn.digitalcloud.training/course/aws-certified-solutions-architect-professional-practice-exams" rel="noopener noreferrer"&gt;Digital Cloud Training practice exams&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Practice Questions&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;They are really good and they manage to be very similar to what you will get on the exam&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My notes can be found here: &lt;a href="https://github.com/Ernyoke/certified-aws-solutions-architect-professional" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/certified-aws-solutions-architect-professional&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>certification</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Terragrunt for Multi-Region/Multi-Account Deployments</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Tue, 07 May 2024 22:38:24 +0000</pubDate>
      <link>https://forem.com/aws-builders/terragrunt-for-multi-regionmulti-account-deployments-1o1</link>
      <guid>https://forem.com/aws-builders/terragrunt-for-multi-regionmulti-account-deployments-1o1</guid>
      <description>&lt;p&gt;Since a few years ago I've been working for a company whose products are used by millions. It feels somewhat refreshing to know that my contributions have an impact on the daily lives of so many people. On the other hand, this work also comes with a lot of anxiety in cases when you have to make decisions, even though these decisions are usually made together with a team of other highly experienced individuals.&lt;/p&gt;

&lt;p&gt;Such a decision was the introduction of Terragrunt in our workflow. Why did we need Terragrunt, you may ask? This is what I will try to answer in the following lines of this article.&lt;/p&gt;

&lt;p&gt;As a disclaimer, this article is subjective, based on my own experience in finding a solution to the problems we had. Usually, there is more than one way to tackle a challenge and in many cases, there are no perfect solutions. Knowing this, I think it is important to address the weaknesses and limitations of your solution, which this article will do later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Terragrunt?
&lt;/h2&gt;

&lt;p&gt;Before going into the "whys", I think it is important to know what is Terragrunt. Now I won't give you their marketing points or any definition copied from sites like Wikipedia, I will try to explain what is the purpose of this tool from my point of view.&lt;/p&gt;

&lt;p&gt;Essentially, Terragrunt is a wrapper around Terraform, acting as an orchestrator over multiple Terraform modules. As developers, we have to organize our Terraform code in &lt;a href="https://developer.hashicorp.com/terraform/language/modules" rel="noopener noreferrer"&gt;modules&lt;/a&gt;. Under to hood, each module gets embedded inside a Terraform project and it is deployed individually. Modules communicate between themselves with outputs and &lt;a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#dependency" rel="noopener noreferrer"&gt;dependency&lt;/a&gt; blocks, while the dependency tree and rollout order are determined by Terragrunt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Terragrunt?
&lt;/h2&gt;

&lt;p&gt;To understand why Terragrunt was chosen, it would make sense to go through a timeline of challenges we encountered. &lt;/p&gt;

&lt;p&gt;Let's assume we have an application spanning through a few micro-services with an SQL database, some static assets, and a few ETL jobs bringing in some data from external providers. We decide we want to migrate everything to the AWS. Our users are from all over the globe, but our main focus is Europe and the US. We have to offer different data to the EU users and the US users, moreover, we would like to reduce the latency of the responses. So it makes sense to deploy the whole stack on 2 different regions. We also want to have the application deployed separately for development and testing, for which we can use different AWS accounts.&lt;/p&gt;

&lt;p&gt;For IaC we decided to use Terraform because we have the most experience with that compared to other options. Having these in mind, the following events happened afterward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We started writing our Terraform code. We put everything in a single Terraform project. We relied on &lt;code&gt;tfvars&lt;/code&gt; files to have inputs for different environments and regions.&lt;/li&gt;
&lt;li&gt;We shortly ran into a scaling problem: attempting to do an &lt;code&gt;apply&lt;/code&gt; went from a few minutes to a few tens of minutes. Moreover, we run into so communication and deployment issues in terms of certain changes being deployed to production before we wanted them.&lt;/li&gt;
&lt;li&gt;Our Terraform project got bigger and bigger. We decide to slice it somehow into smaller pieces. We introduced the internal concept of "stacks" (this was well before the introduction of Terraform Cloud Stacks). From our point of view, a "stack" essentially was a Terraform project deploying a well-defined part of our infrastructure. Each stack could use resources deployed by other stacks by relying on Terraform outputs and &lt;a href="https://developer.hashicorp.com/terraform/language/state/remote-state-data" rel="noopener noreferrer"&gt;&lt;code&gt;terraform_remote_state&lt;/code&gt;&lt;/a&gt; or just by simply using data sources.&lt;/li&gt;
&lt;li&gt;With the introduction of stacks we had different projects for networking, databases, ETL (we used mainly AWS Batch), storage (S3 buckets), and so on. This worked for a while until we ran into another problem. At first, it was easy to follow which stack depends on which other stack, but shortly we ran into the issue of circular dependencies. Stack A could create resources used by stack B, while also relying on resources created by stack B. Obviously, this is bad, and at this point, there is no entity to check and police our dependencies.&lt;/li&gt;
&lt;li&gt;Moreover, we run into another problem. Certain resources are needed only for certain environments. For example, we needed a read replica only for prod, for testing and development we could get by with only the main database. In the beginning, we could solve this by having conditions on whether we want to deploy the resource in the current environment or not. At a certain point, we notice that we have to put these conditions in many places, adding a lot of complexity baggage to our infrastructure code.&lt;/li&gt;
&lt;li&gt;So we decided to introduce Terragrunt. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To answer the initial question, we chose Terragrunt because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It solved the dependency hell we encountered. With Terragrunt we have to be explicit in defining on who does our current module depends.&lt;/li&gt;
&lt;li&gt;It fits the multi-region/multi-account approach. In case we dour our modules wisely, we use only the necessary modules for each region/environment. The catch here is that we have to modularize our code and we do it adequately, which might be not as easy as we would expect.&lt;/li&gt;
&lt;li&gt;By introducing versioning for our modules, we could evolve different environments at their own pace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, all of these come with a price: refactoring. Terragrunt relies on Terraform modules. Our initial code was not as modular as we might expected. So we had to do a lot of refactoring, which also came with an even bigger challenge: state management and transferring resources between states.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Terragrunt Work?
&lt;/h2&gt;

&lt;p&gt;To use Terragrunt, first, we have to be comfortable with &lt;a href="https://developer.hashicorp.com/terraform/language/modules" rel="noopener noreferrer"&gt;Terraform modules&lt;/a&gt;. The concept of modularization is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform provides building blocks such as resources and data sources;&lt;/li&gt;
&lt;li&gt;Some of these resources are often used together (for example: a database and Route 53 record for its hostname)&lt;/li&gt;
&lt;li&gt;It would make sense to group these resources in a reusable container. These reusable containers are called modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modules can communicate between themselves with inputs and outputs. Terragrunt requires that all of our Terraform resources be part of modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up a Terragrunt Project
&lt;/h3&gt;

&lt;p&gt;In the official Terragrunt documentation there is &lt;a href="https://terragrunt.gruntwork.io/docs/features/keep-your-terraform-code-dry/" rel="noopener noreferrer"&gt;a good article&lt;/a&gt; about how to set up a Terragrunt project and where to place modules. In fact, there is also a &lt;a href="https://github.com/gruntwork-io/terragrunt-infrastructure-live-example" rel="noopener noreferrer"&gt;repository&lt;/a&gt; on GitHub providing an example project on how the creators recommend setting up Terragrunt. I certainly recommend going through that repository, because it is a good reference for a starting point. Having that said, I like to structure mine a little bit differently. My recommendation is to have different AWS accounts for each environment. Getting a new account usually is relatively easy to accomplish even if we are working in a corporate environment (your workplace most likely is using AWS Organizations to manage accounts). The existence of multiple accounts does not require additional costs, we only pay for what we use. &lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://github.com/gruntwork-io/terragrunt-infrastructure-live-example" rel="noopener noreferrer"&gt;terragrunt-infrastructure-live-example&lt;/a&gt; the split for the environments is done by &lt;strong&gt;prod&lt;/strong&gt; and &lt;strong&gt;non-prod&lt;/strong&gt; accounts. Each of these is further split by region. The &lt;strong&gt;non-prod&lt;/strong&gt; account is also used for &lt;strong&gt;qa&lt;/strong&gt; and &lt;strong&gt;stage&lt;/strong&gt; environments. This setup can be perfectly acceptable, the one downside being that we will have to think about a naming convention for our resources, since in &lt;strong&gt;non-prod&lt;/strong&gt; we will have the same cloud resources for both &lt;strong&gt;qa&lt;/strong&gt; and &lt;strong&gt;stage&lt;/strong&gt;. While this is not that big of a deal, I prefer to have one environment per account. My proposal for a Terragrunt project setup would look like this (GitHub repository for this example project can be found here: &lt;a href="https://github.com/Ernyoke/tg-multi-account" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/tg-multi-account&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tg-multi-account
│   .gitignore
│   global.hcl
│   terragrunt.hcl
│
├───dev
│   │   account.hcl
│   │
│   └───us-east-1
│       │   region.hcl
│       │
│       ├───alb
│       │       terragrunt.hcl
│       │
│       ├───ecs-cluster
│       │       terragrunt.hcl
│       │
│       ├───ecs-services
│       │   └───frontend
│       │           terragrunt.hcl
│       │
│       └───vpc
│               terragrunt.hcl
│
├───prod
│   │   account.hcl
│   │
│   ├───eu-west-1
│   │   │   region.hcl
│   │   │
│   │   ├───alb
│   │   │       terragrunt.hcl
│   │   │
│   │   ├───ecs-cluster
│   │   │       terragrunt.hcl
│   │   │
│   │   ├───ecs-services
│   │   │   └───frontend
│   │   │           terragrunt.hcl
│   │   │
│   │   └───vpc
│   │           terragrunt.hcl
│   │
│   └───us-east-1
│       │   region.hcl
│       │
│       ├───alb
│       │       terragrunt.hcl
│       │
│       ├───ecs-cluster
│       │       terragrunt.hcl
│       │
│       ├───ecs-services
│       │   └───frontend
│       │           terragrunt.hcl
│       │
│       └───vpc
│               terragrunt.hcl
│
├───qa
│   │   account.hcl
│   │
│   ├───eu-west-1
│   │   │   region.hcl
│   │   │
│   │   ├───alb
│   │   │       terragrunt.hcl
│   │   │
│   │   ├───ecs-cluster
│   │   │       terragrunt.hcl
│   │   │
│   │   ├───ecs-services
│   │   │   └───frontend
│   │   │           terragrunt.hcl
│   │   │
│   │   └───vpc
│   │           terragrunt.hcl
│   │
│   └───us-east-1
│       │   region.hcl
│       │
│       ├───alb
│       │       terragrunt.hcl
│       │
│       ├───ecs-cluster
│       │       terragrunt.hcl
│       │
│       ├───ecs-services
│       │   └───frontend
│       │           terragrunt.hcl
│       │
│       └───vpc
│               terragrunt.hcl
│
└───_env
        frontend.hcl
        vpc.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we have 3 environments: &lt;strong&gt;dev&lt;/strong&gt;, &lt;strong&gt;qa&lt;/strong&gt;, and &lt;strong&gt;prod&lt;/strong&gt;. Each environment should be living in a single AWS account. The root of the project contains configuration (&lt;code&gt;locals&lt;/code&gt;) shared by every environment. If we go inside a directory acting as an environment/account, we have the account-specific properties (&lt;code&gt;account.hcl&lt;/code&gt;). We also create directories here for each region in which we would want to provision things. Navigating one step deeper we find the region-specific configuration (&lt;code&gt;region.hcl&lt;/code&gt;) and all the modules we would like to have in that region.&lt;/p&gt;

&lt;p&gt;Now let's focus on the Terragrunt modules. If we open a configuration, for example for the VPC, a possible implementation would be the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tfr:///terraform-aws-modules/vpc/aws//.?version=5.8.1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;global_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read_terragrunt_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"global.hcl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;global_vars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.project_name}-vpc"&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;cidr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;

&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;private_subnets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.3.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;public_subnets&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.101.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.102.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.103.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;enable_nat_gateway&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;enable_vpn_gateway&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To give a short explanation of what we have here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the &lt;code&gt;terraform&lt;/code&gt; block we have to specify a path to a Terraform module. For example, in this case, we use the VPC module from the &lt;a href="https://github.com/terraform-aws-modules/terraform-aws-vpc" rel="noopener noreferrer"&gt;terraform-aws-modules&lt;/a&gt; open source project for which we either could use the URL of the repository, or we could use the URL provided by the Terragrunt registry. We don't necessarily need to rely on other people's code, we can use modules maintained by ourselves by providing a link to our remote Git repository, or we can even have it point to a local path on our drive.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;include&lt;/code&gt; block is optional. It is used for "inheritance". By inheritance, we can think of it as if the parent configuration file is copy/pasted in the current configuration file. This can be useful because we can share common inputs with other environments/regions. Including them in the current configuration, Terragrunt will automatically provide them to the Terraform module. Also, we have the ability to append/override certain inputs as we wish.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;locals&lt;/code&gt; block essentially acts the same as Terraform &lt;code&gt;locals&lt;/code&gt;. These are local "variables" used in the current configuration. We can also read locals from other configuration files with Terragrunt functions (&lt;code&gt;read_terragrunt_config&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;inputs&lt;/code&gt; are values we provide to the Terraform module. If we use inheritance, the includes provided by the parent configuration are automatically merged with current includes, making our configuration &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself" rel="noopener noreferrer"&gt;DRY&lt;/a&gt;, arguably less readable, more on this later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taking the "DRY" -ness a step further, we can notice that modules such as &lt;code&gt;vpc&lt;/code&gt; are used in each environment/region with little configurational difference. What we can do is extract this configuration into a top-level folder, such as &lt;code&gt;_env&lt;/code&gt;, and rely on the inheritance feature discussed before. &lt;/p&gt;

&lt;p&gt;The extracted file will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ./_env/vpc.hcl&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tfr:///terraform-aws-modules/vpc/aws//.?version=5.8.1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;global_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read_terragrunt_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"global.hcl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;global_vars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.project_name}-vpc"&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;cidr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;

&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;private_subnets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.3.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;public_subnets&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.101.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.102.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.103.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;enable_nat_gateway&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;enable_vpn_gateway&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case our region is not from us-east-1, we can override the availability zones with sensible ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ./qa/eu-west-1/vpc/terragrunt.hcl&lt;/span&gt;

&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"env"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${get_terragrunt_dir()}/../../_env/vpc.hcl"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;azs&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eu-west-1a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More or less this is what we need to know to be able to write Terragrunt code. Now let's discuss deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployments
&lt;/h3&gt;

&lt;p&gt;The deployment of a Terragrunt project can be accomplished with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terragrunt run-all apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we run this command from the root our our project, Terragrunt will attempt to deploy all the resources in all the accounts. This assumes we have the necessary rights to deploy to each account and we also made sure that Terragrunt knows about the IAM role it can assume to do the provisioning (see: &lt;a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#iam_role" rel="noopener noreferrer"&gt;&lt;code&gt;iam_role&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LATER UPDATE&lt;/strong&gt;: It seems like it is not possible to execute &lt;code&gt;run-all apply&lt;/code&gt; from the root of the project in this current example. Terragrunt will fail to find either &lt;code&gt;globals.hcl&lt;/code&gt; or &lt;code&gt;account.hcl&lt;/code&gt;. It appears strange to be unable to find &lt;code&gt;globals.hcl&lt;/code&gt; since this file is in the same directory together with the &lt;code&gt;terragrunt.hcl&lt;/code&gt; configuration file from where it is referenced. It might be understandable to not be able to locate &lt;code&gt;account.hcl&lt;/code&gt;. This file is not in the current root folder, it is in a child folder relative to the root, so &lt;code&gt;find_in_parent_folders&lt;/code&gt; will fail to locate it. This was an oversight on my part while building the example project for this article and I want to apologize for that. Deploying from an environment should work as it is presented in the upcoming lines.&lt;/p&gt;

&lt;p&gt;In case we don't want to deploy everything everywhere at once, we can simply navigate into the folder of the environment/region where we want to provision resources, and we can execute the command there.&lt;/p&gt;

&lt;p&gt;In case we want to deploy certain modules only in a certain region, we can navigate into the folder for that module and execute the command there. An important thing to note here, if we run &lt;code&gt;terragrunt run-all apply&lt;/code&gt; for a module, all the dependencies of that module will also be deployed. This might be time-consuming in cases where we need to execute it frequently (in case we do development for example). As a workaround, we can run &lt;code&gt;terragrunt apply&lt;/code&gt; instead (without the &lt;code&gt;run-all&lt;/code&gt; command). This will omit to roll out all the dependencies. It will rely on previous outputs cached with previous deployments. If there was no previous deployment for a dependency, the command will fail.&lt;/p&gt;

&lt;p&gt;Terragrunt commands are similar to what we've been accustomed to while using Terraform. We can see the plan by executing the &lt;code&gt;plan&lt;/code&gt; command (with certain limitations, more about this below), we can import resources with the &lt;code&gt;import&lt;/code&gt; command, we can &lt;code&gt;force-unlock&lt;/code&gt; the state of a module in case it got stuck with an unsuccessful apply, and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downsides and Limitations
&lt;/h2&gt;

&lt;p&gt;Like every other tool, Terragrunt has its limitations, especially if we are coming from a Terraform setup. While I consider Terragrunt to be a valuable and useful tool, I think is very important to know its limitations in case we consider adopting it.&lt;/p&gt;

&lt;p&gt;The following is a list of challenges that I've encountered during its adoption and day-to-day usage. I imagine there are plenty of others faced by other people, this is not an exhaustive list.&lt;/p&gt;

&lt;p&gt;1. &lt;strong&gt;Steep learning curve and complexity of usage&lt;/strong&gt;: if we are new to Terragrunt, we may get easily overwhelmed by all the new concepts we are introduced to. As we get more familiar with it, we are faced with other challenges, such as configuration inheritance. Having to adhere to practices that make our configuration DRY, can also make it more challenging to understand and follow. &lt;/p&gt;

&lt;p&gt;2. &lt;strong&gt;There &lt;code&gt;plan&lt;/code&gt; command is broken (at least in certain scenarios)&lt;/strong&gt;: this is stated even in the documentation for the &lt;a href="https://terragrunt.gruntwork.io/docs/reference/cli-options/#run-all" rel="noopener noreferrer"&gt;&lt;code&gt;run-all&lt;/code&gt; command&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[WARNING] Using run-all with plan is currently broken for certain use cases. If you have a stack of Terragrunt modules with dependencies between them—either via dependency blocks or &lt;code&gt;terraform_remote_state&lt;/code&gt; data sources—and you’ve never deployed them, then &lt;code&gt;run-all plan&lt;/code&gt; will fail as it will not be possible to resolve the dependency blocks or &lt;code&gt;terraform_remote_state&lt;/code&gt; data sources!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This might seem a non-issue at first, but if we consider also to following note for the &lt;code&gt;apply&lt;/code&gt; command...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[NOTE] Using &lt;code&gt;run-all&lt;/code&gt; with apply or destroy silently adds the &lt;code&gt;-auto-approve&lt;/code&gt; flag to the command line arguments passed to Terraform due to issues with shared stdin making individual approvals impossible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;...we can probably guess why it might be dangerous to simply do deployments. Although, it might not be such drastic of a situation. My recommendation is that if we have doubts, we should restrict roll-outs to individual modules. In the case of modules, we can execute &lt;code&gt;plan&lt;/code&gt; or &lt;code&gt;apply&lt;/code&gt; without the silent auto-approve flag. &lt;/p&gt;

&lt;p&gt;Also, Terragrunt is meant to be used with multiple environments in mind. Having a successful rollout in a non-prod environment should make us confident and prepared for the production rollout.&lt;/p&gt;

&lt;p&gt;3. &lt;strong&gt;There is no easy way to import Terraform resources (at least I'm not aware of any)&lt;/strong&gt;: in case we have a Terraform project and we decide to transform it to Terragrunt, we most likely will have to manually import all the resources into the new state. This might be a non-issue if we could destroy our Terraform stack and re-provision everything with Terragrunt, but this might not be possible in the case of a production environment where availability is important.&lt;/p&gt;

&lt;p&gt;4. &lt;strong&gt;Deployment Speed&lt;/strong&gt;: Terragrunt is running Terraform under the hood. It will invoke Terraform independently for each module, an action that takes time. A mitigation for this is to keep deployments scoped to and apply only what we need. In cases where we need a plan for the whole project (for a security assessment for example), most likely we will still have to wait a lot to get that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives to Terragrunt
&lt;/h2&gt;

&lt;p&gt;At the beginning of this article, we have seen why Terragrunt made sense for us for a business-critical solution. In the previous section, we were also faced with the limitations of this tool. Having in mind all of these, we could want to keep an eye on what other alternatives exist.&lt;/p&gt;

&lt;p&gt;Here are a few examples that can be considered instead of Terragrunt.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/terramate-io/terramate" rel="noopener noreferrer"&gt;Terramate&lt;/a&gt;: It seems like a good alternative, and I think it could have been a better choice for certain issues we had. With Terramate the transition from Terraform might have been easier since Terraform projects can be imported seamlessly. Furthermore, we don't have to think right away about how to modularize everything. The reason it was not chosen, is that my team was mainly familiar with Terragrunt. We had no experience with Terramate, so we decided to play it safely.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.hashicorp.com/blog/terraform-stacks-explained" rel="noopener noreferrer"&gt;Terraform Stacks&lt;/a&gt;: at the point of writing this post, it is still not generally available. It was not even considered by us back then, since it was in private preview and nobody had access to it. It might be a good choice in the future, but for now, it is not something we can use.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.hashicorp.com/terraform/language/state/workspaces" rel="noopener noreferrer"&gt;Terraform Workspaces&lt;/a&gt;: they are a similar approach to having different tfvars files per environment/region, a solution we were extensively using. We found that it is not the best choice since it scales poorly if the infrastructure gets bigger and bigger. However, If you start a project, I still recommend sticking to workspaces at the beginning and moving to something afterward, when it is needed.&lt;/li&gt;
&lt;li&gt;Insert any other tool here: understandably there are many other options out there.  When making a decision for something that will have to be maintained by multiple people for a living, usually we go with the one tool that has to most support on the internet, it is known by most of the people from the team and generally has a good reputation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;In conclusion, Terragrunt is a powerful tool with many functionalities. It is an opinionated way of working with infrastructure. It might not be the best choice for everyone. &lt;/p&gt;

&lt;p&gt;Should I use it for my next project?&lt;br&gt;
It depends. If you did not encounter some of the issues that are aimed to be solved by it, then you probably may not want to use it. It will add considerable maintenance baggage. To quote the Terragrunt author here &lt;a href="https://www.reddit.com/r/Terraform/comments/15242e4/comment/jsmoedj/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;[source]&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt; If you're working on a small project (e.g., a solo project or hobby), none of this matters, and you probably don't need Terragrunt. But if you're working at a company that is using Terraform to manage infrastructure for multiple teams and multiple environments, the items above make it hard to create code that is maintainable and understandable, and that's where Terragrunt can be a great option.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/modules" rel="noopener noreferrer"&gt;Terraform Modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#dependency" rel="noopener noreferrer"&gt;Terragrunt Dependency Blocs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/remote-state-data" rel="noopener noreferrer"&gt;The terraform_remote_state Data Source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://terragrunt.gruntwork.io/docs/features/keep-your-terraform-code-dry/" rel="noopener noreferrer"&gt;Keep your Terraform code DRY&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#iam_role" rel="noopener noreferrer"&gt;&lt;code&gt;iam_role&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://terragrunt.gruntwork.io/docs/reference/cli-options/#run-all" rel="noopener noreferrer"&gt;&lt;code&gt;run-all&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>terragrunt</category>
      <category>aws</category>
    </item>
    <item>
      <title>Fluent Bit with ECS: Configuration Tips and Tricks</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Tue, 26 Dec 2023 11:09:34 +0000</pubDate>
      <link>https://forem.com/aws-builders/fluent-bit-with-ecs-configuration-tips-and-tricks-4acp</link>
      <guid>https://forem.com/aws-builders/fluent-bit-with-ecs-configuration-tips-and-tricks-4acp</guid>
      <description>&lt;p&gt;A while ago I wrote a &lt;a href="https://ervinszilagyi.dev/articles/ecs-custom-logging-with-fluentbit.html" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; about Fluent Bit integration with containers running in an ECS cluster. According to my statistics, this post is one of the most viewed on my blog, so I was determined to write a follow-up for it. I've been using Fluent Bit with ECS for more than a year in a business application running in production. During this period I had the chance to make use of and even abuse several features provided by Fluent Bit.&lt;/p&gt;

&lt;p&gt;Generally speaking my experience with Fluent Bit in the last year was positive. In many cases, I found that it had a steep learning curve, and several times I felt I was doing things that I should not supposed to be doing. In the end, I managed to reconcile myself with how it operates and I can say for sure that it is a fast and very polished product that can handle huge production workloads.&lt;/p&gt;

&lt;p&gt;In this blog post, I will talk about certain tips and tricks for the Fluent Bit configuration file that I found useful. Some of them might be trivial if you already have experience with it. Nevertheless, I think that they might be helpful for anybody interested in introducing Fluent Bit to their cluster of services.&lt;/p&gt;

&lt;p&gt;This post will not provide a guideline on how to set up Fluent Bit. If you are interested in that, please read my previous post on this topic: &lt;a href="//./ecs-custom-logging-with-fluentbit.md"&gt;ECS Fargate Custom Logging with Fluent Bit&lt;/a&gt;. Moreover, there are several useful articles from AWS, such as this one: &lt;a href="https://aws.amazon.com/blogs/opensource/centralized-container-logging-fluent-bit/" rel="noopener noreferrer"&gt;Centralized Container Logging with Fluent Bit&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fluent Bit Configuration Basics
&lt;/h2&gt;

&lt;p&gt;Fluent Bit can be configured with a &lt;code&gt;fluent-bit.conf&lt;/code&gt; configuration file or with YAML configuration. We will focus on the so-called classic &lt;code&gt;.conf&lt;/code&gt; configuration format since at this point the YAML configuration is not that widespread. &lt;/p&gt;

&lt;p&gt;A basic configuration file would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;SERVICE]
    Flush           5
    Daemon          off
    Log_Level       debug

&lt;span class="o"&gt;[&lt;/span&gt;INPUT]
    Name cpu
    Tag  my_cpu

&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name  &lt;span class="nb"&gt;grep
    &lt;/span&gt;Match &lt;span class="k"&gt;*&lt;/span&gt;
    Regex log aa

&lt;span class="o"&gt;[&lt;/span&gt;OUTPUT]
    Name  stdout
    Match my&lt;span class="k"&gt;*&lt;/span&gt;cpu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can notice that a configuration file can have the following sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Service&lt;/em&gt;: defines global settings for the Fluent Bit container. Some examples of these kinds of settings are how often should the container flush its content, the logging level of the Fluent Bit agent, or if we would like to add additional plugins or parsers (more about parsers below).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Input&lt;/em&gt;: defines the source from where the agent will attempt to collect records. Fluent Bit can receive records from multiple sources, such as log streams created by applications and services, Linux/Unix system logs, hardware metrics, Docker events, etc. A full list of inputs can be found in the &lt;a href="https://docs.fluentbit.io/manual/pipeline/inputs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. When we talk about Fluent Bit usage together with ECS containers, most of the time these records are log events (log messages with additional metadata).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Output&lt;/em&gt;: defines the sink, the destination where certain records will go. Fluent Bit supports multiple destinations, such as ElasticSearch, AWS S3, Kafka our event stdout. For a full list, see the official documentation for &lt;a href="https://docs.fluentbit.io/manual/pipeline/outputs" rel="noopener noreferrer"&gt;outputs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Filter&lt;/em&gt;: the name of this section is somewhat misleading in my opinion. Filters can be used to manipulate records, not just for filtering and dropping entries. With filters, we can modify certain fields from the records or we can add/remove/rename certain information. A full list of filters can be found &lt;a href="https://docs.fluentbit.io/manual/pipeline/filters" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not all of these sections are mandatory in a configuration file. Generally, we need at least the input and output sections. The &lt;code&gt;fluent-bit.conf&lt;/code&gt; file is also referred to as the main configuration file. Besides this file, we can have additional configurations, such as parsers. Parsers are used to read and transform raw input records into a structured object, such Lua tables (tables are the equivalent of a dictionary/map in other languages). This is required by the agent to be able to further process them with filters.  &lt;/p&gt;

&lt;p&gt;We can write our own parsers and load them, or we can rely on the ones provided by Fluent Bit itself. It comes with its own pre-configured &lt;code&gt;parser.conf&lt;/code&gt; file (&lt;a href="https://github.com/fluent/fluent-bit/blob/master/conf/parsers.conf" rel="noopener noreferrer"&gt;https://github.com/fluent/fluent-bit/blob/master/conf/parsers.conf&lt;/a&gt;). These parsers support most of the popular log formats, such as Docker, nginx, or syslog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging and Troubleshooting Fluent Bit Configuration File
&lt;/h2&gt;

&lt;p&gt;While working with Fluent Bit I found myself losing a lot of time with deployments. If I wanted to see the effects of certain changes I made in the configuration file, I had to rebuild the Fluent Bit image, push it to an ECR repo, restart the main service which will load the newest version of the sidecar container, and then just wait for the log messages to arrive while hoping to see some meaningful change. This laborious process can be very annoying. It is way wiser to attempt to run the container locally and provide some test input for validating a modification in the configuration file.&lt;/p&gt;

&lt;p&gt;We mentioned that we can have several types of &lt;code&gt;INPUT&lt;/code&gt;s. One of them, having the name of &lt;code&gt;dummy&lt;/code&gt;, was purposefully implemented for quick testig. It accepts a pre-defined JSON as input. It will repeatedly send this input for processing over and over again, simulating a stream of data. Additionally, if we set the &lt;code&gt;OUTPUT&lt;/code&gt; to be &lt;code&gt;stdout&lt;/code&gt;, we will create a way of doing "printf debugging".&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;We create a &lt;code&gt;fluent-bit.conf&lt;/code&gt; file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;INPUT]
    Name   dummy
    Dummy &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;: &lt;span class="s2"&gt;"custom dummy"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;OUTPUT]
    Name   stdout
    Match  &lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We create a Dockerfile for our Fluent Bit image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;FROM amazon/aws-for-fluent-bit:latest

WORKDIR /

ADD fluent-bit.conf fluent-bit.conf

CMD &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/fluent-bit/bin/fluent-bit"&lt;/span&gt;, &lt;span class="s2"&gt;"-c"&lt;/span&gt;, &lt;span class="s2"&gt;"fluent-bit.conf"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We build the Fluent Bit docker image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; fluent-bit-dummy &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We run the image locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; fluent-bit-dummy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will launch the container that will run until we stop it with &lt;code&gt;Ctrl+C&lt;/code&gt; key combination. This execution will produce an output similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; fluent-bit-dummy
WARNING: The requested image&lt;span class="s1"&gt;'s platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
Fluent Bit v1.9.10
* Copyright (C) 2015-2022 The Fluent Bit Authors
* Fluent Bit is a CNCF sub-project under the umbrella of Fluentd
* https://fluentbit.io

[2023/12/24 16:06:59] [ info] [fluent bit] version=1.9.10, commit=557c8336e7, pid=1
[2023/12/24 16:06:59] [ info] [storage] version=1.4.0, type=memory-only, sync=normal, checksum=disabled, max_chunks_up=128
[2023/12/24 16:06:59] [ info] [cmetrics] version=0.3.7
[2023/12/24 16:06:59] [ info] [output:stdout:stdout.0] worker #0 started
[2023/12/24 16:06:59] [ info] [sp] stream processor started
[0] dummy.0: [1703434019.553880465, {"message"=&amp;gt;"custom dummy"}]
[0] dummy.0: [1703434020.555768799, {"message"=&amp;gt;"custom dummy"}]
[0] dummy.0: [1703434021.550525174, {"message"=&amp;gt;"custom dummy"}]
[0] dummy.0: [1703434022.551563050, {"message"=&amp;gt;"custom dummy"}]
[0] dummy.0: [1703434023.551944509, {"message"=&amp;gt;"custom dummy"}]
[0] dummy.0: [1703434024.550027843, {"message"=&amp;gt;"custom dummy"}]
[0] dummy.0: [1703434025.550901801, {"message"=&amp;gt;"custom dummy"}]
[0] dummy.0: [1703434026.549279385, {"message"=&amp;gt;"custom dummy"}]
^C[2023/12/24 16:07:08] [engine] caught signal (SIGINT)
[0] dummy.0: [1703434027.549678344, {"message"=&amp;gt;"custom dummy"}]
[2023/12/24 16:07:08] [ warn] [engine] service will shutdown in max 5 seconds
[2023/12/24 16:07:08] [ info] [engine] service has stopped (0 pending tasks)
[2023/12/24 16:07:08] [ info] [output:stdout:stdout.0] thread worker #0 stopping...
[2023/12/24 16:07:08] [ info] [output:stdout:stdout.0] thread worker #0 stopped
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem with &lt;code&gt;dummy&lt;/code&gt; is that it requires the content of the message to be inline. This can be annoying if we want to give something more complex. For example, a log event generated by the log router of an ECS containers looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_task_arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ecs:region:0123456789012:task/FluentBit-cluster/13EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"container_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/ecs-windows-app-task-1-sample-container-cEXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_cluster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FluentBit-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_container_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sample-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_task_definition_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"container_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"61f5e6EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"log"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_task_definition_family"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"windows-app-task"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could provide this JSON document as a one-liner and call it a day. In my opinion, it would be ideal if we could put this into a file, and point the input to use the content of that file to generate records. Unfortunately, &lt;code&gt;dummy&lt;/code&gt; input is dummy and does not support reading stuff from a file. &lt;/p&gt;

&lt;p&gt;A workaround that I've been using to overcome this limitation, is to have &lt;code&gt;exec&lt;/code&gt; instead of &lt;code&gt;dummy&lt;/code&gt;. &lt;code&gt;exec&lt;/code&gt; can take the content from the standard output of a script and generate records based on that.&lt;/p&gt;

&lt;p&gt;We can provide a simple bash script that reads and outputs the content of a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Read the content of the log entry from a file&lt;/span&gt;
&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;log.json&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Echo the output, which will be the input for Fluent Bit&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can alter the configuration file as such:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;SERVICE]
    Flush        5
    Log_Level    info
    Parsers_File /fluent-bit/parsers/parsers.conf

&lt;span class="o"&gt;[&lt;/span&gt;INPUT]
    Name         &lt;span class="nb"&gt;exec
    &lt;/span&gt;Command      /generate.sh
    Tag          dummy.input
    Parser       json

&lt;span class="o"&gt;[&lt;/span&gt;OUTPUT]
    Name   stdout
    Match  &lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Dockerfile should also be modified to have the bash script and the log entry JSON file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;FROM amazon/aws-for-fluent-bit:latest

WORKDIR /

ADD fluent-bit.conf fluent-bit.conf
ADD log.json log.json
ADD generate.sh generate.sh

RUN &lt;span class="nb"&gt;chmod&lt;/span&gt; +x generate.sh

CMD &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/fluent-bit/bin/fluent-bit"&lt;/span&gt;, &lt;span class="s2"&gt;"-c"&lt;/span&gt;, &lt;span class="s2"&gt;"fluent-bit.conf"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this container locally will print the following over and over again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0] dummy.input: [1703444437.645291508, {"source"=&amp;gt;"stdout", "ecs_task_arn"=&amp;gt;"arn:aws:ecs:region:0123456789012:task/FluentBit-cluster/13EXAMPLE", "container_name"=&amp;gt;"/ecs-windows-app-task-1-sample-container-cEXAMPLE", "ecs_cluster"=&amp;gt;"FluentBit-cluster", "ecs_container_name"=&amp;gt;"sample-container", "ecs_task_definition_version"=&amp;gt;"1", "container_id"=&amp;gt;"61f5e6EXAMPLE", "log"=&amp;gt;"10", "ecs_task_definition_family"=&amp;gt;"windows-app-task"}]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Loading Parsers
&lt;/h2&gt;

&lt;p&gt;Parsers should not be part of the main configuration file, they should be placed into a separate file. Adhering to this requirement, Fluent Bit provides a set of parsers located under &lt;code&gt;/fluent-bit/parsers/parsers.conf&lt;/code&gt; path. For these parsers to be used, we have to load them in the service section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;SERVICE]
    Parsers_File /fluent-bit/parsers/parsers.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parsers can work together with INPUTs, as we have already seen in the case of &lt;code&gt;exec&lt;/code&gt; INPUT. We can also have separate FILTER doing the parsing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name     parser
    Match    dummy.&lt;span class="k"&gt;*&lt;/span&gt;
    Key_Name data
    Parser   dummy_test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a reminder, FILTERs are used to modify data. We will discuss different types of FILTERs in the upcoming paragraphs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modify Records with FILTER
&lt;/h2&gt;

&lt;p&gt;The most basic FILTER operation is the &lt;a href="https://docs.fluentbit.io/manual/pipeline/filters/modify" rel="noopener noreferrer"&gt;Modify&lt;/a&gt;. Modify can be used to do a bunch of changes on a record:&lt;br&gt;
    - add  fields with static values&lt;br&gt;
    - overwrite fields with static values&lt;br&gt;
    - remove fields&lt;br&gt;
    - rename fields&lt;/p&gt;

&lt;p&gt;Aside from static fields, we can refer to environment variables as well.  For example, we can inject the current environment/AWS region in each record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name            modify
    Add environment &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    Add region      &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ENVIRONMENT&lt;/code&gt; and &lt;code&gt;AWS_REGION&lt;/code&gt; are environment variables and they should be specified in the task definition.&lt;/p&gt;

&lt;p&gt;Additionally, the Modify FILTER supports conditional actions. For example, we could apply a renaming only if a certain condition is met, such as the field stars with a particular string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name                                 modify
    Match                                &lt;span class="k"&gt;*&lt;/span&gt;
    Rename    ecs_task_definition_family family
    Condition Key_value_matches ecs_task_definition_family windows.&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above FILTER will rename the &lt;code&gt;ecs_task_definition_family&lt;/code&gt; field to &lt;code&gt;family&lt;/code&gt; if the value of the &lt;code&gt;ecs_task_definition_family&lt;/code&gt; starts with &lt;code&gt;windows.*&lt;/code&gt;. Please note &lt;code&gt;windows.*&lt;/code&gt; is a regular expression. Aside from the &lt;code&gt;Key_value_matches&lt;/code&gt; condition, there are several other conditions we can use. All of them can be found in the &lt;a href="https://docs.fluentbit.io/manual/pipeline/filters/modify#conditions" rel="noopener noreferrer"&gt;Fluent Bit documentation for Modify&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing and Multiple Outputs
&lt;/h2&gt;

&lt;p&gt;One of the most important abilities of the Fluent Bit agent is to offer support for multiple outputs. For example, we could deliver every log message to a centralized logging aggregator which is built upon ElasticSearch, while at the same time, we could direct error messages to an alerting system. To achieve this architecture, we need to introduce the concept of routing records.&lt;/p&gt;

&lt;p&gt;Routing requires the presence of two important other entry properties: &lt;code&gt;Tag&lt;/code&gt; and &lt;code&gt;Match&lt;/code&gt;. When we create an INPUT, we can add an optional &lt;code&gt;Tag&lt;/code&gt; property. Every record originating from this INPUT will carry this tag. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;INPUT]
    Name cpu
    Tag  cpu_usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We collect the CPU usage to generate records. Each record will be tagged with &lt;code&gt;cpu_usage&lt;/code&gt;. Now we can define FILTERs to process only these records with the help of &lt;code&gt;Match&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name          modify
    Match         cpu_usage
    Add   brand   AMD
    Add   mark    Ryzen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each record tagged with &lt;code&gt;cpu_usage&lt;/code&gt; will have a &lt;code&gt;brand&lt;/code&gt; and a &lt;code&gt;mark&lt;/code&gt; field. In case we add another input for collecting the memory usage and we tag these records with &lt;code&gt;mem_usage&lt;/code&gt;, the records originating from the memory INPUT won't receive the &lt;code&gt;brand&lt;/code&gt; and &lt;code&gt;mark&lt;/code&gt; fields. &lt;/p&gt;

&lt;p&gt;Similarly, we can create multiple outputs possessing the &lt;code&gt;Match&lt;/code&gt; property. As an example, we can create an OUTPUT to match everything with &lt;code&gt;cpu_usage&lt;/code&gt; only and build a CloudWatch metric based on this information, while we also could save every event in an S3 bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;OUTPUT]
    Name              cloudwatch_logs
    Match             cpu_usage
    log_stream_name   fluent-bit-cloudwatch
    log_group_name    fluent-bit-cloudwatch
    region            us-west-2
    log_format        json/emf
    metric_namespace  local_cpu_metrics
    metric_dimensions amd_ryzen_7700x
    auto_create_group &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;OUTPUT]
    Name                         s3
    Match                        &lt;span class="k"&gt;*&lt;/span&gt;
    bucket                       fluent-bit-metrics
    region                       us-west-2
    s3_key_format                /&lt;span class="nv"&gt;$TAG&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;2]/&lt;span class="nv"&gt;$TAG&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;0]/%Y/%m/%d/%H/%M/%S/&lt;span class="nv"&gt;$UUID&lt;/span&gt;.gz
    s3_key_format_tag_delimiters .-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, &lt;code&gt;Match&lt;/code&gt; accepts a regular expression. We can have a wildcard (&lt;code&gt;*&lt;/code&gt;) to match everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nest and Lift
&lt;/h2&gt;

&lt;p&gt;When working with Fluent Bit on ECS, generally it is a good idea to configure our services to log in JSON format. Most of the logging libraries support this out of the box. Assuming we are logging everything in JSON format, let's imagine our service generates the following log messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Something happened!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log router from ECS will embed the content of every log message under the &lt;code&gt;"log"&lt;/code&gt; field, the final event having a similar format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_task_arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ecs:region:0123456789012:task/FluentBit-cluster/13EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"container_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/ecs-windows-app-task-1-sample-container-cEXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_cluster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FluentBit-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_container_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sample-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_task_definition_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"container_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"61f5e6EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"log"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Something happened!"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ecs_task_definition_family"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"windows-app-task"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We decided we don't like our log content to be embedded under &lt;code&gt;"log"&lt;/code&gt; property, so we want everything to be on the root level of the record. To do this, we can use the &lt;code&gt;Nest&lt;/code&gt; FILTER. This filter has two operations, the first one being &lt;code&gt;Nest&lt;/code&gt; (again, confusing I know), and the second one is &lt;code&gt;Lift&lt;/code&gt;. In case we want to lift out fields to the root level, we can do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name         nest
    Match        &lt;span class="k"&gt;*&lt;/span&gt;
    Operation    lift
    Wildcard     container_id
    Nested_under log
    Add_prefix   LIFTED_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This FILTER will lift everything situated under the &lt;code&gt;"log"&lt;/code&gt; and put it into the root. The output will be something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;5] dummy.input: &lt;span class="o"&gt;[&lt;/span&gt;1703451474.539715464, &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"stdout"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_task_arn"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ecs:region:0123456789012:task/FluentBit-cluster/13EXAMPLE"&lt;/span&gt;, &lt;span class="s2"&gt;"container_name"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"/ecs-windows-app-task-1-sample-container-cEXAMPLE"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_cluster"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"FluentBit-cluster"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_container_name"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"sample-container"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_task_definition_version"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;, &lt;span class="s2"&gt;"container_id"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"61f5e6EXAMPLE"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_task_definition_family"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"windows-app-task"&lt;/span&gt;, &lt;span class="s2"&gt;"LIFTED_type"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;, &lt;span class="s2"&gt;"LIFTED_message"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"Something happened!"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usually, I recommend adding a prefix to the lifted fields, but this can be omitted.&lt;/p&gt;

&lt;p&gt;Now we are happy with how our record looks, but unfortunately, our colleague does not agree with us. He suggests we keep the &lt;code&gt;"log"&lt;/code&gt; object as it is and move the &lt;code&gt;"container_id"&lt;/code&gt; inside that object. We can accomplish this with &lt;code&gt;Nest&lt;/code&gt; operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name       nest
    Match      &lt;span class="k"&gt;*&lt;/span&gt;
    Operation  nest
    Wildcard   container_id
    Nest_under log
    Add_prefix NESTED_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output after adding this section to the configuration will look similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;3] dummy.input: &lt;span class="o"&gt;[&lt;/span&gt;1703451786.500213512, &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"stdout"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_task_arn"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ecs:region:0123456789012:task/FluentBit-cluster/13EXAMPLE"&lt;/span&gt;, &lt;span class="s2"&gt;"container_name"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"/ecs-windows-app-task-1-sample-container-cEXAMPLE"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_cluster"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"FluentBit-cluster"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_container_name"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"sample-container"&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_task_definition_version"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;, &lt;span class="s2"&gt;"log"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;, &lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"Something happened!"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;, &lt;span class="s2"&gt;"ecs_task_definition_family"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"windows-app-task"&lt;/span&gt;, &lt;span class="s2"&gt;"log"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;{&lt;/span&gt;&lt;span class="s2"&gt;"NESTED_container_id"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"61f5e6EXAMPLE"&lt;/span&gt;&lt;span class="o"&gt;}}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can notice that this output is a little bit funky, since it appears there are two &lt;code&gt;"log"&lt;/code&gt; objects. This is a &lt;a href="https://github.com/fluent/fluent-bit/issues/1177" rel="noopener noreferrer"&gt;"bug"&lt;/a&gt; in the Fluent Bit version used for this blog post. The demos and examples presented in this post are using the latest Fluent Bit docker image maintained by AWS, which at the moment of writing, is based on Fluent Bit &lt;code&gt;1.9.10&lt;/code&gt;. Technically, this issue was &lt;a href="https://github.com/fluent/fluent-bit/commit/1d148860a8825d5f80aef40efd0d6d2812419740" rel="noopener noreferrer"&gt;fixed&lt;/a&gt; in a later version of Fluent Bit. The fix consists of maintaining only the latest key in the table, resulting in an effective overwrite in case the key existed before.&lt;/p&gt;

&lt;p&gt;So, we can enumerate a few caveats for Nest and Lift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As we have seen before, if we would like to nest a field into an already existing field, that is not really possible, even if the receiving field itself is a nested object. Personally, I would have preferred a possibility to merge them, but I'm fully aware that this will come with its own baggage of challenges and edge cases.&lt;/li&gt;
&lt;li&gt;Let's say we have a deeply nested object such as this:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"log"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Something happened!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"stacktrace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case we would like to lift only the &lt;code&gt;"code"&lt;/code&gt; property to the root, we simply can not do this easily. We will have to lift the content of the &lt;code&gt;"log"&lt;/code&gt; first and then the content of &lt;code&gt;"details"&lt;/code&gt;. At this point, we essentially broke the original structure of our JSON, which is probably not what we wanted in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lua Scripting
&lt;/h2&gt;

&lt;p&gt;If we think we need more flexibility for processing records, we can write our own &lt;a href="https://docs.fluentbit.io/manual/pipeline/filters/lua" rel="noopener noreferrer"&gt;embedded filters using Lua&lt;/a&gt; language. &lt;a href="https://www.lua.org/" rel="noopener noreferrer"&gt;Lua&lt;/a&gt; is a highly efficient programming language used mainly for embedded scripting.&lt;/p&gt;

&lt;p&gt;It is relatively easy to integrate a Lua script into a Fluent Bit configuration. First, we have to define a FILTER which will call our script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;FILTER]
    Name    lua
    Match   &lt;span class="k"&gt;*&lt;/span&gt;
    script  script.lua
    call    transform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we have to create a script file (named &lt;code&gt;script.lua&lt;/code&gt; in this case, but we can name it however we want) and write our function (named &lt;code&gt;transform&lt;/code&gt; in this case, but again, we can name this as we wish) which will be invoked for each record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"from_lua"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hello from lua"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few restrictions for this function. The function should accept the following arguments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tag&lt;/code&gt;: tag attached to the record, we discussed tags in detail in the routing section above;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timestamp&lt;/code&gt;: an Unix timestamp attached to each record&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;record&lt;/code&gt;: the record itself. The type of this argument is a Lua &lt;a href="https://www.lua.org/pil/2.5.html" rel="noopener noreferrer"&gt;table&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This function has to return 3 values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;code&lt;/code&gt;: must be one of the following values:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-1&lt;/code&gt;: tells Fluent Bit to drop the current record&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt;: the current record was not modified&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt;: the current record was modified&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt;: the timestamp was modified&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;timestamp&lt;/code&gt;: the Unix timestamp of the record, usually it is returned as it was received in the arguments&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;record&lt;/code&gt;: the record itself, in the form of a Lua table.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;We can do some fairly complex transformations with Lua. My suggestion is to keep it to a minimum. We have to remember that this script will be executed for every record (as long as we did not do a filter before that). Having a heavy and time-consuming transformation will result in our processing lagging, or we will end up dropping records in the worst possible scenario. Moreover, sidecar containers usually use the same resources allocated to the main service. If we attempt to steal a significant amount of resources from the main service, we might disturb its operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The motivation behind this blog post was to share several ideas acquired while working with Fluent Bit sidecar container in production. Some of these might seem boring or obvious to experienced people and that is absolutely fine. Logging should be boring, without any unforeseen surprises. It should just work. &lt;/p&gt;

&lt;p&gt;That being said, I hope some of these tips may be helpful for somebody out there.&lt;/p&gt;

&lt;p&gt;The code for the examples presented in this blog post can be found on GitHub: &lt;a href="https://github.com/Ernyoke/ecs-with-fluentbit" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/ecs-with-fluentbit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fluent Bit Inputs: &lt;a href="https://docs.fluentbit.io/manual/pipeline/inputs" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fluent Bit Outputs: &lt;a href="https://docs.fluentbit.io/manual/pipeline/outputs" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fluent Bit Filters: &lt;a href="https://docs.fluentbit.io/manual/pipeline/filters" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fluent Bit Modify: &lt;a href="https://docs.fluentbit.io/manual/pipeline/filters/modify" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fluent Bit Lua Filter: &lt;a href="https://docs.fluentbit.io/manual/pipeline/filters/lua" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lua programming language: &lt;a href="https://www.lua.org" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lua - Tables: &lt;a href="https://www.lua.org/pil/2.5.html" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>containers</category>
      <category>fluentbit</category>
    </item>
    <item>
      <title>Why I Am Not Able to Remove a Security Group?</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Sun, 10 Sep 2023 22:00:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/why-i-am-not-able-to-remove-a-security-group-467</link>
      <guid>https://forem.com/aws-builders/why-i-am-not-able-to-remove-a-security-group-467</guid>
      <description>&lt;p&gt;If you have a slightly more extended experience with IaC, more specifically with Terraform, you might have run into the following issue:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3yj52vk120039sggu1i.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3yj52vk120039sggu1i.gif" alt="Terraform Remove Security Group Attached to a Lambda" width="720" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This usually happens when we are trying to remove a Lambda Function placed in a VPC. The reason for this is that the removal of the security group is temporarily blocked by one or more network interfaces. &lt;/p&gt;

&lt;p&gt;In the upcoming lines, we will see how can we deal with cases when our security group seemingly cannot be removed. We will discuss what is causing these blockages, and how can we gracefully handle them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a Security Group become unable to be removed?
&lt;/h2&gt;

&lt;p&gt;A security group is a &lt;a href="https://en.wikipedia.org/wiki/Stateful_firewall" rel="noopener noreferrer"&gt;stateful firewall&lt;/a&gt;, the purpose of which is to control what kind of inbound and outbound traffic can be allowed for a resource in a VPC. A security group is always assigned to an ENI (&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html" rel="noopener noreferrer"&gt;Elastic network interface&lt;/a&gt;). This is true, even if the AWS console makes it seem like we assign security groups to all kinds of resources such as EC2 instances, load balancers, Lambda Functions, databases, etc. What is happening in the background is that one or more ENIs will be placed inside our VPC to whom the security group will be assigned. The ENIs will be used by our resource, hence the AWS console will show it like the security group is assigned to the resource itself.&lt;/p&gt;

&lt;p&gt;A security group will be unable to be removed in the following cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is assigned to one or more ENIs: a security group can be assigned to one or more ENIs, moreover an ENI can have &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html#vpc-limits-security-groups" rel="noopener noreferrer"&gt;up to 5 security groups assigned&lt;/a&gt; to it (soft limit, by asking the AWS Support we can increase this limit to 16). If a security group is attached to at least one ENI, we need to either get rid of the ENI or try to de-assign the security group from it in order for the SG to be able to be removed.&lt;/li&gt;
&lt;li&gt;It is referenced by a security group rule: a security group can allow inbound/outbound traffic based on rules. We can use another SG as the source/destination for a rule. If an SG is referenced by a rule from another SG, it can not be removed until the rule is removed/changed.&lt;/li&gt;
&lt;li&gt;The SG is a default SG in a VPC: each VPC automatically gets a security group when it is created. We can get rid of this security group only if we remove the VPC.&lt;/li&gt;
&lt;li&gt;We do not have the privileges to remove the SG: this can happen if the role we are using does not have the necessary permission to do &lt;code&gt;DeleteSecurityGroup&lt;/code&gt; action.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Security Group is Assigned to an ENI
&lt;/h2&gt;

&lt;p&gt;In case we assign a security group to an AWS resource (EC2, Lambda, RDS database, VPC Endpoint, etc.) the security group will always be assigned to a Network Interface (ENI). The AWS console is somewhat misleading because it displays that the security group is assigned to the resource itself, but this is not the case. Since all of this ENI provisioning and SG assignment happens in the background, in most of the cases we are not allowed to temper with the ENI and the security group assignment. Sometimes it can be confusing what is happening under the hood when we do the resource provisioning, so let's see a few examples to understand what is AWS doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2 instances&lt;/strong&gt;: whe we provision an EC2 instance, this will automatically receive a default ENI. This ENI cannot be detached from the instance, hence it cannot be removed. AWS expects us to assign a security group the the instance at the time of creation. If we want to remove this security group, we have to assign another security group to our EC2 instance. We can do this by either going to the ENI console and assigning a security group straight to the ENI, or by going to the EC2 instance and changing the security group in the Security Settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EC2 instances can have also secondary ENIs attached to them. These ENIs are provisioned independently, so we can change the security group assigned to them from the ENI console.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda Functions&lt;/strong&gt;: Lambda Functions require a security group in case we want them to have connectivity to a VPC. If we choose it so, AWS will place an ENI in each subnet we specify, the security group will be assigned to each provisioned ENI. We can change the security group freely if we modify the Lambda Function configuration, but we cannot directly temper with the ENIs. If we decide to remove our functions, the ENIs will also be removed automatically. This removal usually happens with a delay of 10-15 minutes, essentially getting stuck temporarily. We simply have to wait until the removal is finally completed. This can be annoying if we use Terraform IaC for our infrastructure since it will try to remove the security group over and over again (see the GIF from the beginning of the article). If this removal won't happen in time, we can easily end up with an inconsistent Terraform state. What we can do is simply wait and hope that Terraform won't time out. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ECS Fargate Tasks&lt;/strong&gt;: In the case of ECS tasks, each container from the task can have an ENI, depending on the network settings. These ENIs are managed by AWS and we cannot really temper with them. We can change the security groups on the task settings. When the containers are decommissioned if we decide to remove our task, the ENIs will be automatically removed. In most of the cases, this happens instantly, but in very rare instances we can manage to end up with a stuck ENI. If this happens, we could attempt to manually remove the ENI from the AWS console. If this is unsuccessful, we have to write to AWS Support.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;VPC Endpoints&lt;/strong&gt;: VPC Endpoints have multiple benefits for our infrastructure. We can have endpoints for reaching AWS services such as S3, DynamoDB, etc. without the need to have outgoing connectivity to the public internet, or we can have one-to-one connectivity to any instance from a totally different VPC from another AWS account. The restriction is that PrivateLink, the service that powers VPC Endpoints, works at the availability zone level. This means our connecting subnet has to be in the same AZ as the other subnet that is exposing the endpoint. In terms of ENIs and security groups, the idea is the same as with other resources. We get an ENI in each subnet in which we place an endpoint. This ENI is managed by AWS. We can modify the security groups if we go to the VPC Endpoint settings. If we get rid of the VPC Endpoint, the ENI will be removed and the security group will be detached automatically.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can notice a pattern here. If we take any AWS-managed resource that needs access to a VPC, we will end up with a similar networking setup with ENI placement and security group assignment to the ENI. What is important to know are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security groups are assigned to Network Interfaces. In most of the cases, an ENI cannot exist without a security group&lt;/li&gt;
&lt;li&gt;In most cases, ENIs are placed inside our VPC while we provision a resource. At the time of provisioning, we have to assign a security group to the ENI&lt;/li&gt;
&lt;li&gt;Usually, we cannot temper with the ENI, meaning we cannot directly de-associate the security group from it. We can change the security group if we modify the AWS service which uses the ENI&lt;/li&gt;
&lt;li&gt;If we want to remove a security group we have to either:

&lt;ul&gt;
&lt;li&gt;Remove the AWS service which is using the ENI to which our security group is assigned;&lt;/li&gt;
&lt;li&gt;Modify the service that is using the ENI, hence the security group, by assigning another security group to it and removing the one that we would like to remove.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is using my randomly named Security Group?
&lt;/h2&gt;

&lt;p&gt;We finally decided to remove a security group with a random funky name, that we don't remember creating. We suspect it is used by some resources, but we are not really sure which are those. In the AWS console, we navigate to the security group and press the &lt;code&gt;Delete security groups&lt;/code&gt; button. We are greeted with this:&lt;/p&gt;

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

&lt;p&gt;The console tells us, that we cannot remove the security groups because it is used by one or more network interfaces. It also conveniently gives us a link to a list with all of these network interfaces. We click on the link, we get the list of the network interfaces, and after a few moments, we realize we have no idea who is using these network interfaces. &lt;/p&gt;

&lt;p&gt;Before moving on, you may think this scenario is unrealistic, it cannot happen to me, I know every resource I create in my account and I have good naming practices. In an ideal world, our infrastructure would be as clean as possible with well-defined naming conventions. In the real world unfortunately this is not always true. In case you had experience working in AWS accounts where multiple teams deploy their stuff, you may pretty easily end up with resources that do not adhere to any convention you predefined. Moreover, in many cases, the AWS console itself offers the opportunity to create security groups with semi-randomly generated names.&lt;/p&gt;

&lt;p&gt;Coming back to our topic, we have a list with network interfaces, but unfortunately, the console does not help us showing who is using these network interfaces (as long as the ENI is not attached to an EC2 instance). The question is how can we proceed next?&lt;/p&gt;

&lt;p&gt;There are a few tricks that we can use to detect who is using a network interface. In general, we can take a look at the description of the security ENI. This may contain an attachment ID or an ID to a resource. For example, in the case of a Lambda Function, we may have something like this in the description: &lt;code&gt;AWS Lambda VPC ENI-vpc-lambda-f8872d9f-745a-42dd-bca9-3ac0e87ac215&lt;/code&gt;. Here the description tells us the ENI is used by a Lambda Function that is placed in a VPC, the name of the function is &lt;code&gt;ENI-vpc-lambda&lt;/code&gt; and the identifier of the function is this uuid &lt;code&gt;f8872d9f-745a-42dd-bca9-3ac0e87ac215&lt;/code&gt;. Unfortunately, this description format is not something standard and is not documented in the AWS documentation. For other resources, we may get a description using a different format (example: &lt;code&gt;[DO NOT DELETE] ENI managed by SageMaker for Studio Domain(d-vegsk0mcgrdp) - 946a4c21ed31356ee889a8dd95fde7cf&lt;/code&gt; for a security group used by Sagemaker).&lt;/p&gt;

&lt;p&gt;At this point, we may ask ourselves if there is a better solution to find out who is using an ENI. Scouring the internet, I did not find anything to help me out, so I decided to create a tool for myself. I want to introduce &lt;a href="https://github.com/cloud-crafts/sg-ripper" rel="noopener noreferrer"&gt;&lt;code&gt;sg-ripper&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sg-ripper&lt;/code&gt; is a CLI application, developed in Golang, whose purpose is to make our life easier in case we want to do a little bit of cleanup in our list of security groups. It can list all the security groups from an AWS account, it can grab all the ENIs for each security group, and it tries to locate all the other resources that might be relying on those ENIs.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We can list all the security groups, their associated ENIs and the resources that are using those ENIs:&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;We can list all the ENIs directly. In this case, it will show which security group is using each ENI and also which other AWS resources are relying on the ENIs:&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;With &lt;code&gt;sg-ripper&lt;/code&gt; we can also apply a filter to see only certain security groups or ENI in case we don't want to grab all the existing ones from our account. Aside from showing which resource is using an ENI, it can display if security groups are available for removal. If it is not, it will also show some explanation as to why the is the removal blocked.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sg-ripper&lt;/code&gt; is a work-in-progress project, the source code itself is open-source and it can be found on GitHub: &lt;a href="https://github.com/cloud-crafts/sg-ripper" rel="noopener noreferrer"&gt;https://github.com/cloud-crafts/sg-ripper&lt;/a&gt;. Contributions are welcomed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;As our infrastructure is evolving, we may tend to leave unused resources behind such as security groups. Security groups can be removed only if they are not used, not referenced or they are not default in a VPC. &lt;code&gt;sg-ripper&lt;/code&gt; can make our life easier in detecting unused security groups, having an explanation of why a certain security group cannot be removed and point out which ENI/which resource is blocking us from removing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  References:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Stateful_firewall" rel="noopener noreferrer"&gt;Stateful Firewall&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html" rel="noopener noreferrer"&gt;Elastic network interfaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html#vpc-limits-security-groups" rel="noopener noreferrer"&gt;Security groups Limits&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>ecs</category>
      <category>go</category>
    </item>
    <item>
      <title>Disallow GPT Bot from Scraping our Blog Posts</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Tue, 08 Aug 2023 19:49:31 +0000</pubDate>
      <link>https://forem.com/ervin_szilagyi/disallow-gpt-bot-from-scraping-our-blog-posts-38pc</link>
      <guid>https://forem.com/ervin_szilagyi/disallow-gpt-bot-from-scraping-our-blog-posts-38pc</guid>
      <description>&lt;p&gt;Lately, we can bloc GPT bots from scraping our pages for a site that we control, by setting the following lines &lt;a href="https://platform.openai.com/docs/gptbot" rel="noopener noreferrer"&gt;in the &lt;code&gt;robots.txt&lt;/code&gt; file&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: GPTBot
Disallow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I, myself, found out this from a &lt;a href="https://twitter.com/GergelyOrosz/status/1688829094249615360?s=20" rel="noopener noreferrer"&gt;tweet&lt;/a&gt; from Gergely Orosz:&lt;/p&gt;

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

&lt;p&gt;My stance on this is similar to what Gergely is saying. GPT offers no citation to the information it provides. While I did update the &lt;code&gt;robots.txt&lt;/code&gt; file on my personal website, I am also cross-posting to DEV. If we look at the &lt;a href="https://dev.to/robots.txt"&gt;&lt;code&gt;robots.txt&lt;/code&gt;&lt;/a&gt; from DEV.to, we can notice that it does not have the same rule for GPTBot.&lt;/p&gt;

&lt;p&gt;There are thousands of people posting to DEV, many of whom have different views about scraping information for LLM training. I'm in no position to request changes that will affect how the site works. I'm just curious of what is the opinion of other fellow authors about scraping your article for ML training.&lt;/p&gt;

&lt;h1&gt;
  
  
  Does updating your &lt;code&gt;robots.txt&lt;/code&gt; actually solve something?
&lt;/h1&gt;

&lt;p&gt;Obviously not. Adding this statement to your site is just a hint, a request for a bot to please not scrape your data.&lt;br&gt;
This won't stop huge LLM bots (including GPT) to get the information it wants.&lt;/p&gt;

&lt;p&gt;So, I'm curious about what do other people think about this topic? &lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>openai</category>
      <category>gpt3</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Expose our REST API on AWS with a Custom Domain</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Tue, 23 May 2023 21:58:22 +0000</pubDate>
      <link>https://forem.com/aws-builders/expose-our-rest-api-on-aws-with-a-custom-domain-1326</link>
      <guid>https://forem.com/aws-builders/expose-our-rest-api-on-aws-with-a-custom-domain-1326</guid>
      <description>&lt;p&gt;DNS is hard.&lt;/p&gt;

&lt;p&gt;This is absolutely true for huge enterprise networks and distributed systems. With a simple Google search, we can find many IT incidents caused by DNS issues.&lt;/p&gt;

&lt;p&gt;But this is not the topic of this current article. Most of us are not in a position to deal with the DNS for enterprise systems. What we most likely encounter once in a while is "a simple" DNS setup for a REST API. Even in this case, DNS can be confusing for the uninitiated. The purpose of this article is to clear up certain misconceptions and to guide the reader through the steps of exposing a REST API publicly on AWS with a custom domain name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get our own domain
&lt;/h2&gt;

&lt;p&gt;If we are thinking about making our API public to the world, we would want to purchase a custom domain. AWS provides a domain registrar where we can buy domains, but there are better third-party options out in the wild. Purchasing a domain can be an adventure in itself, with each registrar offering different prices depending on the length/wording/choice of the top-level domain. Myself, I own the domain of &lt;a href="https://ervinszilagyi.dev/" rel="noopener noreferrer"&gt;&lt;em&gt;ervinszilagyi.dev&lt;/em&gt;&lt;/a&gt;, and I also have registered &lt;em&gt;ervinszilagyi.xyz&lt;/em&gt; for this tutorial (and for my other projects as well). I'm using GoDaddy domain registrar for my domains, and I will be referring to them in this article. I have no affiliation with them, I just happened to use them for my domain purchases. To follow the steps of this tutorial, our domain registrar of choice should allow changing the nameservers, or if you want to delegate only a subdomain, then it should allow us to register NS records. If these concepts are not clear at this point, we should not worry too much, we will have explanations below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosted Zones and Records
&lt;/h2&gt;

&lt;p&gt;In AWS, everything related to DNS is handled by Route 53 (the name it's a pun, DNS resolution is using port 53).&lt;/p&gt;

&lt;p&gt;Route 53 works with &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html" rel="noopener noreferrer"&gt;Hosted Zones&lt;/a&gt;, which are "databases" or containers for storing and managing our records. We can have 2 types of Hosted Zones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Private Hosted Zones&lt;/em&gt;: they are used for domains inside Amazon VPC (Virtual Private Cloud). They are not resolvable from the public internet. We won't use them in this tutorial, but it is important to be aware of them.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Public Hosted Zones&lt;/em&gt;: they are used for storing records for publicly routable domains. We can access them from the public internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hosted Zones are used to manage &lt;a href="https://en.wikipedia.org/wiki/Domain_Name_System#Resource_records" rel="noopener noreferrer"&gt;records&lt;/a&gt;. Records are used to store information about our domain, for example: if our domain is mapped to an IP address pointing to our backend, we can specify a record with this IP address. There are many different &lt;a href="https://en.wikipedia.org/wiki/List_of_DNS_record_types" rel="noopener noreferrer"&gt;types of records&lt;/a&gt;, for our purposes it is enough if we know about a few of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;A record&lt;/em&gt; (Address record): used to store IPv4 addresses&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AAAA record&lt;/em&gt; : used to store IPv6 addresses&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;CNAME record&lt;/em&gt;: it points to another domain, in cases where we don't have or we simply can not use an IP address for our backend&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;TXT record&lt;/em&gt; (Text record): used to store additional text information. This information can be for other humans or other systems (metadata information)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;SOA record&lt;/em&gt;: Specifies authoritative information about a DNS zone. Each Hosted Zone has one by default. It cannot be removed or modified.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;NS record&lt;/em&gt; (Name server record): identifies the name servers for the hosted zone. Each Hosted Zone automatically gets assigned an NS record with 4 nameservers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hosted Zones can have &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html" rel="noopener noreferrer"&gt;alias records&lt;/a&gt;. Alias records are Amazon Route 53 specific records. The reason for their existence is to overcome a limitation of how DNS Zones work. For a DNS Zone, if we want to register a record pointing to the node of the DNS space (Apex domain), this record has to be either A or AAAA record (remember, both of them point to IP addresses). The problem with this is that in AWS we don't receive static IP addresses for a lot of services (such as S3, CloudFront, API Gateway, etc.). To work around this limitation, AWS introduced Alias records. We will use an Alias record for our REST API below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure a custom domain resolution with a Hosted Zone
&lt;/h2&gt;

&lt;p&gt;Let's say we decided on our domain (which in my case is &lt;em&gt;ervinszilagyi.xyz&lt;/em&gt;) and now we would want to initiate its setup. &lt;/p&gt;

&lt;p&gt;First, we need to create a &lt;em&gt;public&lt;/em&gt; Hosted Zone in Route 53. We should make sure, the name of the Hosted Zone is the same as we have it for our domain.&lt;/p&gt;

&lt;p&gt;In AWS Console we should go to Route 53 -&amp;gt; Hosted Zones -&amp;gt; and press the Create Hosted Zone button. We should be directed to the following form, where we have to fill in our domain name:&lt;/p&gt;

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

&lt;p&gt;We should make sure we select that we want to create a &lt;em&gt;public&lt;/em&gt; Hosted Zone and we should press "Create Hosted Zone". After a few moments, our hosted zone should be up:&lt;/p&gt;

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

&lt;p&gt;We can notice that we have an &lt;code&gt;SOA&lt;/code&gt; record and an &lt;code&gt;NS&lt;/code&gt; record with 4 nameservers. What we have to do next, is to go to our domain registrar (in my case GoDaddy) and change the nameservers for the domain we purchased:&lt;/p&gt;

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

&lt;p&gt;We should copy the nameservers from the Hosted Zone and add them to the GoDaddy settings:&lt;/p&gt;

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

&lt;p&gt;GoDaddy will warn us that changing the nameservers can be dangerous. We should not worry about these alert messages, our nameservers are managed by AWS, we should be fine.&lt;/p&gt;

&lt;p&gt;This change of the nameservers can take up to 48 hours to take effect. From my experience, most of the time, the changes do take effect after a few minutes, but we could never know, so we have to wait until our domain is usable. To check if the changes did take effect, we can use the Unix &lt;code&gt;dig&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dig +short NS ervinszilagyi.xyz
ns-1976.awsdns-55.co.uk.
ns-832.awsdns-40.net.
ns-47.awsdns-05.com.
ns-1142.awsdns-14.org.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query should return back the 4 nameservers, the ones we just configured. &lt;/p&gt;

&lt;p&gt;At this point, we can move on to the next step where we request a TLS certificate for our domain (jump over to the next section, if you don't care about subdomain delegation).&lt;/p&gt;

&lt;h2&gt;
  
  
  Delegate a custom subdomain resolution to a Hosted Zone
&lt;/h2&gt;

&lt;p&gt;I own &lt;a href="https://ervinszilagyi.dev/" rel="noopener noreferrer"&gt;&lt;em&gt;ervinszilagyi.dev&lt;/em&gt;&lt;/a&gt;. This domain resolves to my personal website and blog. Obviously, I don't want to change this behavior, I would like other people to read my blog posts. I would still want to use this domain for my tutorials.&lt;/p&gt;

&lt;p&gt;One way to make use of this domain is to create a subdomain under it and delegate the nameservers resolution for this subdomain to an AWS Hosted Zone. Let's say I would like to register my rest API using &lt;code&gt;rest.ervinszilagyi.dev&lt;/code&gt; domain name.&lt;/p&gt;

&lt;p&gt;As before, in the AWS console we should go to Route53 service and create a &lt;em&gt;public&lt;/em&gt; Hosted Zone for &lt;code&gt;rest.ervinszilagyi.dev&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;When the Hosted Zone is created, we get the NS record with 4 namespaces:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1czhcq3bzzts7vi77szr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1czhcq3bzzts7vi77szr.png" alt="Hosted Zone Created for the Sub-Domain with Records" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have to grab the values from the NS record and navigate to GoDaddy. Selecting our purchased domain we have to create 4 NS records, one for each value:&lt;/p&gt;

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

&lt;p&gt;After we save the records, we should wait a little for the changes to take effect. We can use the &lt;code&gt;dig +short NS tutorial.ervinszilagyi.dev&lt;/code&gt; command to check if our changes are in place. &lt;/p&gt;

&lt;p&gt;We have seen how to set up nameservers for both domains and sub-domains. Moving on with this tutorial, we will use the Hosted Zone created for &lt;code&gt;ervinszilagyi.xyz&lt;/code&gt;. Everything we do with this Hosted Zone will apply to the Hosted Zone created for the sub-domain as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request a TLS certificate for our domain
&lt;/h2&gt;

&lt;p&gt;TLS certificates are used for secure connectivity between our machine and a remote server. Our plan for this tutorial is to expose a REST API, for which we would use API Gateway. API Gateway enforces the usage of a valid certificate for the base path mapping (we will see what base path mapping is in detail below).&lt;/p&gt;

&lt;p&gt;It is very easy to request a certificate with the usage of AWS Certificate Manager. We just have to go into the AWS certificate manager portal from our AWS console and press the request button. We will be redirected to this page:&lt;/p&gt;

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

&lt;p&gt;We need a public certificate, so we have to choose this option. Moving on, we will reach this page:&lt;/p&gt;

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

&lt;p&gt;We have to introduce our domain and we should select &lt;code&gt;DNS validation&lt;/code&gt; option. We have to be able to prove somehow that the domain name for which the certificate is issued is ours. With &lt;code&gt;DNS validation&lt;/code&gt;, AWS will create a record in our Hosted Zone with this proof. For the validation record to be created we may also have to press the &lt;code&gt;Create Record&lt;/code&gt; button after the certificate was issued:&lt;/p&gt;

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

&lt;p&gt;If we go to our Hosted Zone, we should see the newly created record:&lt;/p&gt;

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

&lt;p&gt;We should make sure we have this record and our certificate validation status is green:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Create a REST API
&lt;/h2&gt;

&lt;p&gt;The next step we would want to accomplish is to create the REST API itself. There are several ways of creating and exposing an API in AWS. In most of the cases what we would want to do is to build a REST API using &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;Amazon API Gateway&lt;/a&gt;. Amazon API Gateway is managed service built for managing APIs. It is a front-facing service standing between the user and our backend. It can handle authentication and authorization, TLS encryption, rate-limiting and quota enforcement, and many other things.&lt;/p&gt;

&lt;p&gt;To create a REST API Gateway, from the console we should go to the API Gateway Service and select REST API:&lt;/p&gt;

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

&lt;p&gt;It is important to select the option with the &lt;em&gt;public&lt;/em&gt; REST API. A private API Gateway is accessible only internally from a VPC. Since we want our API to be reachable from the internet, we need a public API Gateway.&lt;/p&gt;

&lt;p&gt;By pressing &lt;em&gt;Build&lt;/em&gt; we are redirected to a page where we should select the protocol for our API Gateway (we want REST, not WebSocket) and we have to give a name to our API Gateway.&lt;/p&gt;

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

&lt;p&gt;After we press &lt;em&gt;Create API&lt;/em&gt;, we should have our API Gateway up and running. We still need to add a method to handle incoming requests.&lt;/p&gt;

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

&lt;p&gt;A method is essentially an HTTP REST verb (GET, POST, PUT, etc.) that does exactly what we would expect, it handles REST API GET/POST/PUT/etc. requests. We can notice that methods can be nested inside resources creating more complex and lengthy request paths. For now, we will keep our path simple and we will place our method in the root of our API Gateway. If we press the tick symbol (✓), we are redirected to a page where we have to set the integration for our method. This is essentially a backend. We will build a &lt;em&gt;Mock&lt;/em&gt; backend for this tutorial, which means that our API Gateway will respond with a static response each time. This is enough for our tutorial, but we can imagine that instead of a mock we could have a Lambda function or a microservice here in a production environment.&lt;/p&gt;

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

&lt;p&gt;Sadly, at this point, our mock will respond with no content. To have a response body, we need to set up an integration response. To do this we have to select &lt;em&gt;Integration Response&lt;/em&gt;:&lt;/p&gt;

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

&lt;p&gt;From there we press the drop-down for the 200 response:&lt;/p&gt;

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

&lt;p&gt;Then we add a Mapping Template with the content type of &lt;code&gt;application/json&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;If we press the tickmark, we should be able to input a response body for the template. We should paste in the following JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Works!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;We should press &lt;em&gt;Save&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Now we also have to &lt;strong&gt;Deploy&lt;/strong&gt; our API Gateway. If we go to the &lt;em&gt;Actions&lt;/em&gt; button, we will get a dropdown menu, from where we should select &lt;em&gt;Deploy API&lt;/em&gt;:&lt;/p&gt;

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

&lt;p&gt;We are asked to create a new Stage. We can name it however we want, so we will simply choose &lt;code&gt;dev&lt;/code&gt;. &lt;/p&gt;

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

&lt;p&gt;We press &lt;em&gt;Deploy&lt;/em&gt; and our REST API should be live. We also get a generated URL, where we can test our GET method with a &lt;code&gt;curl&lt;/code&gt; request.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://ppwm7oataf.execute-api.us-east-1.amazonaws.com/dev
&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"statusCode"&lt;/span&gt;: 200,
    &lt;span class="s2"&gt;"message"&lt;/span&gt;: &lt;span class="s2"&gt;"Works!"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should receive the body we configured above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure basepath mapping for the API
&lt;/h2&gt;

&lt;p&gt;At this point our REST API is live, but it only responds to the URL generated by AWS. Next, what we would want to configure the API to be used with our custom domain (&lt;code&gt;ervinszilagyi.xyz&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In the API Gateway console, on the top-left, we should select &lt;em&gt;Create domain names&lt;/em&gt;. This will take us to another page, where we most likely should not have any domain configured yet in the list. We should press the &lt;em&gt;Create&lt;/em&gt; button&lt;/p&gt;

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

&lt;p&gt;We are taken to another page, again. Here we have to make sure we introduce carefully the following information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain name: this is the domain we own, it should be the same as whatever we introduced for our Hosted Zone above.&lt;/li&gt;
&lt;li&gt;We have a Regional API Gateway, we should leave that option as it is&lt;/li&gt;
&lt;li&gt;For the certificate, we should select the one for our domain. We created a certificate before (&lt;em&gt;Request a TLS certificate for our domain&lt;/em&gt; step). We should be able to see this certificate in the list.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;After pressing create, shortly we are taken to another page. What we have to do now is to set up base path mapping. We tested our API before with the generated URL (&lt;code&gt;https://ppwm7oataf.execute-api.us-east-1.amazonaws.com/dev&lt;/code&gt; in my case). We want to configure our API to use our custom domain (&lt;code&gt;ervinszilagyi.xzy&lt;/code&gt; in my case). To accomplish this, we should select the second tab (&lt;em&gt;API mappings&lt;/em&gt;) and press the &lt;em&gt;Configure API mappings&lt;/em&gt; button:&lt;/p&gt;

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

&lt;p&gt;We should select our API and the stage (&lt;code&gt;dev&lt;/code&gt; in my case). For the &lt;em&gt;Path&lt;/em&gt; we should leave it blank.&lt;/p&gt;

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

&lt;p&gt;This is it. We have the mapping set up. This is good, but at this point, we are still not finished yet. We need one more step to be able to have our REST API exposed with our custom domain. We need to create a record for our API in our Hosted Zone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an Alias record for our API
&lt;/h2&gt;

&lt;p&gt;We should navigate back to Route 53 and select our Hosted Zone. We want to create a record, so we should press the big orange &lt;em&gt;Create record&lt;/em&gt; button.&lt;/p&gt;

&lt;p&gt;We want to create an &lt;code&gt;A&lt;/code&gt; record that is an &lt;em&gt;Alias&lt;/em&gt;. We explained in the beginning what Alias records are, what we have to know now is that we can use an Alias record if we don't have an IP address for the &lt;code&gt;A&lt;/code&gt; record. It is not recommended at all to rely on API addresses for AWS API Gateways, so we should enable the Alias tickbox. &lt;/p&gt;

&lt;p&gt;For the &lt;em&gt;Route traffic&lt;/em&gt; section, we need to select &lt;code&gt;Alias to API Gateway&lt;/code&gt; and we will have to find our API Gateway in the region in which we are working.&lt;/p&gt;

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

&lt;p&gt;We leave the routing policy at the default simple routing option.&lt;/p&gt;

&lt;p&gt;After pressing create, we should see our &lt;code&gt;A&lt;/code&gt; record inside our Hosted Zone:&lt;/p&gt;

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

&lt;p&gt;To check if our domain works, we can use the &lt;code&gt;dig&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dig ervinszilagyi.xyz

&lt;span class="p"&gt;;&lt;/span&gt; &amp;lt;&amp;lt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; DiG 9.16.1-Ubuntu &amp;lt;&amp;lt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ervinszilagyi.xyz
&lt;span class="p"&gt;;;&lt;/span&gt; global options: +cmd
&lt;span class="p"&gt;;;&lt;/span&gt; Got answer:
&lt;span class="p"&gt;;;&lt;/span&gt; -&amp;gt;&amp;gt;HEADER&lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt; &lt;span class="no"&gt;opcode&lt;/span&gt;&lt;span class="sh"&gt;: QUERY, status: NOERROR, id: 52495
;; flags: qr rd ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;ervinszilagyi.xyz.             IN      A

;; ANSWER SECTION:
ervinszilagyi.xyz.      0       IN      A       34.202.63.112
ervinszilagyi.xyz.      0       IN      A       54.209.119.225
ervinszilagyi.xyz.      0       IN      A       3.86.19.75

;; Query time: 50 msec
;; SERVER: 172.24.80.1#53(172.24.80.1)
;; WHEN: Sun May 21 18:56:53 EEST 2023
;; MSG SIZE  rcvd: 100
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should be able to see three &lt;code&gt;A&lt;/code&gt; records with IP addresses. These IP addresses are the public IP addresses for the API Gateway service and they are managed by AWS. They might be different for you, what is important is that we should be able to get some &lt;code&gt;A&lt;/code&gt; records back when doing DNS resolution.&lt;/p&gt;

&lt;p&gt;We should also do a &lt;code&gt;curl&lt;/code&gt; request to see if we get a response from our REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://ervinszilagyi.xyz
&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"statusCode"&lt;/span&gt;: 200,
    &lt;span class="s2"&gt;"message"&lt;/span&gt;: &lt;span class="s2"&gt;"Works!"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can notice that we get the same answer as above. This is great! We have our REST API exposed publicly using our custom domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating all these steps
&lt;/h2&gt;

&lt;p&gt;We can agree on the fact that there are many steps to be taken to have a custom domain set up for a REST API. Fortunately, we can have our infrastructure configured quickly if we write some Terraform code with all these changes. &lt;/p&gt;

&lt;p&gt;This is exactly what I've already done, making the code available for anybody on Github: &lt;a href="https://github.com/Ernyoke/aws-custom-domain-r53" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/aws-custom-domain-r53&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This repository contains 3 Terraform projects (let's call them stacks):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tree
&lt;span class="nb"&gt;.&lt;/span&gt;
├── README.md
├── api-gw
│   ├── api-gw.tf
│   ├── main.tf
│   ├── output.tf
│   ├── route53.tf
│   ├── terraform.tf
│   └── variables.tf
├── certificate
│   ├── main.tf
│   ├── output.tf
│   ├── terraform.tf
│   └── variables.tf
└── route53
    ├── main.tf
    ├── output.tf
    ├── terraform.tf
    └── variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason for this project structure is that setting up the nameservers on GoDaddy (or any other registrar) takes time and manual steps. So, if we would like to deploy the Terraform code from my project, the first thing we should do is go inside the &lt;code&gt;route53&lt;/code&gt; folder and run the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the plan is rolled out successfully, we should see the nameservers in the output. We should copy these nameservers and do the change for our domain in the registrar. We have to make sure our changes are propagated before moving on to the next step.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;certificate&lt;/code&gt; stack creates the TLS certificate and the validation for it. For the validation to succeed, our domain should point to the correct nameservers. The commands for the terraform rollout are the same as we've seen with the &lt;code&gt;route53&lt;/code&gt; stack. &lt;/p&gt;

&lt;p&gt;Last, we should roll out our REST API. After that is successful, we should be able to test it with a &lt;code&gt;curl&lt;/code&gt; request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Setting up a custom domain for a REST API in AWS is not the most complicated procedure in the world. Certainly, it is not something, we may not do on a daily basis, so I think it is a good idea to have it documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Working with hosted zones: &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DNS Records: &lt;a href="https://en.wikipedia.org/wiki/Domain_Name_System#Resource_records" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Domain_Name_System#Resource_records&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Choosing between alias and non-alias records: &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon API Gateway - &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;https://aws.amazon.com/api-gateway/&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>route53</category>
      <category>dns</category>
      <category>apigateway</category>
    </item>
    <item>
      <title>An Introduction to AWS Batch</title>
      <dc:creator>Ervin Szilagyi</dc:creator>
      <pubDate>Sun, 19 Mar 2023 11:38:37 +0000</pubDate>
      <link>https://forem.com/aws-builders/an-introduction-to-aws-batch-2pei</link>
      <guid>https://forem.com/aws-builders/an-introduction-to-aws-batch-2pei</guid>
      <description>&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/what-is-batch.html" rel="noopener noreferrer"&gt;AWS Batch&lt;/a&gt; is a fully managed service that helps us developers run batch computing workloads on the cloud. The goal of this service is to effectively provision infrastructure for batch jobs submitted by us while we can focus on writing the code for dealing with business constraints.&lt;/p&gt;

&lt;p&gt;Batch jobs running on AWS are essentially Docker containers that can be executed on different environments. AWS Batch supports job queues deployed on EC2 instances, on ECS clusters with Fargate, and on Amazon EKS (Elastic Kubernetes Service). Regardless of what we choose for the basis of our infrastructure, the provisioning of the necessary services and orchestration of the jobs is managed by AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components of AWS Batch
&lt;/h2&gt;

&lt;p&gt;Although one of the selling points of AWS Batch is to simplify batch computing on the cloud, it has a bunch of components each requiring its own configuration. The components required for a job executing on AWS Batch service are the following:&lt;/p&gt;

&lt;h3&gt;
  
  
  Jobs
&lt;/h3&gt;

&lt;p&gt;Jobs are Docker containers wrapping units of work which we submit to an AWS Batch queue. Jobs can have names and they can receive parameters from their job definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Job Definitions
&lt;/h3&gt;

&lt;p&gt;A job definition specifies how a job should run. Jobs definitions can have the followings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an IAM role to provide access to other AWS services;&lt;/li&gt;
&lt;li&gt;information about the memory and CPU requirements of the job;&lt;/li&gt;
&lt;li&gt;other properties required for the job such as environment variables, container properties, and mount points for extra storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Job Queues
&lt;/h3&gt;

&lt;p&gt;Jobs are submitted to job queues. The role of a job queue is to schedule jobs and execute them on compute environments. Jobs can have a priority based on which they can be scheduled to run on multiple different compute environments. The job queue itself can decide which job to be executed first on which compute environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute Environments
&lt;/h3&gt;

&lt;p&gt;Compute environments are essentially ECS clusters. They contain the Amazon ECS container instances used for the containerized batch jobs. We can have managed or unmanaged compute environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managed compute environments&lt;/strong&gt;: AWS batch decides the capacity and the EC2 instance type required for the job (in case we decide to run our jobs on EC2). Alternatively, we can use Fargate environment, which will run our containerized batch job on instances entirely hidden from us and fully managed by AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unmanaged compute environments&lt;/strong&gt;: we manage our own compute resources. It requires that our compute environments use an AMI that meets the AWS ECS required AMI specifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multi-node jobs and GPU jobs
&lt;/h2&gt;

&lt;p&gt;AWS Batch supports multi-node parallel jobs that span on multiple EC2 instances. They can be used for parallel data processing, high-performance computing applications, and for training machine learning models. Multi-node jobs can run only on managed compute environments.&lt;/p&gt;

&lt;p&gt;In addition to multi-node jobs, we can enhance the underlying EC2 instances with graphics cards (GPUs). This can be useful for operations relying on parallel processing, such as deep learning.&lt;/p&gt;

&lt;p&gt;AWS Batch also supports applications that use EFA. An &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/efa.html" rel="noopener noreferrer"&gt;Elastic Fabric Adapter (EFA)&lt;/a&gt; is a network device used to accelerate High Performance Computing (HPC) applications using Message Passing Interface (MPI). Moreover, if we would like even better performance for parallel computing, we can have direct GPU-to-GPU communication via &lt;a href="https://aws.amazon.com/blogs/compute/optimizing-deep-learning-on-p3-and-p3dn-with-efa-part-1/" rel="noopener noreferrer"&gt;NVIDIA Collective Communication Library (NCCL)&lt;/a&gt;, which is also built on EFA.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Batch - When to use it?
&lt;/h2&gt;

&lt;p&gt;AWS Batch is recommended for any task which requires a lot of time/memory/computing power to run. This can be a vague statement, so let's see some examples of use cases for AWS Batch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-performance computing: tasks that require a lot of computing power such as running usage analytics tasks on a huge amount of data, automatic content rendering, transcoding, etc.&lt;/li&gt;
&lt;li&gt;Machine Learning: as we've seen before AWS Batch supports multi-node jobs and GPU-powered jobs, which can be essential for training ML models&lt;/li&gt;
&lt;li&gt;ETL: we can use AWS Batch for ETL (extract, transform, and load) tasks&lt;/li&gt;
&lt;li&gt;For any other task which may take up a lot of time (hours/days)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While these use cases may sound cool, I suggest having caution before deciding if AWS Batch is the right choice for us. AWS offers a bunch of other products configured for specialized use cases. Let's walk through a few of these:&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Batch vs AWS Glue/Amazon EMR
&lt;/h3&gt;

&lt;p&gt;A while above it was mentioned that AWS Batch can be used for ETL jobs. While this is true, we may want to step back and take a look at another service, such as AWS Glue. AWS Glue is a fully managed solution developed specifically for ETL jobs. It is a serverless option offering a bunch of choices for data preparation, data integration, and ingestion into several other services. It relies on Apache Spark.&lt;/p&gt;

&lt;p&gt;Similarly, Amazon EMR is also an ETL solution for petabyte-scale data processing relying on open-source frameworks, such as Apache Spark, Apache Hive, and Presto.&lt;/p&gt;

&lt;p&gt;My recommendation would be to use Glue/EMR if we are comfortable with the technologies they rely on. If we want to have something custom, built by ourselves, we can stick to AWS Batch.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Batch vs SageMaker
&lt;/h3&gt;

&lt;p&gt;We've also seen that AWS Batch can be used for machine learning. Again, while this is true, it is a crude way of doing machine learning. AWS offers SageMaker for Machine Learning a data science. SageMaker can run its own jobs that can be enhanced by GPU computing power.&lt;/p&gt;

&lt;p&gt;While SageMaker is a one-stop shop for everything related to machine learning, AWS Batch is an offering for executing long-running tasks. If we have a machine learning model implemented but we just need the computing power to do the training, we can use AWS Batch, other than this probably SageMaker would make way more sense for everything ML-related.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Batch vs AWS Lambda
&lt;/h3&gt;

&lt;p&gt;AWS Lambda can also be an alternative for AWS Batch jobs. For certain generic tasks, a simple Lambda function can be more appropriate than having a fully-fledged batch job. We can consider using a Lambda Function when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the task is not that compute-intensive: AWS Lambda can have up to 6 vCPU cores and up to 10GB of RAM;&lt;/li&gt;
&lt;li&gt;we know that our task would be able to be finished in 15 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we can adhere to these Lambda limitations, I strongly suggest using Lambda instead of AWS Batch. Lambda is considerably easier to set up and it has way fewer moving parts. We can simply focus on the implementation details rather than dealing with the infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an AWS Batch Job
&lt;/h2&gt;

&lt;p&gt;In the upcoming sections, we will put all things together and we will build an AWS Batch job from the scratch. For the sake of this exercise, let's assume we have a movie renting website and we would want to present movie information with ratings from critics to our customers. The purpose of a batch job will be to import a set of movie ratings at certain intervals into a DynamoDB table.&lt;/p&gt;

&lt;p&gt;For the movies dataset, we will use one from &lt;a href="https://www.kaggle.com/datasets/db55ac3dfd0098a0cf96dd542807f9253a16587ff233e06baef372bccfd09942" rel="noopener noreferrer"&gt;Kaggle&lt;/a&gt;, which I had to download first and upload it to an S3 bucket (Kaggle limitation). Usually, if we are running a similar service in production, we will pay for a certain provider which will expose a dataset for us. Since Kaggle does not offer an easy way for automatic downloads, I had to save the dataset first into an S3 bucket.&lt;/p&gt;

&lt;p&gt;Also, one may question the usage of a batch job considering the fact the data size might not be that big. A Lambda function may be sufficient to accomplish the same goal. While this is true, for the sake of this exercise we will stick to batch.&lt;/p&gt;

&lt;p&gt;A simplified architectural diagram of what we would want to accomplish can be seen here:&lt;/p&gt;

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

&lt;p&gt;Creating a batch job requires the provisioning of several of its components. To make this exercise redoable, we will use Terraform for the infrastructure. The upcoming steps can be accomplished from AWS console as well or with the usage of other IaC tools such as CDK. Terraform is mainly a preference of mine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute Environment
&lt;/h3&gt;

&lt;p&gt;The first component of a batch job we will create will be the compute environment. Our batch job will be a managed job running on AWS Fargate. We can write the IaC code for the compute environment as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_batch_compute_environment"&lt;/span&gt; &lt;span class="s2"&gt;"compute_environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;compute_environment_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;

  &lt;span class="nx"&gt;compute_resources&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;max_vcpus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

    &lt;span class="nx"&gt;security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;subnets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;service_role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MANAGED"&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_role_policy_attachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_role_attachment&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can notice in the resource definition that it requires a few other resources to be present. First, the compute environment needs a service role. According to the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/batch_compute_environment#service_role" rel="noopener noreferrer"&gt;Terraform documentation&lt;/a&gt; the service role "allows AWS Batch to make calls to other AWS services on your behalf". With all respect to the people who wrote the documentation, for me personally, this statement does not offer a lot of information. In all fairness, Terraform documentation offers an example of this service role, which we will use in our project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"assume_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"batch.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"service_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-service-role"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assume_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"service_role_attachment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essentially what we are doing here is creating a role with an IAM policy offered by AWS, the name of the policy being &lt;code&gt;AWSBatchServiceRole&lt;/code&gt;. Moreover, we create a trust policy to allow AWS Batch to assume this role. &lt;/p&gt;

&lt;p&gt;Another important thing required by our compute environment is a list of security groups and subnets. I tie them together because they are part of the AWS networking infrastructure needed for the project. A security group is a &lt;a href="https://en.wikipedia.org/wiki/Stateful_firewall" rel="noopener noreferrer"&gt;stateful firewall&lt;/a&gt;, while a subnet is part of a virtual private network. Networking in AWS is a complex topic, and it falls outside the scope of this article. Since AWS Batch requires the presence of a minimal networking setup, this is what we can use for our purposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.1.0.0/16"&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-vpc"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public_subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.1.1.0/24"&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-public-subnet"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.1.2.0/24"&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-private-subnet"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"igw"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-igw"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eip"&lt;/span&gt; &lt;span class="s2"&gt;"eip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_nat_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"nat"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;allocation_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-nat"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;igw&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public_rt"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;igw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-public-rt"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"private_rt"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-private-rt"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public_rt_association"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"private_rt_association"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, this may seem like a lot of code. What is happening here is that we create an entirely new VPC with 2 subnets (a private and a public one). We put our cluster behind a NAT to be able to make calls outside to the internet. This is required for our batch job to work properly since it has to communicate with the AWS Batch API. Last but not least, for the security group, we can use this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"sg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Movies batch demo SG."&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is probably the simplest security group. It allows outbound traffic, denying inbound traffic. Remember, security groups are stateful, so this should be perfect for our use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job Queue
&lt;/h2&gt;

&lt;p&gt;Now that we have the compute environment, we can create a job queue that will use this environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_batch_job_queue" "job_queue" {
  name     = "${var.module_name}-job-queue"
  state    = "ENABLED"
  priority = 1
  compute_environments = [
    aws_batch_compute_environment.compute_environment.arn,
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The definition of a queue is pretty simple, it needs a name, a state (enabled, disabled), a priority, and the compute environment to which it can schedule jobs. Next, we will need a job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Job Definition
&lt;/h3&gt;

&lt;p&gt;For a job definition, we need a few things to specify. Let's see the resource definition first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_batch_job_definition"&lt;/span&gt; &lt;span class="s2"&gt;"job_definition"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-job-definition"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"container"&lt;/span&gt;

  &lt;span class="nx"&gt;platform_capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;container_properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;terraform_remote_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecr_registry_url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:latest"&lt;/span&gt;

    &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TABLE_NAME"&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table_name&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BUCKET"&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FILE_PATH"&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file_path&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;fargatePlatformConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;platformVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LATEST"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;resourceRequirements&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"VCPU"&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.0"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MEMORY"&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2048"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;executionRoleArn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;jobRoleArn&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the platform capabilities, we can have the same job being used for &lt;code&gt;FARGATE&lt;/code&gt; and &lt;code&gt;EC2&lt;/code&gt; as well. In our case, we need only &lt;code&gt;FARGATE&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;For the container properties, we need to have a bunch of things in place. Probably the most important is the repository URL for the Docker image. We will build the Docker image in the next section. &lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;resourceRequirements&lt;/code&gt; we configure the CPU and the memory usage. These apply to the job itself, and they should "fit" inside the compute environment. &lt;/p&gt;

&lt;p&gt;Moving on, we can specify some environment variables for the container. We are using these environment variables to be able to pass input to the container. We could also override the CMD (command) part of the Docker container and provide some input values there, but we are not doing that in this case.&lt;/p&gt;

&lt;p&gt;Last, but not least, we see that the job definition requires 2 IAM roles. The first one is the execution role, which "grants to the Amazon ECS container and AWS Fargate agents permission to make AWS API calls" (according to &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html" rel="noopener noreferrer"&gt;AWS Batch execution IAM role&lt;/a&gt;). The second one is the job role, which is an "IAM role that the container can assume for AWS permissions" (according to the &lt;a href="https://docs.aws.amazon.com/batch/latest/APIReference/API_ContainerProperties.html" rel="noopener noreferrer"&gt;ContainerProperties docs&lt;/a&gt;). Is this confusing for anybody else or just for me? Probably yes... so let's clarify these roles.&lt;/p&gt;

&lt;p&gt;The service role grants permission for the ECS cluster (and the ECS Fargate agent) to do certain AWS API calls. These calls include getting the Docker image from an ECR repository or being able to create CloudWatch log streams.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"ecs_task_execution_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-ecs-task-execution-role"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assume_role_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"assume_role_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ecs-tasks.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"ecs_task_execution_role_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since AWS already provides a policy for the execution role (&lt;code&gt;AmazonECSTaskExecutionRolePolicy&lt;/code&gt;), we can reuse that. &lt;/p&gt;

&lt;p&gt;The job role will grant permissions to the running container itself. In our case, if we need to write entries to a DynamoDB table, we have to provide write permission to the job to that table. Likewise, if we read from an S3 bucket, we have to create a policy with S3 read permission as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"job_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-job-role"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assume_role_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// dynamodb table Write Policy&lt;/span&gt;
&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_write_policy_document"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:DeleteItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:GetItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:PutItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:BatchWriteItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:UpdateItem"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:dynamodb:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:table/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_write_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-dynamodb-write-policy"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_write_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_write_policy_attachment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_write_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// S3 readonly bucket policy&lt;/span&gt;
&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"s3_read_only_policy_document"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:ListObjectsInBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"s3_readonly_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-s3-readonly-policy"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_read_only_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"s3_readonly_policy_attachment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_readonly_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the service role and job role require a trust policy so they can be assumed by ECS. &lt;/p&gt;

&lt;h3&gt;
  
  
  Building the Docker Container for the Job
&lt;/h3&gt;

&lt;p&gt;For our job to be complete, we need to build a Docker container with the so-called "business logic". We can store this container in an AWS ECR repository or on DockerHub. Usually, what I tend to do is to create a separate Terraform project for the ECR. The reason for this choice is that the Docker image should exist in ECR at the moment when the deployment of the AWS Batch job happens. &lt;/p&gt;

&lt;p&gt;The code for the ECR is very simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecr_repository"&lt;/span&gt; &lt;span class="s2"&gt;"repository"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo_name&lt;/span&gt;
  &lt;span class="nx"&gt;image_tag_mutability&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MUTABLE"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also create an output with the Docker repository URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"ecr_registry_url"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This output can be imported inside the other project and can be provided for the job definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "terraform_remote_state" "ecr" {
  backend = "s3"

  config = {
    bucket = "tf-demo-states-1234"
    key    = "aws-batch-demo/ecr"
    region = var.aws_region
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the source code which will do the ingesting of the movie ratings inside DynamoDB, we can use the following Python snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zipfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZipFile&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Downloading data from bucket &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Extracting data!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;zip_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ZipFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zip_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;zip_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;namelist&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_to_dynamo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Parsing csv data!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_content&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

    &lt;span class="n"&gt;dynamo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Starting to write data into table &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_writer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;overview&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;overview&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;release_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;release_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vote_average&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vote_average&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vote_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vote_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;original_language&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;original_language&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;popularity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;popularity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Written &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items into table &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Finished writing data into &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BUCKET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FILE_PATH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;is_env_missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment variable BUCKET is not set!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_env_missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment variable FILE_PATH is not set!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_env_missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;table_name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment variable TABLE_NAME is not set!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_env_missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_env_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Execution finished with one ore more errors!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;download_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;write_to_dynamo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is self-explanatory. We get an archive with a CSV file from a bucket location, we extract that archive and we iterate over the lines while doing batch insert into DynamoDB. We can see that certain inputs such as bucket name, the archive path, and table name are provided as environment variables.&lt;/p&gt;

&lt;p&gt;For the Docker file, we can use the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; public.ecr.aws/docker/library/python:3.9.16-bullseye&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; main.py .&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We build the image with the usual Docker &lt;code&gt;build&lt;/code&gt; (or &lt;code&gt;buildx&lt;/code&gt;) command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; movies-loader &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: the platform flag is important if we are using a MacBook M1, since AWS Batch &lt;a href="https://github.com/aws/containers-roadmap/issues/1652" rel="noopener noreferrer"&gt;does not support ARM/Graviton yet&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;We can push the image to the ECR repository following the push command from the AWS console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggering a Batch Job
&lt;/h2&gt;

&lt;p&gt;There are several ways to trigger batch jobs since they are available as EventBridge targets. For our example, we could have a scheduled EventBridge rule which could be invoked periodically.&lt;/p&gt;

&lt;p&gt;To make my life easier and be able to debug my job, I opted to create a simple &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Step Function&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Step Functions are state machines used for serverless orchestration. They are a perfect candidate for managing running jobs, offering a way to easily see and monitor the running state of the job and report the finishing status of it. We can implement the states of a Step Function using some JSON code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sfn_state_machine"&lt;/span&gt; &lt;span class="s2"&gt;"sfn_state_machine"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-sfn"&lt;/span&gt;
  &lt;span class="nx"&gt;role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sfn_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
    "Comment": "Run AWS Batch job",
    "StartAt": "Submit Batch Job",
    "TimeoutSeconds": 3600,
    "States": {
        "Submit Batch Job": {
            "Type": "Task",
            "Resource": "arn:aws:states:::batch:submitJob.sync",
            "Parameters": {
                "JobName": "ImportMovies",
                "JobQueue": "${aws_batch_job_queue.job_queue.arn}",
                "JobDefinition": "${aws_batch_job_definition.job_definition.arn}"
            },
            "End": true
        }
    }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like everything in AWS, Step Functions require an IAM role as well. The IAM role used in our example is similar to what is given in the &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/batch-job-notification.html" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"sfn_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"batch:SubmitJob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"batch:DescribeJobs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"batch:TerminateJob"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"events:PutTargets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"events:PutRule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"events:DescribeRule"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:events:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:rule/StepFunctionsGetEventsForBatchJobsRule"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"sfn_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-sfn-policy"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sfn_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"sfn_policy_attachment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sfn_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sfn_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out Step Function is required to be able to listen to and create CloudWatch Events, this is why it is necessary to have the policy for the &lt;code&gt;rule/StepFunctionsGetEventsForBatchJobsRule&lt;/code&gt; resource (see &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html" rel="noopener noreferrer"&gt;this StackOverflow answer&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Ultimately we will end up with this simplistic Step Function with only one intermediary state:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;In this article, we've seen a fairly in-depth introduction to the AWS Batch service. We also talked about when to use AWS Batch and when to consider other services that might be more adequate for the task at hand. We have also built a batch job from the scratch using Terraform, Docker, and Python.&lt;/p&gt;

&lt;p&gt;In conclusion, I think AWS Batch is a powerful service and it gets overshadowed by other offerings targeting more specific tasks. While the service itself abstracts away the provisioning of the underlying infrastructure, the whole setup process of a batch job can be still challenging and the official documentation in many cases lacks clarity. Ultimately, if we don't want to get in the weeds, we can rely on a &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/batch/aws/latest" rel="noopener noreferrer"&gt;Terraform module&lt;/a&gt; maintained by the community to spin up a batch job.&lt;/p&gt;

&lt;p&gt;The source code used for this article can also be found on GitHub at this URL: &lt;a href="https://github.com/Ernyoke/aws-batch-demo" rel="noopener noreferrer"&gt;https://github.com/Ernyoke/aws-batch-demo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;AWS Batch Documentation: &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/what-is-batch.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/batch/latest/userguide/what-is-batch.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Terraform Documentation - Compute Environment: &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/batch_compute_environment#service_role" rel="noopener noreferrer"&gt;https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/batch_compute_environment#service_role&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Stateful Firewall: &lt;a href="https://en.wikipedia.org/wiki/Stateful_firewall" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Stateful_firewall&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS Batch - Execution IAM Role: &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS Batch - Container Properties: &lt;a href="https://docs.aws.amazon.com/batch/latest/APIReference/API_ContainerProperties.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/batch/latest/APIReference/API_ContainerProperties.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Step Functions: &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;EFA for AWS Batch: &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/efa.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/batch/latest/userguide/efa.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Optimizing deep learning on P3 and P3dn with EFA: &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/efa.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/batch/latest/userguide/efa.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>batch</category>
      <category>terraform</category>
      <category>containers</category>
    </item>
  </channel>
</rss>
