<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[DEPT® Engineering Blog]]></title><description><![CDATA[Ideas & insights from the DEPT® developer community]]></description><link>https://engineering.deptagency.com/</link><image><url>https://engineering.deptagency.com/favicon.svg</url><title>DEPT® Engineering Blog</title><link>https://engineering.deptagency.com/</link></image><generator>Jamify Blog Starter 2.0</generator><lastBuildDate>Tue, 09 Jun 2026 05:50:45 GMT</lastBuildDate><atom:link href="https://engineering.deptagency.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Migrating from CodeShip to GitHub Actions]]></title><description><![CDATA[Nothing motivates a migration quite like a sunset notice. A few weeks after we moved our client's CI/CD from CodeShip to GitHub Actions, this appeared in the console:

Turns out we migrated just in time.


Why We Moved

Moving away from CodeShip was a matter of improving developer experience and providing a more consistent release experience for our client. While CodeShip Basic offered simplicity, GitHub Actions has better documentation and a larger ecosystem. In addition, CodeShip was outdated ]]></description><link>https://engineering.deptagency.com/migrating-from-codeship-to-github-actions/</link><guid isPermaLink="false">68eed858b86179000191fdc4</guid><dc:creator><![CDATA[Lucas Dachman]]></dc:creator><pubDate>Wed, 15 Oct 2025 17:58:23 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/several-shipping-containers.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/several-shipping-containers.jpeg" alt="Migrating from CodeShip to GitHub Actions"/><p>Nothing motivates a migration quite like a sunset notice. A few weeks after we moved our client's CI/CD from CodeShip to GitHub Actions, this appeared in the console:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/code-ship-end-of-life.webp" class="kg-image" alt="Migrating from CodeShip to GitHub Actions" loading="lazy" width="1017" height="71" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/10/code-ship-end-of-life.webp 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/10/code-ship-end-of-life.webp 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/code-ship-end-of-life.webp 1017w" sizes="(min-width: 720px) 720px"/></figure><p>Turns out we migrated just in time.</p><h2 id="why-we-moved">Why We Moved</h2><p>Moving away from CodeShip was a matter of improving developer experience and providing a more consistent release experience for our client. While CodeShip Basic offered simplicity, GitHub Actions has better documentation and a larger ecosystem. In addition, CodeShip was outdated and lacked modern CI/CD features.</p><ol><li><strong>Inconsistent failures due to resource restraints.</strong> Limited resources in the CI made for a sluggish headless browser, inability to run tests in parallel, and unpredictable build times.</li><li><strong>Poor debugging experience.</strong> CodeShip's SSH debugging feature sounded useful until we discovered the SSH session wasn't the container that ran our build. Build artifacts, test screenshots, and application logs aren’t there for inspection. You're debugging a clean environment that may or may not reproduce your issue.</li><li><strong>IaC locked behind Pro tier.</strong> CodeShip Basic's configuration lives entirely in the UI. If you wanted to version control your build triggers, environment variables, or deployment steps, that would require a Pro license. This meant no audit trail for configuration changes, no ability to review CI/CD changes in pull requests and tribal knowledge about build settings that disappeared when team members left.</li><li><strong>Limited cache control.</strong> CodeShip's caching is remarkably primitive. You dump files into $HOME/cache, and that's it. No cache keys. No expiration control. No branch isolation. The only way to invalidate the cache is through the UI.</li><li><strong>Limited Conditional Logic.</strong> GitHub provides rich conditional expressions that can use paths, commit messages, PR labels and other context. CodeShip provides only a few options for choosing branches or PR events.</li></ol><p><em>The Writing Was On the Wall</em></p><p>A few weeks after our migration, CodeShip's sunset notice appeared in the console. While we didn't know the exact timeline, CodeShip's stagnation was evident: outdated documentation, missing modern CI/CD features, and a developer experience that hadn't evolved in years. The sunset notice validated what we already knew—it was time to move on.</p><h2 id="starting-point">Starting Point</h2><p>CodeShip Basic operated on a straightforward premise: write shell commands in a text input, and they run on build triggers. Since configuration lived in the UI, we tracked our actual build logic in versioned shell scripts.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/project-tests.webp" class="kg-image" alt="Migrating from CodeShip to GitHub Actions" loading="lazy" width="1055" height="1126" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/10/project-tests.webp 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/10/project-tests.webp 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/project-tests.webp 1055w" sizes="(min-width: 720px) 720px"/></figure><p>The environment came pre-configured with common tooling: <code>nvm</code> for Node.js, <code>rvm</code> for Ruby, JDK version switchers, and a running PostgreSQL instance on the default port. This "batteries included" approach reduced initial setup but limited flexibility.</p><p>Build steps and deployment pipelines were both configured through the UI:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/elastic-beanstalk.webp" class="kg-image" alt="Migrating from CodeShip to GitHub Actions" loading="lazy" width="1409" height="955" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/10/elastic-beanstalk.webp 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/10/elastic-beanstalk.webp 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/10/elastic-beanstalk.webp 1409w" sizes="(min-width: 720px) 720px"/></figure><p>Our typical CodeShip workflow looked like this:</p><ol><li>Setup commands - Set language versions, install dependencies</li><li>Test commands - Run test suites (sequentially, due to resource constraints)</li><li>Deployment - Push artifacts to AWS Elastic Beanstalk via UI-configured deployment step</li></ol><p>This simplicity was CodeShip's strength and its limitation. When our needs outgrew what the UI could express, we hit a wall.</p><h2 id="mapping-codeship-%E2%86%92-github-actions">Mapping CodeShip → GitHub Actions</h2><p>Migrating our shell files to GitHub action’s workflow syntax was a significant task. Theoretically, we could have just copied our scripts into a single multiline step. But we would be missing out on core GitHub Actions features. Instead of a simple shell script, GitHub Actions uses a YAML configuration file. Nearly everything about your workflow is declared in this file, from test commands to deployment commands, even container image configuration. Here are the major migrations that were made:</p><h3 id="dependencies">Dependencies</h3><p>In CodeShip, we set the Ruby version using the included <code>rvm</code> version manager and install gems with the <code>bundler</code> gem</p><pre><code class="language-bash">rvm use 3.2.2
gem install bundler
bundle package --all
</code></pre><p>In Github Actions, we use an official action called <code>ruby/setup-ruby@v1</code></p><pre><code class="language-yaml">- uses: ruby/setup-ruby@v1
  with:
    ruby-version: 3.2.2
    bundler-cache: true
</code></pre><p>A similar mapping exists for NodeJS and it’s dependencies.</p><h3 id="postgresql">PostgreSQL</h3><p>In CodeShip, a PostgreSQL server is available on the default port. In GitHub Actions, we setup a service with a <code>postgres</code> image:</p><pre><code class="language-yaml">services:
  postgres:
    image: postgres:14
    ports:
      # Maps tcp port 5432 on service container to the host
      - 5432:5432
    env:
      POSTGRES_HOST_AUTH_METHOD: trust
</code></pre><h3 id="elasticsearch">Elasticsearch</h3><p>Elasticsearch required special handling due to our parallel test setup. Our test suite spawns multiple Elasticsearch processes—one per parallel worker—which means we need the Elasticsearch binary available on the PATH rather than a single service container.</p><p>We download the Elasticsearch tarball and cache it between builds to avoid repeated downloads:</p><pre><code class="language-yaml">- name: Set Path
  run: |
    ES_HOME="$HOME/.cache/elasticsearch-${ES_VERSION}"
    echo "ES_HOME=$ES_HOME" &gt;&gt; "$GITHUB_ENV"
    echo "$ES_HOME/bin" &gt;&gt; "$GITHUB_PATH"
    
- name: Cache Elasticsearch
  uses: actions/cache@v4
  with:
    path: ${{ env.ES_HOME }}
    key: ${{ runner.os }}-es-${{ env.ES_VERSION }}
    
- name: Install Elasticsearch
  run: |
    if [ -e $ES_HOME/bin/elasticsearch ]; then
      echo "Elasticsearch found in cache"
    else
      echo "Elasticsearch not found in cache"
      mkdir -p "$ES_HOME"
      curl -sSLO &lt;https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz&gt;
      curl -sSLO &lt;https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz.sha512&gt;
      shasum -a 512 -c elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz.sha512
      tar -xzf elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz
      mv elasticsearch-${ES_VERSION}/* "$ES_HOME/"
    fi
</code></pre><h3 id="deployment">Deployment</h3><p>CodeShip provided a UI for configuring deployment whereas GitHub Actions expects this to be declared in a workflow. Our deployment involves uploading a zip file to AWS S3 and triggering a deployment in Elastic Beanstalk. Thankfully, a community action exists for this case.</p><pre><code class="language-yaml">- name: Deploy to Elastic Beanstalk
  uses: einaregilsson/beanstalk-deploy@v21      
  env:
    EB_ENV_NAME: ${{ github.ref == 'refs/heads/master' &amp;&amp; vars.EB_ENV_NAME_PRODUCTION || vars.EB_ENV_NAME_STAGING }}
    EB_APP_NAME: ${{ vars.EB_APP_NAME }}
  with:
    aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    application_name: ${{ env.EB_APP_NAME }}
    environment_name: ${{ env.EB_ENV_NAME }}
    region: us-east-1
    version_label: "github-deployment-${{ github.sha }}"
    version_description: ${{ env.VERSION_DESC }}
    deployment_package: ${{ env.ZIP_FILE }}
    existing_bucket_name: elasticbeanstalk-us-east-1-xxxxxxxxxx
    use_existing_version_if_available: true
</code></pre><h3 id="debugging">Debugging</h3><p>CodeShip supported little in the realm of debugging builds. There was an option to SSH into a container but it’s not the instance that the tests ran on. This means that any build assets, logs, or screenshots were not available in the SSH session. In Github Actions, a community member offers an action called <code>mxschmitt/action-tmate</code>. This action creates a <code>tmate</code> session and allows you to SSH into it. You can place this action at any step in a job to inspect and debug. This proved extremely useful during the migration.</p><pre><code class="language-yaml">- name: Step that requires inspection
	run: ...
	
- name: Setup tmate session
  uses: mxschmitt/action-tmate@v3
  
# The workflow continues after the session ends.  
- name: Next Step
	run: ...
</code></pre><h2 id="workflow-highlights">Workflow Highlights</h2><p>Some things to note about the new workflow:</p><ol><li><strong>More Infrastructure as Code.</strong> This centralizes our process in our codebase and allows us to track with version control. Things that moved from the UI console to IaC include build triggers, environment variables, container images, and artifact deployments.</li><li><strong>Different runners for different jobs.</strong> For running tests, we use a Linux instance with 8 cores to run our tests in parallel. For other things like building assets, deploying artifacts or invalidating caches, we use the default runner. GitHub offers 3,000 minutes/month of free actions using the default runners in private repositories and we want to take advantage of that.</li><li><strong>Avoid compiling twice.</strong> We compile assets in staging/production mode at the beginning of the workflow and use that for testing. This saves time and also makes our testing environment more similar to production. Later jobs access the build artifact using GitHub Action’s official <code>actions/upload-artifact</code> and <code>actions/download-artifact</code>.</li></ol><pre><code class="language-yaml"># At the end of the build job
- name: Create deploy artifact
  run: zip -r "$ZIP_FILE" . -x "*.git*" "log/*" "tmp/*" "node_modules/*" "vendor/bundle/*"
- name: Upload artifact
	uses: actions/upload-artifact@v4
  with:
    name: ${{ env.ZIP_FILE }}
    path: ${{ env.ZIP_FILE }} 
    
# ....

# At the beginning of both test and deploy jobs
- name: Download and unpack artifact to workspace
  uses: actions/download-artifact@v4
  with:
    name: ${{ env.ZIP_FILE }}
    path: .
</code></pre><ol start="4"><li><strong>Concurrency control.</strong> This basically cancels any in-progress build when there is a new trigger on the same branch.</li></ol><pre><code class="language-yaml">concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
</code></pre><h2 id="outcome">Outcome</h2><p>This migration was about more than just replacing one tool with another. It was about rethinking our delivery pipeline to achieve faster feedback, reproducible builds, and first-class debugging.</p><p>The results validated the effort:</p><ol><li><strong>Faster builds.</strong> On CodeShip, our full pipeline duration ranged from 24 to 48 minutes. Now, a full pipeline takes about 12-14 minutes. This is mainly due to increased parallelization with larger runners.</li><li><strong>Smaller Bill.</strong> A similarly sized image in CodeShip Pro would have cost $299/mo at the time. With GitHub Actions, we pay based on minutes and our bill last month was around $47.</li><li><strong>Zero resource-related failures since the migration.</strong> No more flaky builds!</li><li><strong>Actually debugging builds</strong> instead of making guesses because the build is not reproducible.</li></ol><p>And we’re just getting started! There is still more to explore in GitHub Actions like matrix builds, reusable workflows and automated dependency updates, but for now, we're thrilled with the results.</p>]]></content:encoded></item><item><title><![CDATA[The problem with nesting theme variants in TailwindCSS, and how you'll eventually be able to fix it]]></title><description><![CDATA[Nesting multiple dynamic theme variants wasn't previously possible in TailwindCSS. But thanks to a new CSS feature, it will be soon!]]></description><link>https://engineering.deptagency.com/tailwindcss-theme-nesting/</link><guid isPermaLink="false">6888f9b1bc51a20001dcbef9</guid><dc:creator><![CDATA[Ethan Standel]]></dc:creator><pubDate>Mon, 22 Sep 2025 19:56:02 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1613981948475-6e2407d8b589?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fG5lc3RpbmclMjBkb2xsJTIwfGVufDB8fHx8MTc1MzgwNzM3MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1613981948475-6e2407d8b589?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fG5lc3RpbmclMjBkb2xsJTIwfGVufDB8fHx8MTc1MzgwNzM3MHww&ixlib=rb-4.1.0&q=80&w=2000" alt="The problem with nesting theme variants in TailwindCSS, and how you'll eventually be able to fix it"/><p>TailwindCSS does a fantastic job of organizing malleable configuration for development teams and allows for building out reusable design systems, allowing new developers to get up and running quickly. But because TailwindCSS entirely circumvents a large part of the complexity of CSS (the whole "<em>cascade</em>" part), it can't necessarily always match the power of CSS pound-for-pound. But there's a cool upcoming CSS feature that will soon close one of the more problematic capability gaps that I've encountered.</p>
<h2 id="the-problem">The problem</h2>
<p>If you visit <a href="https://tailwindcss.com/docs/dark-mode?ref=dept-engineering-blog.ghost.io">the TailwindCSS documentation for how to represent dynamic theming (e.g. dark &amp; light mode)</a>, you'll see an arguably misleading example.</p>
<p><a href="https://tailwindcss.com/docs/dark-mode?ref=dept-engineering-blog.ghost.io"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/09/Screenshot-2025-09-06-at-11.31.47---PM.png" alt="The problem with nesting theme variants in TailwindCSS, and how you'll eventually be able to fix it" loading="lazy"/></a></p>
<p>This shows the TailwindCSS documentation site, which by default renders in either light-mode or dark-mode depending on the viewer's system theme as reported by their browser. But inside of the docs site, there is an example with associated code for how to handle a light-theme as opposed to a dark theme with code to match. This might lead you to assume that it is safely possible to <em>nest content of one theme inside of content of another theme</em>. As that's what must be happening on the TailwindCSS documentation site, as you can see the dark-mode website rendering a light-mode card.</p>
<p>The problem here is that the TailwindCSS site is actually lying to the reader. The code below the example is not actually what's running on the example itself at all. If you inspect each card, you'll see that they are actually entirely different component instances with entirely separate TailwindCSS classes, not using a <code>dark:</code> or <code>light:</code> variant system at all.</p>
<p>As it turns out, nesting themes in TailwindCSS hasn't actually been possible historically. TailwindCSS's recommended solution to multiple theme variants is all-or-nothing, only allowing one active theme at a time. The recommended configurations look like one the following two options.</p>
<blockquote>
<p><strong>Side note:</strong> The configurations seen in the documentation exclusively uses a shorthand syntax equivalent to what's seen below, but I find the expanded syntax to be more explanatory to what a variant selector is representing. This expanded syntax is only necessary if your variant uses multiple selectors with each variant needing multiple instances of <code>@slot</code>.</p>
</blockquote>
<pre><code class="language-css">@custom-variant dark {
  @media (prefers-color-scheme: dark) {
    @slot;
  }
}
@custom-variant light {
  @media (prefers-color-scheme: light) {
    @slot;
  }
}
</code></pre>
<p>This first option prescribes the proper theme to the user based on their system settings. This is effectively how the TailwindCSS documentation site works.</p>
<pre><code class="language-css">@custom-variant dark {
  &amp;:where(.dark, .dark *) {
    @slot;
  }
}
@custom-variant light {
  &amp;:where(.light, .light *) {
    @slot;
  }
}
</code></pre>
<p>This option would allow the theme choice to be made and dynamically updated by users, by assigning a <code>.dark</code> or <code>.light</code> class to a root document element, e.g. <code>&lt;body&gt;</code>.</p>
<p>And of course, you can combine both options by prescribing the default applied class based on the result of calling <code>window.matchMedia("(prefers-color-scheme: dark)")</code> from JavaScript.</p>
<p>However, all of these solutions still have the same problem of not supporting nesting. Of course, the <code>@media</code> queries all represent a document-wide setting for your whole page. But the class solution also has the same limitations. You might think that you could nest use of the theme classes, e.g.</p>
<pre><code class="language-tsx">&lt;body class="light"&gt;
  &lt;CardComponent /&gt;
  &lt;CardComponent class="dark" /&gt;
&lt;/body&gt;
</code></pre>
<p>However, the problem here is that the <code>&lt;CardComponent&gt;</code> with the <code>.dark</code> class is <em>also</em> underneath the <code>.light</code> class. So the styles of <strong>both</strong> variants will apply. This creates undefined behavior in TailwindCSS where the styles which <em>actually</em> get applied is whichever classes are resolved last in the generated output CSS, and TailwindCSS cannot guarantee a generation order. Which means that in this example you'll likely get a combination of the styles of multiple themes. The problem is that CSS has historically lacked a selector to identify <em>proximity</em>, e.g. "prefer this selector because this class is closer to the target element."</p>
<h2 id="the-solution">The solution</h2>
<p>A somewhat recent CSS update has added a new feature called <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries?ref=dept-engineering-blog.ghost.io">Container Queries</a>. This new tool gives developers a bunch of cool new powers for selectors in CSS. However, the specific features which I've found to be the most revolutionary is <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queries?ref=dept-engineering-blog.ghost.io#container_style_queries">Container Style Queries</a>. This API gives us the ability to create a selector that applies styles based upon the value of a particular property. And because the values of properties are inherited by default, this means that we have the ability to bypass the former limitation and do proximity-based-selectors!</p>
<p>So in TailwindCSS, we can create this new configuration!</p>
<pre><code class="language-css">.dark {
  --theme: dark;
}

@custom-variant dark {
  @container style(--theme: dark) {
    @slot;
  }
}

.light {
  --theme: light;
}

@custom-variant light {
  @container style(--theme: light) {
    @slot;
  }
}
</code></pre>
<p>This new configuration allows for the <code>CardComponent</code> example above to run as expected, because the new theme selectors are just selecting "all elements whose <code>--theme</code> property is either <code>light</code> or <code>dark</code>." So for any element, the value of <code>--theme</code> will match the nearest parent in the tree (or themselves) which assigns the <code>--theme</code> property.</p>
<p>Here's <a href="https://stackblitz.com/edit/nested-themes-showcase?file=src%2FApp.tsx&terminal=dev&ref=dept-engineering-blog.ghost.io">a running example of <em>the actual code from the TailwindCSS documentation</em> working as it should</a>!</p>
<h2 id="the-problems-with-the-solution">The problems <em>with the solution</em></h2>
<p>So based on the existence of the example linked above, you might wonder why we can't use this solution right now. Unfortunately this API is very new and is still being implemented in its entirety in browsers.</p>
<p><a href="https://caniuse.com/css-container-queries-style?ref=dept-engineering-blog.ghost.io"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/09/Screenshot-2025-09-07-at-1.44.21---PM.png" alt="The problem with nesting theme variants in TailwindCSS, and how you'll eventually be able to fix it" loading="lazy"/></a></p>
<p>If you check the <a href="https://caniuse.com/css-container-queries-style?ref=dept-engineering-blog.ghost.io">caniuse.com support matrix</a>, it's not looking great. But it's also not nearly as bleak it it looks upon first glance. Notably, no browsers identify as fully supporting the feature, but thankfully it doesn't matter all that much. What most browsers decidedly <em>do not</em> support is selecting based on the value of native CSS properties. But what they <em>do</em> support is selectors based upon custom-properties (also known as CSS variables). So that is to say, if you wanted to get tricky and define your dark mode selector like the following, it wouldn't work in any browser currently.</p>
<pre><code class="language-css">.dark {
  background: black;
  color: white;
}

@custom-variant dark {
  @container style(color: white) {
    @slot;
  }
}
</code></pre>
<p>It seems that browser developers have deemed the need for this as very low and have neglected to implement it. However, as things work today, you could still just do something like the next example (assuming you're utilizing a property whose default value is <code>inherit</code>).</p>
<pre><code class="language-css">.dark {
  background: black;
  --text-color: white;
  color: var(--text-color);
}

@custom-variant dark {
  @container style(--text-color: white) {
    @slot;
  }
}
</code></pre>
<p>So generally that isn't really an issue worth stopping anyone from using this solution. The unfortunate leftover problem is that Firefox has thus far failed to support the feature at any level. But there's good news! We know they're close because if you go to <code>about:config</code> in Firefox, there is a flag for this feature!</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/09/Screenshot-2025-09-07-at-1.58.46---PM.png" alt="The problem with nesting theme variants in TailwindCSS, and how you'll eventually be able to fix it" loading="lazy"/></p>
<p>If you flip the <code>layout.css.style-queries.enabled</code> flag to true, for all my testing, it seems to work perfectly well. This could be blocked from official default support by any number of reasons. So as it stands, it remains disabled and thus using Container Style Queries would exclude Firefox users (or rather, make your site look janky to them). That being said, this will be a powerful tool for use with TailwindCSS, and tons of other contexts, once it gets more wide-reaching support.</p>
]]></content:encoded></item><item><title><![CDATA[A guide to dynamic content personalization in AEM Guides]]></title><description><![CDATA[Whether you're a developer seeking a quick API fix, a marketer requiring platform-specific steps, or a customer simply trying to get something done, generic, static documentation feels like flipping through a dusty manual.

Modern users expect documentation that feels like it was written just for them, tailored to their role, device, and preferences. Think personalized, dynamic, and interactive content that adapts to your taste, just like Netflix tailors shows to yours.

Adobe Experience Manager]]></description><link>https://engineering.deptagency.com/a-guide-to-dynamic-content-personalization-in-aem-guides/</link><guid isPermaLink="false">6839999f79bfdf0001b07516</guid><category><![CDATA[Adobe]]></category><dc:creator><![CDATA[Sadhna Singh]]></dc:creator><pubDate>Mon, 09 Jun 2025 13:07:22 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/personalization.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/personalization.png" alt="A guide to dynamic content personalization in AEM Guides"/><p>Whether you're a developer seeking a quick API fix, a marketer requiring platform-specific steps, or a customer simply trying to get something done, generic, static documentation feels like flipping through a dusty manual.</p><p>Modern users expect documentation that feels like it was written <em>just for them,</em> tailored to their role, device, and preferences. Think personalized, dynamic, and interactive content that adapts to your taste, just like Netflix tailors shows to yours.</p><p>Adobe Experience Manager (AEM) Guides help you deliver highly personalized documents, whether for PDFs or dynamic web outputs, using conditional attributes, profiling, and smart publishing strategies.</p><p>In this blog, we’ll explore:</p><ul><li>How to personalize content in AEM Guides using attributes like audience and platform</li><li>How to set up publishing filters</li><li>How to manage dynamic multi-version outputs from a single DITA source</li><li>Best practices for clean and scalable personalized documentation</li></ul><h2 id="how-does-dynamic-content-personalization-work-in-aem-guides">How does dynamic content personalization work in AEM guides?</h2><p>Dynamic personalization helps you create multiple versions of documentation from a single source by applying filters based on audience, platform, region, product, or any other business-specific need.</p><p>Instead of maintaining multiple documents, you use <strong>DITA attributes</strong> like audience, platform, product, region, etc., and AEM Guides dynamically generate filtered outputs during publishing.</p><p><strong>Examples:</strong></p><ul><li>Admin Manual vs End User Manual Instructions (Software)</li><li>Mobile App User Guide vs Desktop Web User Guide (Platform)</li><li>Country-specific product instructions vs Global version (Product)</li><li>Healthcare Practitioner vs Patient User Guides (Healthcare)</li><li>Different vehicle models and their feature availability based on regions (Automotive)</li><li>Internal vs External policy documents (Finance, Human Resources)</li><li>Free Users Vs. Premium Users (Subscriptions)</li></ul><h2 id="methods-of-personalization-in-aem-guides">Methods of personalization in AEM guides</h2><p>You can personalize content in AEM Guides using:</p><ul><li><strong>Conditional Attributes in DITA Topics</strong></li><li><strong>Profiling Rules</strong></li><li><strong>Filtered Output Presets</strong></li></ul><p>These allow fine-grained control over what content appears in each output without duplicating your DITA source.</p><h3 id="example-1-persona-based-personalization">Example 1: Persona-based personalization&nbsp;</h3><h3 id="%E2%80%94-service-technician-manual-vs-end-user-manual">— Service technician manual vs. end user manual</h3><p><strong>Business Scenario:</strong> A company that manufactures electric scooters needs two different manuals:</p><ul><li>One for Service Technicians who repair and maintain the scooters.</li><li>Another for End Users who simply operate the scooters.</li></ul><p>While much of the content overlaps (like the braking system), the level of detail differs significantly:</p><ul><li><strong>Service Manual</strong>: Technical, procedural, and diagnostic content.</li><li><strong>End User Manual:</strong> Simple, usage-focused instructions.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1706" height="779" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/image.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/image.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2025/06/image.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image.png 1706w" sizes="(min-width: 720px) 720px"/></figure><p>End User Manual ➔ Filter: audience=end-user<br>Service Manual ➔ Filter: audience=service-tech<br><br><strong>Output Presets Setup:</strong><br>audience="end-user" → Visible only in the End User Manual<br>audience="service-tech" → Visible only in the Service Manual</br></br></br></br></br></p><h3 id="example-2-platform-based-personalization">Example 2: Platform-based personalization&nbsp;</h3><h3 id="%E2%80%94-mobile-vs-desktop">— Mobile vs desktop</h3><p><strong>Business Scenario:</strong> A mobile app and desktop platform have different navigation instructions.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image-1.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1691" height="368" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/image-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/image-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2025/06/image-1.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image-1.png 1691w" sizes="(min-width: 720px) 720px"/></figure><p>platform="mobile" → Visible in Mobile user guide<br>platform="desktop" → Visible in Desktop user guide</br></p><p><strong>Output Presets Setup:</strong></p><p>Mobile Documentation ➔ Filter: platform=mobile<br>Desktop Documentation ➔ Filter: platform=desktop</br></p><h3 id="example-3-combining-multiple-attributes">Example 3: Combining multiple attributes&nbsp;</h3><h3 id="%E2%80%94-audience-platform">— Audience + Platform</h3><p>Now, let’s make it even more dynamic!</p><p><strong>Business Scenario:</strong> A banking app has different features for Admins and Users on Mobile and Desktop platforms.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image-2.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1726" height="865" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/image-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/image-2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2025/06/image-2.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image-2.png 1726w" sizes="(min-width: 720px) 720px"/></figure><p>Different content based on <strong>both</strong> audience and platform.</p><p><strong>Output Presets Setup:</strong></p><p>Mobile Admin Guide ➔ Filters: audience=admin, platform=mobile<br>Desktop Admin Guide ➔ Filters: audience=admin, platform=desktop<br>Mobile End User Guide ➔ Filters: audience=end-user, platform=mobile<br>Desktop End User Guide ➔ Filters: audience=end-user, platform=desktop</br></br></br></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides1.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="720" height="343" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides1.png 720w" sizes="(min-width: 720px) 720px"/></figure><h2 id="step-by-step-personalization-process-in-aem-guides"><strong>Step-by-step personalization process in AEM Guides</strong></h2><h3 id="step-1-apply-conditional-attributes-in-dita-topics"><em>Step 1</em>: Apply conditional attributes in DITA topics</h3><p><strong>Goal:</strong> Tag specific content pieces that should only appear for certain audiences, platforms, or products.</p><p><strong>How to do it:</strong></p><ul><li>In AEM Guides Editor, select the paragraph, table, or image you want to condition.</li><li>In the <strong>Properties Panel</strong>, assign attributes like audience, platform, or product.</li></ul><p>Example:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image-3.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1663" height="777" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/image-3.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/image-3.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2025/06/image-3.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/image-3.png 1663w" sizes="(min-width: 720px) 720px"/></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides2.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1600" height="727" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/adobe-guides2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides2.png 1600w" sizes="(min-width: 720px) 720px"/></figure><h3 id="step-2-define-profiling-rules"><em>Step 2</em>: Define Profiling Rules&nbsp;</h3><p><strong>Goal:</strong> Set up rules that define how filtered outputs should behave during publishing.</p><p><strong>How to do it:</strong></p><ul><li>Navigate to <strong>Profiles and Conditions</strong> in AEM Guides.</li><li>Define a new <strong>Profiling Rule Set</strong>.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides3.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1600" height="687" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides3.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/adobe-guides3.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides3.png 1600w" sizes="(min-width: 720px) 720px"/></figure><ul><li>Specify the attributes, values, and labels you need. You can also assign colors to differentiate content visually.</li><li>Once done, save and publish the profiling rule.</li></ul><p>This enables AEM Guides to manage content inclusion or exclusion during output generation automatically.</p><h3 id="step-3-create-and-configure-filtered-output-presets"><em>Step 3</em>: Create and Configure Filtered Output Presets</h3><p><strong>Goal:</strong> Associate profiling rules with specific publishing outputs (such as PDFs or Sites).</p><p><strong>How to do it:</strong></p><ul><li>Open the <strong>Map Dashboard</strong>.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides4.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="720" height="321" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides4.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides4.png 720w" sizes="(min-width: 720px) 720px"/></figure><ul><li>Navigate to <strong>Condition Presets</strong> and click <strong>Create</strong>.</li><li>Click <strong>Add All</strong> on the preset creation screen to import the attributes defined in the profiling rules. Rename the conditions based on the scenario. <em>(For example: Include "admin" and exclude "end-user" for admin-specific outputs.)</em></li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides5.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1600" height="754" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides5.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/adobe-guides5.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides5.png 1600w" sizes="(min-width: 720px) 720px"/></figure><ul><li>Save the conditional preset.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides7.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1600" height="738" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides7.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/adobe-guides7.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides7.png 1600w" sizes="(min-width: 720px) 720px"/></figure><ul><li>Go to <strong>Output Presets</strong> and create a new preset (e.g., PDF Output for Admin). In the <strong>General</strong> section, select the previously saved conditional preset.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides8.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="1600" height="710" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides8.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/06/adobe-guides8.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides8.png 1600w" sizes="(min-width: 720px) 720px"/></figure><ul><li>Click <strong>"Generate"</strong> to produce the final output. </li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides9.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="720" height="344" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides9.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides9.png 720w" sizes="(min-width: 720px) 720px"/></figure><ul><li>Now, when you publish, only content that matches the conditions will be included in the final output. You can also preview filtered content before publishing.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides10.png" class="kg-image" alt="A guide to dynamic content personalization in AEM Guides" loading="lazy" width="720" height="338" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/06/adobe-guides10.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/06/adobe-guides10.png 720w" sizes="(min-width: 720px) 720px"/></figure><p/><h2 id="best-practices-for-dynamic-personalization">Best practices for dynamic personalization</h2><ul><li><strong>Plan attributes carefully</strong> during documentation design (audience, platform, region, etc.).</li><li><strong>Use consistent attribute naming</strong> (case-sensitive in DITA!).</li><li><strong>Keep profiling manageable</strong> — avoid too many overlapping conditions.</li><li><strong>Test outputs separately</strong> to ensure content renders correctly.</li><li><strong>Document your profiling strategy</strong> for the whole team (new writers need to understand the rules easily).</li></ul><h2 id="integrating-user-profile-attributes-for-advanced-personalization">Integrating user profile attributes for advanced personalization</h2><p>While AEM Guides supports rule-based filtering using conditional attributes like audience, platform, and product, personalization can be extended further by integrating with external user repositories. You can configure AEM’s <strong>publish instances</strong> to connect with systems such as LDAP, SAML providers, Adobe IMS, or other user authentication and profile management APIs and databases to fetch user-level profile attributes, including roles, job titles, or departments. These attributes can then be used to dynamically apply filters and personalize documentation experiences based on the user's identity. This setup enables AEM Guides to deliver truly personalized content tailored to individual user profiles.</p><p>Dynamic personalization in AEM Guides is a powerful strategy that simplifies content management and delivers highly targeted, user-specific documentation — all from a single source of truth.</p><p>By applying simple conditional attributes and smart publishing filters, you can significantly reduce maintenance effort, enhance accuracy, and provide a better experience for every user type.</p><p>It’s not just about documentation — it's about delivering the right information to the right user at the right time.&nbsp;</p>]]></content:encoded></item><item><title><![CDATA[How to integrate graphs in AEM guides: A step-by-step guide]]></title><description><![CDATA[Imagine opening a technical document packed with dense paragraphs and endless tables of numbers. Overwhelming, right?

Now imagine the same content — but this time, with vibrant graphs and dynamic charts that instantly tell the story behind the data.

Visuals don’t just beautify documents. They make understanding effortless.

As AEM Guides users, we have the power to transform complexity into clarity. And the best part? Adobe Experience Manager (AEM) Guides gives us the flexibility to integrate ]]></description><link>https://engineering.deptagency.com/how-to-integrate-graphs-in-aem-guides-a-step-by-step-guide/</link><guid isPermaLink="false">680372376ce9b200010602b5</guid><category><![CDATA[Adobe]]></category><dc:creator><![CDATA[Sadhna Singh]]></dc:creator><pubDate>Mon, 28 Apr 2025 16:09:05 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/05/How-to-add-charts-in-AEM-Guides--2-.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/05/How-to-add-charts-in-AEM-Guides--2-.png" alt="How to integrate graphs in AEM guides: A step-by-step guide"/><p>Imagine opening a technical document packed with dense paragraphs and endless tables of numbers. Overwhelming, right?</p><p>Now imagine the same content — but this time, with vibrant graphs and dynamic charts that instantly tell the story behind the data.</p><p><strong>Visuals don’t just beautify documents. They make understanding effortless.</strong></p><p>As AEM Guides users, we have the power to transform complexity into clarity. And the best part? Adobe Experience Manager (AEM) Guides gives us the flexibility to integrate stunning visual representations into both <strong>PDF outputs</strong> and <strong>Sites experiences</strong>.</p><h2 id="industry-use-cases-for-graphs-within-product-documentation"><strong>Industry use cases for graphs within product documentation</strong></h2><p>Integrating charts in technical documentation (techdocs, functional/technical manuals, brochures, etc.) adds a strong visual impact. Here are some of the practical industry examples:</p><p><strong>Manufacturing:</strong> Show production rates and machine uptime/downtime analytics.</p><p><strong>Healthcare and pharmaceuticals:</strong> Visualize clinical trial results, medication efficacy, or treatment comparisons.</p><p><strong>Automotive:</strong> Represent vehicle performance stats and maintenance intervals.</p><p><strong>Software and Technology:</strong> Showcase system performance metrics, application error rates, or API response times.</p><p><strong>Finance:</strong> Graphs for loan trends, risk assessments, or insurance claims analysis.</p><hr><p>In this post, we will explore how to integrate graphs inside <strong>Adobe Experience Manager (AEM) Guides</strong> for both <strong>PDF</strong> and <strong>Sites</strong> outputs.</p><p>We will walk through the complete process — from selecting the proper chart library, importing it into AEM Guides, implementing custom JavaScript, and ensuring adequate rendering for different outputs.</p><p>Whether you are working on structured content for PDFs or dynamic content for web experiences, this blog will help you achieve seamless graph integration.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/image-2.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="893" height="709" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/image-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/image-2.png 893w" sizes="(min-width: 720px) 720px"/></figure><hr><h2 id="overview-graph-integration-in-aem-guides"><strong>Overview: Graph Integration in AEM Guides</strong></h2><p>Adobe Guides supports graph/chart integrations by including JavaScript libraries in templates and authored sections. However, there are some important considerations to keep in mind:</p><ul><li><strong>Supported Libraries: Only</strong> <strong>vanilla JavaScript</strong> (ES5 compatible) libraries are currently supported. Libraries that use <strong>ES6+ features</strong> (like arrow functions, classes, let, const) may not work properly, especially when generating PDFs.</li><li><strong>Use Case Demonstrated: </strong>For this tutorial, we will integrate a simple <strong>Polar Area Chart</strong> and <strong>Line Chart</strong> using a vanilla JavaScript chart library to demonstrate the steps. The JavaScript example provided while demonstrating PDF generation can also be applied similarly for Sites output. Therefore, in the Sites section, only the integration steps are detailed, as the core JavaScript implementation remains the same.</li></ul><hr><h2 id="part-1-graph-integration-for-pdf-output"><strong>Part 1: Graph Integration for PDF Output</strong></h2><p>Let's first see how to integrate a graph that should be rendered when generating PDFs from AEM Guides.</p><h3 id="step-1-select-the-chart-library"><strong>Step 1: Select the Chart Library</strong></h3><ul><li>Choose a JavaScript chart library compatible with ES5.</li><li>In this demonstration, we use a simple Polar Area Chart library written in vanilla JavaScript.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/adobe-library-search--selection.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="720" height="328" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/adobe-library-search--selection.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/adobe-library-search--selection.png 720w" sizes="(min-width: 720px) 720px"/></figure><p><strong>Note:</strong> Ensure the library does not rely on modern JavaScript features beyond ES5.</p><hr><h3 id="step-2-import-the-library-into-templates"><strong>Step 2: Import the Library into Templates</strong></h3><p>In AEM Guides:</p><ul><li>Go to the <strong>Template Editor</strong> section.</li><li>Open the specific <strong>Template</strong> where you want to integrate the graph.</li><li>Use the <strong>Import Section</strong> to upload and include the JavaScript chart library.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/-Importing-library-into-the-Template.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="720" height="409" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/-Importing-library-into-the-Template.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/-Importing-library-into-the-Template.png 720w" sizes="(min-width: 720px) 720px"/></figure><hr><h3 id="step-3-create-an-output-class"><strong>Step 3: Create an Output Class</strong></h3><p>Next, you need to create an <strong>Output Class</strong>:</p><ul><li>Navigate to the <strong>Output Class</strong> section.</li><li>Create a new class (for example: .chart-container).</li><li>Assign this class name where you are authoring your graph/chart in the AEM Guides content (in the Author mode).</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/Outputclass-creation-and-assignment.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="720" height="326" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/Outputclass-creation-and-assignment.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/Outputclass-creation-and-assignment.png 720w" sizes="(min-width: 720px) 720px"/></figure><hr><h3 id="step-4-implement-custom-javascript"><strong>Step 4: Implement Custom JavaScript</strong></h3><ul><li><strong>Create a separate JavaScript file</strong> dedicated to handling the chart functionality.</li><li><strong>Target the container</strong> where the chart should appear — specifically using the .chart-container class that was set in the previous steps.</li><li><strong>Initialize and render the chart dynamically</strong> inside that container by creating a &lt;canvas&gt; element, preparing the chart configuration, and rendering it using a chart library (like Chart.js).</li><li><strong>Support both static and dynamic data sources</strong>:<ul><li><strong>Static data</strong>: You can directly define datasets within the JavaScript file (for example, hardcoding the labels and values).</li><li><strong>Dynamic data</strong>: You can fetch the chart data from various endpoints such as:<ul><li>AEM Content Fragments via GraphQL APIs</li><li>AEM Assets or DAM JSON files</li><li>External REST APIs from third-party services</li><li>Custom servlets or backend endpoints developed within AEM</li></ul></li></ul></li><li>Depending on the use case, the fetch URLs and data processing logic can be customized to feed the chart dynamically at runtime.</li></ul><p>The example below is the JS snippet for the polar chart and Line Chart respectively:</p><p>JavaScript Example: Static Chart Initialization<br>
window.addEventListener('DOMContentLoaded', function () {<br>
window.pdfLayout.onBeforePagination(function () {</br></br></p>
<pre><code>// Create a canvas element
var parent = document.querySelector(".chart-container");
var canvas = document.createElement("canvas");
canvas.classList.add("eligible-categories-bar");

// Avoid duplicate canvas creation
if (document.querySelector(".chart-container canvas.eligible-categories-bar")) {
  return;
}

parent.appendChild(canvas);

var ctx = canvas.getContext('2d');

var mixedChart = new Chart(ctx, {
  type: 'bar', // Define the type of the chart
  data: {
    labels: ["2020", "2021", "2022", "2024"], // X-axis labels
    datasets: [
      {
        label: 'Assets Evaluation', // Dataset label
        data: [12, 19, 3, 5], // Dataset values
        backgroundColor: '#FFD700', // Bar color
        borderColor: '#FFD700', // Border color
        borderWidth: 1 // Border width
      },
      {
        label: 'Selection Process', // Second dataset label
        data: [15, 9, 7, 8], // Second dataset values
        backgroundColor: '#01EA57',
        borderColor: '#01EA57',
        borderWidth: 1
      }
    ]
  },
  options: {
    scales: {
      yAxes: [{
        ticks: {
          beginAtZero: true // Y-axis starts from 0
        }
      }]
    },
    barPercentage: 0.4, // Adjusts width of bars
    categoryPercentage: 0.5 // Adjusts space between groups
  }
});
</code></pre>
<p>});<br>
});</br></p>
<p>JavaScript Example: Static Integration for polar chart</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/pdf-aem.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="719" height="300" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/pdf-aem.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/pdf-aem.png 719w"/></figure><p>JavaScript Example: Dynamic API Integration for Line chart</p><p>window.addEventListener('DOMContentLoaded', function () {</p><pre><code>window.pdfLayout.onBeforePagination(function () {
    // Prevent multiple chart creations
    if (document.querySelector(".chart-container-dynamic .canvas-container")) {
        return;
    }
    // Create a &lt;canvas&gt; for Chart.js 
    var canvas = document.createElement("canvas");
    canvas.className = "canvas-container";
    var chartContainer = document.querySelector(".chart-container-dynamic");
    if (!chartContainer) {
        console.error("Chart container not found");
        return;
    }
    chartContainer.appendChild(canvas);
    var ctx = canvas.getContext("2d");
    // Fetch and render Chart.js chart
    fetch("https://canvasjs.com/services/data/datapoints.php?xstart=1&amp;ystart=10&amp;length=100&amp;type=json")
        .then(function (response) {
            if (!response.ok) {
                throw new Error("Network response was not ok");
            }
            return response.json();
        })
        .then(function (data) {
            var dataPoints = [];
            // Convert to Chart.js format
            for (var i = 0; i &lt; data.length; i++) {
                dataPoints.push({
                    x: data[i][0],
                    y: parseInt(data[i][1], 10)
                });
            }
            // Render Chart.js chart
            new Chart(ctx, {
                type: 'line',
                data: {
                    datasets: [{
                        label: 'External Data',
                        data: dataPoints,
                        borderColor: '#04C2C7',
                        backgroundColor: '#04C2C7',
                        fill: false,
                        tension: 0.3
                    }]
                },
                options: {
                    responsive: true,
                    parsing: false,
                    scales: {
                        x: {
                            type: 'linear',
                            position: 'bottom',
                            title: {
                                display: true,
                                text: 'X'
                            }
                        },
                        y: {
                            title: {
                                display: true,
                                text: 'Y'
                            }
                        }
                    },
                    plugins: {
                        legend: {
                            display: true,
                            labels: {
                                font: {
                                    size: 14,
                                    family: 'Manrope, sans-serif'
                                },
                                color: '#2C343B'
                            }
                        }
                    },
                    layout: {
                        padding: {
                            top: 40,
                            bottom: 20,
                            left: 10,
                            right: 10
                        }
                    }
                }
            });
        })
        .catch(function (error) {
            console.error("Fetching data failed:", error);
        });
});
</code></pre>
<p>});</p>
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/adobe-line-chart-pdf.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="1600" height="752" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/adobe-line-chart-pdf.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/04/adobe-line-chart-pdf.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/adobe-line-chart-pdf.png 1600w" sizes="(min-width: 720px) 720px"/></figure><hr><h3 id="step-5-include-javascript-in-page-layout"><strong>Step 5: Include JavaScript in Page Layout</strong></h3><p>Finally:</p><ul><li>Include both JavaScript files (the library and your implementation script) inside the <strong>Page Layout</strong> of the PDF output configuration.</li></ul><p>This ensures that when the PDF is generated, both the library and your custom logic are loaded correctly.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/-Including-JavaScript-in-Page-Layout.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="720" height="372" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/-Including-JavaScript-in-Page-Layout.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/-Including-JavaScript-in-Page-Layout.png 720w" sizes="(min-width: 720px) 720px"/></figure><h3 id="step-6-enable-javascript-in-pdf-generation-settings"><strong>Step 6: Enable JavaScript in PDF Generation Settings</strong></h3><p>When generating the PDF:</p><ul><li>Go to the <strong>Output Preset</strong> settings.</li><li>Make sure the <strong>"Enable JavaScript"</strong> option is <strong>checked</strong>.</li></ul><p>Without enabling this option, dynamic scripts won't execute while generating the PDF, and charts will not appear.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/Enabling-JavaScript-during-PDF-generation.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="1510" height="767" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/Enabling-JavaScript-during-PDF-generation.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/04/Enabling-JavaScript-during-PDF-generation.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/Enabling-JavaScript-during-PDF-generation.png 1510w" sizes="(min-width: 720px) 720px"/></figure><hr><p>That’s it!</p><p>Now, when you generate the PDF, your chart will render properly inside the document. The polar chart data can be authored as needed, and upon PDF regeneration, the chart will reflect the updated information accordingly.</p><h2 id="part-2-graph-integration-for-sites-output"><strong>Part 2: Graph Integration for Sites Output</strong></h2><p>Now, let's see how you can integrate the same graph into AEM Sites output (for web).</p><p>Fortunately, the process is even simpler.</p><hr><h3 id="step-1-import-library-in-clientlibs"><strong>Step 1: Import Library in Clientlibs</strong></h3><p>In AEM Sites:</p><ul><li>Navigate to <strong>Client Libraries (clientlibs)</strong>.</li><li>Create or update a <strong>clientlib</strong> specific to your project.</li><li>Add the charting library's JavaScript file inside the clientlib's JS folder.</li></ul><p>Example structure:</p><p>/apps/your-site/clientlibs/yourclientlib/js/chart-library.js<br>
/apps/your-site/clientlibs/yourclientlib/js/custom-graph-implementation.js</br></p>
<p>Ensure the clientlibs are properly included via categories in your page templates.</p><hr><h3 id="step-2-write-custom-implementation-for-sites-output"><strong>Step 2: Write Custom Implementation (for Sites Output)</strong></h3><p>Inside your custom JavaScript file (e.g., custom-graph-implementation.js):</p><ul><li>Target the same container class (e.g., .chart-container).</li><li>Write the logic to render the graph when the page loads or when the DOM is ready (DOMContentLoaded or similar event).</li><li>Support both <strong>static</strong> and <strong>dynamic</strong> data sources:<ul><li><strong>Static Data</strong>: Hardcoded inside the JS itself.</li><li><strong>Dynamic Data</strong>: Fetched from external APIs, AEM Content Fragments (via GraphQL), AEM DAM JSON, or custom AEM servlets.</li></ul></li></ul><p><strong>Important for Sites Output:</strong></p><ul><li><strong>By default</strong>, if you fetch data dynamically, it will be <strong>retrieved in real-time</strong> every time the page is rendered.</li><li><strong>However</strong>, if real-time fetching is not desired (for performance or consistency reasons), you can <strong>configure AEM</strong> to <strong>fetch the data once during publish time</strong> and <strong>cache it for all subsequent page loads</strong>.</li><li>This way, the chart will behave like it’s loading <strong>static data</strong> even though it originally came from a dynamic source.</li></ul><p><strong>Example:</strong></p><p>document.addEventListener('DOMContentLoaded', function() {<br>
var chartContainer = document.querySelector('.chart-container');<br>
if(chartContainer) {<br>
drawPolarChart(chartContainer);<br>
}<br>
});</br></br></br></br></br></p>
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/Sample-Sites-output-structure-and-custom-JS.png" class="kg-image" alt="How to integrate graphs in AEM guides: A step-by-step guide" loading="lazy" width="720" height="420" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/04/Sample-Sites-output-structure-and-custom-JS.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/04/Sample-Sites-output-structure-and-custom-JS.png 720w" sizes="(min-width: 720px) 720px"/></figure><hr><h3 id="step-3-generate-and-test"><strong>Step 3: Generate and Test</strong></h3><p>Once the clientlib is properly included:</p><ul><li>Publish or preview your page.</li><li>You should see the chart render dynamically on your AEM Site page.</li></ul><p>No special settings like "Enable JavaScript" are required here because Sites outputs naturally support dynamic JavaScript rendering.</p><hr><h2 id="important-tips"><strong>Important Tips</strong></h2><ul><li><strong>Testing:</strong> Always test your integration both in <strong>Preview Mode</strong> and after <strong>Publishing</strong>.</li><li><strong>Performance:</strong> Keep the chart library lightweight to avoid bloating the PDF or Site loading times.</li><li><strong>Fallback Handling:</strong> Consider adding fallback content if JavaScript is disabled or the chart fails to render.</li></ul><hr><h2 id="conclusion"><strong>Conclusion</strong></h2><p>Integrating graphs into AEM Guides is a powerful way to make your technical documents and Sites more interactive and visually appealing.</p><p>While PDF generation requires careful handling (due to JavaScript execution limitations), the site's output is relatively straightforward.</p><p>By following the structured approach discussed above — choosing the right library, correctly importing it, writing clean custom scripts, and configuring output settings — you can successfully bring dynamic, informative charts into your AEM-based documentation.</p><p>Happy graphing! </p></hr></hr></hr></hr></hr></hr></hr></hr></hr></hr></hr></hr></hr>]]></content:encoded></item><item><title><![CDATA[AI can’t replace experience: Why senior engineers might be more valuable than ever]]></title><description><![CDATA[Recently, I used AI coding tools (Cursor), leading to some crucial reflections for all of us leading engineering teams.]]></description><link>https://engineering.deptagency.com/ai-cant-replace-experience-why-senior-engineers-might-be-more-valuable-than-ever/</link><guid isPermaLink="false">67e57f9a781c3900013ddf10</guid><category><![CDATA[AI]]></category><dc:creator><![CDATA[Jonathan Whiteside]]></dc:creator><pubDate>Mon, 28 Apr 2025 14:05:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/03/coding-with-ai-experience.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/03/coding-with-ai-experience.jpg" alt="AI can’t replace experience: Why senior engineers might be more valuable than ever"/><p>As engineering leaders, many of us find ourselves further from the codebase than we'd like. </p><p>The days of all-night coding sessions fade into memory, replaced by strategic meetings, team management, and architectural oversight. But the itch to build often remains. Recently, I scratched that itch using AI coding tools, leading to some crucial reflections for all of us leading technical teams.</p><h2 id="the-experiment-vibe-coding-on-a-long-haul">The experiment: Vibe coding on a long haul</h2><p>Inspired by "<a href="https://www.youtube.com/shorts/JADQ5FbYem0?ref=dept-engineering-blog.ghost.io" rel="noreferrer">vibe coding</a>" videos showcasing rapid development, I experimented on a long flight. Armed with <a href="https://www.cursor.com/en?ref=dept-engineering-blog.ghost.io" rel="noreferrer">Cursor</a> (an AI-first code editor) and a rough app idea, I dove into tackling frameworks I wasn't familiar with.</p><p>The results were pretty striking. AI handled much of the heavy lifting – setting up dependencies, generating boilerplate code, and navigating unfamiliar territory. Within hours, a non-trivial web app was functional. This demonstrated AI co-pilots' raw speed and potential to accelerate development, particularly in bootstrapping projects or exploring new technologies.</p><h2 id="the-reality-check-the-overly-enthusiastic-junior-dev">The reality check: The "overly enthusiastic junior dev"</h2><p>However, the experience wasn't seamless. It wasn’t far from the experience of collaborating with an "overly enthusiastic junior dev" – fast and full of suggestions, but lacking coherence, consistency, and sometimes making obvious errors (like introducing a redundant CSS framework).</p><p>This, I feel, is where experience is critical. Identifying flawed AI suggestions, debugging generated code, and ensuring architectural soundness requires the seasoned judgment that comes from years of building, shipping, and maintaining software. The AI could generate <em>code</em>, but it couldn't consistently generate <em>wisdom</em>.</p><h2 id="challenging-the-young-persons-game-narrative">Challenging the "young person's game" narrative</h2><p>This experiment prompted reflection on a persistent industry stereotype: is software development still primarily a "young person's game"?&nbsp;</p><p>Statistics support this perception – globally, the largest cohort of developers is 25-34, significantly younger than the average workforce age. The "whizz-kid" archetype endures, often unfairly painting senior engineers as out of touch.</p><p>But if AI tools increasingly automate repetitive tasks and lower the barrier to entry for complex frameworks, the differentiating factor shifts. Boilerplate, syntax, and basic implementation details become less critical. What becomes <em>more</em> critical?</p><ul><li><strong>Judgment:</strong> Knowing <em>what</em> to build and <em>how</em> it fits into the larger picture.</li><li><strong>Trade-offs:</strong> Understanding the long-term implications of technical decisions (scalability, maintainability, security).</li><li><strong>Quality:</strong> Spotting subtle flaws, code smells, and architectural weaknesses before they become major problems.</li><li><strong>Direction:</strong> Guiding the development process, whether the "developer" is human or AI.</li></ul><p>These are the hallmarks of experience. Often, the critical 10% of our skills – the deep understanding and judgment – provides exponential value.</p><h2 id="critical-considerations-for-engineering-leaders">Critical considerations for engineering leaders</h2><p>The rise of AI co-developers isn't just about individual productivity; it forces us, as leaders, to confront significant challenges:</p><ol><li><strong>Developing future seniors:</strong> How do we nurture junior talent? If AI handles the foundational tasks, how do aspiring developers build the deep understanding needed to become tomorrow's seniors? Relying solely on AI risks creating "prompt experts" who lack fundamental coding principles. We must consciously design training, mentorship programs, and team structures that cultivate this deeper knowledge, even as abstraction layers increase.</li><li><strong>Maintaining quality &amp; oversight:</strong> Rapid, AI-generated code demands rigorous validation. Ensuring that human expertise remains in the loop is crucial. Experienced engineers are vital for reviewing AI output, catching subtle errors, and preventing the propagation of bad practices, lest we grumble and clean up messes later.</li><li><strong>Adaptability of senior talent:</strong> Experience is invaluable, but only if coupled with curiosity. Senior engineers must embrace new tools and adapt their workflows. Those who remain stuck in old ways risk becoming less efficient, even with their deep knowledge.&nbsp;</li><li><strong>The power of collaboration:</strong> The ideal future isn't about choosing between youthful energy and seasoned wisdom. It's about combining them. Young devs might bring the fire, but seasoned devs bring the fire extinguisher. Our role as leaders is to foster teams where these strengths complement each other, leveraging AI as a tool for <em>everyone</em>.</li></ol><h2 id="engineering-experience-matters-more-with-ai-code-generation">Engineering experience matters more with AI code generation&nbsp;</h2><p>AI is undeniably reshaping <a href="https://www.deptagency.com/solutions/tech-data/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">software development.</a> But rather than making experienced engineers obsolete, it appears poised to amplify their value. When the grunt work is automated, the focus sharpens on strategic thinking, architectural integrity, and sound judgment – precisely the areas where experience shines.</p><p>As leaders, we must encourage our teams to explore these tools, integrate them thoughtfully into our workflows, and critically evaluate their outputs. Most importantly, we need to actively cultivate and value the deep expertise within our teams and challenge the outdated notion that innovation belongs solely to the young.</p><p>In the age of AI co-pilots, experience isn't just relevant; it's the rudder steering the ship. Let's ensure we're building teams – and a culture – that recognizes its enduring, and perhaps increasing, importance.</p>]]></content:encoded></item><item><title><![CDATA[How to integrate Mailchimp with Next JS and TypeScript]]></title><description><![CDATA[1. Introduction & Overview

In this tutorial, we will be going through how to integrate the email marketing platform Mailchimp with Next.js and TypeScript. When integrating it into an application recently, I ran across numerous issues and thought this tutorial might be useful to others. A GitHub repository is referenced at the end of this tutorial. 

Prerequisites

For your reference, below are the versions we are using in this application. We will be using the app router setup for Next.js.

Ver]]></description><link>https://engineering.deptagency.com/how-to-integrate-mailchimp-with-next-js-and-typescript/</link><guid isPermaLink="false">6799120d269dc20001594bc5</guid><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Dallas Huggins]]></dc:creator><pubDate>Tue, 28 Jan 2025 19:31:23 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/coding-1853305_1280.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/coding-1853305_1280.jpg" alt="How to integrate Mailchimp with Next JS and TypeScript"/><p/><h2 id="1-introduction-overview"><strong>1. Introduction &amp; Overview</strong></h2><p>In this tutorial, we will be going through how to integrate the email marketing platform Mailchimp with Next.js and TypeScript. When integrating it into an application recently, I ran across numerous issues and thought this tutorial might be useful to others. A GitHub repository is referenced at the end of this tutorial.&nbsp;</p><h4 id="prerequisites">Prerequisites</h4><p>For your reference, below are the versions we are using in this application. We will be using the app router setup for Next.js.</p><p><strong>Versions</strong></p><ul><li>Next: 15.1.4</li><li>React: 19.0.0</li><li>TypeScript: 5.6.2</li><li>Node: 22.12.0</li><li>NPM: 10.5.2</li></ul><p>To integrate Mailchimp, you will need to sign up for an account. Install the npm packages <a href="https://www.npmjs.com/package/@mailchimp/mailchimp_marketing?ref=dept-engineering-blog.ghost.io"><u>@mailchimp/mailchimp_marketing</u></a> and <a href="https://www.npmjs.com/package/@types/mailchimp__mailchimp_marketing?ref=dept-engineering-blog.ghost.io"><u>@types/mailchimp__mailchimp_marketing</u></a>. For this tutorial, we are using <code>@mailchimp/mailchimp_marketing</code> version 3.0.80 and <code>@types/mailchimp__mailchimp_marketing</code> version 3.0.21.&nbsp;</p><pre><code>npm install @mailchimp/mailchimp_marketing @types/mailchimp__mailchimp_marketing</code></pre><p>Note, the <a href="https://nodejs.org/api/crypto.html?ref=dept-engineering-blog.ghost.io#nodecrypto-module-methods-and-properties"><u>node:crypto</u></a> module is a built-in module included with Node.&nbsp; It doesn’t require installing, but it needs to be imported into the API endpoint to use the <code>createHash</code> function.&nbsp;</p><h4 id="environment-variables">Environment variables</h4><p>Next you’ll need to set up <code>MAILCHIMP_API_KEY</code>, <code>MAILCHIMP_API_SERVER</code>, and <code>MAILCHIMP_AUDIENCE_ID</code> environment variables. Add these variables to your .env and then include them in your Next config file.&nbsp;</p><p>1. <code>MAILCHIMP_API_KEY</code>: Mailchimp API key</p><p><a href="https://eepurl.com/dyijVH?ref=dept-engineering-blog.ghost.io"><u>This resource</u></a> shows how to find an API key in your Mailchimp account.&nbsp;</p><p>2. <code>MAILCHIMP_API_SERVER</code>: Mailchimp server value</p><p>To find the server value for your account, login to Mailchimp. After authentication, the browser URL will show the server value appended before “admin.mailchimp.com.” For example, if the URL was https://us19.admin.mailchimp.com/ the “us19” portion is the server prefix.&nbsp;</p><p/><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/mail-chimp-url-1.png" class="kg-image" alt="How to integrate Mailchimp with Next JS and TypeScript" loading="lazy" width="1600" height="63" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/01/mail-chimp-url-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/01/mail-chimp-url-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/mail-chimp-url-1.png 1600w" sizes="(min-width: 1200px) 1200px"/></figure><p/><p>3. <code>MAILCHIMP_AUDIENCE_ID</code>: Mailchimp audience ID</p><p>To find the Mailchimp audience ID, go to the Audience section in your account and then go to All contacts, and then to Settings. In the settings, there is an Audience ID field, which you can copy.&nbsp;</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/audience-settings-mailchip.png" class="kg-image" alt="How to integrate Mailchimp with Next JS and TypeScript" loading="lazy" width="1600" height="670" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/01/audience-settings-mailchip.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/01/audience-settings-mailchip.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/audience-settings-mailchip.png 1600w" sizes="(min-width: 1200px) 1200px"/></figure><p/><h2 id="2-endpoint-instructions"><strong>2. Endpoint instructions</strong></h2><p>Since we are using the Next.js app router, we used this path for the endpoint: <code>/src/app/api/mc/subscribeUser</code>. If you are not using the app router, your setup will look slightly different. At the end of this tutorial, I will provide a GitHub repo that you could spin up to test out the setup with your account.&nbsp;</p><p>For route handling, we are using the helpers <code>NextRequest</code> and <code>NextResponse</code> which are imported from next/server. There are other ways to accomplish route handling, check <a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers?ref=dept-engineering-blog.ghost.io#extended-nextrequest-and-nextresponse-apis"><u>this resource</u></a> for more information.&nbsp;</p><p>In this file, we will also be importing Mailchimp, which we installed in the initial steps. We will also be importing <code>createHash</code> from the crypto module (node:crypto), which is included with Node.&nbsp;</p><figure class="kg-card kg-code-card"><pre><code>import { NextRequest, NextResponse } from 'next/server';
import mailchimp from '@mailchimp/mailchimp_marketing';
import { createHash } from "node:crypto";</code></pre><figcaption><p><span style="white-space: pre-wrap;">Imports</span></p></figcaption></figure><p>From here we are ready to set the configuration for Mailchimp. The API key and server environment variables will be passed in as the <code>apiKey</code> and <code>server</code> parameters, as shown below.&nbsp;</p><figure class="kg-card kg-code-card"><pre><code>mailchimp.setConfig({
&nbsp;&nbsp;apiKey: process.env.MAILCHIMP_API_KEY,
&nbsp;&nbsp;server: process.env.MAILCHIMP_API_SERVER,
});</code></pre><figcaption><p><span style="white-space: pre-wrap;">Set config function for Mailchimp module</span></p></figcaption></figure><p>Next, we will be setting up a <code>POST</code> request function. We are only requiring an email to create a list member, but first name and last name will be included if provided. We get the form values passed in using the json method (<code>request.json())</code> and immediately fail if the email is not provided, since this will be required to create a member. We will be sending a failure response using the <code>NextResponse</code> route handler helper.&nbsp;</p><p>We will also check to make sure the audience ID environment variable is available since this will also be required for the request and send a failure response if it is not available.&nbsp;</p><figure class="kg-card kg-code-card"><pre><code>export async function POST (request: NextRequest) {
&nbsp;&nbsp;const body = await request.json();
&nbsp;&nbsp;const { email, firstName, lastName } = body;
&nbsp;&nbsp;if (!email) {
	return NextResponse.json({ error: 'Email is required.' }, { status: 400 });
&nbsp;&nbsp;}
&nbsp;&nbsp;const audienceId = process.env.MAILCHIMP_AUDIENCE_ID;
&nbsp;&nbsp;if (!audienceId) {
	return NextResponse.json({ error: 'Audience required.' }, { status: 400 });
&nbsp;&nbsp;}
&nbsp;&nbsp;try {
	
&nbsp;&nbsp;&nbsp;} catch (error: any) {
	
&nbsp;&nbsp;}
};</code></pre><figcaption><p><span style="white-space: pre-wrap;">Base setup for API endpoint</span></p></figcaption></figure><p>In this implementation, we check if the member exists in Mailchimp for that list so that we can communicate to the user that the failure was due to an account already being created. First, we need to create an MD5 hash of the email using the <code>createHash</code> function provided by the crypto Node module. Then, using the <code>mailchimp.lists.getListMember</code> method, we pass in the audience ID and the email hash.</p><p>If there was a member found, we can check if the status was subscribed. The resource for listing member info is <a href="https://mailchimp.com/developer/marketing/api/list-members/list-members-info/?ref=dept-engineering-blog.ghost.io"><u>available here</u></a> and shows the other values for the status include: "subscribed", "unsubscribed", "cleaned", "pending", "transactional", or "archived". You can choose to handle each of those situations, but here, we are only handling already subscribed members.&nbsp;</p><p>From there, we catch the error response so that it doesn’t fail if the user doesn’t have an account already.&nbsp;</p><p><strong>NOTE</strong>: Mailchimp has a method for adding or updating a list member, which is listed in the resources at the end if you’d prefer to use that method.&nbsp;</p><figure class="kg-card kg-code-card"><pre><code>const emailHash = createHash('md5').update(email).digest('hex');
const isEmailExisting = await mailchimp.lists.getListMember(audienceId, emailHash)
&nbsp;&nbsp;.then((r) =&gt; {
&nbsp;&nbsp;&nbsp;&nbsp;	const isSubscribed = r?.status === 'subscribed';
&nbsp;&nbsp;&nbsp;&nbsp;	return isSubscribed;
&nbsp;&nbsp;})
&nbsp;&nbsp;.catch(() =&gt; false);
if (isEmailExisting) {
&nbsp;&nbsp;	return NextResponse.json({ error: 'Email already subscribed.' }, { status: 400 });
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">Try catch statement with conditional for if list member exists</span></p></figcaption></figure><p>Now we can create the list member. The resource for adding a list member is <a href="https://mailchimp.com/developer/marketing/api/list-members/add-member-to-list/?ref=dept-engineering-blog.ghost.io"><u>available here</u></a>. We will be using the mailchimp.lists.addListMember method and will pass in member data as well as the status of “subscribed.” From there we return the data if the request was successful.</p><figure class="kg-card kg-code-card"><pre><code>const data = await mailchimp.lists.addListMember(audienceId, {
&nbsp;&nbsp;email_address: email,
&nbsp;&nbsp;status: 'subscribed',
&nbsp;&nbsp;merge_fields: {
&nbsp;&nbsp;	FNAME: firstName ?? "",
&nbsp;&nbsp;	LNAME: lastName ?? "",
&nbsp;&nbsp;},
});
return NextResponse.json({ data });</code></pre><figcaption><p><span style="white-space: pre-wrap;">Add list member function</span></p></figcaption></figure><p>Finally, we will handle the error using the NextResponse json helper function (<a href="https://nextjs.org/docs/app/api-reference/functions/next-response?ref=dept-engineering-blog.ghost.io#json"><u>more information here</u></a>). Add the following to the catch statement in the case of an unexpected error.&nbsp;</p><figure class="kg-card kg-code-card"><pre><code>let errorMessage = "";
if (error instanceof Error) {
&nbsp;&nbsp;	errorMessage = error?.message;
} else {
&nbsp;&nbsp;	errorMessage = errorMessage ?? error?.toString();
}
console.error(errorMessage);
return NextResponse.json(
&nbsp;&nbsp;	{ error: "Something went wrong." },
&nbsp;&nbsp;	{ status: 500 }
);</code></pre><figcaption><p><span style="white-space: pre-wrap;">Error handling for API endpoint</span></p></figcaption></figure><p><em>Full snippet for API endpoint:</em></p><figure class="kg-card kg-code-card"><pre><code>import { NextRequest, NextResponse } from "next/server";
import mailchimp from "@mailchimp/mailchimp_marketing";
import { createHash } from "node:crypto";

mailchimp.setConfig({
&nbsp;&nbsp;apiKey: process.env.MAILCHIMP_API_KEY,
&nbsp;&nbsp;server: process.env.MAILCHIMP_API_SERVER,
});

export async function POST(request: NextRequest) {
&nbsp;&nbsp;const body = await request.json();
&nbsp;&nbsp;const { firstName, lastName, email } = body;
&nbsp;&nbsp;if (!email) {
	return NextResponse.json({ error: "Email is required." }, { status: 400 });
&nbsp;&nbsp;}
&nbsp;&nbsp;const audienceId = process.env.MAILCHIMP_AUDIENCE_ID;
&nbsp;&nbsp;if (!audienceId) {
	return NextResponse.json({ error: "Audience required." }, { status: 400 });
&nbsp;&nbsp;}
&nbsp;&nbsp;try {
	// Check if the email exists:
	const emailHash = createHash("md5").update(email).digest("hex");
	const isEmailExisting = await mailchimp.lists
  &nbsp;&nbsp;	.getListMember(audienceId, emailHash)
    &nbsp;&nbsp;	.then((r) =&gt; {
      &nbsp;&nbsp;&nbsp;&nbsp;	const isSubscribed = r?.status === "subscribed";
      &nbsp;&nbsp;&nbsp;&nbsp;	return isSubscribed;
  &nbsp;&nbsp;	})
  &nbsp;&nbsp;	.catch(() =&gt; false);
	if (isEmailExisting) {
  &nbsp;&nbsp;	return NextResponse.json(
    &nbsp;&nbsp;&nbsp;&nbsp;	{ error: "Email already subscribed." },
    &nbsp;&nbsp;&nbsp;&nbsp;	{ status: 400 }
  &nbsp;&nbsp;	);
	}
	// If the email doesn't exist, subscribe:
	const data = await mailchimp.lists.addListMember(audienceId, {
  &nbsp;&nbsp;	email_address: email,
  &nbsp;&nbsp;	status: "subscribed",
  &nbsp;&nbsp;	merge_fields: {
    &nbsp;&nbsp;&nbsp;&nbsp;	FNAME: firstName ?? "",
    &nbsp;&nbsp;&nbsp;&nbsp;	LNAME: lastName ?? "",
  &nbsp;&nbsp;	},
	});
	return NextResponse.json({ data });
&nbsp;&nbsp;} catch (error: unknown) {
	let errorMessage = "";
	if (error instanceof Error) {
&nbsp;&nbsp;		errorMessage = error?.message;
	} else {
&nbsp;&nbsp;		errorMessage = errorMessage ?? error?.toString();
	}
	console.error(errorMessage);
	return NextResponse.json(
  &nbsp;&nbsp;	{ error: "Something went wrong." },
  &nbsp;&nbsp;	{ status: 500 }
	);
&nbsp;&nbsp;}
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">Full snippet for API endpoint</span></p></figcaption></figure><h2 id="3-ui-instructions"><strong>3. UI instructions</strong></h2><p>We can now set up the form component, which will display a form that results in a list of members being created in Mailchimp. Create a React component that renders a basic form that has a submit form and input fields for email, first name, and last name.&nbsp;</p><p>The file needs to start with the <code>“use client”</code> directive, which designates a component to be rendered on the client side. This should be used when creating interactive user interfaces (UI) that require client-side JavaScript capabilities; see <a href="https://nextjs.org/docs/app/api-reference/directives/use-client?ref=dept-engineering-blog.ghost.io"><u>resource here</u></a> for more information.&nbsp;</p><p>The CSS module file we’re importing (<code>embeddedForm.module.css</code>) has style specific to the site we were working on, so I’ll be glossing over that.&nbsp;</p><figure class="kg-card kg-code-card"><pre><code>  "use client";
import css from "./embeddedForm.module.css";

export function EmbeddedForm() {
&nbsp;&nbsp;return (
	&lt;form onSubmit={subscribeUser} className={css.form}&gt;
  &nbsp;&nbsp;	&lt;h2 className={css.header}&gt;Subscribe to our newsletter!&lt;/h2&gt;
  &nbsp;&nbsp;	&lt;div className={css.inputWrapper}&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;label className={css.inputAndLabel}&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;span className={css.inputFieldLabel}&gt;First name&lt;/span&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;input name="firstName" className={css.inputField} /&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;/label&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;label className={css.inputAndLabel}&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;span className={css.inputFieldLabel}&gt;Last name&lt;/span&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;input name="lastName" className={css.inputField} /&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;/label&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;label className={css.inputAndLabel}&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;span className={css.inputFieldLabel}&gt;Email&lt;/span&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;input name="email" type="email" className={css.inputField} /&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;/label&gt;
  &nbsp;&nbsp;	&lt;/div&gt;
  &nbsp;&nbsp;	&lt;button type="submit" value="" name="subscribe" className={css.button}&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	Submit
  &nbsp;&nbsp;	&lt;/button&gt;
	&lt;/form&gt;
&nbsp;&nbsp;);
}

export default EmbeddedForm;</code></pre><figcaption><p><span style="white-space: pre-wrap;">Base </span><code spellcheck="false" style="white-space: pre-wrap;"><span>EmbeddedForm</span></code><span style="white-space: pre-wrap;"> component</span></p></figcaption></figure><p>Now, we will set up the function that uses our new <code>subscribeUser</code> endpoint which creates a list member in Mailchimp. Since we are rendering a form element, the event parameter type will be <code>FormEvent</code> with <code>HTMLFormElement</code> passed in. We will need to import <code>FormEvent</code> from react to accomplish this.&nbsp;</p><p>We will also need to import <code>useState</code> from React. From there, we can set up the <code>isLoading</code> state that we will use to display a loader component, as well as a message state that will show the user the result after they submit the form.&nbsp;</p><p>Within the <code>subscribeUser</code> function, we will use the <code>preventDefault</code> method that cancels the event if it is a default action, such as if someone hits Submit without filling out any information. Then, we will set the <code>isLoading</code> state to true so that the loading spinner will display while attempting the request.&nbsp;</p><p>The form contains a first name, last name, and email, which will be sent to the Mailchimp endpoint for creating a list member. To retrieve the form data, create a new <code>FormData</code> object using the <code>FormData</code> constructor and pass in the <code>currentTarget</code> property from the event which will have the first name, last name and email values provided in the form.&nbsp;</p><p>From there, we use the fetch function to hit the endpoint we created (<code>/api/mc/subscribeUser</code>) and provide the form data included in the form. We will also need to set the message state to share the status with the user, which will be either a success or error message,&nbsp; and set <code>isLoading</code> to false so that the message displays to the user instead of the loader.&nbsp;</p><figure class="kg-card kg-code-card"><pre><code>const subscribeUser = async (e: FormEvent&lt;HTMLFormElement&gt;) =&gt; {
	e.preventDefault();
	setIsLoading(true);
	const formData = new FormData(e.currentTarget);
	const firstName = formData.get("firstName");
	const lastName = formData.get("lastName");
	const email = formData.get("email");
	const response = await fetch("/api/mc/subscribeUser", {
  &nbsp;&nbsp;	body: JSON.stringify({
    &nbsp;&nbsp;&nbsp;&nbsp;	email,
    &nbsp;&nbsp;&nbsp;&nbsp;	firstName,
    &nbsp;&nbsp;&nbsp;&nbsp;	lastName,
  &nbsp;&nbsp;	}),
  &nbsp;&nbsp;	headers: {
  &nbsp;&nbsp;&nbsp;&nbsp;		"Content-Type": "application/json",
  &nbsp;&nbsp;	},
  &nbsp;&nbsp;	method: "POST",
	});
	const json = await response.json();
	const { data, error } = json;
	if (error) {
  &nbsp;&nbsp;	setIsLoading(false);
  &nbsp;&nbsp;	setMessage(error);
  &nbsp;&nbsp;	return;
	}
	setMessage("You have successfully subscribed.");
	setIsLoading(false);
	return data;
};</code></pre><figcaption><p><code spellcheck="false" style="white-space: pre-wrap;"><span>subscribeUser</span></code><span style="white-space: pre-wrap;"> function</span></p></figcaption></figure><p>After creating the function, add a conditional that displays the <code>CircularLoader</code> component when <code>isLoading</code> is set to true. The <code>CircularLoader</code> is featured in the Github repo, but any loader component will do.&nbsp;</p><p>We also need to add a conditional that displays the message if there is one set, which will happen if the request was successful or not.&nbsp;</p><p><em>Full snippet for UI component:</em></p><figure class="kg-card kg-code-card"><pre><code>"use client";
import { FormEvent, useState } from "react";
import CircularLoader from "@/components/loader/circular-loader";
import css from "./embeddedForm.module.css";

export function EmbeddedForm() {
&nbsp;&nbsp;const [isLoading, setIsLoading] = useState(false);
&nbsp;&nbsp;const [message, setMessage] = useState("");

&nbsp;&nbsp;const subscribeUser = async (e: FormEvent&lt;HTMLFormElement&gt;) =&gt; {
	e.preventDefault();
	setIsLoading(true);
	const formData = new FormData(e.currentTarget);
	const firstName = formData.get("firstName");
	const lastName = formData.get("lastName");
	const email = formData.get("email");
	const response = await fetch("/api/mc/subscribeUser", {
  &nbsp;&nbsp;	body: JSON.stringify({
    &nbsp;&nbsp;&nbsp;&nbsp;	email,
    &nbsp;&nbsp;&nbsp;&nbsp;	firstName,
    &nbsp;&nbsp;&nbsp;&nbsp;	lastName,
  &nbsp;&nbsp;	}),
  &nbsp;&nbsp;	headers: {
  &nbsp;&nbsp;&nbsp;&nbsp;		"Content-Type": "application/json",
  &nbsp;&nbsp;	},
  &nbsp;&nbsp;	method: "POST",
	});
	const json = await response.json();
	const { data, error } = json;
	if (error) {
  &nbsp;&nbsp;	setIsLoading(false);
  &nbsp;&nbsp;	setMessage(error);
  &nbsp;&nbsp;	return;
	}
	setMessage("You have successfully subscribed.");
	setIsLoading(false);
	return data;
&nbsp;&nbsp;};

&nbsp;&nbsp;if (message) {
	return &lt;p className={css.errorMessage}&gt;{message}&lt;/p&gt;;
&nbsp;&nbsp;}

&nbsp;&nbsp;if (isLoading) {
	return &lt;CircularLoader /&gt;;
&nbsp;&nbsp;}

&nbsp;&nbsp;return (
	&lt;form onSubmit={subscribeUser} className={css.form}&gt;
  &nbsp;&nbsp;	&lt;h2 className={css.header}&gt;Subscribe to our newsletter!&lt;/h2&gt;
  &nbsp;&nbsp;	&lt;div className={css.inputWrapper}&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;label className={css.inputAndLabel}&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;span className={css.inputFieldLabel}&gt;First name&lt;/span&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;input name="firstName" className={css.inputField} /&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;/label&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;label className={css.inputAndLabel}&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;span className={css.inputFieldLabel}&gt;Last name&lt;/span&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;input name="lastName" className={css.inputField} /&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;/label&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;label className={css.inputAndLabel}&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;span className={css.inputFieldLabel}&gt;Email&lt;/span&gt;
      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	&lt;input name="email" type="email" className={css.inputField} /&gt;
    &nbsp;&nbsp;&nbsp;&nbsp;	&lt;/label&gt;
    &nbsp;&nbsp;	&lt;/div&gt;
  &nbsp;&nbsp;	&lt;button type="submit" value="" name="subscribe" className={css.button}&gt;
  &nbsp;&nbsp;&nbsp;&nbsp;	Submit
  &nbsp;&nbsp;	&lt;/button&gt;
	&lt;/form&gt;
&nbsp;&nbsp;);
}

export default EmbeddedForm;</code></pre><figcaption><p><span style="white-space: pre-wrap;">Full snippet for UI component</span></p></figcaption></figure><h2 id="4-additional-resources"><strong>4. Additional resources</strong></h2><p>To test this functionality from this tutorial, you can clone this Github repository and change the environment variables to reference your own Mailchimp account: <a href="https://github.com/dallashuggins/mailchimp-nextjs?ref=dept-engineering-blog.ghost.io"><u>https://github.com/dallashuggins/mailchimp-nextjs</u></a>.&nbsp;</p><p>Below are the resources listed throughout this tutorial for quick reference.</p><ul><li><a href="https://eepurl.com/dyijVH?ref=dept-engineering-blog.ghost.io"><u>Find API key for Mailchimp</u></a></li><li><a href="https://www.npmjs.com/package/@mailchimp/mailchimp_marketing?ref=dept-engineering-blog.ghost.io"><u>@mailchimp/mailchimp_marketing</u></a>&nbsp;</li><li><a href="https://www.npmjs.com/package/@types/mailchimp__mailchimp_marketing?ref=dept-engineering-blog.ghost.io"><u>@types/mailchimp__mailchimp_marketing</u></a></li><li><a href="https://nodejs.org/api/crypto.html?ref=dept-engineering-blog.ghost.io#nodecrypto-module-methods-and-properties"><u>node:crypto module</u></a></li><li><a href="https://mailchimp.com/developer/marketing/api/list-members/add-member-to-list/?ref=dept-engineering-blog.ghost.io"><u>Add Members to List</u></a></li><li><a href="https://mailchimp.com/developer/marketing/api/list-members/list-members-info/?ref=dept-engineering-blog.ghost.io"><u>List Members</u></a></li><li><a href="https://mailchimp.com/developer/marketing/api/list-members/add-or-update-list-member/?ref=dept-engineering-blog.ghost.io"><u>Add or Update List Member</u></a></li><li><a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers?ref=dept-engineering-blog.ghost.io#extended-nextrequest-and-nextresponse-apis"><u>Route handling with Next.js</u></a></li><li><a href="https://nextjs.org/docs/app/api-reference/directives/use-client?ref=dept-engineering-blog.ghost.io"><u>Use client directive for Next.js</u></a></li></ul><h3 id="5-conclusion"><strong>5. Conclusion</strong></h3><p>We hope you found this tutorial useful for integrating Mailchimp into your Next.js application.&nbsp;</p><p>We welcome any feedback or questions in the comments here or in the linked Github repo. Thanks for reading!&nbsp;</p>]]></content:encoded></item><item><title><![CDATA[Building a future-ready content integration layer with MACH]]></title><description><![CDATA[Many organizations are pursuing a headless strategy for enterprise content management. A common term to describe this approach is MACH: microservices, API-first, cloud, headless. This architecture consists of many services, deployed in the cloud, communicating and producing data via APIs. In MACH architectures, it's good practice to have an integration layer to synthesize these sources of data before providing it to a front-end.

CMS content is a common integration point, as it's close to the fr]]></description><link>https://engineering.deptagency.com/building-a-future-ready-content-integration-layer-with-mach/</link><guid isPermaLink="false">675b285133c33c0001c1b90e</guid><category><![CDATA[cms]]></category><dc:creator><![CDATA[Jon Madden]]></dc:creator><pubDate>Tue, 14 Jan 2025 16:49:28 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/architecture-concept.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/architecture-concept.jpg" alt="Building a future-ready content integration layer with MACH"/><p>Many organizations are pursuing a headless strategy for enterprise content management. A common term to describe this approach is MACH: microservices, API-first, cloud, headless. This architecture consists of many services, deployed in the cloud, communicating and producing data via APIs. In MACH architectures, it's good practice to have an integration layer to synthesize these sources of data before providing it to a front-end. </p><p>CMS content is a common integration point, as it's close to the front-end and benefits from a view of all systems whose data ends up exposed to the end user. Recognizing this, many CMS platforms have a marketplace for integrations with third-party services. However, for various reasons you may want to implement your own integration layer.</p><p>One reason to build your own integration layer is gaps in the integration marketplace. While these marketplaces are mature and offer integrations with many services, they generally depend on the service provider to build and maintain the integration. As such, an integration may be available in one CMS platform but not another. Additionally, because these integrations must cater to all users of the CMS they are typically designed for the most standard use cases and may not work for your situation. Similarly, if your software architecture includes custom-built services that must be synthesized with managed content, you'll need to build the integration yourself.</p><p>You may also want to avoid pre-made integrations to avoid vendor lock-in.<strong><em> </em></strong>Building your own integration layer allows you (but doesn't require you) to separate the CMS platform from the synthesis of content with external data. This kind of separation can be beneficial if you want to replace your current CMS platform, or if you're adding a new one but see some risks that may necessitate a change. </p><p>In this post I'll detail how we built a performant and scalable system of custom integrations that empowered editors to conduct A/B tests and other experiments without depending on developers.</p><h2 id="what-we-built">What we built</h2><p>This retail client brought us in to replace a homegrown CMS, feeding their React Native app, with the enterprise platform <a href="https://www.deptagency.com/partner/optimizely/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">Optimizely</a>. They wanted to integrate personalization, experimentation, and a product information management system. In addition, to save on platform costs and boost performance, they needed thorough server-side caching. </p><p>We delivered on these requirements while also providing front-end access to all integrated data in a single network call. A GraphQL Backend-for-Frontend microservice was built to orchestrate the various systems and serve enriched content. </p><p>Configurable Redis and in-memory caching were also added. But we wanted to knock it out of the park with performance and leave their lean engineering team with a way to quickly build new integrations. </p><p>To accomplish both of these goals, we designed and implemented a unique content enrichment module.</p><h2 id="the-content-enrichment-module">The content enrichment module</h2><p>We had three goals in mind when designing the module:</p><ol><li>To ensure world-class user experience, execute as efficiently as possible by<ol><li>minimizing time waiting for network calls to external services, and </li><li>avoiding redundant iteration of content nodes</li></ol></li><li>To support the client's lean engineering team, allow developers to add new integrations without needing to manipulate the content tree</li><li>To avoid vendor lock-in, don't couple it to any particular CMS platform or tech stack</li></ol><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/content-enrichment-layer-architecture-diagram---b-w.png" class="kg-image" alt="Building a future-ready content integration layer with MACH" loading="lazy" width="2000" height="1047" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/01/content-enrichment-layer-architecture-diagram---b-w.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/01/content-enrichment-layer-architecture-diagram---b-w.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2025/01/content-enrichment-layer-architecture-diagram---b-w.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w2400/2025/01/content-enrichment-layer-architecture-diagram---b-w.png 2400w" sizes="(min-width: 1200px) 1200px"/></figure><p>Content enrichment is a hot path—mobile screens make a content request each time they're loaded—so we needed to go beyond standard caching. Knowing that integration performance is primarily constrained by network requests, we decided to put some limitations on how new integrations are built:</p><ol><li>To update a content block, developers must first define<em> </em>which external data<em> </em>they need. Then, they can define how the data changes the block. </li><li>All external data must be retrieved by implementing a special interface and allowing the module to control how and when data is requested.</li></ol><p>These rules limit developer flexibility when building new integrations. However, in exchange, we get some really nice benefits. </p><p>First, by requiring developers to define enrichment data requirements and entrusting the module to make requests, we can ensure we never request duplicate data and that all data of the same type are requested at once. </p><p>Second, development of a new integration is simple and therefore fast—just implementing a couple of interfaces—and carries low risk of defects. </p><p>Finally, because application of changes is defined on a block-by-block basis and the module handles the complex (and thoroughly tested) task of mutating the content tree, developers spared from understanding the plumbing and can focus on the important business logic. </p><p>Taken together, this means that when developers add new integrations, the most efficient implementation is actually the easiest to build! This module enabled us to quickly and efficiently build out powerful capabilities for content editors, including no-code feature flags, no-code experiments, and real-time native mobile preview.</p><h2 id="the-best-code-is-no-code">The best code is no code</h2><p>When implementing a content architecture it's crucial to consider the editor experience. Editors are responsible for meeting and adapting to business goals and technology is what enables them to do so. However, while technology serves the editorial experience, it can also get in the way. The slow pace of change requests and software releases can severely limit editors' need to adapt to changing business requirements. </p><p>Our client wanted to roll out a full suite of experiments with their new system. For the system to keep up, editors couldn't be made to depend on engineering changes for each experiment. To empower editors, we designed special meta-blocks which allowed A/B tests to be fully implemented from within the CMS—no tickets or PRs required! These blocks connect multiple bits of content to an experiment key and variation flag, which allows the orchestration service to ask the experimentation platform which content to serve to the user. In a similar way, meta-blocks also allow editors to feature-flag their content blocks so they can build while front-end components are still being developed. And, because it's all implemented with our enrichment module, editors can add as many experiments as they need without impacting performance. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/Untitled-3.png" class="kg-image" alt="Building a future-ready content integration layer with MACH" loading="lazy" width="1066" height="570" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2025/01/Untitled-3.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2025/01/Untitled-3.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2025/01/Untitled-3.png 1066w" sizes="(min-width: 720px) 720px"/></figure><h2 id="setting-a-foundation">Setting a foundation</h2><p>To summarize, we built a content integration microservice that uses flexible caching and a unique enrichment module to serve personalized content. In addition to superior customer experience, we helped content editors run A/B tests and feature-flag content without waiting for an engineer to code it. Finally, we improved developer velocity by freeing them from the nitty-gritty of integration code to focus on the big picture. The client team loved what we built, and are now using the design as a foundation for their enterprise content architecture.</p><h2 id=""/>]]></content:encoded></item><item><title><![CDATA[Code cuisine: A recipes app built using the latest Android architecture (2024)]]></title><description><![CDATA[This project aims to showcase many of the latest tools and best coding practices recently introduced by Google for Android development.]]></description><link>https://engineering.deptagency.com/code-cuisine-a-savory-recipe-app/</link><guid isPermaLink="false">663d2e98cce62600017f6ae5</guid><category><![CDATA[Android]]></category><dc:creator><![CDATA[Nomaan Khalid]]></dc:creator><pubDate>Thu, 20 Jun 2024 11:55:41 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/06/Android-architecture-2024.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/06/Android-architecture-2024.jpg" alt="Code cuisine: A recipes app built using the latest Android architecture (2024)"/><p>Welcome to Simple Recipes. </p><p>This app simplifies the way you discover and manage your favorite recipes.  It provides a user-friendly interface made using Jetpack Compose and ensures effortless navigation and accessibility of data for both an online and offline experience.</p><p>This project aims to showcase many of the latest tools and best coding practices recently introduced by Google for Android development. It demonstrates the latest architecture that should now be the standard for all new apps. Implementing this foundation in any new project can instantly address several problems from past development practices. These include boilerplate code, unresponsive UI, tightly coupled components, and a lack of testability and scalability.</p><hr><h3 id="setting-the-stage-with-flavorful-kotlin-and-gradle">Setting the stage with flavorful Kotlin and Gradle</h3><p>At the core of this app is a pure Kotlin codebase. With Gradle as our build system, managing dependencies using version catalogs allows us to add and centralize dependencies and plugins in a scalable way. A fresh configuration or migration are both made easy when using a TOML file for version catalog. This specifies libraries, plugins, and version comprehensively to avoid hardcoding dependency names and scattered versions in individual files. The build process is further accelerated by using the new Kotlin KSP API, which enhances the power of Kotlin. Unlike KAPT, KSP can processes annotations directly in Kotlin code, which enables annotation processors to run up to 2x faster. </p><p>For more info, see: <a href="https://github.com/google/ksp/blob/main/README.md?ref=dept-engineering-blog.ghost.io" rel="noreferrer">Github/KSP</a></p><h3 id="separation-and-prep-for-cooking-success">Separation and prep for cooking success</h3><p>Emphasizing the modular approach to app development, the code is separated into distinct layers. The domain layer contains the business logic, entities, and use cases. The data layer handles data access, manipulation, and interactions with remote and local data sources. Finally, the UI layer focuses on data presentation and event handling. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/06/Screen-Shot-2024-06-07-at-10.48.41-AM.png" class="kg-image" alt="Code cuisine: A recipes app built using the latest Android architecture (2024)" loading="lazy" width="1630" height="710" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/06/Screen-Shot-2024-06-07-at-10.48.41-AM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2024/06/Screen-Shot-2024-06-07-at-10.48.41-AM.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2024/06/Screen-Shot-2024-06-07-at-10.48.41-AM.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/06/Screen-Shot-2024-06-07-at-10.48.41-AM.png 1630w" sizes="(min-width: 720px) 720px"/></figure><p>Koin plays a crucial role in facilitating this approach by providing dependency injection. This promotes modularity and testability by decoupling components and excellent management of component dependencies.</p><h3 id="whipping-up-connectivity-with-retrofit">Whipping up connectivity with Retrofit</h3><p>In a world that vastly relies on networking, Retrofit reigns supreme. For a number of years, Retrofit has stood strong as the leading, most developer-trusted library to simplify the process of making HTTP requests and parsing responses. </p><p>By transforming complex networking into straightforward reusable methods, it enhances readability and maintainability. In a well-structured architecture, only the data layer is aware of data sources. This ensures that changes to the data fetching logic do not affect the UI layer, which simply only makes the data requests. Further, Kotlin Coroutines cohesively works alongside Retrofit to ensure a responsive user experience by streamlining background tasks.</p><h3 id="spicing-up-storage-with-room">Spicing up storage with room</h3><p>With Room, managing recipe data becomes a piece of cake (Pun maybe intended, maybe not). As a powerful ORM solution, Room has become the industry-leading solution for managing persistent data in modern Android applications. Configuring Room starts with creating a database class that specifies entities. Next, entities are defined as data classes where each field represents a column in the table. Finally, a DAO (Data Access Object) manages all the requied database operations. </p><p>Room has introduced robust capabilities to perform database operations asynchronously, and compile time verification of SQL queries never seen before in the world of Android. It seamlessly integrates with other Jetpack components such as ViewModel and StateFlows, and enables a reactive data flow to ensure that the UI remains up to date with the underlying data, and ensures a truly responsive user experience.</p><h3 id="composing-the-cherry-on-top">Composing the cherry on top</h3><p>Jetpack Compose has transformed the way UIs are crafted. Gone are the days of boilerplate XML layout, with forced view binding and a non-responsive layout. With Compose, UI becomes an art form. Components are carefully designed and provide excellent scalability to be reused throughout the app. With its declarative approach and intuitive APIs, UI updates are seamlessly ingrained and straightforward animations empower the developers to bring their UI visions to life.</p><p/><p>Please explore the complete application by visiting the following link: <a href="https://github.com/deptagency/SimpleRecipes?ref=dept-engineering-blog.ghost.io" rel="noreferrer">Simple Recipes</a></p><p>For developers interested in elevating this architecture for their own apps, a skeleton can be found on this link: <a href="https://github.com/deptagency/AndroidStarterCode2024?ref=dept-engineering-blog.ghost.io" rel="noreferrer">Android Starter Code (2024)</a></p></hr>]]></content:encoded></item><item><title><![CDATA[Adobe Experience Manager authoring capabilities in 2024]]></title><description><![CDATA[In this article, we’ll take a look at everything AEM has to offer to an enthusiastic content author with a penchant for productivity.]]></description><link>https://engineering.deptagency.com/adobe-experience-manager-authoring-capabilities-in-2024/</link><guid isPermaLink="false">664f69d21666a00001531bb1</guid><category><![CDATA[Adobe]]></category><dc:creator><![CDATA[Oleg Sidorenko]]></dc:creator><pubDate>Fri, 24 May 2024 15:58:13 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/poster-eds.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/poster-eds.jpg" alt="Adobe Experience Manager authoring capabilities in 2024"/><p>Sitting in the “leaders” spot of CMS analysts’ reports for the last 15+ years, AEM has long been known as the content author’s paradise. </p><p>Although the way content editing works in 2024 is very different from what it was a decade ago, Adobe is a major player in this evolution. With the rise of SPAs and frontend frameworks, an abundance of new channels consuming content as structured data, and everything going cloud, we’re happy to see AEM continue to set standards and stay ahead of the pack. In this article, we’ll take a look at everything AEM has to offer to an enthusiastic content author with a penchant for productivity.</p><h2 id="aem%E2%80%99s-classic-webpage-editor">AEM’s classic webpage editor</h2><p>Funny how time flies —&nbsp;the new state-of-the-art website management "Touch" UI launched with AEM 6.0 in 2014 is what we now (unofficially) call “classic”, while the legacy UI called “Classic” proper is but a fading memory… Anyway, that’s what this editor is —&nbsp;a classic standard for visual component-based webpage assembly with everything edited in context, much copied across the industry.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/1.png" class="kg-image" alt="Adobe Experience Manager authoring capabilities in 2024" loading="lazy" width="1600" height="850" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/05/1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2024/05/1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/1.png 1600w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">AEM standard page editor</span></figcaption></img></figure><p>Authors create pages based on templates. Templates are user-editable and very flexible —&nbsp;they can carry collections of allowed components, apply policies to configure the same components differently for each template, and drive structure with mandatory and optional components and initial content.</p><p>Once in the page editor, authors drop components from a sidebar or click to add them where they need them. Later they can just drag the components around the page into a new position. Selecting a component in the page brings up relevant commands and the dialog for component configurations, behaviours and styling. The page’s own metadata like titles, SEO properties, or data and commerce integrations is also quickly accessed from the page editor.</p><p>Importantly, this editor is where we also work with Experience Fragments (XFs). This allows authors to extract random parts of webpages with one or more components and extract them into reusable preformatted blocks. This is a great way to author and single-source offers, banners, and such&nbsp;— edited in separation and without a URL of their own, XFs can be placed in many pages and updated across multiple locations with a single edit. XFs are also easily pushed from AEM to Adobe Target to power testing and personalization activities — that way, an AEM author can create XF-based offers, and Target users can apply them as needed.</p><p>From the editor, authors have immediate access to AEM Assets, the central DAM holding all of the images, videos and documents. They can navigate DAM locations and filter assets by titles or metadata like tags or media type, then drag directly into the page or into component dialogs. No need to link multiple crops of the same image for responsive websites like with many other CMS —&nbsp;AEM Assets can take care of these automatically, so only one master asset needs to be managed and placed in pages.</p><p>This is the proven experience in managing web-channel content specifically. And when some content is common across several channels, that data is managed in a headless fashion but placed in webpage components just as easily as content fragments. With many companies recently learning the hidden costs of going fully headless the hard way, the WYSIWYG webpage editor is as popular as ever, upholding the golden mean of modern content management — manage web-only content in visual web components; keep omnichannel data headless and channel-agnostic.</p><p>You might have also heard of the SPA editor. While, indeed, it allows editing single-page applications in AEM, it is not a different editor. You can treat it more like a tech enabler — once configured, it simply allows editing SPAs in the same familiar page editor. Given the complexities and limitations of setting up the SPA editor in AEM, it’s giving way to the new Universal Editor, which we’ll look at later.</p><h2 id="visual-site-tree-management">Visual site tree management</h2><p>Alongside the page editors, AEM offers visual management of website structure in intuitive trees (among other useful views). Any node in that tree functions as a folder and a page at the same time, so just go ahead and create pages under pages without worrying about 404s for folders without any content to display. Pages can be copied and moved in the tree, which directly translates into the URL structure of the published website. This being visual and intuitive is a welcome change from simpler CMS that requires tedious — and pun intended, sluggish —&nbsp;slug and route management for URLs. This is exactly the case with most headless CMS that have no love lost with the web channel, treating it as no more special than any others and not even offering website sitemaps or site searches.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/2.png" class="kg-image" alt="Adobe Experience Manager authoring capabilities in 2024" loading="lazy" width="1600" height="898" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/05/2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2024/05/2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/2.png 1600w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Site tree navigation in AEM Sites</span></figcaption></img></figure><p>This intuitive tree management makes AEM particularly strong at multisite and multi-language management. With AEM’s translation framework facilitating the process, authors maintain copies of translated content as "language masters" that are manually updated after changes accumulate in the base website language. These “language copies” are then dynamically cloned (as “live copies”) for reuse to one or more country sites that need content in that language. For example, France, Switzerland, and Canada sites would clone the content from the French language master branch, and Austria, Germany, and Switzerland would mirror the German language master. Alongside these managed pages, authors can add ad hoc ones at any time on any country site, creating effective combinations of global vs. local content.</p><p>In this browsing UI, once one or more pages or sections are selected, all relevant management, publication, and approval commands are immediately available in context.</p><h2 id="content-fragment-editor">Content Fragment editor</h2><p>So what about headless content? </p><p>AEM natively supports every scenario on a spectrum between traditional ("coupled" or "headful") website operation and fully headless content with API delivery. Every instance of any headless content type in AEM is a directly editable object in the DAM, called a content fragment (CF). Admins can visually model the structure for content fragments (an exercise similar to crafting content models in other CMS), and authors get a slick, focused editor.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/image2.png" class="kg-image" alt="Adobe Experience Manager authoring capabilities in 2024" loading="lazy" width="1600" height="1141" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/05/image2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2024/05/image2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/image2.png 1600w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">AEM's updated Content Fragment editor</span></figcaption></img></figure><p>The editor provides access to CF fields and variations (alternate sets of fields), surfacing the relevant controls for each field type and practical metadata. This editor can also be customized to expose custom commands or additional fields.</p><p>Content fragments are, in essence, JSON data, which immediately allows serving them via AEM’s APIs and GraphQL interface, as well as linking them to the familiar web authoring components. However, their headless nature makes them impossible to preview —&nbsp;the editor can’t know which fields will every possible channel picks and how they will be visualized. And that’s where the Universal Editor comes to the rescue.</p><h2 id="universal-editor">Universal Editor</h2><p>As one of the latest additions to the author’s toolset in AEM, the Universal Editor (UE) delivers on a seemingly simple promise: no matter how your website is engineered, it can be edited in the UE, and your changes will seamlessly flow back to whatever content sources there are behind it. In time, that can even be a mix of Adobe and non-Adobe sources, but for the time being, any websites managed in AEM are supported — traditional (HTL-based), headless, hybrid, SPAs and the latest ultra-performant addition, sites on Adobe Edge Delivery Services.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/4-ue-1.png" class="kg-image" alt="Adobe Experience Manager authoring capabilities in 2024" loading="lazy" width="1176" height="744" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/05/4-ue-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2024/05/4-ue-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/4-ue-1.png 1176w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">AEM's Universal Editor</span></figcaption></img></figure><p>It takes minimum developer effort to add UE compatibility to any website front end. Then the magic kicks in: authors visit the webpage’s URL in the editor, click around and see the page come alive with editable components and their configurations. Moreover, any changes they make sync right back to the content source. If that source is a content fragment — it will be modified without the author needing to find that fragment and change it through the non-visual CF editor. And if the source is a traditional page or an EDS website, the changes will persist in AEM's content repository just as well.</p><p>Most practical editing features, like the rearrangement of components on the page and adding new ones, are already supported, but as of 2024, the UE is still new and not yet at feature parity with the classic page editor. Yet it’s easy to envision the future where it fully takes over, replacing the Touch UI page editor first for the use cases covered with the SPA editor and gradually for all the other ones. E.g., with Edge Delivery Services, Adobe is betting on the UE rather than the classic editor to provide visual authoring for content natively stored in AEM (as opposed to content served via cloud documents — more on that below).</p><h2 id="aem-and-edge-delivery-services">AEM and Edge Delivery Services</h2><p>First things first: EDS is not a feature of AEM, it is actually a globally distributed content delivery infrastructure used by AEM as well as other Adobe solutions like Experience Platform and Adobe I/O. However, AEM benefits immensely from the ability to deliver lightweight websites at top speeds with Lighthouse 100 web vitals thanks to this global delivery layer and content loading optimisations.</p><p>As far as authors are concerned, this very new addition to AEM also brings a radically new concept of document-based authoring. The underlying idea is simple: every webpage starts as a document somewhere. It undergoes some editing, proofreading, and approvals —&nbsp;but then it has to be <em>copied and pasted</em> into CMS components or content fragments piece by piece. If only we could skip the copypasting and just continue using that document as a page source directly as is! Well, with EDS, you do exactly that: put your docs and sheets in Microsoft Office 365 or Google Workspace in a folder structure that mimics the website, and their content magically turns into your EDS website’s pages. Talk about shortening the learning curve! Was there any resume in the last 20 years where the candidate did not claim to be an expert in Office apps? With EDS in AEM, that’s all you need to author professional websites (although it’s not the only way to for author EDS).</p><p>With very few simple formatting conventions, documents feed the pages and their metadata, and sheets power the forms, tables and configurations. Adding a component is just a matter of adding a table with data.</p><p>It’s important to remember that cloud documents are not the only way to publish to EDS. AEM can deliver content that it stores and manages natively to EDS as well —&nbsp;authors edit it using the Universal Editor. Being native to AEM, such websites support most AEM features including multi-site management.</p><p>It is also possible to use EDS to render super-performant Adobe Commerce web fronts and easily extend the standard PDP pages with custom offers.</p><p>EDS is a great solution for smaller projects and campaigns, but it’s not too difficult to start migrating entire website sections. With AEM as a Cloud Service, Adobe provides CDN-level controls to serve parts of the same website with EDS and others from AEM Publishers. The authors would simply edit different parts of the website in different editors offered by AEM.</p><h2 id="dita-editor-for-aem-guides">DITA editor for AEM Guides</h2><p>Say what? Let’s unpack that a little:)</p><p>Editing technical documentation and manuals, product information sheets, legal documents and annual reports, corporate filings and other thorough, well-organised or regulated information is a world in itself. A world where a lot depends on consistent application of terminology and convenient reuse of copy fragments, assembly from modular copy blocks, versioning, and easy repurposing of text for publishing in various formats.</p><p>DITA is an XML-based file format purpose-built for exactly those purposes. Modular “topics” are authored with clear structure (think focused content blocks) and can reference other topics. They are then assembled into maps that represent articles or collections of articles. With AEM Guides, DITA data is natively supported with another dedicated editor (as well as with a FrameMaker integration for those who prefer it for DITA work).</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/AEM-Guides-UI.png" class="kg-image" alt="Adobe Experience Manager authoring capabilities in 2024" loading="lazy" width="2000" height="1098" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/05/AEM-Guides-UI.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2024/05/AEM-Guides-UI.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2024/05/AEM-Guides-UI.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/AEM-Guides-UI.png 2000w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">DITA editor in AEM Guides</span></figcaption></img></figure><p>With AEM Guides, the base experience can be likened to working with content fragments. Authors pick templates that define topic structure similar to how the CF models define CF structure. Unlike CFs, a DITA topic template is not as restrictive — authors can repeat and rearrange elements. The elements represent different sections like body text, headings, info blocks, process steps, tables, etc. DAM media is immediately available for placement as it sits in the same repository&nbsp;— AEM Guides is built on top of AEM Assets.</p><p>AEM Guides authors also get DITA-specific tools to assemble topics into maps and then further link some maps into larger maps, depending on how they organize their body of work. They have explicit control over document versions and metadata to automate the data-driven assembly of documentation for different products or customers. Right from the editor, they can take part in reviews and then publish the final maps as website content, PDF, or other document formats — or serve the content headlessly. The look and feel are defined in templates for each individual output type, so DITA editors don’t need to worry about anything but the logical structure of their content.</p><p>The modular nature of DITA content makes it very practical for translation. Rather than send entire large documents for (re)translation, with modular content, it’s easy to only translate new or changed topics, saving time and budgets. And the whole process is managed with AEM’s native translation framework, so the incoming translations are directly integrated into the right context, avoiding error-prone copypasting.</p><p>DEPT® is one of very few Adobe solution partners in the world implementing AEM Guides for our clients worldwide. If modular document management sounds like something your business needs, we can explain everything you need to know about the solution and implement it on your existing or new AEM setup.</p><h2 id="dept%C2%AE-as-your-adobe-partner">DEPT® as your Adobe partner</h2><p>We skipped over a couple of things, like the visual editor for Adaptive Forms in AEM Forms or most things DAM, but we are always there to cover anything and everything AEM or Adobe Experience Cloud if you need help or consultation. Do reach out, and we’ll help you get the most out of your Adobe investment with the help of our 500+ experts worldwide.</p><p>As an <a href="https://www.deptagency.com/partner/adobe/?ref=dept-engineering-blog.ghost.io"><u>Adobe Experience Cloud solution partner</u></a>, we take in the requirements of your business and implement AEM to make sure it is set up to bring the most value for your investment. A big part of that is not tech —&nbsp;it’s the process for efficient management of your content operations and content supply chain. We help you set up and optimize the content practice and enable authors to be productive and self-sufficient in their content entry and management activities. </p><p>Together, we assess the most suitable setup to help your editors do their job with the least friction and implement your AEM websites and headless content pipelines using an optimum combination of the options explained in the article. And suppose ever the capacity of your in-house content teams maxes out. In that case, we step in to offer quick and competent authors who take care of your BAU tasks while at the same time contributing improvements for the optimization of the process and toolset. Get in touch!</p>]]></content:encoded></item><item><title><![CDATA[How to break away from the standard ChatGPT interface]]></title><description><![CDATA[How can we have a stylized and easily readable UI from ChatGPT? What we originally thought was simple turned out to be more challenging. ]]></description><link>https://engineering.deptagency.com/how-to-break-away-from-the-standard-chatgpt-interface/</link><guid isPermaLink="false">664cc7411666a00001531a6b</guid><category><![CDATA[AI]]></category><category><![CDATA[Chatgpt]]></category><dc:creator><![CDATA[Corey Tess]]></dc:creator><pubDate>Thu, 23 May 2024 11:46:29 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1444703686981-a3abbc4d4fe3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fHN0YXJ8ZW58MHx8fHwxNzE2Mzk4ODQyfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1444703686981-a3abbc4d4fe3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fHN0YXJ8ZW58MHx8fHwxNzE2Mzk4ODQyfDA&ixlib=rb-4.0.3&q=80&w=2000" alt="How to break away from the standard ChatGPT interface"/><p>A while back, someone tossed around this cool idea to build an app that would leverage AI to provide users with a goal-oriented product strategy.</p>
<p>By using an LLM, specifically ChatGPT in this case, we could crunch the data of things like market analysis and pricing structures. This all happens with the natural language processing abilities LLMs provide. On top of that, we then get natural language back, which makes things easier to understand and implement.</p>
<p>The basic concept was that a user enters information about a goal or set of goals, and optionally some context about the company. A goal could be something as simple as, “We want to increase conversion rates on our landing page.” The additional context could be anything from what the landing page is for, target audience, amount of traffic, where you advertise this page, or pretty much any other information a user may think applies to achieving that goal.</p>
<p>At first glance, this was dead simple. OpenAI could not have made talking to ChatGPT with their API easier, as long as you want to build an interface similar to a chatbot or assistant. What if you wanted to break out of that design, though? Why would you want to break away from the standard chat interface? Great question, let me explain.</p>
<p>Most use cases with the current batch of AI apps work as a conversation. You ask a question, you get an answer, repeat. But In our use case the responses had more data than just a conversational message. The response we get back after ChatGPT does its magic, contains an initiative a company would undertake to achieve the goal they set out. An initiative will have activities to perform for that initiative, and there are plans to add more items such as details and documents for those activities.</p>
<p>This kind of data isn’t going to work well in a chat bubble. That led to the idea of having a more stylized and easily readable UI, and that led to how we talk to ChatGPT.</p>
<h2 id="unpacking-chatgpt">Unpacking ChatGPT</h2>
<p>As mentioned above, ChatGPT interacts with the user in a chat-like way. You type something in, ChatGPT shows the lovely animated bubble to let us know it is replying, and eventually, you see text typing onto the screen as if someone were typing it in real time. The result is that most ChatGPT apps follow this same pattern and design.</p>
<p>But what happens behind the scenes while ChatGPT is “typing” its response?</p>
<p>At first, I thought the answer was simple: streaming data. Upon further digging, however, I learned that ChatGPT uses a type of stream called Server-Sent Events (SSEs). Not only was this format new to me, but it was also not what I was expecting.</p>
<p>In my experience, I usually see streaming data responses in somewhat usable chunks of data, the equivalent of complete sentences as opposed to snippets or phrases. But, with ChatGPT, users get back tokens in each SSEs. Tokens are generally 1-4 characters long. This could be a single punctuation mark or four characters within a word.</p>
<p>Either way, we’re not receiving a complete sentence, and definitely not a usable JSON object in a single response.</p>
<p>While there is an option not to stream the responses and simply let ChatGPT return the complete response when done, in our use case, that option was too slow.</p>
<p>We needed to find some sort of middle ground to get the streaming speed but have complete sets of data to pass to the UI.</p>
<h2 id="building-a-buffer">Building a buffer</h2>
<p>After asking around and getting some direction from others here at DEPT®, it became clear the best solution for creating that middle ground would be to build some sort of buffer. We need a place to catch the data coming from ChatGPT, handle formatting and validating that data, and then pass that on to the rest of the application.</p>
<p>In our app, we ask the user for at least a single input consisting of a business goal, typically something long or short-term. We send prompt ChatGPT with some information, including:</p>
<ul>
<li>The role it is acting as</li>
<li>A desired format</li>
<li>Two to three examples, also called Multi-Shot Prompting</li>
<li>The user's input</li>
</ul>
<p>ChatGPT tokenizes both requests coming in and responses going out. It essentially takes the text and chops it up into small bits for things like validating requests/responses that aren't too large and calculations for billing. The tokens are not usually a full word. The general rule of thumb is roughly four characters is equal to a token, but that’s definitely not a hard rule.<br>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/chatgpt-token-example.png" alt="How to break away from the standard ChatGPT interface" loading="lazy"><br>
<em>Example of how ChatGPT turns text into tokens.</em></br></img></br></p>
<p>Each SSE contains a token from the ChatGPT response, so we need to keep track of what was sent previously and what just came in.</p>
<p>As the store is updated, we constantly check to see if what we have in the store can be used in the UI. Everyone loves RegEx, so we have one here that checks to see if the data returned contains the pieces we need for the UI. Once that condition is satisfied, that entire chunk of the message is passed off to a formatter that converts the text to JSON to be stored in another part of the Svelte store.</p>
<p>When a completed message is found, we also remove it from the main buffer so we don’t have duplicate results.</p>
<p>In our results section of the page we have a UI component that is subscribed to (Svelte magic here again) the results array in the store. As a new complete (i.e. formatted and validated) result gets into the array, the UI updates to display it. The end result from the stream of SSEs ends up looking like this:<br>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/05/chatgpt-interface.png" alt="How to break away from the standard ChatGPT interface" loading="lazy"><br>
<em>The results on the right have been through the buffer and formatter.</em></br></img></br></p>
<h2 id="final-thoughts-and-lessons-learned">Final thoughts and lessons learned</h2>
<p>When we first wrote this buffer system, ChatGPT 3.5 was good at getting us the format we wanted but not great.</p>
<p>Recently, however, both ChatGPT 3.5 and 4 have been updated with the ability to return JSON directly. With this update, the reliability of getting the format we coded the UI around has increased significantly, enough that we’re planning on refactoring it to use the updated JSON output.</p>
<p>Going forward, I think we’ll always need a validator in place to ensure the response we receive contains what we need for the UI. As we continue to explore, I’m hoping we can simplify the validation process and maybe remove the formatting helper altogether.</p>
<p>Alongside the model updates, there have also been updates to the OpenAI library we use for types. That library didn’t support streaming easily when we first wrote this system, so I’d like to revisit that to try and improve efficiency and reliability there as well.</p>
]]></content:encoded></item><item><title><![CDATA[Diagrams as code: Making documentation more useful]]></title><description><![CDATA[Discover a solution to the challenge of maintaining up-to-date documentation, by creating diagrams using code. This approach makes it easier for engineers to create and update documentation, and helps new team members develop mental models based on accurate information.]]></description><link>http://ashwinsundar.com/blog/diagrams-as-code</link><guid isPermaLink="false">65e5023b984e4d0001c5fb2f</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Ashwin Sundar]]></dc:creator><pubDate>Wed, 03 Apr 2024 12:46:11 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/diagram-as-code-concept.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/diagram-as-code-concept.jpg" alt="Diagrams as code: Making documentation more useful"/><p>A major pain point in the process of maintaining documentation is that, while a product is in development, documentation tends to go stale quickly. This can occur for a number of reasons:</p><ul><li>Engineers don't know how to create useful documentation</li><li>Documentation is kept separately from the work being done</li><li>Only a small subset of engineers are tasked with creating and maintaining documentation</li></ul><p>The first problem is a large challenge. Learning how to write good documentation is an entire course. Learning how to create good diagrams is an entire course.</p><p>Fortunately, the last two problems can be partially addressed relatively easily - by saving diagrams as code.</p><h2 id="definitions">Definitions</h2><ul><li><u>Diagram</u>: A visual representation of an engineered system</li><li><u>Code</u>: Text that makes a machine do things</li><li><u>Version Control</u>: A system that tracks atomic changes to a file</li></ul><p>Put that all together:</p><ul><li><u>Diagrams as code</u>: A text file that is parsed to generate an image, and which can be committed to version control.</li></ul><h2 id="why-save-diagrams-as-code">Why save diagrams as code?</h2><ul><li>Diagrams should not be an artistic exercise</li><li>Diagrams should be version-controlled with reliable tools</li><li>Diagrams should be useful for new team members</li></ul><h3 id="1-engineering-diagrams-arent-a-form-of-artistic-expression">1. Engineering diagrams aren't a form of artistic expression</h3><p>Picture this scenario - you construct a perfect symmetrical system diagram, arranging subsystem components in rounded boxes at the vertices of an equilateral pentagon. It is beautiful; it is pristine.</p><p>And then someone decides to add a subsystem.</p><p>The solution is simple:</p><ul><li>Care less about whether boxes in a diagram line up</li><li>Care more about what the boxes actually communicate</li></ul><h3 id="2-diagrams-should-be-committed-to-version-control">2. Diagrams should be committed to version-control</h3><p>Many WYSIWYG/visual-first tools have poor internal implementations of "version-control." These tools typically allow a user to "checkpoint" an image manually. However, the checkpoints often have cryptic names, such as "v.203". If a mistake is made, there is no way to easily figure out the last "good" state of a diagram.</p><p>The solution here is to use a text-based diagramming tool, so one may take advantage of fully-featured version-control systems, such as <code>git</code>. Mistakes can be traced with <code>git bisect</code>.The commit history can easily be searched from the command line. All of the powerful capabilities of <code>git</code> can be used to track changes to a diagram.</p><h3 id="3-diagrams-should-be-useful-to-new-team-members">3. Diagrams should be useful to new team members</h3><p>Finally—and most importantly—diagrams must be useful to new team members. Imagine a new member joining the team who needs to understand the architecture of a codebase. Naturally, they will reach for documentation, but they discover that the documentation is out of date.</p><p>Stale documentation can be worse than no documentation. New team members cannot distinguish stale from up-to-date documentation, and will develop an incorrect mental model of the system. This can be very difficult to correct once the misunderstanding is complete.</p><p>The solution is to keep documentation as close to code as possible. Ideally, it should live in the same repo as the code. Every pull request should involve reviewing relevant documentation, and making updates as needed. Fifteen minutes of extra documentation work in each PR will save significant time trying to re-explain how a system works to a team member who has learned the wrong information.</p><h3 id="how-does-one-actually-create-a-diagram-as-code-diagram">How does one actually create a "diagram as code" diagram?</h3><p>There has recently been a renaissance of "diagram as code" tools. With support from GitHub (including native rendering in repositories), <a href="https://mermaid.js.org/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">Mermaid.js</a> appears to be leading the pack. Other popular options include <a href="https://zenuml.com/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">ZenUML</a> and <a href="https://plantuml.com/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">PlantUML</a>.</p><p>But what about tools like LucidChart, diagrams.net, and Microsoft Visio? These tools are popular for remote whiteboarding sessions. Why can't the outputs of those tools simply be committed to version control?</p>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>Tool</th>
<th>Can be VC'd in e.g. git</th>
<th>Text -&gt; Image</th>
<th>Addressable in PR</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mermaid.js</td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: green">Yes</span></td>
</tr>
<tr>
<td>ZenUML</td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: green">Yes</span></td>
</tr>
<tr>
<td>PlantUML</td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: green">Yes</span></td>
</tr>
<tr>
<td>draw.io/diagrams.net</td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: red">No</span></td>
<td><span style="color: red">No</span></td>
</tr>
<tr>
<td>LucidChart</td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: red">No</span></td>
<td><span style="color: red">No</span></td>
</tr>
<tr>
<td>MS Visio</td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: red">No</span></td>
<td><span style="color: red">No</span></td>
</tr>
<tr>
<td>Cell phone pictures of whiteboards</td>
<td><span style="color: green">Yes</span></td>
<td><span style="color: red">No</span></td>
<td><span style="color: red">No</span></td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<p>In the chart above, the following criteria have been selected:</p><ul><li>Artifacts can be version-controlled in e.g. <code>git</code></li><li>Artifacts can be defined using pure text, which is then parsed to create a diagram</li><li>Artifacts can be atomically addressed in a pull request</li></ul><p>In theory, one may commit any file type to version control. In practice, there is limited value to using version-control to track changes to a .svg or .jpeg file type, file types which are used to represent vector graphics and images, respectively. A .svg contains too much non-value-add information, used to describe what a graphic looks like. The signal-to-noise ratio in a diff'd image file is extremely low, in other words.</p><p>On the other hand, diff'd text files have a much higher signal-to-noise ratio. Each diff'd character corresponds to a visible change in the generated output of the diagramming tool.</p><h2 id="examples">Examples</h2><p>Enough pedantry, let us take a look at a couple of examples. I have taken a liking to a diagramming tool called Mermaid.js lately, so all of the following examples will use that tool.</p><h3 id="example-1-sequence-diagrams">Example 1: Sequence Diagrams</h3><blockquote>...a sequence diagram captures the behavior of a single scenario. The diagram shows a number of example objects and the messages that are passed between these objects within the user case.<br><br>-- Fowler, Martin. UML Distilled: A Brief Guide to the Standard Object Modeling Language. 3rd ed., 2003</br></br></blockquote><p>As the textbook definition alludes to, a sequence diagram can be used to describe any set of systems that share messages. To keep the analogy concrete, let us look at an example of a theoretical message transit service.</p><p>Consider a system composed of an <code>API</code> subsystem, <code>Platform</code> subsystem, and <code>IoT Service</code> subsystem. The API is responsible for handling the external interface. The Platform is responsible for handling "business logic." The IoT Service is responsible for hosting the MQTT messaging service.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Screenshot-2024-03-26-at-5.00.59-PM.png" class="kg-image" alt="Diagrams as code: Making documentation more useful" loading="lazy" width="713" height="383" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/03/Screenshot-2024-03-26-at-5.00.59-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Screenshot-2024-03-26-at-5.00.59-PM.png 713w"><figcaption><span style="white-space: pre-wrap;">sequence diagram for a back-end service</span></figcaption></img></figure><p>A minimalist, clean, and informative diagram (such as the one above) is created with the following mermaid.js code:</p><pre><code class="language-git">sequenceDiagram

participant API as API
participant F as Platform
participant IoT as IoT Service

F-&gt;&gt;IoT: attempt authenticated connection to MQTT broker
IoT--&gt;&gt;F: confirm connection

loop Every 20s
    F-&gt;&gt;API: request messages
    API--&gt;&gt;F: send messages

    F-&gt;&gt;IoT: post message to MQTT broker at topic {deviceID}/{msgId}
end
</code></pre><p>What happens if one wants to add a new database service to the diagram, perhaps in-between the <code>Platform</code> and <code>IoT Service</code> subsystems?</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Screenshot-2024-03-26-at-4.53.34-PM.png" class="kg-image" alt="Diagrams as code: Making documentation more useful" loading="lazy" width="941" height="775" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/03/Screenshot-2024-03-26-at-4.53.34-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Screenshot-2024-03-26-at-4.53.34-PM.png 941w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">updated sequence diagram</span></figcaption></img></figure><p>In a traditional WYSIWYG editor, this task could take some time and incur significant frustration because many distinct GUI elements must be manually moved or re-drawn. Not the case in a text-first diagramming tool:</p><figure class="kg-card kg-code-card"><pre><code class="language-git"> sequenceDiagram
 
 participant API as API
 participant F as Platform
+participant Pg as Postgres DB
 participant IoT as IoT Service
 
 F-&gt;&gt;IoT: attempt authenticated connection to MQTT broker
 IoT--&gt;&gt;F: confirm connection
 
+F-&gt;&gt;Pg: attempt authenticated connection to DB
+Pg--&gt;&gt;F: confirm connection
+
 loop Every 20s
+    F-&gt;&gt;Pg: request timestamp of last message pull 
+    Pg--&gt;&gt;F: send timestamp
+
+    F-&gt;&gt;Pg: update start_timestamp to now
+
     F-&gt;&gt;API: request messages
     API--&gt;&gt;F: send messages
 
+    F-&gt;&gt;Pg: request device ID 
+    Pg--&gt;&gt;F: send device ID
+
     F-&gt;&gt;IoT: post message to MQTT broker at topic {deviceID}/{msgId}
 end
</code></pre><figcaption><p><span style="white-space: pre-wrap;">diff generated with </span><code spellcheck="false" style="white-space: pre-wrap;"><span>git diff --no-index {file1} {file2}</span></code></p></figcaption></figure><p>One new participant and a handful of new messages are all that need to be defined, and Mermaid.js takes care of figuring out how the boxes and arrows should be arranged. As mentioned earlier, every highlighted line in the diff corresponds to a visible change in the diagram. Excellent!</p><h3 id="example-2-activity-diagrams">Example 2: Activity Diagrams</h3><blockquote>Activity diagrams are a technique to describe procedural logic, business process, and work flow.<br><br>-- Fowler, Martin. UML Distilled.</br></br></blockquote><p>Activity diagrams are similar to state diagrams, except that they model the activity of system, as opposed to the various states that a system can exist in. UML purists may cringe at the use of state diagram syntax to describe an activity diagram, but the behavior of a system can still be effectively communicated.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Screenshot-2024-03-26-at-5.04.59-PM.png" class="kg-image" alt="Diagrams as code: Making documentation more useful" loading="lazy" width="636" height="1061" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/03/Screenshot-2024-03-26-at-5.04.59-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Screenshot-2024-03-26-at-5.04.59-PM.png 636w"><figcaption><span style="white-space: pre-wrap;">A complex activity diagram modelling a back-end service</span></figcaption></img></figure><p>Imagine editing this diagram in a WYSIWYG editor. Not fun. In a text-based diagramming tool, the task is a breeze - this entire diagram can be defined in less than 75 lines of code, including comments for clarity:</p><pre><code class="language-shell">stateDiagram-v2
  # State Definitions
  ## Main start conditions
  Q_cache_exists : Cache exists?
  Q_checkLastRecovery : lastRecoveryAttempt &gt; 15 mins?

  ## Composite States
  mbRecovRoutine : Mailbox Recovery Routines
  msgRetrievalRoutine : Message Retrieval Routines

  ## Mailbox Recovery Routines
  retrieveInvalidMbs : SELECT * FROM mailbox \n WHERE errorMsg IS NOT NULL
  errCorrect : Attempt error correction
  writeLog : Write to log
  deletePgError : UPDATE mailbox SET errorMsg = NULL

  ## Message Retrieval Routines
  retrieveValidMbs : SELECT * FROM mailbox \n WHERE errorMsg IS NULL \n AND updatedAt &gt; global.lastKnownUpdatedAt
  checkMsgs : Check for new messages 
  Q_maxRetryExceed : Max retry exceeded?

  ### Success States
  retrieveMsgs : Retrieve messages from Api
  sendToMqtt : Post messages to MQTT broker

  ### Failure States
  removeMbFromCache : Remove Mailbox from local cache
  writeErrToPg : UPDATE mailbox SET errorMsg = json(error)

  # State Transitions
  ## Start state
  [*] --&gt; Q_cache_exists

  ## Mailbox Recovery Routines
  Q_cache_exists --&gt; Q_checkLastRecovery: yes
  Q_checkLastRecovery --&gt; retrieveInvalidMbs: yes
  retrieveInvalidMbs --&gt; mbRecovRoutine
  state mbRecovRoutine {
    [*] --&gt; errCorrect
    errCorrect --&gt; writeLog : correction fails
    writeLog --&gt; [*]
    errCorrect --&gt; deletePgError: correction succeeds
    deletePgError --&gt; [*]
  }

  ## Message Retrieval Routines
  Q_cache_exists --&gt; retrieveValidMbs: no
  Q_checkLastRecovery --&gt; retrieveValidMbs : no
  retrieveValidMbs --&gt; msgRetrievalRoutine
  mbRecovRoutine --&gt; retrieveValidMbs
  state msgRetrievalRoutine {
    [*] --&gt; checkMsgs
    checkMsgs --&gt; retrieveMsgs: Mailbox connection succeeds
    checkMsgs --&gt; Q_maxRetryExceed  : Mailbox connection fails
    Q_maxRetryExceed --&gt; checkMsgs : no
    Q_maxRetryExceed --&gt; removeMbFromCache : yes
    removeMbFromCache --&gt; writeErrToPg
    writeErrToPg --&gt; [*]: sleep 15s
    retrieveMsgs --&gt; sendToMqtt
    sendToMqtt --&gt; [*]: sleep 15s
  }
</code></pre><h2 id="conclusion">Conclusion</h2><p>Prefer diagrams as code.</p><ul><li>It makes developers want to work on documentation because it looks like (and is) code.</li><li>It allows one to take advantage of powerful open-source version-control tools, such as git.</li><li>It helps documentation stay up-to-date and remain useful for new team members.</li></ul>]]></content:encoded></item><item><title><![CDATA[Managing risk in software projects - Six lessons learned from a recent MVP]]></title><description><![CDATA[These are my six picks of lessons learned and challenges we overcame from a recent software project. As Mike Tyson once said, "Everyone has a plan until they get punched in the mouth."]]></description><link>https://engineering.deptagency.com/real-life-software-project-lessons/</link><guid isPermaLink="false">65f45df55185580001012316</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Simon Ingeson]]></dc:creator><pubDate>Wed, 27 Mar 2024 11:36:58 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1624969862293-b749659ccc4e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fGRlYWRsaW5lfGVufDB8fHx8MTcxMDUxMzcyM3ww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1624969862293-b749659ccc4e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fGRlYWRsaW5lfGVufDB8fHx8MTcxMDUxMzcyM3ww&ixlib=rb-4.0.3&q=80&w=2000" alt="Managing risk in software projects - Six lessons learned from a recent MVP"/><p>I've worked on a substantial software project for one of our clients for the last eight months. It was a great and fun project with lots of lessons learned, excellent teammates, and an incredible client. We hit our milestones and the final deadline despite unexpected challenges. In the spirit of sharing what we learned, this post summarizes what we did, what worked, and what didn't.</p><h2 id="about-the-project"><strong>About the project</strong></h2><p>We set out to build a minimum viable product for a new online learning platform with custom native iOS and Android apps and a Node.js backend to support both apps and handle third-party integrations. On the DEPT® side, we added to the team over time, but it consisted of three iOS developers, three Android developers, two backend developers, one designer, and one project manager. The client, an owner of various physical product brands, was pretty new to agile software development, so we helped educate them on the processes involved throughout the project.</p><p>When I joined the project, we had already completed a <a href="https://www.deptagency.com/service/engineering/technology-strategy-architecture/architecture-sprints/?ref=dept-engineering-blog.ghost.io">discovery sprint</a> for the client and had set the scope, requirements, and deadline for the MVP launch—all essential things to set us up for success. </p><h2 id="lessons-learned"><strong>Lessons learned</strong></h2><h3 id="lesson-1-manage-expectations-with-thought-and-care"><strong>Lesson 1: manage expectations with thought and care</strong></h3><p>The first step to ensuring we hit the MVP launch deadline was to get a list of the main features (or epics) and assign a rough estimate for their implementation time. I made a Gantt chart to visualize this for the client, help us determine whether we could achieve our goal, and plan resource usage. However, we stayed flexible and adapted throughout the project. We work in an agile way, after all.</p><p>Unfortunately, that chart put us a couple of months after the desired deadline. One common practice is to add extra time to consider various parameters, such as:</p><ul><li>Feature freeze date milestone</li><li>Final QA and polish</li><li>Security audits</li><li>App Store/Play Store review delays</li><li>Holidays, PTO, and illness</li><li>Unexpected technical challenges</li></ul><p>This list is not exhaustive, but it illustrates that things&nbsp;<a href="https://engineering.deptagency.com/why-software-development-estimates-are-so-often-wrong?ref=dept-engineering-blog.ghost.io">can take a left turn in many ways</a>. So, accounting for these is essential to keeping you honest about the needed time. By accepting more risk, you can significantly reduce the time. The client wanted this in our case, and we obliged. However, we only did this after negotiating some features as stretch goals and setting the expectation that cutting down the time will carry additional risk and may require us to punt features after launch.</p><h3 id="lesson-2-define-roles-and-responsibilities"><strong>Lesson 2: define roles and responsibilities</strong></h3><p>Starting a project with new team members, a client not overly familiar with an agile process, and no officially dedicated PM meant we were on our own in managing ceremonies. To at least get us started, we had daily stand-ups and some weekly meetings with the client to ensure we kept moving forward on features. While it was a rough start, we did get things done as most of the team was very experienced, but the solo Android developer needed help to keep up.</p><p>Eventually, one additional Android developer joined; a month later, a third joined. The client stepped in to help fill the need for a PM to get more structure, although this was a bandaid at best as this person also had other responsibilities, which meant they couldn't focus on keeping a nice backlog of features. It pressured the team to add tickets and determine their requirements and scope, which took away their work time. I tried to take on this role initially, but it did not scale as I had other responsibilities as a tech lead and one of the two backend developers. In short, my workload increased, and my productivity suffered.</p><p>This setup lasted a few months until the temporary PM left the client. We had a good flow going but needed a real PM to step in and take pressure off the team. Finally, the client agreed that we could bring one on. Doing their best to avoid upsetting how we worked, our PM took ownership of the ceremonies and provided a more structured approach to our processes. Once the dust settled, we were full steam ahead.</p><p>We had a typical set of agile ceremonies:</p><ul><li>Daily stand-ups lasted up to 15 minutes.</li><li>Backlog refinement sessions were held twice per sprint and lasted 30-60 minutes. They require detailed features, well-defined scopes, and developers preparing estimates.</li><li>Sprint retrospectives once per sprint for about 1 hour. They are essential to help streamline processes and celebrate wins.</li><li>We held stakeholder meetings as often as needed but usually once weekly for 30-60 minutes. We usually had them without the broader team for efficiency and to avoid polluting their calendars.</li><li>Design reviews once per week for about one hour. These worked great for us as the design was still evolving throughout the project. The whole team could discuss and fine-tune features and requirements based on what was technically feasible.</li></ul><p>We also had a weekly internal developer-only meeting to discuss technical implementations and a meeting with someone on the client's IT team to help unblock us as needed. That last one was important as we used systems set up by the client, so we had limited permissions to adjust configurations on our own.</p><h3 id="lesson-3-be-intentional-with-communication"><strong>Lesson 3: be intentional with communication</strong></h3><p>Ceremonies gave us plenty of time for scheduled synchronous communication with specific contexts and topics to discuss. But, having options for asynchronous and ad-hoc synchronous communication was also crucial for our success. Err on the side of over-communicating. It's vital when working in a fully remote team.</p><p>While we didn't have official communication guidelines, we shared a sense of how we wanted to communicate. In retrospect, formalizing these guidelines would've been helpful, mainly as we added new team members.</p><p>Some examples of asynchronous communication are:</p><ul><li>We used direct and group Slack messages mainly to initiate synchronous huddles. We kept most messages in shared channels and tagged the relevant people to ensure the team was aware of ongoing conversations. Figma was our design tool of choice. Within it, the whole squad sent asynchronous messages (comments) to follow up on specifics and implementation details.</li><li>Backlog tickets held not only the scope and specs of the feature but also comments and references to the design files.</li><li>We had code reviews for the developers to discuss implementation details and keep us honest about code quality.</li><li>Finally, we also had the occasional email for things that didn't fit any other mediums or involved people needing access to the above tools.</li></ul><p>We had a few unspoken rules of thumb when it made sense to move from async messages to a huddle or scheduled meeting:</p><ul><li>Three or fewer involved: typically an unscheduled huddle.</li><li>Three or more involved: should schedule a meeting, ideally on the same day.</li><li>Does my question have follow-up questions?</li><li>Is there likely some back and forth to figure out the answer?</li><li>Is there an issue affecting a feature, and will we need to devise a compromise?</li><li>Do I need technical feedback for a design decision?</li></ul><h3 id="lesson-4-be-flexible-on-tooling"><strong>Lesson 4: be flexible on tooling</strong></h3><p>As you might've guessed from how we communicate, we're big fans of Slack for productivity here. However, as much as possible, we work with the client and their needs, whether it's simply a preference on their end, established systems, or security and compliance reasons. In this case, we used Microsoft Teams. It worked, but we had severe connection issues. These issues caused communication problems and made it hard for us to stay in touch. Thankfully, the client agreed to move most communication to a separate Slack workspace.</p><p>Another tool we used based on the client's preference was Azure DevOps. Unfortunately, in our case, no one on our project team had experience with it. Similar to Teams, it does what you need it to:</p><ul><li>Tracking features and bugs in tickets</li><li>Store source code using Git</li><li>Build and deploy pipelines.</li></ul><p>Due to our inexperience, it took us some time to get familiar with it. Considering all the customization options, we could have set it up better. However, it was too late to make significant changes when our PM joined.</p><p>While it's a bit out of my wheelhouse, we also used Figma for the designs and graphical resources such as icons and colors. I'm glad we did; it is a great tool to collaborate on a design. We all dropped in comments there, asking for clarifications and requesting changes. Cleverly, our designer set up a separate file for the design work ready to be implemented by developers, so we only had to go back and make changes sometimes.</p><p>In an ideal world, you want to pick your favorite tools. Sometimes what you have works out, and sometimes it won't. It's essential to recognize the latter and adjust as needed. If the client is willing, this may be easier, but be flexible and work with the client.</p><h3 id="lesson-5-allow-time-for-testing-third-party-services"><strong>Lesson 5: allow time for testing third-party services</strong></h3><p>The project required some specific technologies. Specifically, streaming videos and storing content in a way that a non-technical person can easily manage. Like tooling, picking these services can make your life as a developer easy or difficult.</p><p>Additionally, as we were building mobile apps with a subscription feature, we were also at the mercy of Apple's App Store and Google's Play Store, neither of which is trivial to work with.</p><p>I won't give away the names of the services we chose here, but I can share some nuggets on things to consider. First, for any third-party service you need to use, take advantage of a trial and build a small proof-of-concept to test it out. It can inform you and the client which option works best for your scenario.</p><p>Second, some services might be optional but will likely help simplify the implementation and speed things up. If so, build up a good case for why the client should budget for that, too. It would likely have helped us out a lot with subscriptions.</p><h3 id="lesson-6-be-prepared-to-compromise"><strong>Lesson 6: be prepared to compromise</strong></h3><p>As mentioned earlier, we set the expectation early with the client that we had taken on a significant risk with a tight timeline. It paid off when we hit a snag with subscriptions and notifications. For both, we relied on some members not directly associated with the project on the client's side. We lacked permission to make the necessary changes ourselves.</p><p>We were halfway through the project timeline when we finally configured subscriptions. I had hoped it would be one of the first significant features we had finished as it carried the most risk. But it got delayed beyond our control. We worked through all that and got it working because we worked with the client to understand the effort put into this and the cause of the delays.</p><p>We had to bump another technically complex feature, which put us at risk of failing to hit our milestones. So we talked it through with the client and compromised a bit. Instead of supporting native notifications on day one, we'd support email notifications. It also meant the frontend developers could avoid that feature almost entirely and instead focus on finishing up other, more important features.</p><h2 id="to-summarize-have-a-plan-but-adapt-as-the-circumstances-change">To summarize, have a plan but adapt as the circumstances change. </h2><p>In the end, we did hit all of our target dates. We've wrapped things up with a big bow and wrote a ton of documentation for future travelers. We impressed the client and made some new friends along the way. And isn't that the most important thing for any journey?</p>]]></content:encoded></item><item><title><![CDATA[Actual portable scripting with Nix]]></title><description><![CDATA[Solving the challenge of scripting in DevOps: Introducing Nix for consistent environments.]]></description><link>https://engineering.deptagency.com/nix/</link><guid isPermaLink="false">659825cceb9611000171ad1e</guid><category><![CDATA[DevOps]]></category><dc:creator><![CDATA[Phil Proctor]]></dc:creator><pubDate>Tue, 12 Mar 2024 11:12:11 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Actual-portable-scripting-Nix.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/03/Actual-portable-scripting-Nix.jpg" alt="Actual portable scripting with Nix"/><p>Scripting is among the most common tasks in the world of DevOps.</p>
<p>But have you ever run into the situation where the moment someone else goes to run one of your scripts, it immediately fails because their environment is slightly different from your own? What about your CI environment, is it different too? Creating consistent environments is a consistent pain point.</p>
<p>So what next? Write up a set of instructions for what utilities need to be installed? Create a <code>Dockerfile</code> with all of the <code>apt-get</code> installs? Do instructions for people, Dockerfile for CI? Containerize everything and map host paths to the host machine's config files and whatnot?</p>
<p>Each of these solutions comes with their own compromises. Additionally, these solutions are very likely to break over time when <a href="https://learn.microsoft.com/en-us/cli/azure/upcoming-breaking-changes?ref=dept-engineering-blog.ghost.io">packages update with breaking changes</a>, packages get removed, or any number of other scenarios. This leads to having to update instructions, Dockerfiles, scripts, and so forth at what is likely a very inconvenient time and after you've already forgotten how those scripts work!</p>
<p>Well, we don't have to deal with that... ok, maybe still a little, but at least our packages won't change underneath us.</p>
<h2 id="introducing-nix">Introducing Nix</h2>
<p>So what is Nix exactly? Just another package manager? It's a bit more than that, but to fully understand, consider how these problems are solved elsewhere. We are likely all familiar at this point with the package management systems that come with various languages.</p>
<ul>
<li>Nuget for C#</li>
<li>NPM for node.js</li>
<li>pip/venv for Python</li>
<li>The list here could get very long...</li>
</ul>
<p>If you think about a generic package management solution for scripting, you are probably thinking about <code>apt-get</code> or <code>yum</code>. These, however, have several distinct disadvantages over the solutions listed above:</p>
<ul>
<li>These utilities are tied to the OS and thus vary significantly between machines (redhat, debian, arch, etc.)</li>
<li>It is unlikely that the versions of the utilities two people end of using are the same (my version of bash is 5.2.15, what's yours?)</li>
<li>They don't provide any package locking type solutions</li>
<li>Each package manager doesn't even have the same library of utilities available</li>
</ul>
<p>Nix fixes all of these problems. Additionally, it fixes it regardless of the OS the user is using and allows you to fully define <strong>everything</strong> about the execution environment of the scripts you are running. Nix is powerful enough that it can even be used to replace the package management systems above, though usually the preferred route is to simply tie into those systems.</p>
<p>Too good to be true? Well, no system is perfect. There are significant drawbacks to Nix, including poor documentation and a difficult to understand configuration language. Fortunately, for our purposes here, these drawbacks will be mitigated by setting up a relatively unchanging scaffolded environment.</p>
<h2 id="bowls-of-nix-flakes">Bowls of Nix flakes</h2>
<p><a href="https://nixos.wiki/wiki/Flakes?ref=dept-engineering-blog.ghost.io">Nix Flakes</a> is the system we use to provide both package locking and execution environment management. I have created <a href="https://github.com/philproctor/nix-script-runner-demo?ref=dept-engineering-blog.ghost.io">this demo here</a> to show a solid setup for using flakes, so let's break down the demo and see how it works!</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/01/nix-shell-1.gif" alt="Actual portable scripting with Nix" loading="lazy"/></p>
<p>From the above demo, you can see that when you run one of the scripts in the demo are run for the first time, all of the dependent utilities for the script are automatically downloaded and made available to the script environment! This means that any utilities that you need in your script, such as <code>jq</code>, <code>kubectl</code>, or just about anything else are automatically pulled without you as a user needing to think about it at all! Your end users do not need to preinstall <strong>anything</strong> except for nix itself!</p>
<h2 id="our-customized-scaffold">Our customized scaffold</h2>
<p>The scaffolding is primarily handled through the <code>flake.nix</code> file, but additionally includes an easy-of-use wrapper for running scripts.</p>
<h3 id="the-run-wrapper">The ./run Wrapper</h3>
<p>The <a href="https://github.com/philproctor/nix-script-runner-demo/blob/main/run?ref=dept-engineering-blog.ghost.io">./run wrapper</a> is used to make executing individual scripts easier, especially for users of your project who are not familiar with the (quite quirky) nix command line. Additionally, the wrapper includes some basic messaging if the user doesn't have the nix tools installed or forgets to specify which script to execute. As most of this wrapper is self-explanatory, let's move on to the guts of our scaffolding: <code>flake.nix</code></p>
<h3 id="diving-in-to-our-customized-nix-flake">Diving in to our Customized Nix Flake</h3>
<p>One of the biggest concerns I have had with using the Nix tooling in projects with a wider and mixed skillset is that we don't want to require everyone on the team or users of the project to need to learn and understand <strong>Yet Another Domain Specific Language™</strong> just to solve package management requirements around our primary scripts. So <a href="https://github.com/philproctor/nix-script-runner-demo/blob/main/flake.nix?ref=dept-engineering-blog.ghost.io">here's out attempt to gain these advantages without requiring users to fully understand Nix</a>.</p>
<p>The basic structure of using <code>inputs = {...</code> and <code>outputs = {...</code> is defined by the <a href="https://nixos.wiki/wiki/Flakes?ref=dept-engineering-blog.ghost.io#Flake_schema">Flake schema</a>. You may notice that we are using <code>nixpkgs-unstable</code> as one of our inputs and that might be a tad alarming to some to see that, however it's worth remembering that our automatically generated <code>flake.lock</code> will ensure that whatever versions of packages we use do not change without us intentionally changing them. If the use of the unstable channel is concerning, however, it can be locked to one of the stable releases such as <code>23.11</code> at the time of this writing.</p>
<p>Next up, we define our dependencies:</p>
<pre><code class="language-bash">scriptDeps = with pkgs; [
    nixFormatter
    jq
    git
    gnugrep
    curl
    kubectl
];
</code></pre>
<p>Every one of these packages will be installed to the environment that our scripts are executed in. For any utilities you want to add, all you have to do is search for them <a href="https://search.nixos.org/packages?ref=dept-engineering-blog.ghost.io">in the NixOS package search</a> and add them to the list! For example, we added <code>kubectl</code> to the list from <a href="https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=kubectl&ref=dept-engineering-blog.ghost.io">this search</a></p>
<p>Next, we want to make it so that the addition or removal of scripts can be done without needing to touch our Nix code at all. The way we accomplish this is by having our flake scan the <code>scripts/</code> directory for files that have some predetermined extensions. This means that if someone wants to add a new script to our project, all they have to do is add it to the <code>scripts/</code> directory with one of the extensions that we specify; no nix code required!</p>
<p>We also want to allow multiple "types" of scripts, automatically determined by extension, so that we can change script headers, footers, or even dependencies based on the extension of the script. While the demo only includes BASH scripts, this could also allow for running any other types of scripts such as python, go, or so on. In our demo, we specify two extensions <code>.std.sh</code> and <code>.tf.sh</code> like so:</p>
<pre><code class="language-bash"># headers here, defined outside the list so they can refer to each other
stdShHeader = ''
    #!${pkgs.stdenv.shell}
    set -Eeou pipefail
    export PATH="$PATH:${scriptEnv}/bin"
'';
tfShHeader = ''
    ${stdShHeader}
    echo 'Running the extra tasks for .tf.sh'
'';

# Define metadata for each file suffix and the headers/exec command to attach to them
scriptSuffixes = [
    {
    suffix = ".std.sh";
    header = stdShHeader;
    command = "exec";
    }
    {
    suffix = ".tf.sh";
    header = tfShHeader;
    command = "exec";
    }
];
</code></pre>
<p>In our usage here, we're defining <code>stdShHeader</code> and <code>tfShHeader</code> outside of our list of suffixes to keep it easy to refer to other headers, but these could just as easily be defined inline with our list.</p>
<p>Finally, the real workhorse of our customized flake. This is where we do the actual walking of the <code>scripts/</code> directory using the metadata defined in the list above:</p>
<pre><code class="language-bash">scriptMappings = builtins.map
        # for each script suffix we...
        (typeAttrs:
        let
            # Find all scripts in the directory with our expected suffix
            scriptsFound = builtins.filter (name: lib.hasSuffix typeAttrs.suffix name) scriptDirScripts;
            # Map those found scripts to command names such that `format.std.sh` becomes `format`
            scriptNames = builtins.map (name: builtins.replaceStrings [ typeAttrs.suffix ] [ "" ] name) scriptsFound;
            # Create a list of maps where the command is set to "name" and the path to the script is set to "value", e.g. [{"name":"format","value":"./scripts/format.std.sh", ...}]
            scriptAttrLists = builtins.map (name: { name = name; value = scriptDir + "/${name}${typeAttrs.suffix}"; }) scriptNames;
            # Convert that list of maps into a single mapping where command name is the key, path is the value. e.g. {"format":"./scripts/format.std.sh", ...}
            scriptAttrs = builtins.listToAttrs scriptAttrLists;
            # Finally, instead of JUST the path, add the full formatting of the wrapper script (including the header and exec command) to the values
            # {"format": "&lt;all contents of wrapper script&gt;", ...}
            scriptContents = builtins.mapAttrs
            (name: value: ''
                ${typeAttrs.header}
                ${typeAttrs.command} ${value} "$@"
            '')
            scriptAttrs;
        in
        scriptContents)
    scriptSuffixes;
</code></pre>
<p>We'll let the comments in the above snippets do most of the talking, but basically we are creating a script mapping from each of the scripts found and adding the script headers and exec command. This mapping effectively creates a "wrapper" script for each one of the scripts found that might look something like this for the command <code>./run format</code>:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -Eeou pipefail
export PATH="$PATH:/nix/store/generated-env-dir/bin"
exec ./scripts/format.std.sh "$@"
</code></pre>
<h3 id="the-help-command">The Help Command</h3>
<p>We also want to ensure that our code is as self-documenting as possible while also keeping documentation simple. While this isn't a replacement for complete documentation by other means, maintaining quick help text can be made semi-automatic. To do this, we can just add another script file to our project called <code>help.std.sh</code>. You can <a href="https://github.com/philproctor/nix-script-runner-demo/blob/main/scripts/help.std.sh?ref=dept-engineering-blog.ghost.io">see the full contents of the demo help script here</a>.</p>
<p>In a nutshell, what we do is this:</p>
<ul>
<li>Scan the <code>scripts/</code> directory for files that end in the extensions that we care about (<code>.std.sh</code> and <code>.tf.sh</code>)</li>
<li>For each script that we find, use <code>grep</code> to find the first line of code that starts with <code># HELPTEXT:</code></li>
<li>Output each script command along with the HELP output in a human readable format.</li>
</ul>
<p>The result looks like this:</p>
<pre><code class="language-bash">$ ./run help
Usage: ./run &lt;command&gt; [args...]

Standard commands:
  format                        Format flake.nix
  help                          Display this help output
  update-packages               Update the flake.lock with the latest version of all dependencies

Terraform commands:
  test                          Demo for files with a .tf.sh extension instead

note: any arguments passed after &lt;command&gt; are passed directly to the script that handles that command.
</code></pre>
<h2 id="final-results">Final results</h2>
<p>What we're left with after doing this project is a structure that looks like this:</p>
<ul>
<li>📂 <code>scripts/</code>
<ul>
<li>📄 <code>help.std.sh</code></li>
<li>📄 <code>format.std.sh</code></li>
<li>📄 <code>test.tf.sh</code></li>
<li>📄 <code>update-packages.std.sh</code></li>
</ul>
</li>
<li>📄 <code>flake.nix</code></li>
<li>📄 <code>flake.lock</code></li>
<li>📄 <code>run</code></li>
</ul>
<p>The maintenance once the boilerplate is done is simple:</p>
<ul>
<li>Any scripts that we want to add to this project can then simply be added to the <code>scripts/</code> folder with one of our extensions and it will be autodetected as a new <code>run</code> command. Remember that scripts need to be in git <code>git add</code> and need to be executable <code>chmod +x</code>.</li>
<li>Any new dependencies can be added to <code>scriptDeps</code> inside of <code>flake.nix</code></li>
<li>Updating the dependency lock can be done with <code>./run update-packages</code></li>
<li>New types of scripts, such as python scripts, can be supported by amending <code>scriptSuffixes</code> in <code>flake.nix</code> and updating dependencies</li>
<li>New users of the project do not need to install <strong>any</strong> dependencies except for Nix itself. Our <code>run</code> script will dump a message telling them to install Nix if it's missing.</li>
</ul>
<p>So with a little up-front work, we have a project with scripts that will run on anyone's machine, works in any CI environment, self-documents its own commands, is easy to extend and maintain, and best of all it doesn't require anything other than Nix to run anywhere!</p>
<p>Would you use this yourself?</p>
]]></content:encoded></item><item><title><![CDATA[Achieving agility and flexibility with a hybrid approach in Adobe Experience Manager]]></title><description><![CDATA[Implementing AEM with a hybrid approach offers benefits like agility, scalability, and flexibility. Yet, it requires careful planning, design, and execution, considering technical and organizational factors to ensure robustness and scalability.]]></description><link>https://engineering.deptagency.com/achieving-agility-and-flexibility-with-aem-hybrid-implementation/</link><guid isPermaLink="false">65cf74d5a736620001ba9df4</guid><category><![CDATA[Adobe]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Shruti Damle]]></dc:creator><pubDate>Thu, 29 Feb 2024 13:00:56 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDZ8fFdlYiUyMGRldmVsb3BtZW50JTIwfGVufDB8fHx8MTcwODMzNDY2NHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDZ8fFdlYiUyMGRldmVsb3BtZW50JTIwfGVufDB8fHx8MTcwODMzNDY2NHww&ixlib=rb-4.0.3&q=80&w=2000" alt="Achieving agility and flexibility with a hybrid approach in Adobe Experience Manager"/><p/><h2 id="introduction">Introduction</h2><p>Implementing AEM with a hybrid approach offers benefits like agility, scalability, and flexibility.</p><p>Yet, it requires careful planning, design, and execution, considering technical and organizational factors to ensure robustness and scalability.</p><h2 id="aem-solution-approaches">AEM solution approaches</h2><p>AEM (Adobe Experience Manager) is a content management system (CMS) that allows organizations to manage their digital content across various channels. AEM offers three primary approaches to building web applications: fullstack, headless and hybrid.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/aem-diagram.png" class="kg-image" alt="Achieving agility and flexibility with a hybrid approach in Adobe Experience Manager" loading="lazy" width="1600" height="681" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/02/aem-diagram.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2024/02/aem-diagram.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/aem-diagram.png 1600w" sizes="(min-width: 1200px) 1200px"/></figure><p>A <strong><em>Full Stack AEM architecture</em></strong> is a unified system, often referred to as the Traditional CMS.&nbsp;</p><p>This approach reduces the overall platform management costs significantly, as it consolidates all technology stacks into a single architecture. Additionally, it enables the utilization of Out-of-the-Box (OOTB) components, streamlining and expediting the development process.</p><p>Once an AEM project is created, the JS and CSS from the ui.frontend module are compressed and stored in the app/project-name/clientlibs directory.</p><p>Within the AEM component, Sightly (HTL - HTML Template Language) can be employed within HTML to dynamically render content from Sling models. All aspects of content creation, styling, delivery, and presentation are centralized within AEM, affording complete control over content editing.</p><p>However, it's important to note that content cannot be directly exposed and disseminated to multiple external channels outside of the system in this setup.</p><p>On the other hand, <strong><em>Headless architecture</em></strong> decouples the frontend and backend, with the term "Headless" indicating the absence of a specific presentation channel being tied to it.</p><p>It offers the potential for enhanced performance and facilitates multi-channel delivery. It has an API-driven model, where AEM content is transmitted in JSON format to various channels, enabling these channels to create tailored designs.</p><p>In the Headless Architecture, content fragments and content fragment models are accessed using the GraphQL API or Asset Rest API, reducing reliance on the backend. In this setup, content is provided as a service API, enabling interaction between the backend and any frontend, accessible via any device. It necessitates increased involvement from frontend developers. </p><p>In this framework, content creation occurs within AEM, while styling, presentation, and delivery occur on a separate platform. It represents a contemporary development approach for implementing experiences across websites.</p><p><em><strong>A Hybrid CMS</strong> </em>represents an ideal amalgamation of strengths. Within AEM, we can employ Hybrid Single Page Applications (SPAs) to seamlessly blend the characteristics of both Traditional and Headless architectures.</p><p>This approach combines the efficiency and user-friendliness of a traditional CMS with the adaptability and scalability inherent in a headless development framework.</p><p>Depending on specific business requirements, we can opt for either the traditional or headless approach. However, once a choice is made regarding the frontend, the hybrid solution may not offer the same degree of flexibility that a purely headless approach can provide.</p><p>With a hybrid CMS, we can either create our own templates or utilize pre-existing ones, enabling us to make content readily available and easily reusable across various channels and devices by utilizing APIs. Out-of-the-box components can streamline the delivery of content to the web.</p><h2 id="options-for-hybrid-solutions-within-the-aem-ecosystem">Options for Hybrid solutions within the AEM ecosystem</h2><p/><h3 id="aem-spa">AEM SPA</h3><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/aem-spa.png" class="kg-image" alt="Achieving agility and flexibility with a hybrid approach in Adobe Experience Manager" loading="lazy" width="952" height="617" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/02/aem-spa.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/aem-spa.png 952w" sizes="(min-width: 720px) 720px"/></figure><p>The SPA Editor, provided by AEM, offers a solution for integrating React and Angular applications directly with AEM for in-context editing. It is most effective when: </p><ul><li>You intend to develop your entire frontend for a site using React or Angular.</li><li>You aim to minimize the familiarity required with AEM for your frontend developers.</li><li>You require SPA-controlled routing and want to serve SPA web pages from AEM.</li></ul><p>Content authoring for SPA components remains stored in the JCR but is presented as a JSON representation through component mapping instead of crafting AEM HTL templates. This JSON can then be exposed to other channels similarly to before. </p><p>An advantageous aspect of this approach is that Adobe has re-implemented many Core Components in React and Angular, facilitating direct parent/child SPA component relationships and familiar data flows for SPA developers.</p><p>However, this solution has some limitations within AEM due to SPA-controlled rendering instead of HTML templates. For an up-to-date list of limitations, please refer to the Adobe documentation. </p><p>Despite these limitations, the SPA Editor is actively maintained and developed by Adobe, raising hopes for the gradual removal of these constraints.</p><h3 id="aem-remote-spa">AEM Remote SPA</h3><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/UbmTbm06QWVBQT1FoJ10VF_bUhDmKsqYbv1G_PJDeusOG7ULbFItUcU0iAnQpLXcAgPlgprjS0b8B8_NeFLy5g6r81m_85TH28xb96coDNWmt_eSv5EhiHfzPEdCRjFRXpqd3WnLfqOskKAT2Z1GF98.png" class="kg-image" alt="Achieving agility and flexibility with a hybrid approach in Adobe Experience Manager" loading="lazy" width="853" height="752" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2024/02/UbmTbm06QWVBQT1FoJ10VF_bUhDmKsqYbv1G_PJDeusOG7ULbFItUcU0iAnQpLXcAgPlgprjS0b8B8_NeFLy5g6r81m_85TH28xb96coDNWmt_eSv5EhiHfzPEdCRjFRXpqd3WnLfqOskKAT2Z1GF98.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/UbmTbm06QWVBQT1FoJ10VF_bUhDmKsqYbv1G_PJDeusOG7ULbFItUcU0iAnQpLXcAgPlgprjS0b8B8_NeFLy5g6r81m_85TH28xb96coDNWmt_eSv5EhiHfzPEdCRjFRXpqd3WnLfqOskKAT2Z1GF98.png 853w" sizes="(min-width: 720px) 720px"/></figure><p>Remote SPA, another AEM-provided solution, enables externally hosted React applications to be editable within AEM. In function, it parallels the SPA Editor but with the SPA server delivering the pages instead of AEM. This allows the engineering team to predominantly construct site components outside of AEM and independently scale page traffic from AEM. </p><p>Drawbacks of this approach include the necessity for separate server infrastructure, distinct deployment cycles, the same limitations as SPA Editor, and it only supports React currently.</p><p>Nevertheless, this might represent the optimal choice for integrating authoring into an existing SPA application or meeting project deadlines for a team less familiar with AEM.</p><h2 id="final-thoughts">Final thoughts</h2><p>In conclusion, an AEM hybrid approach using React combines the benefits of headless and headful approaches, providing organizations with the flexibility to use the latest front-end technologies while still leveraging AEM's powerful content management and personalization features and even when the trade off can look higher the benefits in the long term are bigger. </p><p>Need AEM support? DEPT® is a <a href="https://www.deptagency.com/partner/adobe/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">platinum Adobe solution partner</a>. </p>]]></content:encoded></item><item><title><![CDATA[Using dynamic components in your MPA]]></title><description><![CDATA[So you need to introduce some dynamic components to your MPA code-base. What should you do? This article aims to discuss different scenarios and how they can be handled.]]></description><link>https://engineering.deptagency.com/dynamic-components-in-your-mpa/</link><guid isPermaLink="false">65ca2960dc04860001d5d70a</guid><dc:creator><![CDATA[Ethan Standel]]></dc:creator><pubDate>Tue, 27 Feb 2024 13:04:05 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/scroll.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/02/scroll.jpg" alt="Using dynamic components in your MPA"/><p>You have a multi-page application (MPA) which renders your frontend and it meets 99% of your needs. Your HTML is rendered from a server-side templating language like ASP, Razor, JSP, Django Templates, Pug, Handlebars, etc.</p>
<p>It's lightweight, fast, and ideal in its technical simplicity. Up until now, any dynamic behavior you've needed has been easy enough to add via progressive enhancement in vanilla JS using imperative programming. But then the newest business requirement comes in and it's a real doozy. You know immediately: this would be easy to do as part of an SPA using a frontend framework like React or Vue, but this feels like a huge ask under the current architecture.</p>
<p>So you need to introduce some dynamic components to your MPA code-base.</p>
<p>What should you do? This article aims to discuss different scenarios and how they can be handled in the order of most ideal to least ideal. These scenarios and my recommended solutions are not entirely exhaustive, but recent work forced me to consider these scenarios more directly. Hopefully my experience and research can be an aid to you and your work.</p>
<h2 id="scenario-1-your-existing-stack-already-accounts-for-this">Scenario 1: Your existing stack already accounts for this!</h2>
<p>The first step I recommend would be to see if your current toolchain already has a built in solution that fits your needs.</p>
<p>The examples in this category are somewhat artificially limited, as there are many tech stacks which account for having dynamic client-side behavior with server-side rendering. However, there's only a few cases where that hybrid behavior isn't at the core of how that technology functions. For example, it would be a rare case for someone to use NextJS without the explicit understanding that you naturally have access to client-side React when you need it. But there are some frameworks for which this kind of feature isn't on the front-page of the documentation.</p>
<h3 id="blazor">Blazor</h3>
<p>If you've found yourself in this position and are lucky enough to already be using C#, .NET, and Razor templates then the good news is that you won't have to stray far to create dynamic components in a technology which you are already comfortable with. Your toolchain has actually already considered this for you in the form of <a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor?ref=dept-engineering-blog.ghost.io">Blazor</a>. Blazor is a technology which gives you access to a superset of Razor templating syntax to support the addition of dynamic client-side behavior. The C# code in Blazor files compiles down to WASM to allow you to use the same tools on the client and server.</p>
<pre><code class="language-razor">&lt;button @onclick="Increment"&gt;The count is @count&lt;/button&gt;

@code {
    private int count = 0;

    private void Increment()
    {
        count++;
    }
}
</code></pre>
<p><em>A simple Blazor counter component</em></p>
<h3 id="considerations-for-blazor">Considerations for Blazor</h3>
<p>If this is your scenario, then you can probably stop reading as Blazor is likely the right technology for your situation.</p>
<p>That being said, it's not a perfect choice. <a href="https://krausest.github.io/js-framework-benchmark/2024/table_chrome_121.0.6167.85.html?ref=dept-engineering-blog.ghost.io">Blazor is slow</a>. It is among the slowest client-side frameworks. This is because the current model for WASM is not intended for building full applications. WASM is intended to offload high-intensity logic from JavaScript to a more performant platform and then communicate the results of that workload back to JavaScript for your application to utilize. For WASM to support a full application and be able to dynamically update the DOM, it must come with a binding-library which exposes all the functions it needs as a bridge back to JavaScript, and that bridge is a <strong>major</strong> performance bottleneck. As such, Blazor is so slow that I personally can only recommend it in this exact scenario: You already have a .NET application, and you have now realized that you need dynamic client-side components. But I can't in good faith recommend starting a new application with the intention of heavily relying on Blazor.</p>
<h3 id="phoenix-liveview">Phoenix LiveView</h3>
<p>There are often features that come with some backend frameworks that give you a bit of prebuilt JavaScript to interface with some more advanced features of the backend framework. If you were making an application using Elixir, Phoenix, and HEEx templates, then you may already have a solution that could work for you in <a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html?ref=dept-engineering-blog.ghost.io">Phoenix LiveView</a>. LiveView is a tool within the Phoenix framework that takes advantage of the BEAM VM's excellent concurrency safety &amp; performance to give stateful updates via a socket connection. This allows the server to own and update UI state which may be exactly what you need to create your dynamic component without having to reach for another tool outside of your current stack.</p>
<h3 id="challenges-considerations-for-phoenix-liveview">Challenges &amp; considerations for Phoenix LiveView</h3>
<p>LiveView is a great tool for its use-cases but those cases can be limited. And obviously, running stateful updates for dynamic behavior on your server can create some scaling challenges. This also makes your client-side state very limited.</p>
<h2 id="scenario-2-your-existing-stack-can-be-stretched-to-meet-your-needs">Scenario 2: Your existing stack can be stretched to meet your needs!</h2>
<h3 id="htmx">HTMX</h3>
<p>If you find yourself in the scenario for which your existing architecture can't solve this problem for you, you may want to consider <a href="https://htmx.org/?ref=dept-engineering-blog.ghost.io">HTMX</a>. HTMX is a single JavaScript bundle that allows you to write dynamic behavior driven entirely by the server. This is similar in concept to Phoenix LiveView but it doesn't entirely rely on sockets or the BEAM concurrency model. In the HTMX model, your server exposes endpoints that act like component templates. These endpoints return HTML fragments, rather than a full document.</p>
<p><em>server.js</em></p>
<pre><code class="language-js">const context = { count: 0 };

on.post("/increment", (request) =&gt; {
    context.count++;
    return render(request, "counter.html", context);
});
</code></pre>
<p><em>counter.html</em></p>
<pre><code class="language-html">&lt;button hx-post="/increment" hx-swap="outerHTML"&gt;
    The count is {{ count }}
&lt;/button&gt;
</code></pre>
<p><em>A very primitive counter component implemented in HTMX</em></p>
<h3 id="challenges-considerations-for-htmx">Challenges &amp; considerations for HTMX</h3>
<p>HTMX can be a very powerful option that allows for a wide range of added capabilities with minimal additions to your architecture. You can continue to use your existing server &amp; templating system as you were before. However, it is not without its drawbacks &amp; complexities. The core challenge of this model can be seen in the above example: all component state must now live on the server, and more ideally in your user's sessions.</p>
<p>In the example above, the <code>context</code> is global to the server. So in that example all users would share one single value for <code>count</code>. In an SPA model, simple pieces of state like this will be naturally segregated to each user's browser environment, whereas the HTMX model forces even simple pieces of dummy state to be maintained by your server. In an SPA, the memory would naturally be dumped when the user navigates or closes the tab/window, whereas in HTMX the server must make a standardized decision for when to stop holding onto that state. And of course where the performance of Blazor DOM updates was limited to the slow WASM to JavaScript bridge, the performance of HTMX updates are limited to the speed of the network. While this means that your initial page loads are faster because your users aren't downloading templates for components that haven't had their state modified yet, this could be considered the worst case scenario for real-time behavior performance.</p>
<p>HTMX has proven to be a <a href="https://htmx.org/essays/a-real-world-react-to-htmx-port/?ref=dept-engineering-blog.ghost.io">fantastic solution in many cases</a>, but it isn't applicable to all scenarios. If you need lightweight interactivity sprinkled throughout a website, then HTMX could be the ideal solution for your use-case. But if you need dense &amp; responsive interactivity, then it may not meet your needs.</p>
<h2 id="scenario-3-your-stack-cant-meet-your-needs">Scenario 3: Your stack can't meet your needs.</h2>
<p>After reviewing the existing tools available in your stack and considering tools like HTMX, you may still find your options lacking for what you are being tasked to build. So instead, you want to bring in an SPA-like JavaScript component development flow alongside your existing application. So does this mean it's time to bring in React? There are still some more ideal options to consider first. The challenge with React is that there's no natural interface between server-generated HTML and rendering a declared React component. But is there a platform which offers a natural interface between HTML &amp; rendering a declared component? Thankfully yes! There are several frameworks which run on the back of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components?ref=dept-engineering-blog.ghost.io">web-component</a> architecture which provide us with exactly such a model.</p>
<h3 id="lit">Lit</h3>
<p>Lit is a framework by Google which allows you to make web-components in a simple and standardized fashion. The key advantage of having your components registered as web-components is that the process of mounting your components happens naturally in the browser's custom elements API. However, the actual development experience is very akin to developing with React class components, rendering using template strings rather than JSX.</p>
<pre><code class="language-ts">import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";

@customElement("lit-counter")
export class MyElement extends LitElement {
  @property({ type: Number })
  count = 0;

  private _increment() {
    this.count++;
  }

  render() {
    return html`
      &lt;button @click=${this._increment}&gt;The count is ${this.count}&lt;/button&gt;
    `;
  }
}
</code></pre>
<p><em>A counter component in Lit</em></p>
<p>As long as the bundled JavaScript output is added to any &amp; all pages which need these components within your MPA, then all you have to do to add this component is put <code>&lt;lit-counter&gt;&lt;/lit-counter&gt;</code> in your HTML and the component will be naturally instantiated by the browser. So that means no added script tags which target a <code>&lt;div&gt;</code> with a particular <code>#id</code> on it. Just use your components in a natural fashion.</p>
<h3 id="stencil">Stencil</h3>
<p>Stencil is a very similar tool to Lit with slightly different design decisions &amp; UX. Stencil aims to be slightly closer to React, and thus uses JSX. However, it also uses the same concept of decorators which denote reactive properties which trigger a re-render.</p>
<pre><code class="language-tsx">import { h, Component, Prop } from '@stencil/core';

@Component({ tag: 'stencil-counter' })
export class Counter {
  @Prop() count = 0;

  increment() {
    this.count++;
  }

  render() {
    return &lt;button onClick={() =&gt; this.increment()}&gt;The count is {this.count}&lt;/button&gt;;
  }
}
</code></pre>
<p><em>A counter component in Stencil</em></p>
<p>As can be seen from the two examples, Stencil &amp; Lit share very similar architecture both from a DX perspective as well as an implementation perspective. However, Stencil's use of JSX gives it a few advantages. Namely, that Stencil components are actually internally strongly typed with TypeScript. Part of the Stencil compilation process builds out your component tags with their props to the global JSX namespace. So if I wanted to use the above Stencil component in another component and I wrote <code>&lt;stencil-counter count="5" /&gt;</code>, I would actually get a compilation error from TypeScript that informs me that property <code>count</code> must be a <code>number</code> and not a <code>string</code>. Comparably, when using the Lit example, <code>&lt;lit-counter count="5"&gt;&lt;/lit-counter&gt;</code> would actually be the proper syntax, and you could only get a <em>runtime</em> error if you passed in a string which could not be converted to a number.</p>
<h3 id="challenges-considerations-for-both-stencil-lit">Challenges &amp; considerations for both Stencil &amp; Lit</h3>
<p>Both Lit &amp; Stencil are built to use the web-component architecture, so there are certain integrations that will not work as expected by default. For instance, if your project uses an atomic CSS tool like Tailwind or Bootstrap then it may have some small integration hurdles with these technologies. web-components are defined as a series of browser native features used together. One of those technologies is the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components?ref=dept-engineering-blog.ghost.io#shadow_dom">shadow DOM</a>, which creates an isolated environment for each of your component instances to run in. That way your components don't have to worry about adding styles which affect the rest of the document and they don't have to worry about being affected by the styles of the rest of the document. This can obviously create a very safe &amp; stable development experience, but if your architecture relied on styles coming from a large shared style sheet, then this could be a major hindrance to you.</p>
<p>Lit &amp; Stencil are both built with the full structure &amp; safety of the web-component architecture in mind, but luckily they both have escape hatches to avoid things like the shadow-DOM. With Lit, you just need to override the <code>createRenderRoot</code> method in your component. Normally this method returns a shadow-root but instead you can have it return <code>this</code> because custom-element classes are, themselves, DOM elements that extend the <code>HTMLElement</code> class. In Stencil, you only need to add the <code>shadow: false</code> option to the configuration object passed to your <code>@Component</code> decorator. This means that with very little work both of these tools can be made to support the architecture of most projects as needed, but they are not without unique considerations.</p>
<h2 id="scenario-4-youve-been-prescribed-a-solution">Scenario 4: You've been prescribed a solution.</h2>
<p>We've all been there: Your project manager tells you that another development team has already built the component in React. They even bundled it into a library, so all you need to do is drop in the library right? But you know it's not that easy. You're not using React. You're not even really using a frontend framework. So how can we make this process of utilizing React in your MPA as painless as possible?</p>
<h3 id="bundling-a-vite-app-into-an-mpa">Bundling a Vite app into an MPA</h3>
<p>There are steps that just can't be skipped surrounding the bundling, building, and exporting process. In my experience, I've found that utilizing Vite is the best way to go these days. It solves the most problems up front, has the fastest performance, and requires very little in terms of configuration overrides.</p>
<p>If you instantiate a Vite application inside your MPA repository, you will naturally get a <code>vite.config.js</code> file. In the Vite config, if you set <code>config.build.rollupOptions.output.manualChunks</code> to <code>undefined</code> and set <code>config.build.rollupOptions.output.entryFileNames</code> to something simple like <code>"app.js"</code>, you will remove all chunking &amp; file-name hashing from the build output so that your JS bundle will always be one file with a consistent name. This makes it much easier to link to from your MPA. If you want to invalidate old script builds from cache for users, then you can easily bust the cache by requesting the JavaScript file with an arbitrary param like your latest server start time or time from your most recent build. So in a Node server, for example, the built JavaScript file could be requested from <code>/app.js?cacheBust=${performance.timeOrigin}</code>.</p>
<p>The only other configuration option that <em>must</em> be updated at this point is <code>config.build.outDir</code> which is where you want all output files to be placed. Be warned, that if you've defined a custom <code>config.root</code> then your <code>outDir</code> will be relative to that <code>root</code>. You'll want to make the value of the <code>outDir</code> point to the directory where your backend wants you to place statically hosted files. I would also personally recommend that you put the <code>outDir</code> as its own subdirectory which is marked in your <code>.gitignore</code> so that you can avoid committing build outputs to your actual repository.</p>
<p>Finally, you may want to add a <code>postbuild</code> script to clean up artifacts which you don't want in the output. In the case of Vite, the root of each build is actually the <code>index.html</code> file, so you'll probably just want to delete the copy of that file in your <code>outDir</code> after every build completion.</p>
<p>Once all of these items are complete, then you can just add the Vite installation &amp; testing steps to your existing CI/CD flow and add the build step to fire before your backend build. If some of these steps feel a little bit like a code-smell, they should. This is inherently the twisting of a tool to be what we need it to be. This is not exactly how Vite is intended to be used, but this is the position you will sometimes find yourself in. And from my past experience, I would argue that this makes for better long-term maintenance than fully building out exactly what you need with a custom Webpack configuration.</p>
<h3 id="exposing-react-components-to-your-mpa">Exposing React components to your MPA</h3>
<p>Now that you have a JavaScript bundle being built and linked to by your MPA, how can we best expose React components to your existing templating? For this step, I recommend looking at all the advantages of the prior technologies and do your best to roll them into this solution. You're not going to find a better solution for exposing JavaScript components to HTML than custom elements, so why not lean into it!</p>
<pre><code class="language-tsx">import { createRoot } from "react-dom/client";
import { type ComponentType, useState } from "react";

abstract class ReactMountingElement extends HTMLElement {
  abstract readonly Component: ComponentType;

  readonly #root = createRoot(this);

  connectedCallback() {
    this.#root.render(&lt;this.Component /&gt;);
  }

  disconnectedCallback() {
    this.#root.unmount();
  }
}

const Counter = () =&gt; {
  const [count, setCount] = useState(0);

  return (
    &lt;button onClick={() =&gt; setCount((count) =&gt; count + 1)}&gt;
      The count is {count}
    &lt;/button&gt;
  );
};

const exposedComponents: Record&lt;string, ComponentType&gt; = {
  "react-counter": Counter,
};

Object.entries(exposedComponents).forEach(([tag, Component]) =&gt;
  window.customElements.define(
    tag,
    class extends ReactMountingElement {
      Component = Component;
    }
  )
);

</code></pre>
<p><em>A React counter component which is exposed as a custom element</em></p>
<p>The above code shows a very simple pattern for exposing React components as custom elements. Once the above code is added to the bundle loaded into your MPA, you can create instances of the counter component using <code>&lt;react-counter&gt;&lt;/react-counter&gt;</code>! This gives you the simplest possible interface between React &amp; your backend HTML templating engine. This doesn't solve <em>all</em> challenges, but it's a very simple baseline to build off of. From here, you can make decisions that fit your needs around things like accepting children to your component, managing incoming props, and especially non-children HTML/JSX props. For those solutions, I think utilizing a web-component model using <code>&lt;template&gt;</code> &amp; <code>&lt;slot&gt;</code> elements would be the best approach but that doesn't make it easily solved.</p>
<h3 id="the-drawbacks-to-this-solution">The drawbacks to this solution</h3>
<p>The obvious drawback here is in the fact that we are once again bending a tool slightly outside of how it is intended to be used. And for that reason, we have to re-solve problems that are solved by default in the solutions for the previous scenarios. What was supposed to be the easy solution, "just implement an already existing React library," has become an avalanche of custom infrastructure problems that must all be solved (or will likely eventually need to be solved).</p>
<h2 id="to-summarize">To summarize...</h2>
<p>There are so many different versions of this scenario that you might find yourself in. You may discover that it was never an issue because you can integrate something like Blazor into your already existing app. Or you might discover that HTMX meets your needs and you can run all the dynamic components out of server session state. But if neither of these cases meet your needs, but you still have full control over the technology used, I cannot recommend enough leaning into frameworks &amp; tools that have this situation in mind like Lit or Stencil. However, if you find yourself needing to integrate a specific frontend framework into your MPA, I hope that this article has shown you that it's within reach.</p>
<h2 id="sidebar-protecting-yourself-from-these-scenarios-in-the-future">Sidebar: Protecting yourself from these scenarios in the future</h2>
<p>There are a lot of new technologies that are appearing in the web development world that solve these kinds of problems by default. Obviously tools like Next, Nuxt, Angular Universal (or Analog.js), SvelteKit, SolidStart, or QwikCity all exist as meta-frameworks around existing frontend frameworks. However, there are also tools which allow you the simplicity of server-side only rendering, while being well prepared for an escape hatch into the frontend frameworks. At the forefront of this model and a technology that I can personally recommend, is <a href="https://astro.build/?ref=dept-engineering-blog.ghost.io">Astro</a>.</p>
<p>Astro is built to default for simplicity by shipping zero JavaScript to the client by default, so it is incredibly fast. However, Astro is also built for adaptability by offering a suite of <a href="https://docs.astro.build/en/guides/integrations-guide/?ref=dept-engineering-blog.ghost.io">plugins, adapters, and integrations</a>. Astro uses SSG by default for maximum performance, but it has SSR adapters to allow it to run best however it's deployed. Astro ships no JavaScript by default, but it has plug-ins for every major frontend framework so that components in that framework can be used naturally in Astro's templates. When you mount a React component in Astro, the React plugin will offer you the full control for exactly how you want that component to hydrate via <a href="https://docs.astro.build/en/reference/directives-reference/?ref=dept-engineering-blog.ghost.io#client-directives">a series of directives</a>.</p>
<p>So if you want to build a web application with maximum foresight for scenarios like this, then picking a meta framework would be a great way to secure yourself when complex business requests come in. But picking a framework agnostic platform like Astro can give you maximum performance and technical simplicity early in a project, while giving you the ability to easily grow your architectural complexity.</p>
]]></content:encoded></item><item><title><![CDATA[Django front-end simplified]]></title><description><![CDATA["Front-end" in web development refers to the visual appearance of an application or website. In Django, a web development framework, the front-end is created in DTL (Django Template Language), which is a superset of HTML (Hypertext Markup Language).

DTL represents the "what" of the page - the actual contents. The "how" of the page - how the page appears - is defined by CSS (Cascading Style Sheets).


Django template language (DTL) files

Here's an example of a DTL file:


stats-pane.html

{% lo]]></description><link>https://ashwinsundar.com/blog/compiled/django-front-end.html</link><guid isPermaLink="false">65a4763d7000a20001d3464c</guid><dc:creator><![CDATA[Ashwin Sundar]]></dc:creator><pubDate>Thu, 25 Jan 2024 14:10:30 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/01/Django-front-end-simplified.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2024/01/Django-front-end-simplified.jpg" alt="Django front-end simplified"/><p>"Front-end" in web development refers to the visual appearance of an application or website. In Django, a web development framework, the front-end is created in DTL (Django Template Language), which is a superset of HTML (Hypertext Markup Language).</p><p>DTL represents the "what" of the page - the actual contents. The "how" of the page - how the page appears - is defined by CSS (Cascading Style Sheets). </p><h2 id="django-template-language-dtl-files">Django template language (DTL) files</h2><p>Here's an example of a DTL file:</p><h3 id="stats-panehtml"><code>stats-pane.html</code></h3><pre><code class="language-djangotemplate">{% load static %} &lt;link rel = "stylesheet" href = "{% static 'css/stats-pane.css' %}"&gt;
&lt;div class = "stats-pane"&gt;
    &lt;div class = "stat-component-A"&gt;
        {% include "components/stat-component.html" with title="On Target" numdetail=stats.on_target_count %}
    &lt;/div&gt;
    &lt;div class = "stat-component-B"&gt;
        {% include "components/stat-component.html" with title="At Risk" numdetail=stats.at_risk %}
    &lt;/div&gt;
    &lt;div class = "stat-component-C"&gt;
        {% include "components/stat-component.html" with title="Efficiency Index" numdetail=stats.efficiency green=9 yellow=4 red=0 %}
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>Here is what is happening in this file:</p><ul><li>The file loads a CSS stylesheet called <code>stats-pane.css</code></li><li>The file establishes scaffolding for three components - <code>stat-component-A</code>, <code>stat-component-B</code>, and <code>stat-component-C</code></li><li>Each <code>div.stat-component</code> includes a template file called <code>stat-component.html</code><ul><li>Information is passed to these templates using the <code>with</code> keyword</li></ul></li></ul><p>Here are the contents of the referenced <code>stat-component.html</code> file:</p><h3 id="stat-componenthtml"><code>stat-component.html</code></h3><pre><code class="language-djangotemplate">{% extends "components/generic-square.html" %}
{% block content %}
    {% load static %} &lt;link rel = "stylesheet" href = "{% static 'css/components/stat-component.css' %}"&gt;
    &lt;div class = "stat-container"&gt;
        &lt;div class = "stat-title"&gt;
            {{ title }}
        &lt;/div&gt;
        {% if numdetail &gt; green %}
        &lt;div class = "stat-numdetail stat-green"&gt;
        {% elif numdetail &gt; yellow %}
        &lt;div class = "stat-numdetail stat-yellow"&gt;
        {% elif numdetail &gt; red %}
        &lt;div class = "stat-numdetail stat-red"&gt;
        {% else %}
        &lt;div class = "stat-numdetail"&gt;
        {% endif %}
            {{ numdetail }}
        &lt;/div&gt;
    &lt;/div&gt;
{% endblock content %}
</code></pre>
<ul><li>The file <a href="https://docs.djangoproject.com/en/5.0/ref/templates/language/?ref=dept-engineering-blog.ghost.io#id1" rel="noreferrer"><code>extends</code></a> an existing template called <code>generic-square.html</code>. This means that some pre-existing content has been defined in the <code>generic-square.html</code> template, and this file will override some of the content.</li><li>The file loads a CSS stylesheet called <code>stat-component.css</code>.</li><li><code>{% block content %}</code> represents the start of the overrideable section. All content from here until <code>{% endblock content %}</code> will override the section in the <code>generic-square.html</code> file of the same name.</li><li>The file defines a framework for containing a single statistic - <code>stat-title</code> and multiple <code>stat-numdetail</code> components, each wrapped in conditional logic.<ul><li>Conditional logic determines whether particular styles shall be rendered. For example, if the <code>numdetail</code> variable is greater than the <code>green</code> variable, the statistic shall be displayed with the class <code>stat-green</code>. This class merely colors the text of the component in green.</li></ul></li></ul><h2 id="cascading-style-sheets-css-files">Cascading Style Sheets (CSS) Files</h2><p>CSS describes how the elements of an HTML page appear. Here is the CSS file used by the <code>stats-pane.html</code> file described in the first section:</p><h3 id="stats-panecss"><code>stats-pane.css</code></h3><pre><code class="language-css">.stats-pane {
  display: flex;
  flex-direction: row;
  gap: 10px;
  justify-content: center;
  align-items: center;
}
</code></pre>
<ul><li>The text before the <code>{}</code>, <code>.stats-pane</code>, is called an element selector. We know this element selector applies to classes because the statement begins with a <code>.</code>. If this applied to a different selector, such as <code>id</code>, it would begin with a different character.</li><li>This definition applies styling characteristics to elements that have an attribute of <code>class = "stats-pane"</code>. <ul><li>In this case, the stats-pane is defined as a layout style called <code>flexbox</code>[^flexbox], and the elements of the flexbox should appear as rows. The gap between each element is set to 10 pixels, and finally the contents are center-aligned.</li></ul></li></ul><h2 id="summary">Summary</h2><ul><li>Django Template Language (DTL) files represents the "what" of the page:<ul><li>What are the structural elements (i.e. HTML elements)?</li><li>What is the basic logic for the page (i.e. embedded scripting)?</li></ul></li><li> Cascading Style Sheets (CSS) files define the "how" of the page:<ul><li>How are elements rendered (i.e. styles)?</li><li>How do those elements appear on the page (i.e. animations, transitions)?</li></ul></li></ul><p>While DTL contains some Python-esque language features, it only permits a small subset of the Python language to be run. This is a design choice - permitting arbitrary Python code to run can be dangerous. That said, DTL is fairly powerful and extensible via <a href="https://docs.djangoproject.com/en/5.0/howto/custom-template-tags/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">custom filters and tags</a>.</p><p>CSS takes some time to become acquainted with and ultimately master. The core specification is constantly improving. <a href="https://web.dev/learn/css?ref=dept-engineering-blog.ghost.io">This guide</a> is a good way to understand the modern contours and best practices of CSS in 2024.</p>]]></content:encoded></item><item><title><![CDATA[Prisma vs Kysely]]></title><description><![CDATA[We decided to give Prisma a try recently to see how well it performed. Prisma impressed us with its developer experience (DX) and abstractions. Yet, we encountered challenges and performance issues. It even forced us to shape the database schema based on its limitations.]]></description><link>https://engineering.deptagency.com/prisma-vs-kysely/</link><guid isPermaLink="false">648370a99cab1e000127ea50</guid><category><![CDATA[Platforms]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Simon Ingeson]]></dc:creator><pubDate>Wed, 20 Dec 2023 13:21:21 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1503039153293-d4d2ba067754?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGZ1dHVyZSUyMGxpZ2h0c3xlbnwwfHx8fDE3MDI4NDg1Mzh8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1503039153293-d4d2ba067754?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGZ1dHVyZSUyMGxpZ2h0c3xlbnwwfHx8fDE3MDI4NDg1Mzh8MA&ixlib=rb-4.0.3&q=80&w=2000" alt="Prisma vs Kysely"/><p>So, we set out to find a replacement to better fit our needs. Having worked with Knex before, we were very impressed with <a href="https://github.com/kysely-org/kysely?ref=dept-engineering-blog.ghost.io">Kysely</a>. It's a modern query builder with a great TypeScript focus. As we'll see here, it also performs a lot better than Prisma.</p><p>To set the context a bit, the project we're working on uses Nest.js and it has a database with around 30 tables. It has support for localization and several many-to-many relations. It's a backend for iOS and Android apps. We'll use a sample database with a similar size since we cannot share the database schema here.</p><p>We spent a couple of days getting acquainted with Kysely before deciding on our approach. Our setup allowed us to refactor from Prisma piece by piece. This is my attempt at documenting my findings and thoughts on why Prisma didn't cut it for us. Fair warning: opinions ahead, your miles may vary, etc.</p><h3 id="high-level-comparisonprisma-vs-kysely">High-level comparison - Prisma vs Kysely</h3><p>Prisma sits somewhere between a traditional ORM and a query builder. Prisma uses a single schema file for defining the model. It then uses that to generate database migrations in SQL files. This alone makes Prisma's DX one of the best among ORMs and query builders. Prisma still lacks some common features though, such as compatibility with views, custom column constraints, and handling nullable unique indexes.</p><p>Prisma also heavily abstracts SQL, so it's pretty much impossible to optimize the SQL it runs. For example, Prisma's generated SQL prefers executing several queries over using joins. Depending on your use case, this might be a good or bad thing.</p><p>In contrast, Kysely is a query builder. The downside (upside) is that you don't get as many batteries included as you would with Prisma. It does have some migration support but is not as well-defined as Prisma's. You also don't get the TypeScript types generated for you out of the box, but there are tools to help you there. For example, you can use <a href="https://github.com/RobinBlomberg/kysely-codegen?ref=dept-engineering-blog.ghost.io">kysely-codegen</a> to generate TypeScript definitions from your database. Or use prisma-kysely to reuse a Prisma schema file. Because it's a lot closer to writing raw SQL, it can also be a lot more powerful.</p><p>Not having everything included can be a good thing. With Kysely you can pick whichever database driver you need ("dialect" in Kysely's terms). You can use the official pg driver, or you can use the newer postgres.js alternative. We did not notice a considerable performance difference between the two though. Likely because Kysely already does the heavy lifting.</p><p><a href="https://github.com/drizzle-team/drizzle-orm?ref=dept-engineering-blog.ghost.io" rel="noopener noreferrer nofollow">Drizzle</a> deserves an honorable mention here. It is another query builder with features like Kysely. One upside with Drizzle is that their migrations, like Prisma, are only SQL. Also, you write Drizzle's schemas in TypeScript but don't use the up/down approach of Kysely. This post won't cover it in detail, but we'll reference it in some places as an extra point of comparison.</p><h3 id="performancefetching-1000-films">Performance - fetching 1,000 films</h3><p>This is using the <a href="https://www.postgresqltutorial.com/postgresql-getting-started/postgresql-sample-database/?ref=dept-engineering-blog.ghost.io">DVD Rental sample database from PostgreSQL Tutorial</a>. I've set up a <a href="https://github.com/smonn/prisma-vs-kysely?ref=dept-engineering-blog.ghost.io">sample repository</a> for you to run the queries below and play around further with each setup. As a bonus, there's also a Drizzle test included.</p><p>The idea is this: fetch the most recent 1,000 films with their categories and actors included. Order films alphabetically if they have the same release year. Also, order categories and actors alphabetically.</p><p>Note: Prisma's <code>prisma db pull</code> command does not let you map names to camel-case. Kysely's <code>CamelCasePlugin</code> can do this for you though. If we wanted to achieve the same result in Prisma we'd have to manually add <code>@map(...)</code> and <code>@@map(...)</code> as needed. To save some time, the generated Prisma schema uses <code>snake_case</code>.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const latestFilms = await db
  .selectFrom("film")
  .limit(1000)
  .orderBy(["film.releaseYear desc", "film.title asc"])
  .selectAll("film")
  .groupBy("film.filmId")
  .select((eb) =&gt; [
    jsonArrayFrom(
      eb
        .selectFrom("filmCategory")
        .innerJoin(
          "category",
          "category.categoryId",
          "filmCategory.categoryId"
        )
        .where("filmCategory.filmId", "=", eb.ref("film.filmId"))
        .orderBy(["category.name asc"])
        .selectAll("category")
    ).as("categories"),
    jsonArrayFrom(
      eb
        .selectFrom("filmActor")
        .innerJoin("actor", "actor.actorId", "filmActor.actorId")
        .where("filmActor.filmId", "=", eb.ref("film.filmId"))
        .orderBy(["actor.lastName asc", "actor.firstName asc"])
        .selectAll("actor")
    ).as("actors"),
  ]);</code></pre><figcaption><p><span style="white-space: pre-wrap;">Kysely sample</span></p></figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const latestFilmsQuery = await prisma.film.findMany({
  include: {
    film_actor: {
      include: {
        actor: true,
      },
      orderBy: [
        { actor: { last_name: "asc" } },
        { actor: { first_name: "asc" } },
      ],
    },
    film_category: {
      include: {
        category: true,
      },
      orderBy: [{ category: { name: "asc" } }],
    },
  },
  take: 1000,
  orderBy: [{ release_year: "desc" }, { title: "asc" }],
});</code></pre><figcaption><p><span style="white-space: pre-wrap;">Prisma sample</span></p></figcaption></figure><p>At first glance, Prisma wins because the "query" looks so simple. But when we consider how Prisma executes the queries, it's not as simple anymore. Yes, it's a bit of a contrived example, but it does highlight some interesting points. I've run into similar queries in various projects, so it's not an unlikely scenario.</p><p>The queries are not a perfect apples-to-apples comparison. As mentioned, out of the box, Prisma doesn't do inner joins as our Kysely query does. It runs a separate query for each table it encounters. Prisma will only rely on joins if needed for filters. This is likely fine for smaller sets of data and can even outperform Kysely's query. But once you start fetching many rows, Kysely performs a lot better.</p><p>On my machine, I usually see Kysely averages around 50 ms for the above, while Prisma averages 110 ms. Again, the queries differ, but they do give the same core data back.</p><p>We also tried Prisma's preview feature <code>relationJoins</code>. While it's closer to what our Kysely query does, it's a lot slower at 240 ms. Finally, Drizzle runs that same query at around 75 ms using the <code>pg</code> driver. Surprisingly, its <code>postgres.js</code> driver was a bit slower.</p><p>One can also argue that Kysely's result is easier to reason about. With Prisma, you get all the nested many-to-many types in there too. If we were working on a backend API, then our front-end developers would not be happy if we returned that as-is. With Kysely you just get the data you requested and nothing more.</p><h3 id="migrationsa-few-options">Migrations - a few options</h3><p>You could keep using Prisma's migration style and generate types via <a href="https://github.com/valtyr/prisma-kysely?ref=dept-engineering-blog.ghost.io">prisma-kysely</a>. This works well as long as you don't need to do something the Prisma schema does not support. Support for views is coming soon, but based on the preview feature they will be a bit awkward to manage.</p><p>Prisma's schema does not work with custom check constraints or multi-column unique indexes. If you customize the generated SQL, Prisma may not do what you might expect.</p><p>Another option is to use Kysely's <a href="https://kysely.dev/docs/migrations?ref=dept-engineering-blog.ghost.io">built-in migrations</a>. If you're familiar with Knex this should be trivial and a good approach. I never went this route so I can't say whether it comes with any challenges. One upside is that you just write TypeScript and don't need to learn Prisma's schema oddities.</p><p>A third option is to use a third-party CLI for forward-only SQL migrations. You can also write some custom scripts for that. The upside of this approach is that it forces you to understand the underlying schema. And then rely on kysely-codegen to generate the type definitions.</p><p>Finally, you could use <a href="https://github.com/drizzle-team/drizzle-kysely?ref=dept-engineering-blog.ghost.io">Drizzle Kit to manage your schema</a> but still use Kysely to write your queries. The Drizzle ORM includes support for that out of the box.</p><h3 id="compatibilitymore-than-you-bargained-for">Compatibility - more than you bargained for</h3><p>Prisma is very heavy to install at over 15 MB. Kysely, in comparison, takes a bit over 2 MB. This makes Prisma not the ideal choice when running code in a serverless context. As we understand it, Prisma relies on a 4 MB WASM file for its query engine, written in Rust. There's nothing wrong with Rust, but Kysely is simply a lot leaner.</p><p>Prisma's query engine needs to support PostgreSQL, SQLite, MongoDB, and more. With Kysely, like many other Node.js database libraries out there, you only need to install a driver.</p><h3 id="summarywrong-batteries">Summary - wrong batteries</h3><p>We're not the first to sing high praise of Kysely and feel let down by Prisma. Kysely will be a main contender for us to interact with SQL from now on. One takeaway from all this is that you should consider what you need out of a library, but also what you don't need. If not, you will fight against it and lose productivity.</p>]]></content:encoded></item><item><title><![CDATA[Flexible UI components made easy with Jetpack Compose]]></title><description><![CDATA[Learn how to create reusable UI components with Google's new modern UI toolkit for Android.]]></description><link>https://engineering.deptagency.com/flexible-composables/</link><guid isPermaLink="false">6512ebce905b4d0001c27a74</guid><category><![CDATA[Platforms]]></category><category><![CDATA[Jetpack Compose]]></category><dc:creator><![CDATA[Nomaan Khalid]]></dc:creator><pubDate>Fri, 08 Dec 2023 14:18:49 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/1_2v6zotc8p-bt9oX2mI0vkQ.webp" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/1_2v6zotc8p-bt9oX2mI0vkQ.webp" alt="Flexible UI components made easy with Jetpack Compose"/><p>Unlike the traditional XML layouts, Compose makes it easy to create UI components that are easily scalable, reusable, and provide a consistent experience for your application's users.</p><p>When creating the UI components, remember to follow these rules:</p><ol><li>Avoid hardcoding any text, states, or values</li><li>Keep the size of the container scalable</li><li>Do not hold any logic in your UI</li><li>Keep it well documented, and add previews to show different ways of usage</li></ol><p>Let's dive into it and see some examples of how we can take the Compose approach to create the following layouts:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/Screen-Shot-2023-11-01-at-9.29.34-AM.png" class="kg-image" alt="Flexible UI components made easy with Jetpack Compose" loading="lazy" width="2000" height="525" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/11/Screen-Shot-2023-11-01-at-9.29.34-AM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/11/Screen-Shot-2023-11-01-at-9.29.34-AM.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/11/Screen-Shot-2023-11-01-at-9.29.34-AM.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/Screen-Shot-2023-11-01-at-9.29.34-AM.png 2012w" sizes="(min-width: 720px) 720px"/></figure><h2 id="identify-similarities">Identify similarities</h2><p>Although they might look different at first glance, we can identify the following similarities:</p><ol><li>Both are contained in a scalable box with rounded corners</li><li>Both have a box outline of equal width</li></ol><p>Secondly, they have the following properties, which can be customized with parameters. These include:</p><ol><li>The outline color</li><li>The box background color</li></ol><h2 id="creating-a-reusable-component">Creating a reusable component</h2><p>Using this information, we can setup a scalable box like this:</p><pre><code class="language-kotlin">@Composable
fun ScalableBox(
    boxColor: Color = Color.White,
    outlineColor: Color = Color.Unspecified,
    boxOnClick: () -&gt; Unit = {},
    modifier: Modifier,
    content: @Composable () -&gt; Unit = {},
) {
    Box(
        modifier = modifier
            .clip(RoundedCornerShape(12.dp))
            .background(boxColor)
            .border(
                width = 2.dp,
                color = outlineColor,
                shape = RoundedCornerShape(12.dp)
            )
            .clickable { boxOnClick }
            .padding(16.dp)
    ) {
        content()
    }
}

</code></pre>
<p>This Composable has some fixed properties such as padding, rounded corner radius, and outline width. Further, it has some customizable properties such as box color, outline color, and the content that will be contained inside the box. These are all passed as parameters.</p><p>Now, we can enclose our main UI elements in this Scalable Box, passing it the properties we need accordingly.</p><p>The code for the yellow-bordered, gray background box:</p><pre><code class="language-kotlin">@Composable
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, widthDp = 300, heightDp = 200)
fun ParagraphFieldPreview() {
    Column(Modifier.padding(16.dp)) {
        ScalableBox(
            boxColor = Color.DarkGray,
            outlineColor = Color.Yellow,
            modifier = Modifier
        ) {
            Text(
                text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " +
                        "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " +
                        "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
                color = Color.White
            )
        }
    }
}
</code></pre>
<p>And the code for the black bordered, white background box:</p><pre><code class="language-kotlin">@Composable
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, widthDp = 300, heightDp = 200)
fun ImageAndTextFieldPreview() {
    Column(Modifier.padding(16.dp)) {
        ScalableBox(
            boxColor = Color.White,
            outlineColor = Color.Black,
            modifier = Modifier
        ) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Image(
                    painter = painterResource(id = R.drawable.ic_launcher_foreground),
                    contentDescription = null,
                    modifier = Modifier.size(60.dp),
                    colorFilter = ColorFilter.tint(Color.Black.copy(alpha = 0.8f))
                )
                Text(text = "Lorem ipsum dolor")
            }
        }
    }
}
</code></pre>
<p>Both Composable uses ScalableBox to configure their container properties and create the enclosed UI.</p><p>And just like that, you have created your first flexible UI components with Jetpack Compose!</p><h2 id="wrapping-up">Wrapping up</h2><p>That all sounds great, but what if your requirements need you to create a color-changing spiral box? You can still use the same scalable box component!</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/spiral.gif" class="kg-image" alt="Flexible UI components made easy with Jetpack Compose" loading="lazy" width="600" height="380" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/spiral.gif 600w"/></figure><p>Check out the full source code below to find out how: <br><a href="https://github.com/deptagency/JetpackComposeExample?ref=dept-engineering-blog.ghost.io">https://github.com/deptagency/JetpackComposeExample</a></br></p><p>Cheers!</p>]]></content:encoded></item><item><title><![CDATA[How to implement KYC/AML protocols for an NFT marketplace]]></title><description><![CDATA[Learn how to implement KYC/AML protocols for an NFT marketplace. ]]></description><link>https://engineering.deptagency.com/how-implement-kyc-aml-protocols-nft-marketplace/</link><guid isPermaLink="false">6549498d880df90001944fbd</guid><dc:creator><![CDATA[Dallas Huggins]]></dc:creator><pubDate>Tue, 05 Dec 2023 14:16:45 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/unsplash-image.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/unsplash-image.jpg" alt="How to implement KYC/AML protocols for an NFT marketplace"/><p>In our previous blog post, we went over what KYC/AML is and why your company may need it: <a href="https://engineering.deptagency.com/why-you-need-kyc?ref=dept-engineering-blog.ghost.io" rel="noreferrer">https://engineering.deptagency.com/why-you-need-kyc</a>. </p><p>In this article, we will be going over how we approached KYC and AML measures for an NFT collectible marketplace for a major sports organization, a marketplace for digital collectibles on the Algorand blockchain. First, we'll talk about the KYC process, then how we addressed customer support, and finally, what some of the challenges were in building and maintaining this system.</p><p><strong>Acronyms</strong></p><ul><li>NFT = Non-Fungible Token</li><li>KYC = Know Your Customer</li><li>AML = Anti-Money Laundering</li><li>SSN = Social Security Number</li><li>PII = Personal Identifiable Information</li><li>PEP = Politically Exposed People</li></ul><h2 id="%F0%9F%8F%85-background">🏅 Background</h2><p>In the NFT collectible marketplace we built, users can sign up and purchase packs that have an unknown assortment of collectibles, or they can purchase from the secondary marketplace, where individual collectibles are sold for a price determined by the seller. Making the platform accessible to people new to blockchain technologies was a top priority, so accepting credit card payments (in addition to crypto) was important. This, however, increases the risk of fraudulent transactions.</p><p>We used Circle for the underlying financial technologies. When purchases are made using credit cards, those funds are converted to USDC-A (USDC on the Algorand blockchain) which are kept in an Algorand wallet that is created per account. Users purchase “credits,” which means loading USDC into the account’s wallet. Circle is a financial institution that has to abide by Anti-Money Laundering (AML) laws, and we entered a KYC reliance agreement with Circle that dictates what KYC and AML processes we need to have in place for compliance. </p><p>Our contract with Circle details that we are responsible for enacting fraud detection on their behalf according to the established AML Policy, and therefore, we needed to find a provider to assist with identity verification (KYC) and flagging suspicious or fraudulent behavior. This provider cannot make final decisions; that responsibility is on us, but they can assist in collecting the data needed to make a final decision. </p><p>Initially, when trying to find a provider, Trulioo, Jumio, and Onfido were recommended to us, and all seemed to have services for what we needed. See the previous KYC article for advice on what to look for when choosing a provider. For us, the features we needed supported included: </p><ul><li>Identity verification</li><li>Documentary verification</li><li>Biometric verification </li><li>Ongoing monitoring against AML watchlist</li></ul><p>All of the mentioned providers appeared to have products to accomplish what we needed, and Trulioo and Onfido seemed the best match for the technologies we were using. We were impressed with Onfido’s related products, and we opted to use them as our KYC provider. </p><h2 id="%F0%9F%AA%AA-the-kyc-process">🪪 The KYC Process</h2><p>When a user goes through the KYC process, they have to enter personal details, provide documentary evidence (such as a passport), and take a selfie to confirm their identity. Checks are run against AML watchlists - sanctions, politically exposed persons or PEP, and adverse media lists. Onfido is used for data collection and analysis; the workflow logic was contained within Onfido Studio workflows, dictating the flow a user goes through after initiating verification and what data is gathered. There is also logic that lives within our application; we have our own user statuses that we choose for an account after comparing Onfido’s results to the criteria that lives within the application. </p><p>Since not everyone is going through the KYC verification process, we built gates throughout the application, both on the frontend and backend, which check the user account status when certain functionality is attempted. When the thresholds are reached, a user will be prompted to go through the KYC verification process to continue transacting on the site. Most commonly users are prompted to go through KYC during the checkout flow, or when attempting to make a payout. </p><p>Crypto purchases have some additional restrictions, as certain locales are prohibited from making payments using this method, and we are required to check if any crypto addresses used matched sanctions lists. This feature wasn’t supported by our KYC provider Onfido, but was required for compliance with the AML policy, so we opted to use Chainalysis’ free crypto sanctions product, which allows us to check if an address is sanctioned. At the time we were initially looking into a solution Algorand did not have any sanctioned addresses but it was still a requirement to be monitoring for sanctioned addresses in case that changed. </p><h3 id="making-programmatic-decisions">Making programmatic decisions</h3><p>Initially we thought that we’d be able to make most decisions automatically based on the results from Onfido’s API, but we quickly realized that there are many situations where we could not be confident in the results. In fact, it would be faster to tell you what we could confidently make programmatic decisions rather than what we couldn’t. </p><ul><li>Happy path: A user goes through KYC verification without issue, provides a document they are confirmed as the owner of, is not from a restricted locale, and there’s no matching activity on AML watchlists. This user would be placed into a Clear status in our system and would be able to transact freely on the platform, though we would continue to monitor AML lists. </li><li>Unhappy path: A user is from a prohibited region. We would know if the user is using a restricted locale during the verification workflow when the user provided address data, as well as if they used a form of identification from that region, so we can more confidently restrict based on that data. </li><li>Unhappy path: Discrepancy between the document and selfie provided. This indicates the person is using someone else’s identification method fraudulently, and would result in them being restricted on the platform. </li></ul><p>For all other unhappy paths, we place the user into Manual Review. This status requires further exploration by someone on the compliance team. There are regular situations that could lead to an unclear result, such as submitting a blurry photo for the document or providing an unclear selfie. If there is a problem with the data provided, the customer service team can request the user try verification again by placing into the appropriate status for them to retry, and communicating the issue. </p><p>The most common reason, however, that someone falsely ends up being flagged during the process is due to AML watchlists. We check AML watchlists to identify if any active users match sanctions, PEP, or adverse media watchlists, all of which would be prohibited. The AML watchlists very often have false matches, with an emphasis on the adverse media and politically exposed persons (PEP) watchlists. Due to the many false positives, this requires human intervention to analyze the record and determine if the user is the one identified by the watchlist. This analysis takes time, as you have to review the data that was flagged by Onfido as well as the user’s provided data initially and make a decision on if that is the same person. Sometimes it’s obvious, but other times it can be difficult to determine. It is also wise to analyze other user interaction data to make sure there’s no suspicious behavior flagged for any users that are looked into as well. </p><p>Since further exploration is often needed when users are placed into Manual Review, this had implications for customer support. We knew customer support is a necessary part of the KYC process, but it was underestimated how much of a lift this would be. </p><h2 id="%F0%9F%92%81-customer-support">💁 Customer Support</h2><p>Users would often write in with basic questions, some of which the customer service team can be prepared to answer, but the vast majority of the time questions were handed off to those familiar with the AML policies. It was pivotal to have decision maker(s) who could guide the customer service team and help them with what information to share and what not to share, and who can do further analysis to determine what action should be taken and communicate to the development team if any behavior needs to be verified.</p><p>In order to support the compliance team, which was doing the more in-depth analysis, we built various tools for analyzing user behavior. We started out using a headless CMS to build out administrative functionality, but we transitioned to using a product called Retool since it made creating these tools much easier. </p><p>We found the following tools very useful in our KYC exploration.</p><h3 id="%F0%9F%92%BB-administrative-tools">💻 Administrative tools</h3><p>Many tools were built to support the application, but I’d consider the following tools most critical when analyzing fraudulent or suspicious activity. It was also necessary to educate the compliance team on how to use the tools, and to support them by creating new tools as needed to assist in customer support. </p><p><strong>1) User Diagnostics</strong></p><p>The most widely used application we created in Retool was for user diagnostics. This enabled searching for users using various criteria, and if a user is selected, this would populate information on associated payments, payment cards, transactions, payouts, marketplace listings, login history, notes, and more. </p><p>The only action that could be taken in this application is changing the user status and optionally adding notes. This was a requirement when analyzing users in manual review, since a final status needs to be chosen that either enables or restricts their account. </p><p><strong>2) Fraud Detection </strong></p><p>Circle would periodically send us a CSV with information about refunds and chargebacks so we built a tool that directly ingested this CSV and processed it. This allowed us to respond quickly when users were flagged. </p><p>We also built out functionality that would allow us to use IP addresses and user agent information to help identify accounts that are likely owned by the same user. It is not reliable to use IP addresses to indicate the same person logged into an account. IP addresses are shared by more than one household in certain locales, and VPNs add a whole other layer of complication. It can, however, be helpful to analyze in addition to other user behavior. </p><p><strong>3) Marketplace statistics</strong></p><p>One thing the compliance team needed to look for was items being sold for suspiciously high prices. We built functionality around analyzing marketplace statistics to determine reasonable prices for marketplace items. Minimums, maximums, averages, and medians were collected per collectible in a materialized view in the database and displayed within Retool. </p><p><strong>4) Track bot activity</strong></p><p>Users would alert us to bots being used on the site, but there were legitimate uses and we were not able to simply ban all bots. Therefore, we built functionality using Google Recaptcha that tracks scores for each purchase event. The score indicates the likelihood of it having been a bot. We also built a tool in Retool for listing purchase events with this score, along with data on the buyer and seller. This allowed us to keep tabs on where bots were being used, so we could analyze each situation more effectively. </p><p><strong>5) Process refunds</strong></p><p>In certain cases users had started transacting on the site and then were banned. This could happen, for instance, if a user is from a restricted country but that information was not provided on sign up. We needed to build support for refunding users that had loaded funds but were in a restricted state and not able to cash out. </p><h3 id="%F0%9F%94%94-notification-system">🔔 Notification System</h3><p>A notification system has been critical for alerting the compliance team and/or customer service team to certain suspicious activity within the application. Often suspicious activity is not clear-cut enough to act on programmatically, so often what we decided to do was to identify the behavior, and then alert the team, as opposed to taking automatic action. Alerts go out when there are: </p><p><strong>1) Overly-priced marketplace items</strong></p><p>As mentioned, we built a materialized view within our database that compiled data on sales statistics. This was then used to determine reasonable prices for sales on the marketplace and allows us to alert the compliance team whenever a suspiciously high-priced item is sold on the marketplace. </p><p><strong>2) Maximum amount of payment cards is exceeded</strong></p><p>A user can only have a small number of cards in their account to prevent fraudulent behavior where someone creates an account and then tries to use various stolen cards for purchase. The compliance team is alerted whenever a card is added to a user account, after a certain threshold. </p><p><strong>3) Workflow completed with a Rejected or Manual Review result</strong></p><p>The user is alerted anytime the verification workflow is completed for their account. If the account is moved to manual review or restricted, then the compliance team is notified. The user diagnostics tool can be used for further analysis, either searching for all users in review, or searching for a particular user.  </p><p><strong>4) AML watchlist report completed</strong></p><p>An ongoing monitoring subscription is made in Onfido for every verified user on the platform. The monitor continuously checks AML watchlists for any matches for that user. If added to an AML watchlist, the user's account will be restricted or moved into manual review, and the compliance team is notified so they can assess the situation. </p><h2 id="%E2%9B%B0%EF%B8%8F-biggest-challenges">⛰️ Biggest challenges </h2><p>There are high stakes in the KYC world. You think you’ve thought of everything, and the fraudsters find a new way! The KYC process and gating of features should be thoroughly tested, and new issues reported by customers should be looked into within a short time frame. We found other users were a big help in finding suspicious behavior, and users will write in with questions which can uncover flaws which need to be addressed. You’ll thank yourself that you’re responsive and looking into each report when there’s a serious issue that needs attention as quickly as possible. Thankfully we were able to resolve each situation quickly, and always made modifications to deter future situations. </p><h3 id="challenge-1-preventing-duplicate-accounts">Challenge 1: Preventing duplicate accounts</h3><p>In our situation, we chose to not require KYC verification for every user. There would be reduced complexity if all users went through verification, but that would also deter some users. We opted to only require KYC when certain limitations were reached according to our AML Policy. This makes it easier to sign up, but also adds complexity, as we have to enable/disable functionality for more situations. </p><p>Since we are not requiring KYC for every user, we do not know it’s the same user. Users are prohibited from having multiple accounts, but that of course did not deter many from doing just that. We needed to find a way to identify duplicate accounts. We did not have a way of being sure without requiring verification for all, but we did come up with ways to help us determine. </p><p>One situation that presented itself was that users being flagged in chargebacks were being found to use the same card with multiple accounts. We built out the functionality to restrict users if they used a card already being used by another account. If users wrote in to dispute, we would explain the requirement and after they agreed to respect that, we would enable their account again, assuming there wasn’t anything else to be suspicious about. </p><p>In addition, we started tracking IP addresses and user agents. This is not reliable, as IP addresses aren’t unique per household in every locale, and it’s also acceptable for two people to live together and have an account. It is also easy to get around IP checks by using a VPN, and on the flip side, multiple people could be using the same VPN IP address. It is, however, a helpful piece of information to analyze while analyzing other information. If a user has a matching IP address and user agent associated with multiple accounts, it’s likely they are the same person. </p><p>These methods helped us narrow down, but they weren't the silver bullet we were looking for. We assessed fingerprinting services but opted out because we were skeptical about how useful they would be to us. Our team found through testing that it too frequently could not identify duplicates, and the cost was too significant to warrant without being able to use it with more confidence. </p><p>The more we searched, the more we felt confident that the only reliable way to find duplicate accounts was through KYC verification. Instead of requiring verification on sign-up, we decided to implement additional gates that would require verification to use additional features. In addition, we planned to require documentary verification for every user instead of having certain locales have the option to provide an SSN in place of a government document. This would allow us to run biometric verification for every user, and Onfido even has a product called Known Faces that would alert us to the same person associated with different accounts. We had not yet implemented this change, so I cannot speak to how effective it is. Still, we hoped to reduce the number of duplicate accounts by requiring KYC verification more frequently and then watching for matching faces between different accounts. </p><h3 id="challenge-2-reducing-fraudulent-transactions-on-the-marketplace">Challenge 2: Reducing fraudulent transactions on the marketplace </h3><p>The secondary marketplace is where we found the majority of suspicious behavior. Users can sell items they purchased on the marketplace for a predetermined amount. One fraudulent scheme we identified is when a user sells items on the marketplace, and then signs into other accounts to purchase the items using stolen credit cards. Ultimately, fraudulent users are deterred because to withdraw, you have to complete KYC verification. It’s possible someone hacks into an account that has KYC verification completed, which would be the jackpot for a fraudster, but that had not been the case with any situations we came across. We did have one user complete verification in order to gather funds after hacking into a user’s account; we could report that user so they would be flagged for other platforms, but they clearly found the reward to be more attractive than that. </p><p>After analyzing the user behavior, we decided to implement a couple features that would help deter similar fraudulent activity on the platform. I’ll go into each below. </p><p><strong>a) Restrict the amount of cards a user could have in one account</strong></p><p>By restricting the amount of cards a user can have saved in their account, we can cut down on the number of fraudsters that load stolen cards into their account. Most users have one card saved in their account, maybe two. It would be unusual to have a large assortment, which is common with fraudulent situations. </p><p>The feature we implemented would check for the number of cards associated with a user when a purchase was made. If over the threshold and KYC verification has not yet been completed, the user will be required to complete verification. If over the threshold and the user has completed KYC verification, the compliance team will be notified for each additional card added. When we released this feature, all user accounts with cards above the threshold were moved to a status that would require KYC verification to continue using the platform. </p><p><strong>b) Prevent multiple accounts from having the same card associated</strong></p><p>Duplicate accounts are prohibited on the platform, so if there is a card matching numerous accounts, it is suspicious and could mean that someone is trying to load stolen cards into multiple accounts. Our payment provider, Circle, has a fingerprint value for each payment card used. We use this fingerprint value to determine if multiple accounts are using the same payment card and restrict those users. </p><p>If the user reaches out to the compliance team, their account may be unblocked if there is a reasonable explanation. One reasonable situation we identified was that some users were using a service that protects the card number, where temporary numbers are generated that will result in the card being charged but hide the actual card number from the payment service. If a user used a service of this type, they would quickly be flagged as having too many cards. </p><p><strong>c) Track overly-priced items</strong></p><p>To review what was discussed in the Administrative Tools section, a materialized view was created in the database that tracked averages, min, max, and medians for collectibles on the marketplace. These values were then used programmatically to determine if there was a suspiciously-high-priced item on the marketplace that was just sold. Users may accidentally list an item for too high of a price, but if that item is sold, we can assume that was a fraudulent transaction. No user would opt to purchase the much more expensive version of the same item.</p><p><strong>d) Track too high of a success rate</strong></p><p>A success rate that is too high can indicate fraudulent activity. If a user sold every item they tried to sell, that would be highly suspicious. We planned to build support for tracking this information and adding it to the notification system to alert the compliance team. </p><p>Users flagged for too high of a success rate may first be flagged for selling high-priced items, but it's also possible someone games the system selling lower-priced items, so tracking success rate is another important indicator. </p><h3 id="challenge-3-communicating-kyc-process-to-users">Challenge 3: Communicating KYC process to users</h3><p>Many updates were made to increase clarity for users around the KYC process. These decisions were usually made by the development and administrative teams. It could have been helpful to have more design expertise involved in those decisions, as it is difficult to properly communicate the process to a user. In an ideal world, a KYC process would be user-tested to see where confusion lies. </p><p>A confusing KYC process is frustrating to users. Your users must trust your platform to make purchases, and confusion can lead to distrust. There are also really high stakes with KYC since fraudulent users could lead to legal repercussions as well. It’s to everyone’s benefit that the process be smooth and seamless, and adequate attention should be paid to making this process as seamless as any checkout flow. </p><p>It does seem the industry is moving towards in-house KYC done by payment providers. This comes with its benefits and drawbacks. Our setup had a lot of flexibility, which may not be the case when KYC is done in-house, since that provider is assuming all that risk. It would greatly simplify several aspects and may reduce the complexity in communicating to users. I’d need to use a service that had KYC in-house before making an effective comparison. </p><h2 id="final-thoughts">Final Thoughts</h2><p>Some final thoughts from our experience implementing KYC/AML protocols for an NFT marketplace…</p><p><strong>There's no one-size-fits-all solution.</strong></p><p>Your specific KYC verification and AML protocols will depend entirely on your business plan and the amount of risk you’re taking. There may be unique requirements provided by service providers as well. </p><p><strong>While KYC for every user would have been much simpler, it would have provided barriers to purchase.</strong></p><p>And anyone who does funnel optimization will tell you that you should remove barriers to purchase, not add them…</p><p>But playing devil’s advocate, if KYC is required for basic functionality that any user would need to use, such as payouts, there may be an argument for providing that barrier earlier on. </p><p><strong>It's expensive to build and maintain</strong></p><p>This can be a difficult pill to swallow if there isn't steady revenue coming in or if the majority of sales are for low-price tag items. You'll find that services that provide KYC in-house often have a much higher minimum price requirement.</p><p><strong>Fiat transactions made KYC verification necessary</strong></p><p>Supporting payments and payouts using fiat currencies was a primary goal of this project, but if supporting fiat was not a requirement, KYC verification could have been avoided altogether. Blockchain address verification against sanctions lists might still be necessary, but it is a much lighter lift. This would have greatly simplified the project, but not supporting fiat transactions would have scared away any customers new to the crypto space. </p><p><strong>If the service you are building is successful, it only becomes more challenging.</strong></p><p>Consider it a compliment if the fraudsters find you! The more popular the service is, the quicker someone is going to find a critical issue or a way to exploit the system. Be responsive in reviewing issues, and regularly and thoroughly test the verification and security protocols so you can find any issues as quickly as possible. </p><hr><p>I hope our takeaways from implementing a verification system can be useful to you in your project. There is a lot to understand in the KYC/AML world, so if you’re new to the topic, try not to get overwhelmed too quickly. For us, there have been a lot of iterations and a lot of learning over time. It’s also a rapidly evolving industry. If you are implementing for your own project, the best takeaway may be that it’s complicated. Consider the verification process and related security protocols an integral part of your system that requires as much attention as any critical path, even if it is not utilized by every user. </p><p>And if you want to implement a verification system for your <a href="https://www.deptagency.com/service/engineering/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">project</a> and want to work with someone towards that goal, hit us up! We’d love to take that on with you. </p></hr>]]></content:encoded></item><item><title><![CDATA[Prop drilling in React: Solutions and trade-offs]]></title><description><![CDATA[Prop Drilling is the act of passing data, in this case, react props, through several nested layers of components before it reaches the component that needs the data.

Is this a problem in and of itself? It depends (which is the best answer for a question). Props are part of the “React way” of managing state, so using them, even passing them through nested components, isn’t necessarily bad. It depends on the scale.

For small applications, this is not an issue. You can use either of the approache]]></description><link>https://engineering.deptagency.com/prop-drilling-in-react-solutions-and-trade-offs-2/</link><guid isPermaLink="false">6566562fffd18400019b45c5</guid><dc:creator><![CDATA[Tim Chase]]></dc:creator><pubDate>Thu, 30 Nov 2023 15:37:33 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/prop-drilling-react.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/prop-drilling-react.jpg" alt="Prop drilling in React: Solutions and trade-offs"/><p/><p>Prop Drilling is the act of passing data, in this case, react props, through several nested layers of components before it reaches the component that needs the data. </p><p>Is this a problem in and of itself? It depends (which is the best answer for a question). Props are part of the <em>“React way”</em> of managing state, so using them, even passing them through nested components, isn’t necessarily bad. It depends on the scale.​</p><p>For small applications, this is not an issue. You can use either of the approaches featured in this article or none. When a React app grows larger, Prop Drilling becomes an issue. It can make things hard to read, and it couples the parent and child components together, making it difficult to reuse components. When does a small app become a large one? Good question, moving on.</p><p>​Let's set up a scenario to better understand what's happening. Let's say we have an app that is set up something like this image.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/AppDataFlow.png" class="kg-image" alt="Prop drilling in React: Solutions and trade-offs" loading="lazy" width="1624" height="1247" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/11/AppDataFlow.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/11/AppDataFlow.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/11/AppDataFlow.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/AppDataFlow.png 1624w" sizes="(min-width: 720px) 720px"/></figure><p>The Data Page component is fetching our data to be used. Part of that data (tags, prices, attributes, etc.) needs to be used by our filter and sort buttons and their respective modal/form/popover/etc.  That code might look something like this…</p><pre><code class="language-jsx">DataPage (){
  const [filterData, setFilterData] = useState({})
  // ...
  return
    (
      &lt;OptionsBar MetaData={filterData} /&gt;
      &lt;DataArea /&gt;
    )
}

// OptionsBar.jsx
OptionsBar({filterData}){
  // ...
  return (
    &lt;SearchBar /&gt;
    &lt;FilterAndSort filterData={filterData}/&gt;
  )
}

// FilterAndSort.jsx
FilterAndSort({}){
  // ...
  return (
    &lt;Filter filterData={filterData} /&gt;
  )
}

// Filter.jsx
Filter({filterData}){
  // ...
  return(
    &lt;&gt;
      &lt;Button&gt;Filter&lt;/Button&gt;
      &lt;Modal
        content={&lt;FilterForm filterForm={filterData}/&gt;} 
      /&gt;
    &lt;&gt;
  )
}
</code></pre>
<p>As you can see, the <code>filterData</code> prop gets passed through several intermediate components before it eventually gets used in the <code>filterForm</code>.</p><hr><h2 id="enter-context">Enter Context:</h2><p>One way to solve the problem of prop drilling is to use React’s built-in contextAPI. Creating a context provider allows every child of that provider, regardless of how deeply nested it is, access to the context data. That implementation might look something like this.​</p><pre><code class="language-jsx">// App.jsx
import { createContext, useContext } from "react";

DataPage(){
  const [filterData, setFilterData] = useState({})
  // Create Context
  const dataContext = createContext();
  
  ...

  return (
    &lt;dataContext.Provider value={{data: filterData}}&gt;
      &lt;OptionsBar /&gt;
      &lt;DataArea /&gt;
    &lt;/dataContext.Provider&gt;
  )
}

// Filter.jsx
  export Filter({}){
    const { data } = useContext(dataContext)
    return(
      &lt;&gt;
        &lt;Button&gt;Filter&lt;/Button&gt;
        &lt;Modal
          content={&lt;FilterForm filterForm={data}/&gt;} 
        /&gt;
      &lt;&gt;
    )
  }

</code></pre>
<p>By using context, we skip over all the intermediate components and access our data exactly where we need it. </p><p>Problem solved! Reacts own documentation suggests using context to solve the issue of prop drilling, <em>"Using context, we can avoid passing props through intermediate elements"</em><a href="https://legacy.reactjs.org/docs/context.html?ref=dept-engineering-blog.ghost.io"><sup>4</sup></a>. With how easy that solution was, it will be tempting to use context everywhere. This is where we can run into some issues. Let's say we add a <em><code>userContex</code></em>, <em><code>themeContext</code></em>, and a <em><code>authContext</code></em> and end up with a render that looks something like this.</p><figure class="kg-card kg-code-card"><pre><code class="language-jsx">```jsx
render (
  &lt;userContex.Provider value={userData}&gt;
    &lt;themeContext.Provider value={theme}&gt;
      &lt;authContext.Provider value={authentication}&gt;
        &lt;dataContext.Provider value={{data: filterData}}&gt;
          &lt;OptionsBar /&gt;
          &lt;DataArea /&gt;
        &lt;/dataContext.Provider&gt;
      &lt;/authContext.Provider&gt;
    &lt;/themeContext.Provider&gt;
  &lt;/userContex.Provider&gt;
)
```</code></pre><figcaption><p><span style="white-space: pre-wrap;">Too many contexts</span></p></figcaption></figure><p>Now, we have contexts inside of other contexts, leading to unnecessary complexity and confusion. Tracking down bugs or making updates can become a chore with a touch of hide and seek. "Was <em><code>userLanguage</code></em> in authentication or userData?" It is also important to remember that using context means that any component and/or their children that access the context data are tightly coupled, thus reducing its re-usability. </p><p>​There is another caveat to using context. This is especially true when the data in the context provider is an object. If a value in that object gets updated, react will replace the whole object with a new one<a href="https://react.dev/reference/react/useContext?ref=dept-engineering-blog.ghost.io#optimizing-re-renders-when-passing-objects-and-functions"><sup>1</sup></a>. This will trigger a re-render on all components using that context and possibly their children. So if that update happens to be on a high-level parent component or a root level one, it could potentially re-render the entire page.​</p><p>React's documentation offers a warning about the use of context​.</p><blockquote>
<p>Context is very tempting to use! However, this also means it’s too easy to overuse it. Just because you need to pass some props several levels deep doesn’t mean you should put that information into context.<a href="https://react.dev/learn/passing-data-deeply-with-context?ref=dept-engineering-blog.ghost.io#before-you-use-context"><sup>1</sup></a></p>
</blockquote>
<p>​and again​</p><blockquote>
<p>Before You Use Context<br>
Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult.&gt;If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context.<a href="https://legacy.reactjs.org/docs/context.html?ref=dept-engineering-blog.ghost.io#before-you-use-context"><sup>4</sup></a></br></p>
</blockquote>
<hr><h2 id="component-composition">Component Composition:</h2><p>As seen above, React’s documentation suggests that "Component Composition" is a better solution. In other words, put your components in areas where they can access the props they need while skipping over the intermediaries. You can do this by lifting components up in the tree and using child components as props.</p><p>​There are a couple different approaches to component composition. One way we could do this is by moving our components up the tree and wrapping them inside of the parent component. In the example below, we could move all of our imports up to the <code>DataPage</code> and wrap them inside of each other allowing the <code>filterData</code> prop to be passed directly to the <code>Filter</code> component.</p><pre><code class="language-jsx">// DataPage.jsx
DataPage(){
const [filterData, setFilterData] = useState({})
  // ...
  return (
    &lt;&gt;
      &lt;OptionsBar&gt;
        &lt;FilterAndSort&gt;
          &lt;Filter filterData={filterData}/&gt;
        &lt;/FilterAndSort&gt;
      &lt;/OptionsBar&gt;
      &lt;DataArea /&gt;
    &lt;/&gt;
  )
}
// OptionsBar.jsx
OptionsBar({ children }){
  // ...
  return (
    // ...
    { children }
  )
}
// FilterAndSort.jsx
FilterAndSort({children}){
  // ...
  return (
    // ...
    { children }
  )
}

// Filter.jsx
Filter({filterData}){
  // ...
  return(
    &lt;&gt;
      &lt;Button&gt;Filter&lt;/Button&gt;
      &lt;Modal
        content={&lt;FilterForm filterForm={filterData}/&gt;} 
      /&gt;
    &lt;&gt;
  )
}</code></pre><p>Admittedly, this is an extreme example and could impact the readability, size, and complexity of our parent component. Luckily, we have flexibility with how we compose our components! We could instead move the wrapping one level down into the <code>OptionsBar</code> component to make things more readable.​</p><pre><code class="language-jsx">// DataPage.jsx
DataPage(){
const [filterData, setFilterData] = useState({})
  // ...
  return (
    &lt;&gt;
      &lt;OptionsBar filterData={filterData}/&gt;
      &lt;DataArea /&gt;
    &lt;/&gt;
  )
}
// OptionsBar.jsx
OptionsBar({filterData}){
  // ...
  return (
    &lt;Search /&gt;
    &lt;FilterAndSort&gt;
      &lt;Filter filterData={filterData}/&gt;
    &lt;/FilterAndSort&gt;
  )
}</code></pre><p>​Another approach would be to alter our components to have props that take components. This way we can set our component up in the parent, pass our props to it, and then pass that whole component as a prop.​</p><pre><code class="language-jsx">// OptionsBar.jsx
OptionsBar({filterData}){
  const filter = &lt;Filter filterData={filterData} /&gt;
  ...
  return (
    &lt;Search /&gt;
    &lt;FilterAndSort filter={filter} sort={...} /&gt;
  )
}

// FilterAndSort.jsx
FilterAndSort({filter, sort}){
  return (
    &lt;div&gt;
      { filter }
      { sort }
    &lt;/div&gt;
  )
}</code></pre><h2 id="recap">Recap<strong>:</strong></h2><div class="kg-card kg-callout-card kg-callout-card-green"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">“There are no solutions. There are only trade-offs” - Thomas Sowell</div></div><p>So, context can solve the issue of prop drilling by allowing data to be accessed right where we need it. But, it comes with issues of making components hard to reuse and unnecessary re-renders. Component Composition is a better solution. We can retain the flexibility, re-usability, and readability of components. But, it can lead to large and complex parent components depending on how you compose your components (see what I did there). Of course, you are not limited to choosing just one or the other. The best solution for your app may be a mix of both.​​</p><p>References:</p><p><em>1. https://react.dev/reference/react/useContext</em></p><p><em>2. https://react.dev/learn/passing-data-deeply-with-context</em></p><p><em>3. https://legacy.reactjs.org/docs/composition-vs-inheritance.html</em></p><p><em>4. https://legacy.reactjs.org/docs/context.html</em></p></hr></hr>]]></content:encoded></item><item><title><![CDATA[Convert GET to POST Inline with Java and Spring Gateway]]></title><description><![CDATA[You have a third-party provider that you need to obtain news information from and the only API they have for this is a POST with a request body, yet this clearly should be a GET process since the API is simply retrieving data and nothing transactional is happening.]]></description><link>https://engineering.deptagency.com/convert-get-to-post-inline-in-java-with-spring-gateway/</link><guid isPermaLink="false">65563db62a64e10001c758de</guid><category><![CDATA[java]]></category><dc:creator><![CDATA[Kathy King]]></dc:creator><pubDate>Thu, 16 Nov 2023 16:53:21 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/chris-robert-n-tIXOJfVA4-unsplash-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/chris-robert-n-tIXOJfVA4-unsplash-1.jpg" alt="Convert GET to POST Inline with Java and Spring Gateway"/><p>You have a third-party provider that you need to obtain news information from. </p><p>The only API they have for this is a POST with a request body, yet this clearly should be a GET process since the API is simply retrieving data and nothing transactional is happening. </p><p>You are trying to keep your internal APIs as standard as possible and so would like the front-end to request this data as a GET with query parameters. If you are using Java and a Spring gateway, one solution is to receive the GET from the front-end and convert it to a POST inline prior to reaching out to the provider with the request.</p><p>We can make use of a request decorator in a gateway filter to alter the original request. This decorator extends ServerHttpRequestDecorator which wraps another ServerHttpRequest and delegates all methods to it. Subclasses can override specific methods selectively. We are overriding the getMethodValue and getURI methods. We override the getMethodValue method to return HttpMethod.POST.name() and we override the getURI method to remove the query parameters from the request.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/CleanShot-2023-11-16-at-08.09.08@2x.png" class="kg-image" alt="Convert GET to POST Inline with Java and Spring Gateway" loading="lazy" width="1436" height="1698" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/11/CleanShot-2023-11-16-at-08.09.08@2x.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/11/CleanShot-2023-11-16-at-08.09.08@2x.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/CleanShot-2023-11-16-at-08.09.08@2x.png 1436w" sizes="(min-width: 720px) 720px"/></figure><p>We still have the request body to deal with. In our route definition, we apply the filter above before modifying the request body to ensure that any new headers we added in the filter are preserved. You can use the ModifyRequestBody filter to modify the request body before it is sent downstream by the gateway. </p><p>We use the NewsBody class to convert the query parameters into a json string suitable for a request body.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/CleanShot-2023-11-16-at-08.11.39@2x.png" class="kg-image" alt="Convert GET to POST Inline with Java and Spring Gateway" loading="lazy" width="1434" height="172" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/11/CleanShot-2023-11-16-at-08.11.39@2x.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/11/CleanShot-2023-11-16-at-08.11.39@2x.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/CleanShot-2023-11-16-at-08.11.39@2x.png 1434w" sizes="(min-width: 720px) 720px"/></figure><p>Full route example:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/CleanShot-2023-11-16-at-08.13.05@2x.png" class="kg-image" alt="Convert GET to POST Inline with Java and Spring Gateway" loading="lazy" width="1436" height="440" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/11/CleanShot-2023-11-16-at-08.13.05@2x.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/11/CleanShot-2023-11-16-at-08.13.05@2x.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/CleanShot-2023-11-16-at-08.13.05@2x.png 1436w" sizes="(min-width: 720px) 720px"/></figure><p>We have successfully received a GET from our front-end and converted it to a POST for our provider. Spring gateway offers many <a href="https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories.html?ref=dept-engineering-blog.ghost.io" rel="noreferrer">built-in gateway filters</a> for adjusting requests and responses like Retry GatewayFilter Factory and JsonToGrpc GatewayFilter Factory.</p>]]></content:encoded></item><item><title><![CDATA[A comprehensive guide to understanding the SOLID principles]]></title><description><![CDATA[Through each of these sections, we’ll focus on various design principles, with particular attention to the five SOLID principles, which will be illustrated through examples.]]></description><link>https://engineering.deptagency.com/guide-solid-principles/</link><guid isPermaLink="false">6536976a1ce9e00001a2aae6</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Reza Erami]]></dc:creator><pubDate>Thu, 09 Nov 2023 14:43:39 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/pexels-Christina-Morillo.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/pexels-Christina-Morillo.jpg" alt="A comprehensive guide to understanding the SOLID principles"/><p>A design principle empowers you to handle the complexity of the design process more efficiently, serving as a best practice to simplify design. Design Principles don’t provide a direct answer, but they offer a framework for approaching and solving problems.</p><p>Introduced by Robert C. Martin in 2000 in his paper on design principles and patterns, let’s first understand the distinctions between design principles and patterns.</p><p>Differences between design principles and design patterns:</p>
<!--kg-card-begin: html-->
<table dir="ltr" border="1" cellspacing="0" cellpadding="0" style="white-space: normal;">
<tbody>
<tr>
<td><strong>Aspect</strong></td>
<td><strong>Design Patterns</strong></td>
<td><strong>Design Principles</strong></td>
</tr>
<tr>
<td>Purpose</td>
<td>Reusable solutions for common problems</td>
<td>Guidelines and best practices for design and problem-solving</td>
</tr>
<tr>
<td>Application</td>
<td>Direct solutions or answers to specific problems</td>
<td>Overarching approaches to follow best practices</td>
</tr>
<tr>
<td>Flexibility</td>
<td>Specific and should be applied when needed</td>
<td>Broad and applicable in various design situations</td>
</tr>
<tr>
<td>Interaction with others</td>
<td>Can be used in conjunction with design principles</td>
<td>Complement each other and can sometimes conflict</td>
</tr>
<tr>
<td>Examples</td>
<td>Singleton, Observer, Factory Method, etc.</td>
<td>SOLID principles, DRY (Don't Repeat Yourself), KISS (Keep It Simple, Stupid), etc.</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<p>Through each of these sections, we’ll focus on various design principles, with particular attention to the five SOLID principles, these will be illustrated through examples.</p><p>SOLID is a widely embraced principle for Object-Oriented programming languages. It’s an acronym for five principles:</p><ul><li>Single Responsibility Principle (<strong>SRP</strong>)</li><li>Open-Closed Principle (<strong>OCP</strong>)</li><li>Liskov Substitution Principle (<strong>LSP</strong>)</li><li>Interface Segregation Principle (<strong>ISP</strong>)</li><li>Dependency Inversion Principle (<strong>DIP</strong>)</li></ul><h2 id="single-responsibility-principle-writing-clean-and-maintainable-code">Single Responsibility Principle: Writing clean and maintainable code</h2><p>SRP suggests that classes, modules, and functions should each have a singular focus. The approach involves breaking down these entities into smaller components, each handling a distinct task. This strategy accelerates development and testing, enhancing understanding of each component’s individual role. By adhering to this principle, you can evaluate class scope to ensure it doesn’t breach SRP.</p><p>Let’s say we want to create a user class in which we obtain an image for the user and upload it to the S3 bucket. After obtaining the path to the uploaded file, we will update the database, and set the file path as the profile path for the user. So, let’s start with an empty class containing an empty method called <em>updateAvatar</em></p><pre><code class="language-js">class UserService{
  public async updateAvatar(id: number, file: any) {
    try {
      // connect to S3
      // upload to S3
      // get path
      // find user and update it with given path
    } catch (error) {
      console.error('Avatar update failed:', error);
    }
  }
}

</code></pre>
<p>As you can see, we have an empty method for updating the avatar, which receives the path and updates the database. Now, let’s proceed to actually push it to S3.</p><pre><code class="language-js">class UserService{
  public async updateAvatar(id: number, file: any) {
    try {
      const s3 = new AWS.S3();
      const params = {
         Bucket: 'my-bucket',
         Key: `avatars/${this.id}-${Date.now()}.jpg`,
         Body: file,
      };

      const uploadResult = await s3.upload(params).promise();
      const avatarPath = uploadResult.Location;
    } catch (error) {
      console.error('Avatar update failed:', error);
    }
  }
}
</code></pre>
<p>Now that we have the upload result, we can update the database.</p><pre><code class="language-js">const user = await this.usersRepository.findById(id);
user.avatarPath = avatarPath;
await user.save();
</code></pre>
<p>So, we’re going to end up with a class that looks like this:</p><pre><code class="language-js">class UserService {
  private usersRepository: UserRepository;

  constructor(usersRepository: UserRepository) {
   this.usersRepository = usersRepository;
  }

  public async updateAvatar(id: number, file: any) {
    try {
      const s3 = new AWS.S3();
      const params = {
        Bucket: 'my-bucket',
        Key: `avatars/${this.id}-${Date.now()}.jpg`,
        Body: file,
      };

      const uploadResult = await s3.upload(params).promise();
      const avatarPath = uploadResult.Location;

      const user = await this.usersRepository.findById(id);
      user.avatarPath = avatarPath;
      await user.save();
    } catch (error) {
      console.error('Avatar update failed:', error);
    }
  }
}
</code></pre>
<p>As you can see in the example above, both uploading a file to S3 and interacting with the database, specifically writing to it, have been combined within a single method. This means that our <em>updateAvatar</em> method has two responsibilities:</p><ol><li>Uploading a file.</li><li>Writing to the database.</li></ol><p>This practice contradicts the first principle of SOLID, which is the Single Responsibility Principle. Now, let’s refactor the code:</p><ol><li>First, we create a “<em>File</em>” class that is responsible for uploading files.</li><li>Then, we create a “<em>UserUpdater</em>” class, which is responsible for updating user-related information.</li><li>Finally, we can include the file upload method inside our <em>updateAvatar</em> method to orchestrate these two methods together.</li></ol><p>By separating these responsibilities into distinct classes, we adhere to SOLID principles and maintain a more modular and maintainable codebase.</p><pre><code class="language-js">class FileService {
  public async upload(file: any): Promise&lt;string&gt; {
    const s3 = new AWS.S3();
    const extension = this.getFileExtension(file);
    const params = {
      Bucket: 'my-bucket',
      Key: `avatars/${Date.now()}.${extension}`,
      Body: file,
    };

    const uploadResult = await s3.upload(params).promise();
    return uploadResult.Location;
  }

  public getFileExtension(file: any): string {
    const fileName = file.name || '';
    const parts = fileName.split('.');
    if (parts.length &gt; 1)
      return parts[parts.length - 1];
    return '';
  }
}
</code></pre>
<p>As you can see, our <em>FileService </em>class now has the sole responsibility of handling files and includes a method to upload a file. Now, let’s refactor the “<em>User</em>” class as well to adhere to the Single Responsibility Principle (SRP).</p><pre><code class="language-js">class UserService {
  private usersRepository: UserRepository;
  private fileService: FileService;

  constructor(usersRepository: UserRepository, fileService: FileService) {
    this.usersRepository = usersRepository;
    this.fileService = fileService;
  }

  public async update(id: number, fields: Record&lt;string, any&gt;) {
    let user = await this.usersRepository.findById(id);
    user = {...user, ...fields};
    await user.save();
    return user;
  }
 
  public async updateAvatar(id: number, file: any) {
    try {
      const avatarPath = await this.fileService.upload(file);
      await this.update(id, { avatar: avatarPath });
    } catch (error) {
      console.error('Avatar update failed:', error);
    }
  }
}
</code></pre>
<p>With this approach, we’ll have code that is more testable and reusable. The file uploader can be reused in other services, and the user update functionality can be used to update other fields of the user. However, when used together within the <em>updateAvatar </em>method, they will cooperate seamlessly. This design promotes modularity and maintainability in our codebase.</p><h2 id="open-closed-principle-extending-your-code-without-modification">Open-Closed Principle: Extending your code without modification</h2><p>The Open-Closed Principle states that objects or entities should be open for extension but closed for modification. This means they should be extendable without altering their core implementation. This principle can be applied in Object-Oriented Programming (OOP) by creating new classes that extend the original class and override its methods, rather than modifying the original class directly.</p><p>In functional programming, this can be achieved through the use of function wrappers, where you can call the original function and apply new functionality to it without changing the original function itself.</p><p>The decorator design pattern is also a useful tool for adhering to this design principle. With decorators, you can attach new responsibilities or behaviors to objects without modifying their source code, thus keeping them closed for modification and open for extension.</p><p>In the example below, we are going to expand the “<em>updateAvatar” </em>method to include validation, preventing any non-image extensions from being uploaded. Let’s begin by modifying the “<em>updateAvatar” </em>method first.</p><pre><code class="language-js">public async updateAvatar(id: number, file: any) {
    try {
        // Get the file extension
        const fileExtension = this.fileService.getFileExtension(file);

        // Check if it's JPG, throw an error if not
        if (["jpg", "png"].includes(fileExtension.toLowerCase())) {
            throw new Error('Unsupported avatar format. Only image is allowed.');
        }

        const avatarPath = await this.fileService.upload(file);
        await this.update(id, {avatar: avatarPath});
        console.log('Avatar updated successfully.');
    } catch (error) {
        console.error('Avatar update failed:', error);
    }
}
</code></pre>
<p>As you can see, the code above violates the Open-Closed Principle (OCP), which is the second principle of SOLID, as it requires us to modify our code if we want to add more extensions. Now, let’s refactor this code to adhere to the OCP.</p><ol><li>We will create a file validator where we can pass a file and the expected extension.</li><li>Then, our validator will check if the file extension matches the expected extension before allowing it to proceed.</li></ol><p>Let’s begin by adding new methods to the <em>“File” </em>class.</p><pre><code class="language-js">class FileService {
    public async upload(file: any, extension: string): Promise&lt;string&gt; {
        const s3 = new AWS.S3();
        const params = {
            Bucket: 'my-bucket',
            Key: `avatars/${Date.now()}.${extension}`,
            Body: file,
        };

        const uploadResult = await s3.upload(params).promise();
        return uploadResult.Location;
    }

    public validate(file: any, supportedFormats: string[]): boolean {
        const fileExtension = this.getFileExtension(file);

        if (supportedFormats.includes(fileExtension.toLowerCase()))
            return true;

        throw new Error("File extension not allowed!");
    }

    public getFileExtension(file: any): string {
        const fileName = file.name || '';
        const parts = fileName.split('.');
        if (parts.length &gt; 1)
            return parts[parts.length - 1];
        return '';
    }
}
</code></pre>
<p>As you can see, the old method for uploading a file has not been modified; instead, we have simply added new methods to the File class.</p><p>The second step is to add the validation method to our <em>“updateAvatar”</em> function.</p><pre><code class="language-js">public async updateAvatar(id:number, file:any) {
    try {
        this.fileService.validate(file, ["jpg"]);
        const avatarPath = await this.fileService.upload(file);
        await this.update(id, {avatar: avatarPath});
    } catch (error) {
        console.error('Avatar update failed:', error);
    }
}
</code></pre>
<p>Although there is still a minor concern — what if we had more supported formats? — you are completely correct. In that case, we would need to modify this method. So, let’s perform another refactoring on the code. We will separate the supported image formats from the <em>“updateAvatar ” </em>method and move them to global variables or application configuration, where we will only manage the constants, not the logic.</p><pre><code class="language-js">// config.js

export const SUPPORTED_IMAGE_FORMATS = ["jpg", "png", "jpeg", "svg", "webp"];
</code></pre>
<p>And then we can use these constants in our <em>“updateAvatar”</em> method.</p><pre><code class="language-js">this.fileService.validate(file, SUPPORTED_IMAGE_FORMATS);
</code></pre>
<p><strong>Note: The reason we haven’t directly imported this constant into the “<em>Validate” </em>method is to keep it reusable. This way, we can use the “<em>Validate”</em> method for videos, documents, and other extensions as well.</strong></p><h2 id="liskov-substitution-principle-inheritance-and-polymorphism-done-right">Liskov Substitution Principle: Inheritance and polymorphism done right!</h2><p>LSP suggests that any superclass needs to be replaceable with its subclasses without breaking the application. This means that if we have class A and class B is extended from class A, and there is a client for class B (a function, module, or anything that uses any property or method from class B), it should be able to use class A instead. So, if you use a superclass instance instead of a subclass, everything should still work correctly.</p><p>Let’s take a look at this example of the “<em>UserService”</em>, where we aim to implement new methods for database operations.</p><pre><code class="language-js">class UserService {
    private usersRepository: UserRepository;

    constructor(usersRepository: UserRepository) {
        this.usersRepository = usersRepository;
    }

    public async create(fields: Record&lt;string, any&gt;) {
        const user = await this.usersRepository.insert(fields);
        return user;
    }

    public async get(id: number) {
        const user = await this.usersRepository.findById(id);
        return user;
    }

    public async update(id: number, fields: Record&lt;string, any&gt;) {
        let user = await this.usersRepository.findById(id);
        user = {...user, ...fields};
        await user.save();
        return user;
    }

    public async delete(id: number) {
        await this.usersRepository.delete(id);
    }
}
</code></pre>
<p>In this example, the “<em>UserService </em>”has implemented all the necessary methods from the “<em>UserRepository</em>”, which adds a new layer to our application for performing database operations. These methods not only interact with the database but also allow us to perform additional tasks, manipulate the results, or add validation.</p><p>However, let’s say we want to create similar services for other entities, such as “Roles,” where we manage roles and permissions. In this case, we would create a new class for the “Roles” service.</p><pre><code class="language-js">class RoleService {
    private roleRepository: RoleRepository;

    constructor(roleRepository: RoleRepository) {
        this.roleRepository = roleRepository;
    }

    public async create(fields: Record&lt;string, any&gt;) {
        const user = await this.roleRepository.insert(fields);
        return user;
    }

    public async get(id: number) {
        const user = await this.roleRepository.findById(id);
        return user;
    }

    public async update(id: number, fields: Record&lt;string, any&gt;) {
        let user = await this.roleRepository.findById(id);
        user = {...user, ...fields};
        await user.save();
        return user;
    }

    public async delete(id: number) {
        await this.roleRepository.delete(id);
    }
}
</code></pre>
<p>As you can see, these services are nearly identical, with the only difference being the injection of a different repository for each service. To reduce code duplication and promote reusability, let’s start by creating a superclass that handles the repository integration. We can then extend this superclass in subclasses for different entities.</p><pre><code class="language-js">class BaseService {
    private repository;

    constructor(repository) {
        this.repository = repository;
    }

    public async create(fields: Record&lt;string, any&gt;) {
        const user = await this.repository.insert(fields);
        return user;
    }

    public async get(id: number) {
        const user = await this.repository.findById(id);
        return user;
    }

    public async update(id: number, fields: Record&lt;string, any&gt;) {
        let user = await this.repository.findById(id);
        user = {...user, ...fields};
        await user.save();
        return user;
    }

    public async delete(id: number) {
        await this.repository.delete(id);
    }
}
</code></pre>
<p>Now that we have our “<em>BaseService </em>”class, we can extend it to create the “<em>UserService ”</em>and “<em>RoleService ”</em>classes. This approach allows us to reuse common functionality and minimize code duplication.</p><pre><code class="language-js">class UserService extends BaseService {
    constructor(repository: UserRepository) {
        this.repository = repository;
    }
}

class RoleService extends BaseService {
    constructor(repository: RoleRepository) {
        this.repository = repository;
    }
}
</code></pre>
<p>As you can see, the “<em>UserService </em>”and “<em>RoleService </em>”classes no longer contain repository integration; they simply switch the repository they use. This demonstrates the beauty of the Liskov Substitution Principle (LSP), where we can replace these subclasses with their superclass, such as “<em>BaseService</em>”, without causing the application to crash.</p><p>Now, let’s take a look at the code where an instance of “<em>UserService </em>”is created.</p><pre><code class="language-js">const userService = new UserService(new UserRepository());
</code></pre>
<p>The code where an instance of “<em>UserService </em>”is created can be replaced by the superclass.</p><pre><code class="language-js">const userService = new BaseService(new UserRepository());
</code></pre>
<p>You can replace it with any other repository to create a new service for different repositories or entities.</p><h2 id="interface-segregation-principle-flexible-interfaces-for-specific-needs">Interface Segregation Principle: Flexible interfaces for specific needs</h2><p>ISP refers to implementing only the methods of an interface that are needed. So, clients of an interface should not be forced to implement all the methods of an interface if those methods are not used. In other words, interface segregation points out the fact that having many small interfaces is more beneficial than having one general interface.</p><p>Let’s examine this example where we have a “<em>UserService </em>”class extended by the “<em>CustomerService </em>”and “<em>SellerService </em>”classes. In this design, the “<em>UserService </em>”has its own interface that enforces the subclasses extended from it to implement methods that may not necessarily be needed.</p><p>Now, let’s begin by creating the interfaces for our services.</p><pre><code class="language-js">interface BaseServiceInterface {
  create(fields: Record&lt;string, any&gt;): Promise&lt;User&gt;;

  get(id: number): Promise&lt;User&gt;;

  update(id: number, fields: Record&lt;string, any&gt;): Promise&lt;User&gt;;

  delete(id: number): Promise&lt;void&gt;;
}

interface UserServiceInterface extends BaseServiceInterface {
  getReviews(userId: number): Promise&lt;Review[]&gt;;

  getOrders(userId: number): Promise&lt;Order[]&gt;;

  getSells(userId: number): Promise&lt;Order[]&gt;;

  getShops(userId: number): Promise&lt;Shop[]&gt;;
}
</code></pre>
<p>Now, let’s implement the necessary methods that our “<em>UserService </em>”is missing. This will ensure that the subclasses, such as “<em>CustomerService </em>”and “<em>SellerService</em>”, can provide their own implementations for these methods as needed.</p><pre><code class="language-js">class UserService extends BaseService implements UserServiceInterface {
  private reviewService: ReviewService;
  private orderService: OrderService;
  private shopService: ShopService;

  constructor(
    repository: UserRepository,
    reviewService: ReviewService,
    orderService: OrderService,
    shopService: ShopService,
  ) {
    this.repository = repository;
    this.reviewService = reviewService;
    this.orderService = orderService;
    this.shopService = shopService;
  }

  getReviews(userId: number): Promise&lt;Order[]&gt; {
    this.reviewService.findAll({userId})
  }

  getOrders(userId: number): Promise&lt;Order[]&gt; {
    this.orderService.findAll({userId})
  }

  getSells(userId: number): Promise&lt;Order[]&gt; {
    this.orderService.findAll({sellerId: userId})
  }

  getShops(userId: number): Promise&lt;Order[]&gt; {
    this.shopService.findAll({userId})
  }
}
</code></pre>
<p>As you can see, our “<em>UserService </em>”has implemented all the methods it needs and will inherit the remaining methods such as “get,” “create,” etc., from “<em>BaseService</em>”.</p><p>Now, let’s proceed to extend our “<em>UserService </em>”into “<em>CustomerService </em>”and “<em>SellerService</em>”.</p><pre><code class="language-js">class CustomerService extends UserService implements UserServiceInterface {
  //
  //
}

class SellerService extends UserService implements UserServiceInterface {
  //
  //
}
</code></pre>
<p>However, there is an issue: a regular customer can’t have sales or a shop, while a seller can’t post a review or have orders. Therefore, we need to override these methods to prevent them from being used.</p><pre><code class="language-js">class CustomerService extends UserService implements UserServiceInterface {
  getSells() {
    throw new Error("User is not a seller");
  }

  getShops() {
    throw new Error("User is not a seller");
  }
}

class SellerService extends UserService implements UserServiceInterface {
  getReviews() {
    throw new Error("User is not a seller");
  }

  getOrders() {
    throw new Error("User is not a seller");
  }
}
</code></pre>
<p>The problem has been resolved, and instances from the customer or seller can now only use the allowed methods. However, this example currently violates the Interface Segregation Principle by having one large, generic interface instead of many small interfaces. To rectify this, we can follow these steps:</p><ol><li>Break the “<em>UserServiceInterface </em>”into two interfaces: “<em>CustomerServiceInterface </em>”and “<em>SellerServiceInterface</em>”.</li><li>Move methods that belong to one of these interfaces but not the other. If there are common methods, they can remain in the “<em>UserServiceInterface</em>”.</li><li>Instead of implementing the generic interface on “<em>SellerService </em>”and “<em>CustomerService </em>”classes, we will implement the specific interfaces.</li></ol><p>Let’s begin with these steps.</p><pre><code class="language-js">interface UserServiceInterface extends BaseServiceInterface {
  //
}

interface CustomerServiceInterface extends UserServiceInterface {
  getReviews(userId: number): Promise&lt;Review[]&gt;;

  getOrders(userId: number): Promise&lt;Order[]&gt;;
}

interface SellerServiceInterface extends UserServiceInterface {
  getSells(userId: number): Promise&lt;Order[]&gt;;

  getShops(userId: number): Promise&lt;Shop[]&gt;;
}
</code></pre>
<p>So, we have just split our interface into two smaller interfaces. Next, we will move the necessary methods into the related classes only.</p><pre><code class="language-js">class CustomerService extends UserService implements CustomerServiceInterface {
  private reviewService: ReviewService;
  private orderService: OrderService;

  constructor(
    repository: UserRepository,
    reviewService: ReviewService,
    orderService: OrderService,
  ) {
    this.repository = repository;
    this.reviewService = reviewService;
    this.orderService = orderService;
  }

  getReviews(userId: number): Promise&lt;Order[]&gt; {
    this.reviewService.findAll({userId})
  }

  getOrders(userId: number): Promise&lt;Order[]&gt; {
    this.orderService.findAll({userId})
  }
}

class SellerService extends UserService implements SellerServiceInterface {
  private orderService: OrderService;
  private shopService: ShopService;

  constructor(
    repository: UserRepository,
    orderService: OrderService,
    shopService: ShopService,
  ) {
    this.repository = repository;
    this.orderService = orderService;
    this.shopService = shopService;
  }

  getSells(userId: number): Promise&lt;Order[]&gt; {
    this.orderService.findAll({sellerId: userId})
  }

  getShops(userId: number): Promise&lt;Order[]&gt; {
    this.shopService.findAll({userId})
  }
}
</code></pre>
<p>As you can see, we no longer have methods and properties that are not used in those classes, and there is no requirement to implement unused methods. This adheres to the Interface Segregation Principle and results in more focused and efficient interfaces for each class.</p><h2 id="dependency-inversion-principle-building-flexible-and-maintainable-software-designs">Dependency Inversion Principle: Building flexible and maintainable software designs!</h2><p>The Dependency Inversion Principle (DIP) suggests that high-level modules should not have direct dependencies on low-level modules. Instead, both high-level and low-level modules should depend on abstractions or interfaces. Furthermore, abstractions should not rely on implementation details; rather, implementation details should depend on abstractions. By adhering to this principle, the risk of unintended side effects in high-level modules caused by changes in low-level modules is minimized. With the introduction of an abstract layer, dependencies are inverted, reducing the traditional top-down dependency structure.</p><p>Let’s examine an example involving a “<em>UserService</em>” and “<em>RoleRepository</em>” class. In this scenario, the “<em>UserService” </em>includes an implemented “<em>getRole</em>” method to retrieve the role of a user.</p><pre><code class="language-js">class UserService extends BaseService {
  constructor(repository: UserRepository) {
    this.repository = repository;
  }
}
</code></pre>
<p>In the previous examples, the “<em>UserService</em>” implemented the “<em>BaseService</em>”. However, to add a new method called “<em>getRole</em>”, we will include it in the “<em>UserService</em>” to retrieve the user’s role. Additionally, within the “<em>getRole</em>” method, we will create an instance of “<em>RoleRepository</em>” to access the “<em>RoleRepository</em>”.</p><pre><code class="language-js">class UserService extends BaseService {
  constructor(repository: UserRepository) {
    this.repository = repository;
  }

  public async getRole(userId: number): Promise&lt;Role&gt; {
    const roleRepository = new RoleRepository();
    const user = await this.get(userId);
    const role = await roleRepository.findById(user.roleId);
    return role;
  }
}
</code></pre>
<p>With the current approach, we can achieve our desired functionality, but we are also violating the fifth principle of SOLID, which is Dependency Inversion. This is because our “<em>UserService </em>” is dependent on the details of the “<em>RoleRepository</em>” and creates an instance of it.</p><p>To address this issue, we need to inject the <em>“RoleRepository”</em> as a dependency into the “<em>UserService” </em>instead of creating a class instance within it. Furthermore, since “<em>RoleRepository” </em>implements “<em>BaseRepository</em>”, which is an abstract class, we can ensure that the get method exists in that abstract class. This approach helps us avoid relying on implementation details and promotes better adherence to the Dependency Inversion Principle.</p><pre><code class="language-js">class UserService extends BaseService {
  private roleRepository: RoleRepository;

  constructor(repository: UserRepository, roleRepository: RoleRepository) {
    this.repository = repository;
    this.roleRepository = roleRepository;
  }

  public async getRole(userId: number): Promise&lt;Role&gt; {
    const user = await this.get(userId);
    const role = await this.roleRepository.findById(user.roleId);
    return role;
  }
}
</code></pre>
<p>In the refactored code, we can easily replace “<em>RoleRepository” </em>with any other repository that handles roles. An improvement in this code is that, instead of using the “<em>RoleRepository” </em>class, we can utilize the “<em>RoleService”</em>. This approach keeps the “<em>RoleRepository” </em>layer isolated within the “<em>RolesModule”</em>, promoting a more modular and maintainable design.</p><pre><code class="language-js">class UserService extends BaseService {
  private roleService: RoleService;

  constructor(repository: UserRepository, roleService: RoleService) {
    this.repository = repository;
    this.roleService = roleService;
  }

  public async getRole(userId: number): Promise&lt;Role&gt; {
    const user = await this.get(userId);
    const role = await this.roleService.get(user.roleId);
    return role;
  }
}
</code></pre>
<h2 id="concluding-the-understanding-of-the-solid-principles">Concluding the understanding of the SOLID Principles</h2><p>In this exploration of SOLID principles, we've delved into key principles that serve as guiding lights in software design. These principles, namely the Single Responsibility Principle (SRP), Open-Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP), play a pivotal role in shaping robust, maintainable, and flexible software architectures.</p><ol><li><strong>Single Responsibility Principle (SRP):</strong> Keep classes, modules, and functions focused on a single task. This streamlines development, testing, and code understanding.</li><li><strong>Open-Closed Principle (OCP):</strong> Design code to be open for extension but closed for modification. This allows you to add new features without changing existing code.</li><li><strong>Liskov Substitution Principle (LSP):</strong> Subclasses should be able to replace their superclasses without causing issues. This ensures that new subclasses fit seamlessly into existing code.</li><li><strong>Interface Segregation Principle (ISP):</strong> Create small, focused interfaces rather than large, unwieldy ones. This prevents unnecessary method implementations and keeps code clean.</li><li><strong>Dependency Inversion Principle (DIP):</strong> High-level modules should not directly depend on low-level modules. Both should rely on abstractions, reducing the risk of unintended side effects when making changes.</li></ol><p>By applying these principles wisely, <a href="https://www.deptagency.com/service/engineering/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">s</a><a href="https://www.deptagency.com/service/engineering/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">oftware development</a> becomes more manageable, adaptable, and efficient. SOLID principles are valuable tools for building resilient, maintainable, and extensible software systems. </p>]]></content:encoded></item><item><title><![CDATA[Why software development estimates are so often wrong]]></title><description><![CDATA[Why are software development estimations so hard to get right? Here are some ways to get more accurate. ]]></description><link>https://engineering.deptagency.com/why-software-development-estimates-are-so-often-wrong/</link><guid isPermaLink="false">6543dc38bfaa040001b75523</guid><category><![CDATA[People]]></category><dc:creator><![CDATA[Matt Daigle]]></dc:creator><pubDate>Fri, 03 Nov 2023 14:07:21 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/software-development.jpg" medium="image"/><content:encoded><![CDATA[<h3 id="tldr-estimates-should-be-used-as-a-vital-sign-for-the-health-of-the-project-not-as-a-deadline">TLDR: Estimates should be used as a vital sign for the health of the project, not as a deadline.</h3><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/11/software-development.jpg" alt="Why software development estimates are so often wrong"/><p/><p>When starting a new project, one of the first questions that executives and senior managers always ask is, “How long is this going to take?”&nbsp;</p><p>Usually, that question is trickled down through the team’s hierarchy to the engineers tasked with building the thing before the estimate is then bubbled back up to the stakeholders. The stakeholders then treat it like a development schedule and use it to set a rigid deadline. By the end of the project, the estimate is, more often than not, wildly inaccurate and the team embarks on a journey of self-reflection to figure out what went wrong.</p><h2 id="why-do-we-underestimate-how-long-projects-will-take">Why do we underestimate how long projects will take?</h2><p>Every team building a new product runs into this issue at some point in the product development process, so why does it still occur so frequently?&nbsp;</p><h3 id="naivety">Naivety</h3><p>One problem with the initial estimate is they’re done when you know the least about the project, its trajectory, and what hurdles you might run into.&nbsp;</p><p>That’s like trying to estimate how long it will take a tree to reach its full height when it’s only a seedling. Things that you can’t predict might affect the growth of that tree (wildfires, pests, drought, disease, thermonuclear war, etc.), so it’s best to use that estimate as a fluid target that could (and should) change when new information about the project becomes available to you.</p><h3 id="hopeless-optimism">Hopeless Optimism</h3><p>When you ask a kid what they want to be when they grow up, they’re going to give you their pipe dream job. Most people like to plan for only the best-case scenario. Engineers are no different.</p><p>Software engineers are usually quite accurate at estimating the best-case scenario for how long a project should take.&nbsp;</p><p>The problem is that unforeseen issues that add to the overall development time aren’t usually considered for that initial project estimate. Some examples of those issues might be&nbsp;</p><ul><li>Bugs</li><li>Dependencies on the schedules of 3rd parties</li><li>Changing project requirements</li><li>Researching and learning a new technology</li><li>Developing a new technology</li><li>Team member turnover and holidays/vacations.</li></ul><h2 id="how-can-you-estimate-software-development-more-accurately">How can you estimate software development more accurately?</h2><p>Estimating accurately is incredibly hard for a reason: you’re trying to predict the future with limited information. There simply isn't a reliable formula for development. However, here are some things we routinely do to get as close as possible:</p><h3 id="more-discovery-time-will-lead-to-a-better-defined-problem">More discovery time will lead to a better-defined problem</h3><p>Ironically, the best way to improve the accuracy of a development timeline is to spend more time. Of course, not just time, but valuable time via a comprehensive discovery or <a href="https://www.deptagency.com/service/engineering/technology-strategy-architecture/architecture-sprints/?ref=dept-engineering-blog.ghost.io">architecture sprint</a>.&nbsp;</p><p>Understand and test the perceived problem against technology restraints and user feedback.</p><p>To identify problems, interview current and prospective users, conduct research, and look at what already exists in the market. To map solutions, you’ll need to consider various factors, including market opportunity, cost to build, risks, time to market, and value.&nbsp;</p><p>On top of these factors, it can be hard for engineering teams to objectively assess concepts because teams tend to fall in love with their ideas. To know what to build, an outside perspective is always helpful.&nbsp;</p><p>If you don’t have the luxury of extended discovery time, here are some slightly more clunky ways to create estimates:&nbsp;</p><h3 id="high-middle-and-low">High, middle, and low&nbsp;</h3><p>It can be useful to come up with estimates for best-case, worst-case, and most-likely scenarios. This is helpful in a few ways:</p><ol><li>It gets you actively thinking about issues that might derail a project, and helps you to consider how likely it is that they are to occur.</li><li>When the project is complete, you can look back at these estimates to see which was the most accurate, giving you some hard data to use for your next estimate.&nbsp;</li></ol><h3 id="double-your-estimate">Double your estimate</h3><p>Yes, we’re serious. Come up with a best-case estimate and then multiply that number by two to account for all of the hiccups. If the project includes a lot of features or technologies that your team or vendor does not have experience with, multiply by three. Estimating this way, more often than not, comes close to how long the project will actually take.</p><h2 id="what-if-you-have-a-hard-deadline">What if you have a hard deadline?</h2><p>For most projects, you have a list of features that need to be completed, and your <a href="https://www.deptagency.com/en-us/service/engineering/?ref=dept-engineering-blog.ghost.io">engineering team</a> estimates how long it will take to complete them all.&nbsp;</p><p>But sometimes, you’re instead given an immovable deadline. In those cases, it’s a matter of determining how many features your team can complete in a given amount of time. The same rules for estimating apply to this scenario, but you’ll estimate each individual feature and then implement them in order of highest priority until you’ve reached your time budget.</p><p>If you have a hard deadline and a hard list of features that need to be completed, make an estimate for the list and be ruthless about feasibility. If it looks challenging, your team and stakeholders need to have honest conversations (as soon as possible) about where time can be gained:</p><ul><li>Can you add more designers and developers?</li><li>Can any of the features be simplified?</li><li>Can you forgo manually building any features by pulling in 3rd party SDKs?</li></ul><h2 id="don%E2%80%99t-become-too-attached-to-your-estimate">Don’t become too attached to your estimate&nbsp;</h2><p>Estimates are most valuable when used as a vital sign for the overall health of the project. You shouldn’t treat them as a one-and-done way to set an immovable deadline, especially at the beginning of the project when you know the least. That will only cause tension and resentment between the engineers and the stakeholders when things invariably don’t go according to plan.</p><p>At the end of the day, estimates are just a snapshot in time. When you gain more information about the project, you should be using that to reevaluate your estimate. Your estimate should be continually updated to be a reflection of reality. Furthermore, you should be recording that information for use with future estimates. That way, you won’t have to guess, you’ll know.</p>]]></content:encoded></item><item><title><![CDATA[Writing a Modelica interpreter: How complexity forced us to use simple design patterns]]></title><description><![CDATA[Anyone who's written a Domain-specific language (DSL) or found themselves accidentally entrenched in a shotgun parser knows the pains involved in processing a grammar. We found ourselves needing to take a predefined Abstract Syntax Tree (AST) and generate a UI from it. The pain we experienced along the way forced us into a disciplined simplicity from which some pretty useful design patterns emerged. In this blog post, we share the lessons we learned from situations where complexity led us into a]]></description><link>https://engineering.deptagency.com/writing-a-modelica-interpreter-how-mind-numbing-complexity-forced-us-to-use-simple-design-patterns/</link><guid isPermaLink="false">6526e5b41c437700016a66c4</guid><category><![CDATA[Modelon]]></category><category><![CDATA[Modelica]]></category><category><![CDATA[DSL]]></category><dc:creator><![CDATA[Daren Keck]]></dc:creator><pubDate>Wed, 18 Oct 2023 13:40:53 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/photo-1598810132152-e70c939a1dfa-copy.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/photo-1598810132152-e70c939a1dfa-copy.jpg" alt="Writing a Modelica interpreter: How complexity forced us to use simple design patterns"/><p>Anyone who's written a Domain-specific language (<a href="https://en.wikipedia.org/wiki/Domain-specific_language?ref=dept-engineering-blog.ghost.io">DSL</a>) or found themselves accidentally entrenched in a <a href="https://stackoverflow.com/questions/50852226/what-does-shotgun-parser-mean/50852772?ref=dept-engineering-blog.ghost.io#50852772">shotgun parser</a> knows the pains involved in processing a grammar. We found ourselves needing to take a predefined Abstract Syntax Tree (AST) and generate a UI from it. The pain we experienced along the way forced us into a disciplined simplicity from which some pretty useful design patterns emerged. In this blog post, we share the lessons we learned from situations where complexity led us into a set of simple patterns.</p>
<h2 id="so-whats-modelica">So, what's Modelica?</h2>
<p>Modelica is a class-based language for describing mathematical behavior. Those classes can be composed to create models, and in turn, those models can then be used to simulate the behavior of systems. In our case, we were <a href="https://www.deptagency.com/case/developing-an-open-source-hvac-controls-design-tool/?ref=dept-engineering-blog.ghost.io">simulating HVAC systems</a>.</p>
<p>In addition, Modelica is useful across many other engineering disciplines. A key feature of Modelica is that it allows the user to adjust parameters without rewriting code.</p>
<p>Modelica does not have a UI, but through interpretation of its annotation system, we can programmatically generate one.</p>
<h2 id="our-task">Our task</h2>
<p>Below is a Modelica model with two parameters, <code>hello</code> and <code>allow_hello</code>. <code>hello</code> as an input needs to be conditionally enabled based on the expression: <code>allow_hello == true</code></p>
<pre><code class="language-modelica">within ExamplePackage;
  model TestModel "Test Model"
    parameter String hello="World"
      "Hello"
      annotation (Dialog(enable=allow_hello == true));
    parameter Boolean allow_hello=true
      "Enable";
end TestModel;
</code></pre>
<p>From this Modelica model, we are provided with a custom AST representation. We received the model in an AST format (written in JSON) so it could be <a href="https://en.wikipedia.org/wiki/Serialization?ref=dept-engineering-blog.ghost.io">serialized</a> and sent over the wire to the UI.</p>
<p>In other words, we need to go from this:</p>
<pre><code class="language-json">{
    "within": "ExamplePackage",
    "class_definition": [
      {
        "class_prefixes": "model",
        "class_specifier": {
          "long_class_specifier": {
            "identifier": "HelloModel",
            "description_string": "Test Model",
            "composition": {
              "element_list": [
                {
                  "component_clause": {
                    "type_prefix": "parameter",
                    "type_specifier": "String",
                    "component_list": [
                      {
                        "declaration": {
                          "identifier": "hello",
                          "modification": {
                            "equal": true,
                            "expression": {
                              "simple_expression": "\"World\""
                            }
                          }
                        },
// ...
</code></pre>
<p>To this:</p>
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/enable_disable.gif" class="kg-image" alt="Writing a Modelica interpreter: How complexity forced us to use simple design patterns" loading="lazy" width="290" height="120"/></figure><p>Labels, data types, and initial values seemed relatively simple, but the input 'hello' is conditionally enabled based on the expression:</p>
<p><code>allow_hello == true</code></p>
<p>This means we need to be able to resolve variables, and resolving variables means we have to understand scope, assignments, and much more. It's a hard problem.</p>
<h2 id="compressing-the-problem-parsing-the-ast-to-a-simplified-format">Compressing the problem: Parsing the AST to a simplified format</h2>
<p>Ultimately, we want to arrive at a more simplified representation of the AST that only contains the parameters we care about. Something like the following:</p>
<pre><code class="language-json">{
  "nodes": [
    {
      "path": "TestModel.hello",
      "type": "String",
      "value": "Hello World",
      "enable": {
        "operator": "==",
        "operands": ["TestModel.allow_hello", true]
      },
      "childNodes": []
    },
    {
      "path": "TestModel.allow_hello",
      "type": "boolean",
      "value": "true",
      "enable": "true",
      "childNodes": []
    }
  ]
}
</code></pre>
<p>Ultimately, the flow of information will be as follows:</p>
<figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/flow_of_info.png" class="kg-image" alt="Writing a Modelica interpreter: How complexity forced us to use simple design patterns" loading="lazy" width="765" height="109" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/flow_of_info.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/flow_of_info.png 765w"/></figure><h2 id="generating-a-simplified-structure-from-a-deeply-nested-ast">Generating a simplified structure from a (deeply-nested) AST</h2>
<p>Given that we're provided with the AST, our first task is to create a simplified and flat schema structure that can be easily interpreted within a browser to display the UI.</p>
<p>When the parser finds a new type, the AST representation is mapped to the simplified schema. The user-facing text, initial value, type, and enable/disable behavior are extracted into a mostly flat structure.</p>
<p>This map allows us to greatly reduce the amount of data needed. The AST representation of the models was around 40MB while the simplified format was closer to 4MB.</p>
<h3 id="removing-implicit-imports-and-simplifying-type-references">Removing implicit imports and simplifying type references</h3>
<p>The parser steps through the AST and, as types and parameters are discovered, it inserts that type and its simplified representation into a <a href="https://en.wikipedia.org/wiki/Associative_array?ref=dept-engineering-blog.ghost.io">dictionary</a>.</p>
<pre><code class="language-typescript">{
    {[key: string]: SimplifiedDataType} // 'key' is the absolute path
}
</code></pre>
<p>In our 'Hello World' example, we find the <code>String</code> and <code>boolean</code> primitive types. Classes can also be assigned to parameters, e.g. <code>SubFolder.MyCustomClass</code>. Modelica has multiple implicit strategies for finding type definitions. For example, <code>SubFolder.MyCustomClass</code> could imply that <code>MyCustomClass</code> is defined in a file located at <code>./SubFolder/MyCustomClass.mo</code>. The parser implements these rules, finds the appropriate file, and parses the definition into the dictionary. What was previously a file tree is now a single, easily serializable dictionary with the file tree structure (<code>./SubFolder/MyCustomClass.mo</code>) flattened into a key (<code>SubFolder.MyCustomClass</code>).</p>
<h3 id="creating-a-tree-representation-making-relations-explicit">Creating a tree representation: Making relations explicit</h3>
<p>The AST references different types without direct links. The parser extracts these types into a flat, easily accessible dictionary of types. The components that make up a class definition are explicitly connected through the attribute 'children', a list of absolute paths referencing other types in the dictionary of types.</p>
<p>When iterating through a class definition, the parser builds up a tree of relationships between parameters and types.</p>
<p>Expanding on the original <code>TestModel</code>:</p>
<pre><code class="language-modelica">within ExamplePackage;
  model TestModel "Test Model"
    parameter String hello="World"
      "Hello"
      annotation (Dialog(enable=allow_hello == true));
    parameter Boolean allow_hello=subModel.nestedBoolean
      "Enable";
    parameter SubFolder.SubModel subModel(nestedBoolean=true)
      "A model with its own list of parameters";
end TestModel;
</code></pre>
<p>The parameter <code>subModel</code> has been added with a type <code>SubFolder.SubModel</code>. A child parameter of SubModel <code>nestedParameter</code> has been assigned <code>false</code> and the value of <code>allow_hello</code> is now an assignment from the instance value <code>subModel.nestedBoolean</code>.</p>
<p>After iterating through the parameter list, the dictionary of simplified types looks like this:</p>
<pre><code class="language-json">{
  "TestModel.hello": { "type": "String" },
  "TestModel.enable": { "type": "Boolean" },
  "TestModel.subModel": { "type": "SubFolder.SubModel" },
  "SubFolder.SubModel": {
    "type": "SubFolder.SubModel",
    "children": ["SubFolder.SubModel.nestedParam"]
  }, // &lt;-- SubModel class definition
  "SubFolder.SubModel.nestedBoolean": { "type": "String" } // &lt;-- nested param defined in SubModel
}
</code></pre>
<p>This allows us to follow the related types from the <code>children</code> of <code>TestModel</code> to build a tree that looks as shown below:</p>
<figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/test_model_tree.png" class="kg-image" alt="Writing a Modelica interpreter: How complexity forced us to use simple design patterns" loading="lazy" width="873" height="474" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/test_model_tree.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/test_model_tree.png 873w"/></figure><p>Notice that there are parameters (like TestModel.subModel) and definitions (like SubFolder.SubModel) mixed together. I'll discuss more about this later.</p>
<p>This data structure is helpful for understanding the structure of a given class. No implicit behavior around importing files or class relationships is required to understand the structure of <code>TestModel</code>.</p>
<h3 id="and-more">And more</h3>
<p>There are additional features that were also parsed and simplified but we will not cover here, including:</p>
<ul>
<li>Simplifying dependency injection</li>
<li>Fitting within the simplified schema the polymorphic behavior that enabled swapping types (imagine a system that allows for selecting a branch to traverse)</li>
<li>Formatting assigned values (right-hand-side values) into a common shape</li>
<li>Flattening inherited properties</li>
</ul>
<p>We didn't need expressive, flexible grammar to get to the features we needed for the UI. The parser simplifies the AST representation into a more explicit, less flexible tree representation that worked for our purpose. <strong>Less variability and implicit behavior meant simpler algorithms for interpretation on the front end</strong>.</p>
<h2 id="writing-an-interpreter">Writing an interpreter</h2>
<p>With our simple grammar in place, we can return to the original expression we were attempting to resolve:</p>
<pre><code class="language-typescript">allow_hello == true;
</code></pre>
<p>To resolve this, a few things needed to be understood:</p>
<ol>
<li>How do you evaluate the operator (<code>==</code>) and it's operands (<code>allow_hello</code>, <code>true</code>)</li>
<li>How do you get the value of <code>allow_hello</code></li>
<li>How do you incorporate user input to determine the value of <code>allow_hello</code></li>
</ol>
<h3 id="evaluating-expressions-without-variables">Evaluating expressions (without variables)</h3>
<p>Before attempting to solve an expression with variables, we have to deal with the basics of evaluation with constants and operators. How do you handle different operators? How do you deal with expressions like <code>3 &lt; 5</code>, <code>true &amp;&amp; (3 &lt; 4 || 2 &gt; 1)</code>?</p>
<p>We came up with a schema that looked like this:</p>
<pre><code class="language-typescript">export type Expression = {
  operator: string;
  operands: Array&lt;Literal | Expression&gt;;
};
</code></pre>
<p>This type of structure allows for relatively simple evaluation function:</p>
<pre><code class="language-typescript">function evaluate(expression: Expression) {
  switch(expression.operator) {
    case '&lt;': {
	    // reduce the list of operands into a single value
      const accumulator;
      expression.operands.forEach(operand =&gt; {
        if (isExpression(operand)) {
          operand = evaluate(operand); // recursive call
        }
        // operator specific reduction with the accumulator
        operatorReduction(accumulator, operand);
      });

      return accumulator;
    }
}
</code></pre>
<p>Each operator (e.g. <code>&lt;</code>, <code>==</code>, etc.) can be separated into a case in the switch statement.</p>
<p>Nested expressions could be handled by making a recursive call.</p>
<h3 id="resolving-variables">Resolving variables</h3>
<p>With the basics of evaluation in place, we can now address variable resolution.</p>
<p>Like before, there was a goal in mind for what variable resolution would look like:</p>
<pre><code class="language-typescript">const context = new Context(userSelections, testModel);
context.getValue(`allow_hello`); // returns 'true'
</code></pre>
<p>A model should be able to be loaded (<code>testModel</code>) with a class constructor (<code>Context</code>), and the created 'context' instance should then be queryable like entering variable names in a REPL.</p>
<p>With that in mind, we now have to figure out how to determine the initial values of <code>testModel</code>.</p>
<p>Returning to the tree view of TestModel, if we add the initial value to each node we get the following:</p>
<figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/test_model_tree_w_values.png" class="kg-image" alt="Writing a Modelica interpreter: How complexity forced us to use simple design patterns" loading="lazy" width="1094" height="370" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/test_model_tree_w_values.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/10/test_model_tree_w_values.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/test_model_tree_w_values.png 1094w"/></figure><p>What this view captures is the tree of dependencies that make up our dictionary of variable names and their values. If TestModel is loaded, the following variables and values should be collected in a dictionary that represents our current scope.</p>
<pre><code class="language-typescript">{
    "hello": "hello world",
    "enable": true,
    "subModel.nestedBoolean": true
}
</code></pre>
<p>Notice that the keys in scope do not match the original node identifiers. Instead we are using 'instance paths'. Instance paths are formed by appending parameter names together. Why use instance paths? Types like <code>SubFolder.Submodel</code> can be reused but the instance paths <code>subModel.nestedBoolean</code> is unique.</p>
<p>Notice as well that the value <code>subModel.nestedBoolean</code> resolves to <code>true</code> even though the node <code>SubFolder.SubModel.nestedBoolean</code> sets the same instance path to <code>false</code>. The <code>SubFolder.SubModel.nestedBoolean</code> node represents the original definition of the class parameter. The <code>TestModel.subModel</code> node is the instance that includes an override of the original value of 'nestedBoolean'. Overrides must take precedent over the downstream definition.</p>
<p>This behavior can be implemented using a <a href="https://en.wikipedia.org/wiki/Tree_traversal?ref=dept-engineering-blog.ghost.io#Pre-order,_NLR">pre-order tree traversal</a>. This is a depth-first algorithm that processes the current node before visiting child nodes. In this example, that means getting the values assigned by the node <code>TestModel.subModel</code> before visiting the child node <code>SubFolder.SubModel.nestedBoolean</code>.</p>
<h3 id="and-more">And More</h3>
<p>The logic around tree traversal and the process of building up 'scope' was slightly more complicated.</p>
<ul>
<li>We ignored class definitions when building instance paths. As part of that we had to indicate in the simple schema what nodes were class definitions</li>
<li>Child node traversal was dependency driven. When a variable is referenced that is not yet in the scope dictionary, we jump to the dependency's branch first. For our example, when the variable <code>subModel.nestedBoolean</code> is reached in the middle branch and not found in scope, a recursive call is made to process the <code>TestModel.subModel</code> branch first.</li>
</ul>
<figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/test_model_tree_branch_order.png" class="kg-image" alt="Writing a Modelica interpreter: How complexity forced us to use simple design patterns" loading="lazy" width="1107" height="490" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/test_model_tree_branch_order.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/10/test_model_tree_branch_order.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/test_model_tree_branch_order.png 1107w"/></figure><h3 id="generating-ui-and-incorporating-user-input">Generating UI and incorporating user input</h3>
<p>With variables resolved and expressions evaluated, UI can now be generated.</p>
<p>We need to map from parameter values and types to a UI component. If the parameter is of type boolean, we need a checkbox component, if it is an enum we need a dropdown.</p>
<p>Like before, a pre-order tree traversal was used to visit each node of the template. This time, child nodes could be visited in order as all nodes were populated with a value. No need to worry about dependency-driven loading.</p>
<p>The same string-building logic that was used to build up an instance path was also used to label and identify the corresponding UI. On user input, the value is stored using that instance path. As the user makes selections, a mirror object to the variables dictionary is built up using those instance path keys.</p>
<p>Because the inputs used the same variable instance names, incorporating these selections into the concept of 'context' was straightforward. When resolving a variable value, always check what's in the selection dictionary before going to the variables dictionary.</p>
<h3 id="arrival">Arrival</h3>
<p>We have arrived! We can now resolve the expression:</p>
<pre><code class="language-typescript">allow_hello == true;
</code></pre>
<p>Templates provided as an expansive AST are converted to a paired down 'simple schema' that strips away extraneous detail and features of the Modelica language.</p>
<p>We are able to understand both what to do with the operator (<code>==</code>) and operands (<code>allow_hello</code> and <code>true</code>)</p>
<p>The simple schema can be interpreted to generate all variables and definitions currently in scope, meaning we can resolve the value <code>allow_hello</code>.</p>
<p>Finally, that value can be used to conditionally enable our Hello World UI.</p>
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/enable_disable-1.gif" class="kg-image" alt="Writing a Modelica interpreter: How complexity forced us to use simple design patterns" loading="lazy" width="290" height="120"/></figure><h2 id="general-tips-for-reducing-complexity">General tips for reducing complexity</h2>
<h3 id="use-best-practices-to-think-in-terms-of-interfaces-not-implementation">Use best practices to think in terms of interfaces, not implementation</h3>
<p>They’re best practices for a reason, and using them allows you to spend less time wading through implementation details. Some I’d like to highlight:</p>
<p>Single purpose classes and functions: identify the concept/action/behavior you're trying to encapsulate and make sure your class/function sticks to it.<br>
Modify state deliberately in well known parts of code. Rely on pure functions as much as possible to keep the non-deterministic harder-to-test portions of code limited.<br>
Use types in weakly typed languages</br></br></p>
<p>Consistency around these practices allows for predictability. With predictability you can understand more about what a function does just by its signature.</p>
<h3 id="nurture-the-mental-model">Nurture the mental model</h3>
<p>Find a simplified representation to underpin understanding of something complex. A mental model is the internal representation of a problem in your head, and it should be a clarifying force: terse enough to be quick to reason about but accurate enough that it can be used as a heuristic for implementation details. A good way to test the effectiveness of your mental model is to describe it to someone else who is unfamiliar with the project.</p>
<p>Also, take time with the actual language used in the project, both for variable naming and general descriptions. What is the difference between ‘name’ and ‘id’? When do I use the term ‘node’ vs. ‘parameter’ vs. ‘input’? A shared understanding of these terms provide an implicit hint on implementation and usage.</p>
<p>Conversely, the wrong terms can be a near constant source of obfuscation, tripping up discussions by inserting confusion.</p>
<h3 id="testing-offloads-thinking">Testing offloads thinking</h3>
<p>Tests offload thinking by allowing your code contracts and corner cases to be enshrined in nice re-runnable tests. When implementing some new behavior, you can primarily focus on that specific feature, without the background noise of special cases.</p>
<p>Tests are also a good way to keep your headspace thinking in terms of interfaces. Tests are clients of the code you are writing, so you immediately test out the interface you are establishing. Additionally, when an interface needs to be changed, you get to double check that change by updating the appropriate test.</p>
<p>A good test suite allows you to take bigger risks with your code.</p>
<h3 id="set-up-your-code-for-play">Set up your code for play</h3>
<p>Make it easy to ‘play’ with your code. A combination of fast tests, quick validation cycles, and sound strategies for reverting a commit can provide a degree of safety that allows for play.</p>
<p>Tests that can quickly run on change can help course correct an implementation strategy.</p>
<p>Having a fast validation cycle (time from code change to result) also allows for fast course corrections. If you are using a weak typed language use the types for clearly defined interfaces. Most editors provide instantaneous feedback on whether or not your new code works.</p>
<p>And in git, make commits with the revert in mind. Like functions or classes, encapsulate the functional change the commit includes. Then if you do have to revert, you can cleanly remove work around one feature without having to revert something unrelated.</p>
<p>All of this is working towards a frictionless, safe development environment. A feeling of safety is important for creativity, and creativity is definitely needed when working through hard problems.</p>
]]></content:encoded></item><item><title><![CDATA[How & why I used chat GPT to convert templates]]></title><description><![CDATA[How we bridged Backend with Frontend by converting template syntax and how AI helped.


What is templating?

In web templating, a template acts as a blueprint or a skeleton for the web page. It contains the static parts of the page layout and defines placeholders or variables where dynamic content can be inserted.

I believe every developer is familiar with it, as it comes from the ‘ancient’ web development times and has been around for decades.


The ISML templating

ISML stands for “Intershop ]]></description><link>https://engineering.deptagency.com/chatgpt-to-convert-templates/</link><guid isPermaLink="false">6523e4b40386780001638c21</guid><category><![CDATA[Chatgpt]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Mykyta Meshcheriakov]]></dc:creator><pubDate>Mon, 16 Oct 2023 15:11:30 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/abstract-template-concept.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/abstract-template-concept.jpg" alt="How & why I used chat GPT to convert templates"/><p>How we bridged Backend with Frontend by converting template syntax and how AI helped. </p><h2 id="what-is-templating">What is templating?</h2><p>In web templating, a template acts as a blueprint or a skeleton for the web page. It contains the static parts of the page layout and defines placeholders or variables where dynamic content can be inserted.</p><p>I believe every developer is familiar with it, as it comes from the ‘ancient’ web development times and has been around for decades.</p><h2 id="the-isml-templating">The ISML templating</h2><p>ISML stands for “Intershop Scripting Markup Language” and is a templating language specifically used in the context of Salesforce Commerce Cloud - a cloud platform for creating e-commerce sites.&nbsp;</p><p>No need for a detailed description of the platform, but it uses MVC principles, has JavaScript on the backend, and has a unique templating language called ISML. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-1.png" class="kg-image" alt="How & why I used chat GPT to convert templates" loading="lazy" width="1600" height="701" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/template-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/10/template-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-1.png 1600w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Example of an ISML template</span></figcaption></img></figure><p>You can distinguish it from other templating languages by specific tags that only work with ISML. The tags are responsible for loops, conditions, and the inclusion of other templates. Most templating languages have those, but there are slight differences in syntax.</p><h2 id="compatibility-problem">Compatibility problem</h2><p>Even though the majority of the templating languages use similar approaches and structure, the syntax is something that varies from one  templating to another.&nbsp;</p><p>As an example, let’s see how a single template can look in two different templating languages: one in Nunjucks and another one in ISML:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-2.png" class="kg-image" alt="How & why I used chat GPT to convert templates" loading="lazy" width="1572" height="316" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/template-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/10/template-2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-2.png 1572w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A Nunjucks template</span></figcaption></img></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-3.png" class="kg-image" alt="How & why I used chat GPT to convert templates" loading="lazy" width="1572" height="316" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/template-3.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/10/template-3.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-3.png 1572w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The same template in ISML</span></figcaption></img></figure><p>The structure is similar, but the syntax is different, meaning only one of two templates will work in ISML and one with Nunjucks. That’s quite normal in the development world and usually is not a problem since you don’t need to transform from one template syntax to another. This was a given until we started to see problems with ISML templating - it has to be run on a server only.&nbsp;</p><p>This significantly slows down templating because each change has to be uploaded to a server to become visible. The full template must be uploaded to the server when you change a line, tag name, or HTML attribute. And then, to validate the change, you must reload the resulting page.&nbsp;</p><p>This circle takes around five seconds to execute, which does not sound like a lot until you do it a hundred times. And that is exactly what happens when a template is developed according to a required design.&nbsp;</p><p>So we started looking into seeing the changes locally without using a remote server. That’s when we come across the Nunjuck's templating language.&nbsp;</p><h2 id="nunjucks-and-conversion">Nunjucks and conversion</h2><p>Nunjucks helps you create templates faster than via ISML because templates don’t have to be uploaded to a server. Setting up a localhost server is all you need, and it comes with a standard Webpack configuration familiar to most FE developers. <br><br>But having a template in Nunjucks will not work for SFCC without the obvious syntax change I mentioned earlier. And each Nunjucks template would require at least some manual changes to the syntax. But does it have to be really manual? Of course not!</br></br></p><p>At DEPT®, we started using the OpenAI platform to convert the templates from Nujucks to ISML. This changed the workflow we had in place for a typical SFCC project. Instead of writing an ISML template, we write a Nunjucks one and then convert it to ISML. This also allowed us to reduce the learning curve for FE developers because they don’t have to learn ISML syntax and SFCC specifications as a new platform. Instead, they only have to work with the more popular and familiar Nunjucks templates.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-4.png" class="kg-image" alt="How & why I used chat GPT to convert templates" loading="lazy" width="997" height="591" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/10/template-4.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/template-4.png 997w" sizes="(min-width: 720px) 720px"><figcaption><b><strong style="white-space: pre-wrap;">The example of a template conversion with ChatGPT(OpenAI)</strong></b></figcaption></img></figure><p>So our new development process right now is looking like this:</p><ul><li>Frontend team writes templates using Nujucks</li><li><strong>Backend team converts the templates to ISML using the AI</strong></li><li>Backend team makes sure the templates working as expected and commits the changes into the repository</li></ul><p>As a result:</p><ul><li>Frontend team saves a lot of time as they don’t have to deal with the server-side rendering and can test the templates locally</li><li>Frontend team does not have to learn ISML templating or SFCC specifications</li><li>Backend team does not spend time on onboarding the Frontend team on the SFCC platform&nbsp;&nbsp;</li></ul><h2 id="conclusion">Conclusion</h2><p>So this is one case for us as a company for a template conversion. This is most likely not a very common problem; for some, it does not sound like a problem. However, it’s a good example of how an AI can connect two different expertise: frontend development and cloud-based backend.&nbsp;</p>]]></content:encoded></item><item><title><![CDATA[Custom Shapes in Jetpack Compose]]></title><description><![CDATA[Learn how Jetpack Compose makes creating custom shapes in your UI with some basic trigonometry.]]></description><link>https://engineering.deptagency.com/custom-shapes-in-jetpack-compose/</link><guid isPermaLink="false">650db261a8508c00019f3166</guid><category><![CDATA[Process]]></category><category><![CDATA[Jetpack Compose]]></category><dc:creator><![CDATA[Maksim Golendukhin]]></dc:creator><pubDate>Fri, 13 Oct 2023 13:24:58 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/beach-1868716.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/beach-1868716.jpg" alt="Custom Shapes in Jetpack Compose"/><p>Working with custom shapes has been a pain to work with for a long time in Android development. Yes, Android provides some tools to customize views for developers, but I was never satisfied with the achieved result. Take the triangle button, for instance; a true Material Design triangle button has dynamic shadows and a triangle ripple effect while pressing. Despite several approaches and the ability to see something that resembled a triangle, there were still compromises with mimicking real shadows and rendering with an unacceptable rectangular ripple effect. Eventually, I gave up on the existing tooling.</p><p>Now, with the introduction of <a href="https://developer.android.com/jetpack/compose?ref=dept-engineering-blog.ghost.io" rel="noreferrer">Jetpack Compose</a>, developers' hands are freed, and they are allowed to customize views in different ways. By overriding the one-function interface I achieved everything I wanted with a minimum amount of code with some basic trigonometry. Let me share my experience with you in this article.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/shaped-button.gif" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="400" height="400"><figcaption><span style="white-space: pre-wrap;">How our final button should appear</span></figcaption></img></figure><p>As you can see - it is a triangle with rounded corners. Corner ratios can be set up dynamically. It has real shadows, and they react to pressing. The ripple effect also does not exceed the shape of the button. The size can be of any dimension the UI requires.</p>
<!--kg-card-begin: html-->
<p>In order to render the button, Compose has the <code>@Compose Button</code> function. Among all the parameters, let’s take a closer look at <code>shape: Shape</code>. As you might have guessed, we will provide an instance of the <code>Shape</code> interface here. Despite this parameter having a default value, <code>RoundedCornerShape()</code> is usually provided here as an argument with a radius of the corner. The <code>modifier</code> and <code>elevation</code> parameters allow us to assign a size and an elevation. This is what we have for now so far.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">Button(
   onClick = { },
   modifier = Modifier.size(width = 40.dp, height = 30.dp),
   shape = RoundedCornerShape(5.dp),
   contentPadding = PaddingValues(4.dp),
   elevation = ButtonDefaults.elevatedButtonElevation(defaultElevation = 6.dp)
) {
   Text(
       text = "+1",
       fontSize = 8.sp
   )
}
</code></pre>
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/Rectangular-Button.gif" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="400" height="400"/></figure>
<!--kg-card-begin: html-->
<p>Of course, in order to customize the shape, we will implement <code>Shape</code> interface by overriding its single function
<code>fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline</code>. Here, we have a <code>size</code> parameter,  which will be provided to our implementation with every recomposition. We will utilize this argument in order to fit the view into this <code>size</code>.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">class TriangleShape : Shape {
   override fun createOutline(
       size: Size,
       layoutDirection: LayoutDirection,
       density: Density
   ) = Outline.Generic(
       Path().apply {
           val x = size.width
           val y = size.height
           
           moveTo(0f, 0f)
           lineTo(x, y / 2)
           lineTo(0f, y)
       }
   )
}
</code></pre>

<!--kg-card-begin: html-->
<p><code>Outline</code> here is the borders of the view that we want to customize. It is a sealed class with three classes extending it - <code>Rectangle</code>, <code>Rounded</code>, and <code>Generic</code>. What we need to do is create an instance of <code>class Generic(val path: Path) : Outline()</code> by passing an instance of <code>Path</code>. This class actually contains a list of functions that allow us to draw everything we might want.</p><p>For now, let’s start with something relatively simple and just implement a rectangular button with no <code>roundRadius</code>. As was mentioned before, will need implementation of the Shape interface. We will use <code>fun moveTo(x: Float, y: Float)</code> and <code>fun lineTo(x: Float, y: Float)</code>. x and y here are coordinates in pixels.</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/triangle-button.gif" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="400" height="400"/></figure>
<!--kg-card-begin: html-->
<p>A couple of words regarding this new class. Again, we can easily access view width and height via <code>size</code> argument. Here, we need first to land on the top left corner of the triangle, which is the zero point of the coordinate system, by invoking the <code>moveTo</code> function. Then, we draw a line to the middle of the right edge of the view - <code>lineTo(x, y / 2)</code>. Finally, we draw a line to the bottom left corner of the triangle - <code>lineTo(0f, y)</code>. No need to connect the last point with the first - it will be done for us by drawing a line automatically.</p><p>Updated <code>Button</code> function</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">Button(
   onClick = { },
   modifier = Modifier.size(width = 40.dp, height = 30.dp),
   shape = TriangleShape(),
   contentPadding = PaddingValues(4.dp),
   elevation = ButtonDefaults.elevatedButtonElevation(defaultElevation = 6.dp)
) {
   Text(
       text = "+1".uppercase(),
       modifier = Modifier.offset(x = -(40.0 * 1 / 5).roundRadiusToInt().dp),
       fontSize = 8.sp
   )
}
</code></pre>

<!--kg-card-begin: html-->
<p>Here we simply pass a new instance of <code>TriangleShape</code> as shape parameter. It’s worth mentioning that due to the peculiarity of the triangle form, the centered text shifts towards the right corner. Moving it to 20% of width to the left makes it look a bit better: <code>Modifier.offset(x = -(40.0 * 1 / 5).roundRadiusToInt().dp)</code>.</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>In general, we are ready to implement more complicated shapes. Since the majority of our future calculations will involve the right triangle, let me refresh some basics of trigonometry.</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/right-triangle.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="861" height="450" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/right-triangle.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/right-triangle.png 861w" sizes="(min-width: 720px) 720px"/></figure>
<!--kg-card-begin: html-->
<p>A right triangle is described with 5 values - two legs(<strong>x</strong> and <strong>y</strong>), hypotenuse(<strong>h</strong>) and two angles opposite to legs(<strong>alpha</strong> and <strong>beta</strong>). If we know the values of any two sides of a triangle or one side and angle, we can calculate the rest of them.<br>
The sine of an angle is the ratio of the opposite leg to the hypotenuse - <strong>sin(alpha) = x / h</strong>.<br>
The Cosine of an angle is the ratio of the adjacent leg to the hypotenuse - <strong>cos(alpha) = y / h</strong>.<br>
The tangent of an is the ratio of the opposite leg to adjacent - <strong>tan(alpha) = x / y</strong>.<br>
Angle alpha is equal to the arctangent of the ratio of the opposite leg to adjacent  - <strong>alpha = atan(x/y)</strong>.</br></br></br></br></p><p>Ok, now starts the tricky part. This is the scheme of the button. We need to draw the <strong>△ABC</strong> but with rounded corners, so the figure is contained in <strong>DGPHKJE</strong>.</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/start-blueprint-1.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="960" height="720" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/start-blueprint-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/start-blueprint-1.png 960w" sizes="(min-width: 720px) 720px"/></figure><p>First what we need here is the radius of the circle which will be at the corners of the button. Since we want it to be configurable by the developer let’s add an argument in our class.</p>
<pre><code class="language-Kotlin">class TriangleShape(private val roundRadius: Float) : Shape
</code></pre>
<p>Also let’s add top level value which will convert dp to pixels in MainActivity.</p>
<pre><code class="language-Kotlin">private val Dp.float: Float get() = this.value * getSystem().displayMetrics.density
</code></pre>
<p>Now we can pass desired roundRadius as parameter.</p>
<pre><code class="language-Kotlin">shape = TriangleShape(4.dp.float),
</code></pre>

<!--kg-card-begin: html-->
<p>Besides two previous functions, we will use the additional function of the <code>Path</code> class.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">fun arcToRad (
    oval: Rect,
    startAngleRadians: Float,
    sweepAngleRadians: Float,
    forceMoveTo: Boolean
)
</code></pre>

<!--kg-card-begin: html-->
<p>To explain the meaning of these parameters, let’s take a close look at the right angle, for example.</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/right-corner.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="619" height="358" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/right-corner.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/right-corner.png 619w"/></figure>
<!--kg-card-begin: html-->
<p>
 <ul>
     <li><code>oval: Rect</code> - In our case, an oval is a circle into which a square is inscribed. There are different methods on how to init Rect object, but we will use <code>fun Rect(center: Offset, radius: Float)</code>
In our case, <code>center</code> is the coordinates of point N and <code>radius</code> is a <code>roundRadius</code> argument.</li>
     <li><code>startAngleRadians: Float</code> - Compose considers point <strong>P</strong> as the starting point for describing angles with clockwise direction. For instance - 6 o’clock - is <strong>𝜋 / 2</strong> radians, 9 o’clock - <strong>𝜋</strong> radians and 12 o’clock - is <strong>-𝜋 / 2</strong>. Of course, 3 o’clock is 0 radians. In our case - the negative value of angle <strong>GNP</strong>.</li>
     <li><code>sweepAngleRadians: Float</code> -  is how large the arc is. Value of angle <strong>GNH</strong> for our example <code>forceMoveTo: Boolean</code> is always <code>false</code>.</li>
 </ul>
</p>

<p>So, the information we need to draw this shape - Coordinates of points <strong>D</strong>, <strong>G</strong>, <strong>K</strong>, <strong>E</strong>, <strong>M</strong>, <strong>N</strong>, <strong>O</strong> and angles <strong>∠GNH</strong>, <strong>∠KOJ</strong> and <strong>∠EMD</strong>.
</p>

<p>Let’s wrap it all together and calculate these values.</p>

<p>Before doing that, we will need values of angles of the triangle - <strong>∠EAD</strong> and <strong>∠GCH</strong>. Values of <strong>∠EAD</strong> and <strong>∠KBJ</strong> are equal since the triangle is isosceles. Triangle <strong>△ACR</strong> is a right triangle and we know it sides (<strong>width</strong> and <strong>height / 2</strong>) it is possible to easily calculate angle <strong>∠EAD</strong> using <code>fun atan(x: Float): Float</code></p><p><center><strong>tan(∠EAD) = RC / AR</strong> => <strong>∠EAD = atan(RC / AR)</strong></center></p>

<p>In the code, I will deliberately use article notation so it could be easier to follow the logic</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">val RC = size.width
val AR = size.height / 2


val DAE = atan(CR / AR)
</code></pre>

<!--kg-card-begin: html-->
<p>For the next part, here is the top left corner with additional segments for calculations.</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/top-right-corner.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="517" height="677"/></figure>
<!--kg-card-begin: html-->
<p>Here we need to calculate coordinates of point <strong>D</strong>. To do it, we need to calculate <strong>AQ</strong> and <strong>QD</strong> of <strong>△AQD</strong>. What else do we know about this triangle? We can calculate the value of <strong>∠DAQ</strong>. Since <strong>∠EAQ</strong> is right</p><p/><center><strong>∠DAQ = 𝜋 / 2 - ∠EAD</strong></center>

<p>If we have known the value of hypotenuse <strong>AD</strong>, the rest of the calculations are trivial. Let’s take a look at another <strong>△ADM</strong>. We know it’s leg <strong>DM</strong> - it is the <code>roundRadius</code> argument. We also know that the center of a circle inscribed in an angle lies on the bisector. Consequently:</p>
<p><center><strong>∠DAM = ∠EAD / 2</strong>,</center>
<center><strong>tan(∠DAM)  =  MD / AD</strong> => <strong>AD = MD / tan(∠DAM)</strong></center></p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">val DAQ = PI.toFloat() / 2 - DAE
val DAM = DAE / 2
val AD = roundRadius / tan(DAM)
</code></pre>

<!--kg-card-begin: html-->
<p>Now we are ready to calculate coordinates of <strong>D</strong></p><p><center><strong>sin(∠DAQ) = DQ / AD</strong> => <strong>DQ = AD * sin(∠DAQ)</strong></center><center><strong>cos(∠DAQ) = AQ / AD</strong> => <strong>AQ = AD * cos(∠DAQ)</strong></center></p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">val DQ = AD * sin(DAQ)
val AQ = AD * cos(DAQ)
</code></pre>

<!--kg-card-begin: html-->
<p>Finally, we are ready to use the first <code>Path</code> function.
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">moveTo(AQ, DQ)
</code></pre>

<!--kg-card-begin: html-->
<p>Let’s take a look at the right side of the triangle.
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/right-corner-2.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="621" height="363" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/right-corner-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/right-corner-2.png 621w"/></figure>
<!--kg-card-begin: html-->
<p>Next, we need to draw a line to point G. We already know the coordinates of point <strong>C</strong>. We can derive coordinates of point <strong>G</strong> by subtracting <strong>CT</strong> and <strong>GT</strong> from <strong>x</strong> and <strong>y</strong> of point <strong>C</strong> respectively. These values are legs of the right <strong>△CGT</strong>.<br>
As in the previous example, we can calculate the legs of the right triangle if we know one of its angles and hypotenuse - <strong>∠GCN</strong> and <strong>CG</strong> in our case.<br>
<strong>∠GCN</strong> is a half of <strong>∠GCH</strong>. Since we know that the sum of angles of a triangle is <strong>𝜋</strong> radians and our triangle is isosceles, and basis angles are equal to <strong>DAE</strong></br></br></p><p><center><strong>∠GCN = (𝜋 - 2 * DAE) / 2</strong></center></p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p><strong>CG</strong> is also a leg of another right <strong>△CGN</strong> with known <strong>∠GCN</strong> and leg - <code>roundRadius</code>  argument or <strong>GN</strong>.</p>
	<p><center><strong>tan(∠GCN) = GN / CG</strong> => <strong>CG = GN / tan(∠GCN)</strong></center></p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Once we know <strong>CG</strong> we can calculate <strong>GT</strong> and <strong>CT</strong></p>
    <p><center><strong>sin(∠GCN) = GT / CG</strong> => <strong>GT = CG * sin(∠GCN)</strong></center>
	<center><strong>cos(∠GCN) = CT / CG</strong> => <strong>CT = CG * cos(∠GCN)</strong></center></p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Coordinates of point <strong>G</strong>: <strong>CR</strong> - <strong>CT</strong>, <strong>AR</strong> - <strong>GT</strong></p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">val GCN = (PI.toFloat() - 2 * DAE) / 2
val CG = roundRadius / tan(GCN)
val GT = CG * sin(GCN)
val CT = CG * cos(GCN)

lineTo(CR - CT, AR - GT)
</code></pre>

<!--kg-card-begin: html-->
<p>Now we have to draw an arc. Here, we need the center of the circle - coordinates of <strong>N</strong> and two angles - <strong>∠CNG</strong> and <strong>∠GNH</strong>. Since it is one of angle of right triangle <strong>△CNG</strong> and other angle is known,</p>
<p>    <center><strong>∠CNG = 𝜋 / 2 - ∠GCN</strong></center>
	<center><strong>∠GNH = 2 * ∠CNG</strong></center></p>

<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>For point <strong>N</strong> we know y coordinate. Let’s now calculate <strong>CN</strong>.</p>
<p><center><strong>sin(∠GCN) = GN / CN</strong> => <strong>CN = GN / sin(∠GCN)</strong></center></p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Now we can draw the arc.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">val CN = roundRadius / sin(GCN)
val CNG = PI.toFloat() / 2 - GCN
arcToRad(Rect(Offset(CR - CN, AR), roundRadius), -CNG, 2 * CNG, false)
</code></pre>

<!--kg-card-begin: html-->
<p>The coordinates of point K are calculated similarly as we did for point Q. We can reuse precalculated values and apply them here.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">val AB = size.height
lineTo(AQ, AB - DQ)
</code></pre>

<!--kg-card-begin: html-->
<p>Moving next to the bottom left of the triangle.</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/bottom-left-corner.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="623" height="719" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/bottom-left-corner.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/bottom-left-corner.png 623w"/></figure>
<!--kg-card-begin: html-->
<p>We also need coordinates of point <strong>O</strong> and two angles for <strong>arc</strong> function - <strong>∠KOU</strong> and <strong>∠JOK.</strong> In order to calculate <strong>BV</strong> and <strong>OV</strong> as <strong>x</strong> and <strong>y</strong> coordinates, we need to know hypotenuse <strong>BO</strong> and value of <strong>∠OBV</strong> for right <strong>△BOV</strong>. It is worth noticing that <strong>∠OBV</strong> is equal to the sum of <strong>∠DAM</strong> and <strong>∠DAQ</strong> because it lies on the opposite side of the isosceles triangle. Also, admit that hypotenuse <strong>BO</strong> for the <strong>△BVR</strong> is also hypotenuse for <strong>△BKO</strong>. Leg <strong>KO</strong> and angle <strong>∠KBO</strong> are equal to <code>roundRadius</code> argument and <strong>∠DAM</strong> angle. Let’s calculate <strong>BO</strong> within sine of angle as we did it before.</p>
<p>
<center><strong>sin(∠KBO) = KO / BO</strong> => <strong>BO = KO / sin(∠KBO) =  KO / sin(∠DAM)</strong></center></p>
<p>Now calculating <strong>BR</strong> and <strong>RO</strong> could be done via sine and cosine.</p>
<p><center><strong>sin(∠OBV) = BR / BO</strong> => <strong>BR = sin(∠OBV) * BO</strong></center>
<center><strong>cos(∠OBV) = RO / BO</strong> => <strong>RO = cos(∠OBV) * BO</strong></center></p>
<p>Now we need to calculate <strong>∠KOU</strong>. <strong>∠UOV</strong> is right. That means</p>
<p><center><strong>∠KOU = 𝜋 / 2 - ∠KOV</strong></center></p>
<p>We can easily find <strong>∠KOV</strong> by subtracting <strong>∠BOV</strong> from <strong>∠BOK</strong>. Both of them can be calculated.</p>

<p>Consider the right <strong>△BOV</strong> first. Here, <strong>∠BOV = 𝜋 / 2 - ∠OBV</strong>. And in the same way we can easily calculate for <strong>△BOK</strong>.</p>
<p><center><strong>∠BOK = 𝜋 / 2 - ∠KBO = 𝜋 / 2 - ∠DAM</strong></center></p>
<p>Eventually,</p>
<p>
	<center><strong>∠KOV = ∠BOK - ∠BOV</strong></center>
	<center><strong>∠KOU = 𝜋 / 2 - ∠KOV</strong></center>
    </p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Also, let’s calculate <strong>∠JOK</strong>. We know that the sum of angles of any rhombus is equal to <strong>2 * 𝜋</strong>. We also know that in the kite <strong>BJOK</strong>, there are two right angles <strong>∠BJO</strong> and <strong>∠BKO</strong> by the definition of the circle inscribed in an angle. Also we know <strong>∠JBK</strong> - it is equal to <strong>∠DAE</strong>.</p>
	<center><strong>∠JOK = 2 * 𝜋 - 𝜋 / 2 - 𝜋 / 2 - ∠DAE = 𝜋 - ∠DAE</strong></center>
<p>Now we have all the information for drawing rect.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">val OBR = DAM + DAQ
val BO = roundRadius / sin(DAM)
val BR = BO * cos(OBR)
val RO = BO * sin(OBR)

val BOR = PI.toFloat() - OBR
val BOK = PI.toFloat() - DAM
val KOR = BOK - BOR
val KOU = PI.toFloat() / 2 - KOR
val JOK = PI.toFloat() - DAE
arcToRad(Rect(Offset(BR, AB - RO), roundRadius), KOU, JOK, false)
</code></pre>

<!--kg-card-begin: html-->
<p>Finally we have to draw left top arc.</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/top-left-corner.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="517" height="677"/></figure>
<!--kg-card-begin: html-->
<p>We have everything that needs to draw a line to the top left corner.</p>
<p><strong>AE</strong> is equal to precalculated <strong>AD</strong>.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">lineTo(0f, AD)
</code></pre>

<!--kg-card-begin: html-->
<p>The coordinates of point M can be calculated within <strong>BR</strong> and <strong>RO</strong> segments. We also need a starting angle. Since <strong>ME</strong> is perpendicular to the tangent, we can figure out that the initial angle is <strong>𝜋</strong>. Sweeping angle was calculated before and it is equal to <strong>∠JOK</strong>.</p>
<!--kg-card-end: html-->
<pre><code class="language-Kotlin">arcToRad(Rect(Offset(BR, RO), roundRadius), PI.toFloat(), JOK, false)
</code></pre>
<p>And this is our final result.</p>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/shaped-button.gif" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="400" height="400"><figcaption><span style="white-space: pre-wrap;">How our final button should appear</span></figcaption></img></figure><p>And final code.</p>
<pre><code class="language-Kotlin">class TriangleShape(private val roundRadius: Float) : Shape {
   override fun createOutline(
       size: Size,
       layoutDirection: LayoutDirection,
       density: Density
   ) = Outline.Generic(
       Path().apply {
           val CR = size.width
           val AR = size.height / 2

           val DAE = atan(CR / AR)
           val DAQ = PI.toFloat() / 2 - DAE
           val DAM = DAE / 2
           val AD = roundRadius / tan(DAM)
           val DQ = AD * sin(DAQ)
           val AQ = AD * cos(DAQ)

           //move to point D
           moveTo(AQ, DQ)

           val GCN = (PI.toFloat() - 2 * DAE) / 2
           val CG = roundRadius / tan(GCN)
           val GT = CG * sin(GCN)
           val CT = CG * cos(GCN)

           // line to point G
           lineTo(CR - CT, AR - GT)

           val CN = roundRadius / sin(GCN)
           val CNG = PI.toFloat() / 2 - GCN
           // right arc
           arcToRad(Rect(Offset(CR - CN, AR), roundRadius), -CNG, 2 * CNG, false)

           val AB = size.height

           // line to point K
           lineTo(AQ, AB - DQ)

           val OBV = DAM + DAQ
           val BO = roundRadius / sin(DAM)
           val BV = BO * cos(OBV)
           val OV = BO * sin(OBV)

           val BOV = PI.toFloat() - OBV
           val BOK = PI.toFloat() - DAM
           val KOV = BOK - BOV
           val KOU = PI.toFloat() / 2 - KOV
           val JOK = PI.toFloat() - DAE

           // bottom left arc
           arcToRad(Rect(Offset(BV, AB - OV), roundRadius), KOU, JOK, false)

           // line to point E
           lineTo(0f, AD)

           // top left arc
           arcToRad(Rect(Offset(BV, OV), roundRadius), PI.toFloat(), JOK, false)
       }
   )
}
</code></pre>
<p>The final blueprint with all auxiliary segments.</p>
<figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/blueprint.png" class="kg-image" alt="Custom Shapes in Jetpack Compose" loading="lazy" width="805" height="631" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/blueprint.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/blueprint.png 805w" sizes="(min-width: 720px) 720px"/></figure><p>Despite the overwhelming amount of calculations they are all pretty simple trigonometric and geometrical rules. Defining any other shapes won’t be a big issue once it is sorted out for a triangle.</p>
<p>Link to <a href="https://github.com/deptagency/CustomShapeButton?ref=dept-engineering-blog.ghost.io">Github</a>. Commits are for every step we have done. I deliberately didn’t simplify calculations or unite any steps to make it as clear as possible. Feel free to reuse this code in the way that fits you best or contact me directly if any questions still arise.</p>
</p></p>]]></content:encoded></item><item><title><![CDATA[Headless HTML: A light speed content migration approach]]></title><description><![CDATA[This article outlines a strategy for migrating content from a traditional CMS to a Headless CMS, focusing on HTML-to-rich-text conversion challenges. It suggests migrating raw HTML and applying scoped CSS during rendering for old articles, allowing gradual updates to maintain content integrity.]]></description><link>https://engineering.deptagency.com/headless-html-a-light-speed-content-migration-approach/</link><guid isPermaLink="false">65173ec042a1e3000176075f</guid><category><![CDATA[Platforms]]></category><category><![CDATA[cms]]></category><dc:creator><![CDATA[Aaron Weissman]]></dc:creator><pubDate>Mon, 09 Oct 2023 14:32:35 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/coding-at-lightspeed-in-the-forest-3.png" medium="image"/><content:encoded><![CDATA[<h2 id="a-simple-strategy-for-migrating-content-from-a-traditional-cms-to-a-headless-cms-without-difficult-and-costly-data-munging">A simple strategy for migrating content from a traditional CMS to a headless CMS without difficult and costly data munging</h2><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/10/coding-at-lightspeed-in-the-forest-3.png" alt="Headless HTML: A light speed content migration approach"/><p/><h3 id="the-problem">The Problem</h3>
<p>Moving from a traditional Content Management System to a Headless CMS, and need to convert a large number of articles in HTML to JSON/Rich Text data. These formats do not translate 1:1, so you would need a custom script to make that transformation.</p>
<h3 id="the-challenge">The Challenge</h3>
<p>Even simple HTML does not translate 1:1 to rich text. Something like text that is bold, italic and has a hyperlink may be nested three levels deep, whereas rich text will simply apply three attributes to that text block. Writing a script to convert that HTML into Rich Text when there is NO custom CSS is a challenge all it's own. If the text includes any custom styles, or more complex semantic HTML, this becomes an enormous development task that can take weeks at a minimum or months if there is a greater level of complexity or a large number or entries with unique formatting.</p>
<p>Here's an example of what a simple transformation would look like:</p>
<h4 id="html">HTML:</h4>
<pre><code class="language-html">&lt;p&gt;
  &lt;b&gt;
    &lt;em&gt;
      &lt;a href="https://example.com"&gt;Example&lt;/a&gt;
    &lt;/em&gt;
  &lt;/b&gt;
&lt;/p&gt;
</code></pre>
<h4 id="rich-text">Rich Text:</h4>
<pre><code class="language-json">[
  {
    "type": "paragraph",
    "children": [
      {
        "type": "text",
        "text": "Example",
        "marks": [
          {
            "type": "bold"
          },
          {
            "type": "italic"
          },
          {
            "type": "link",
            "attrs": {
              "href": "https://example.com"
            }
          }
        ]
      }
    ]
  }
]
</code></pre>
<p>If you have custom class names included in your rich text, most, if not all, rich text converters available on NPM will remove those class names when converting the HTML to Rich Text, resulting in a loss of potentially necessary styling data as well as making it impossible to go back through and find those text blocks that need to be styled a certain way.</p>
<p>One common example of this is the use of block quotes and pull quotes. Some CMSs might use the html <code>blockquote</code> element properly; many of them do not. Even if the CMS properly implemented this HTML tag, if you are writing articles that differentiate between a pull quote and a block quote, then you would have needed custom class names to style those quotes accordingly. In this case, any default conversion script will immediately lose that difference, requiring a huge chunk of time to go through and visually compare the original article to the data entered in the new headless CMS. If you were doing this via script, then you potentially need to handle a wide array of versions of the quote. It's not as simple as using the right module in the new headless CMS, but also converting all the text inside of the block properly (remember our bold, nested link above).</p>
<p>While this is one example, some other ones I've run into are integrated graphs where the HTML is using data attributes (these may be lost or require regex to pull out the right values from them), images (and their custom aspect ratios applied by CSS classes), and HTML tables (also known as the bane of rich text's existence). Each of these elements will require a custom script to convert it from the HTML to whichever Rich Text format you're using in whichever Headless CMS platform you've chosen. In some cases, like for tables or graph integrations, this will vary from project to project and may require recursive checks (think anchor tags nested inside of tables with lots of different <code>colspan</code>'s) to figure out how to format this into your own flavor of rich text.</p>
<h3 id="the-solution">The Solution</h3>
<p>My favorite, lightly hacky solution for this issue is to simply migrate over the raw HTML into your new headless CMS and then apply some scoped CSS to the content when rendering the article. Use this approach for migrated articles and your fancy new rich text for all articles moving forward.</p>
<h3 id="the-details">The Details</h3>
<p>The real trick to this solution is to use it for the old articles but not the new ones. The gist of this is to script over all the articles into the new headless cms into a read only text field that's only visible on these old articles. Then use your WYSIWYG editor for any new articles being created.</p>
<p>To do this, I'll create an additional field called something like <code>contentType</code> with two options: "html" and "richText". Then, as I'm migrating over the old articles, I'll set the value to <code>contentType: "html"</code>. In the new CMS, hide the <code>contentType</code> field for any new articles being created and default the value of it to "richText". That way, new articles won't even know that HTML is an option, but if a content editor needs to go back and work on an old article, they'll see that it was ported over as HTML. At this point, if they need to edit that old article, they have two options: reach out to a developer to figure out how to edit the HTML, or build the article from scratch. While this may sound needlessly difficult ("Why not just allow them to edit the HTML?" you ask...), this is very much on purpose. We really do not want raw HTML being passed to the site. It's rare that these old articles will need any editing at all, and when they do, the time spent converting them to the new format is worthwhile, as it's likely that any old articles that require an update are ones that are frequently visited and therefore should reflect the new format of the CMS.</p>
<p>On the frontend, where we render these articles, it's as simple as doing a check on the <code>contentType</code> field and then rendering as you would otherwise. If the <code>contentType</code> is "html", then we use <code>dangerouslySetInnerHTML</code> for React, <code>v-html</code> for Vue, or whatever frontend framework you're using.</p>
<h4 id="sanityio-example"><a href="https://www.sanity.io/docs/schema-types?ref=dept-engineering-blog.ghost.io">Sanity.io</a> Example</h4>
<pre><code class="language-js">{
  title: 'Article',
  name: 'article',
  type: 'document',
  fields: [
    {
      title: 'Title',
      name: 'title',
      type: 'string'
    },
    {
      title: 'Content Type',
      name: 'contentType',
      type: 'string',
      options: {
        list: [
          {title: 'HTML', value: 'html'},
          {title: 'Rich Text', value: 'richText'}
        ]
      },
      initialValue: 'richText',
      hidden: ({ parent }) =&gt; !parent?.html?.length // Hide this field if the `HTML` field is empty
    },
    {
      title: 'HTML',
      name: 'html',
      type: 'text',
      hidden: ({ parent }) =&gt; !parent?.html?.length // Hide this field if the `HTML` field is empty
      rows: 50 // Make it easier to see the HTML when quick scanning it
    },
    {
      title: 'Content',
      name: 'content',
      type: 'array',
      of: [{type: 'block'}] // This is the Sanity name for rich text
    }
  ]
}
</code></pre>
<h4 id="react-example">React example:</h4>
<pre><code class="language-jsx">import React from 'react';
import { PortableText } from '@portabletext/react';

export default function HtmlOrRichText({ block }) {
  if (block.contentType === 'html') {
    return (
      &lt;div
        className="utility-class-to-style-raw-html"
        dangerouslySetInnerHTML={{ __html: block.htmlContent }}
      /&gt;
    );
  }

  return (
    &lt;PortableText
      value={block.richTextContent}
      components={{
        blockQuote: ({ value }) =&gt; (
          &lt;blockquote className="block-quote-class"&gt;{value}&lt;/blockquote&gt;
        ),
        pullQuote: ({ value }) =&gt; (
          &lt;blockquote className="pull-quote-class"&gt;{value}&lt;/blockquote&gt;
        ),
      }}
    /&gt;
  );
}
</code></pre>
<h3 id="the-cleanup">The Cleanup</h3>
<p>The biggest issue with this approach is that, unless you spend an insane amount of time writing super specific CSS selectors (in which case you probably should have just written the data migration script anyway), you're likely going to have to accept a simplified approach to your styling on these old articles. In <em>most</em> cases, this is fine. Often, the majority of these old articles haven't been visited in a while, and it's more about the content they hold for SEO purposes, than it is ensuring that they perfectly adhere to the new branding guidelines. If this is unacceptable, then this approach is not for you.</p>
<p>Given this shortcoming, I recommend simply going through and updating the top performing articles into the new format. With most Headless CMS, this is a fairly painless process that takes at most 10 or so minutes per article (often much less), depending on the length and complexity of the article and the new format. Now you can be sure that the top performing articles are styled in the new format, while also feeling confident that none of the old content has been lost in the other articles that have been migrated over. In some cases, the other, lower performing articles can be converted one by one after launch of the new site. In other cases, it's perfectly fine to leave them as is.</p>
<h3 id="gotchas-notes">Gotchas &amp; Notes</h3>
<p>A few notes for anyone taking up this approach:</p>
<ul>
<li>Make sure you convert image URLs. If the images are hosted on the old CMS, the URL may be something like: <code>https://old_cms_url.your_site.com/images/some-random-string</code>. You'll need to port these images over to the new site as part of the migration. This would be necessary whether you port over the article as HTML or convert it to rich text, so you're doing this either way. For this process, just make sure you do a find all and replace of the <code>old_cms_url</code> and replace the full <code>src</code> with the URL from your new CDN.</li>
<li>It's worth finding some libraries to help with the rendering of the HTML. Here's one that pairs with chakra-ui: <a href="https://www.npmjs.com/package/@nikolovlazar/chakra-ui-prose?ref=dept-engineering-blog.ghost.io">https://www.npmjs.com/package/@nikolovlazar/chakra-ui-prose</a>, but there are lots out there to help with this. Remember, the idea of this approach is to move quickly.</li>
<li>Make sure you're not siloed on how this all works. It's important that other people have context for what you're doing and why you're doing this. Otherwise you are going to have go back and script the whole thing all over again.</li>
<li>Be aggressive about collecting other data from the posts. Don't rely on this approach for things like title, related articles, or any other type of tracking/SEO data that you need for the article. That should all be ported into your new headless CMS just like it would otherwise. This is just for the CONTENT of the article to make it simpler to render it properly.</li>
<li>Beware of script tags. If you had users jamming script tags into the old articles, make sure you double check them to be sure they are working, or yank them out and covert those articles manually (if this is tenable). If there are too many script tags to port over manually, then, once again, this process is not for you.</li>
<li>Save your work. Track your script in a repo so you can see what changes have happened to it over time in case you did something that caused a data loss.</li>
<li>Save your data. Even though the old data is likely to take up an enormous amount of space, find a way to hang onto it well through the site launch. Use an S3 bucket or something if you have to, but make sure you can go back and grab the original data in case something got lost in translation here.</li>
</ul>
<p/>]]></content:encoded></item><item><title><![CDATA[Rapid Enterprise Development with RedwoodJS]]></title><description><![CDATA[Get an overview of the RedwoodJS framework by following this quick tutorial and building a sample application.]]></description><link>https://engineering.deptagency.com/rapid-enterprise-development-with-redwoodjs/</link><guid isPermaLink="false">650b2f0041e0af00019d49c0</guid><category><![CDATA[Platforms]]></category><category><![CDATA[RedwoodJS]]></category><dc:creator><![CDATA[Caleb White]]></dc:creator><pubDate>Mon, 25 Sep 2023 16:46:22 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/billy-huynh-4M1wucdlANA-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/billy-huynh-4M1wucdlANA-unsplash.jpg" alt="Rapid Enterprise Development with RedwoodJS"/><p>Redwood quietly entered the framework world in March 2020, with an enterprise first approach as “the framework for Startups” (Next.js, Remix, etc). If you haven’t heard of Redwood it was founded and created by Tom Preston-Warner, co-founder of Github, creator of TOML language, and many other ventures.</p><p>Redwood describes itself as,<strong> “<strong>an opinionated, full-stack, JavaScript/TypeScript web application framework designed to keep you moving fast as your app grows from side project to startup.”</strong></strong></p><p>What makes RedwoodJs unique as a framework is that it doesn’t exactly reinvent the wheel, but instead uses industry standard tools that we would already use in an opinionated full-stack framework. It uses creative and smart integrations with boilerplate code that feels like the monolith we never knew we needed. The backend stack runs on Prisma, Graphql, and Node. This is independent from the frontend, but easily integrates with it using "cells”.</p><p>A cell in Redwood is a collection of frontend code used to query to API layer that allows you to quickly add boilerplate code you need to get an application to MVP quickly and easily. Just as the backend uses industry standard tools, the frontend follows that same pattern. When scaffolding your component, you also get a full component built out for you including a storybook story already ready to view before you begin development, Jest tests setup and working, and a functional base component.</p><p>Let's build a quick to-do app connected to a Postgres database on <a href="https://railway.app/?ref=dept-engineering-blog.ghost.io">Railway</a>. We will explore installing and setting up a Redwood app, setup a schema, scaffold our component, set up some routing, and quickly add styles.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-19-at-11.02.52-AM.png" class="kg-image" alt="Rapid Enterprise Development with RedwoodJS" loading="lazy" width="786" height="656" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/Screenshot-2023-09-19-at-11.02.52-AM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-19-at-11.02.52-AM.png 786w" sizes="(min-width: 720px) 720px"/></figure><hr><h3 id="setting-up-redwood">Setting up Redwood</h3><!--kg-card-begin: markdown--><p>Setting up Redwood is quite easy. First we will use the <code>redwood-app</code> package to install our project:</p>
<pre><code class="language-bash">yarn create redwood-app dept-todos --typescript
</code></pre>
<p>This will create our project, initialize a git repo, and perform our init commit. Now we can swap to the <code>dept-todos</code> folder created during setup:</p>
<pre><code class="language-bash">cd dept-todos
</code></pre>
<p>Next, we will want to set up Tailwind on our project.  With Redwood that is also a very simple CLI command:</p>
<pre><code class="language-bash">yarn rw setup ui tailwindcss
</code></pre>
<!--kg-card-end: markdown--><p><em><strong>Side note before continuing...</strong></em><br>If you didn’t want to use tailwind in a project, it is just as easy to setup a sass configuration. Instead of running the tailwind setup above, you could simply run the following command to add a sass setup:</br></p><!--kg-card-begin: markdown--><pre><code class="language-bash">yarn workspace web add -D sass sass-loader
</code></pre>
<!--kg-card-end: markdown--><h3 id="adding-a-schema">Adding a schema</h3><p>Redwood uses Prisma as the ORM for the API layer. If you are not familiar with Prisma, they have great documentation and use cases on their <a href="https://www.prisma.io/docs?ref=dept-engineering-blog.ghost.io">website</a>.</p><p><strong><em>After creating a postgres database </em></strong>on Railway<strong><em> </em></strong>and <strong><em>adding the postgres address to our <code>.env</code> file</em></strong> at the root of our directory, we are now ready to set our schema.</p><!--kg-card-begin: markdown--><p>We will keep this schema very simple with one model for our todos, located at <code>api/db/schema.prisma</code>.</p>
<!--kg-card-end: markdown--><p>Replace the contents with our new todo schema:</p><!--kg-card-begin: markdown--><pre><code class="language-sql">datasource db {
    provider = &quot;postgresql&quot;	
    url = env(&quot;DATABASE_URL&quot;)
 }
    
generator client {
    provider = &quot;prisma-client-js&quot;
    binaryTargets = &quot;native&quot;
}

model Todo {
    id        Int      @id @default(autoincrement())
    body      String   @db.VarChar(255)	
    completed Boolean  @default(false)	
    createdAt DateTime @default(now())	
    updatedAt DateTime @updatedAt
}
</code></pre>
<!--kg-card-end: markdown--><p>As you can see we have an id, body, completed status, and a timestamp - a very simple setup for our todos. We now need to migrate and deploy our schema changes to our database. This can be done easily with Prisma.</p><!--kg-card-begin: markdown--><pre><code class="language-bash">yarn redwood prisma migrate dev
</code></pre>
<!--kg-card-end: markdown--><p>Prisma will run its migration CLI tool and prompt you to name the migration for your schema changes. We just named this migration <code>todos model</code>.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-13-at-4.11.24-PM.png" class="kg-image" alt="Rapid Enterprise Development with RedwoodJS" loading="lazy" width="706" height="236" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/Screenshot-2023-09-13-at-4.11.24-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-13-at-4.11.24-PM.png 706w"/></figure><p>We have now saved a migration file to our repository and migrated the changes to our database on Prisma.</p><p>Now that the database changes are active, let's scaffold our Todos. Scaffolding is easy and should be familiar to those who have worked with Ruby/Rails, we can run the following command to get our todo’s component ready.</p><!--kg-card-begin: markdown--><pre><code class="language-bash">yarn rw generate scaffold todo
</code></pre>
<!--kg-card-end: markdown--><p>That created a folder in our components on the frontend, set up the associated routing in our router file, and set the the API files needed for basic CRUD functionality.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-14-at-11.53.52-AM.png" class="kg-image" alt="Rapid Enterprise Development with RedwoodJS" loading="lazy" width="732" height="448" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/Screenshot-2023-09-14-at-11.53.52-AM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-14-at-11.53.52-AM.png 732w" sizes="(min-width: 720px) 720px"/></figure><p>As you can see, Redwood set up our todo’s component on the frontend and a CRUD file for the API. <br><br>Our Scaffolding created components, pages, services, a todo layout, and more for us with one simple command. This can be a very time consuming step during the initial setup of a traditional application and with Redwood we are able to go from install to running in less than 5 minutes. </br></br></p><p>Start up your development server and see the todo app in action!</p><!--kg-card-begin: markdown--><pre><code class="language-bash">yarn rw dev
</code></pre>
<!--kg-card-end: markdown--><p>The Vite instance and your API layer will start and you should be able to see your site now live at <a href="http://localhost:8910/?ref=dept-engineering-blog.ghost.io">http://localhost:8910/</a>.<br><br>While our app is already functional, it’s not really great to use out of the box. In the next section we will style and refactor some of the boilerplate files to improve the user experience and deploy our application!</br></br></p><h3 id="time-to-style">Time to style</h3><p>Now that we have our core API setup for an app, let’s create a homepage to view our app. We can use the CLI for this as well.</p><!--kg-card-begin: markdown--><pre><code class="language-bash">yarn redwood generate page home /
</code></pre>
<!--kg-card-end: markdown--><p>This adds a homepage route to our Router. We will use this page during our refactor to make our app more usable. It can be found at <code>/web/src/Routes.tsx</code>.<br><br>In the Router, you will see our todo model was added under the <code>todos</code> route during the scaffolding process:<br/></br></br></p><!--kg-card-begin: markdown--><pre><code class="language-tsx">import { Set, Router, Route } from '@redwoodjs/router'

import ScaffoldLayout from 'src/layouts/ScaffoldLayout'

const Routes = () =&gt; {
  return (
    &lt;Router&gt;
      &lt;Set wrap={ScaffoldLayout} title=&quot;Todos&quot; titleTo=&quot;todos&quot; buttonLabel=&quot;New Todo&quot; buttonTo=&quot;newTodo&quot;&gt;
        &lt;Route path=&quot;/todos/new&quot; page={TodoNewTodoPage} name=&quot;newTodo&quot; /&gt;
        &lt;Route path=&quot;/todos/{id:Int}/edit&quot; page={TodoEditTodoPage} name=&quot;editTodo&quot; /&gt;
        &lt;Route path=&quot;/todos/{id:Int}&quot; page={TodoTodoPage} name=&quot;todo&quot; /&gt;
        &lt;Route path=&quot;/todos&quot; page={TodoTodosPage} name=&quot;todos&quot; /&gt;
      &lt;/Set&gt;
      &lt;Route notfound page={NotFoundPage} /&gt;
    &lt;/Router&gt;
  )
}

export default Routes
</code></pre>
<!--kg-card-end: markdown--><p>Let’s modify the router so our todos will render on the root path:</p><!--kg-card-begin: markdown--><pre><code class="language-tsx">import { Set, Router, Route } from '@redwoodjs/router'

import ScaffoldLayout from 'src/layouts/ScaffoldLayout'

const Routes = () =&gt; {
  return (
    &lt;Router&gt;
      &lt;Set wrap={ScaffoldLayout} title=&quot;Todos&quot; titleTo=&quot;todos&quot; buttonLabel=&quot;New Todo&quot; buttonTo=&quot;newTodo&quot;&gt;
        &lt;Route path=&quot;/new&quot; page={TodoNewTodoPage} name=&quot;newTodo&quot; /&gt;
        &lt;Route path=&quot;/{id:Int}/edit&quot; page={TodoEditTodoPage} name=&quot;editTodo&quot; /&gt;
        &lt;Route path=&quot;/{id:Int}&quot; page={TodoTodoPage} name=&quot;todo&quot; /&gt;
        &lt;Route path=&quot;/&quot; page={TodoTodosPage} name=&quot;todos&quot; /&gt;
      &lt;/Set&gt;
      &lt;Route notfound page={NotFoundPage} /&gt;
    &lt;/Router&gt;
  )
}

export default Routes</code></pre>
<!--kg-card-end: markdown--><p>Excellent! Now you can see our vanilla scaffold functional on our root path.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-14-at-12.32.35-PM.png" class="kg-image" alt="Rapid Enterprise Development with RedwoodJS" loading="lazy" width="1001" height="481" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/Screenshot-2023-09-14-at-12.32.35-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/Screenshot-2023-09-14-at-12.32.35-PM.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-14-at-12.32.35-PM.png 1001w" sizes="(min-width: 720px) 720px"><figcaption>View of the homepage root of our application</figcaption></img></figure><p>When we generated the scaffold earlier, we also created a todos folder that contains both our components and our cells, the frontend code that will host our graphql calls and hydrate our components.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-22-at-11.05.16-AM.png" class="kg-image" alt="Rapid Enterprise Development with RedwoodJS" loading="lazy" width="330" height="199"><figcaption>Structure of the scaffolded Todo model</figcaption></img></figure><p>First we are going to install <a href="https://lucide.dev/?ref=dept-engineering-blog.ghost.io">Lucide icons</a> to update the generic links. We do this by first navigating into our web directory where react is present:</p><!--kg-card-begin: markdown--><pre><code class="language-bash">cd web
yarn add lucide-react
</code></pre>
<!--kg-card-end: markdown--><p>Then we can replace the contents of Todos component at  <code>web/src/components/Todo/Todos/Todos.tsx</code> with the following snippet:</p><!--kg-card-begin: markdown--><pre><code class="language-tsx">import { FileEdit, XCircle } from 'lucide-react'
import type {
  DeleteTodoMutationVariables,
  EditTodoById,
  FindTodos,
  UpdateTodoInput,
} from 'types/graphql'

import { Form, CheckboxField } from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'

import { QUERY } from 'src/components/Todo/TodosCell'
import { truncate } from 'src/lib/formatters'

const DELETE_TODO_MUTATION = gql`
  mutation DeleteTodoMutation($id: Int!) {
    deleteTodo(id: $id) {
      id
    }
  }
`

const UPDATE_TODO_MUTATION = gql`
  mutation UpdateTodoMutation($id: Int!, $input: UpdateTodoInput!) {
    updateTodo(id: $id, input: $input) {
      id
      body
      completed
      createdAt
      updatedAt
    }
  }
`

const TodosList = ({ todos }: FindTodos) =&gt; {
  const [deleteTodo] = useMutation(DELETE_TODO_MUTATION, {
    onCompleted: () =&gt; {
      toast.success('Todo deleted')
    },
    onError: (error) =&gt; {
      toast.error(error.message)
    },
    refetchQueries: [{ query: QUERY }],
    awaitRefetchQueries: true,
  })

  const onDeleteClick = (id: DeleteTodoMutationVariables['id']) =&gt; {
    if (confirm('Are you sure you want to delete todo ' + id + '?')) {
      deleteTodo({ variables: { id } })
    }
  }

  const [updateTodo] = useMutation(UPDATE_TODO_MUTATION, {
    onCompleted: () =&gt; {
      toast.success('Todo updated')
      navigate(routes.todos())
    },
    onError: (error) =&gt; {
      toast.error(error.message)
    },
  })

  const onSave = (input: UpdateTodoInput, id: EditTodoById['todo']['id']) =&gt; {
    updateTodo({ variables: { id, input } })
  }

  return (
    &lt;div className=&quot;flex justify-center px-8&quot;&gt;
      &lt;table className=&quot;container max-w-4xl&quot;&gt;
        &lt;tbody&gt;
          {todos.map((todo) =&gt; {
            if (!todo.completed) {
              return (
                &lt;tr
                  key={todo.id}
                  className={`flex items-center p-4 transition-opacity ${
                    todo.completed &amp;&amp; 'opacity-25'
                  }`}
                &gt;
                  &lt;td&gt;
                    &lt;Form&gt;
                      &lt;CheckboxField
                        id=&quot;completed&quot;
                        name=&quot;completed&quot;
                        onChange={() =&gt;
                          onSave({ completed: !todo.completed }, todo.id)
                        }
                        defaultChecked={todo.completed}
                        className=&quot;rw-input h-4 w-4&quot;
                        errorClassName=&quot;rw-input rw-input-error&quot;
                      /&gt;
                    &lt;/Form&gt;
                  &lt;/td&gt;
                  &lt;td className=&quot;flex-1 px-2&quot;&gt;{truncate(todo.body)}&lt;/td&gt;
                  &lt;td&gt;
                    &lt;nav className=&quot;rw-table-actions gap-2&quot;&gt;
                      &lt;Link
                        to={routes.editTodo({ id: todo.id })}
                        title={'Edit todo ' + todo.id}
                        className=&quot;text-gray-500 hover:text-green-500&quot;
                      &gt;
                        &lt;FileEdit /&gt;
                      &lt;/Link&gt;
                      &lt;button
                        type=&quot;button&quot;
                        title={'Delete todo ' + todo.id}
                        className=&quot;text-gray-500 hover:text-red-500&quot;
                        onClick={() =&gt; onDeleteClick(todo.id)}
                      &gt;
                        &lt;XCircle /&gt;
                      &lt;/button&gt;
                    &lt;/nav&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              )
            } else {
              return (
                &lt;tr key=&quot;notodos&quot; className=&quot;w-full text-center&quot;&gt;
                  No todos. Please add a todo to create a list!
                &lt;/tr&gt;
              )
            }
          })}
        &lt;/tbody&gt;
      &lt;/table&gt;
    &lt;/div&gt;
  )
}

export default TodosList

</code></pre>
<!--kg-card-end: markdown--><p>We can now see our newly designed todos at the root of our application (<a href="http://localhost:8910/?ref=dept-engineering-blog.ghost.io">http://localhost:8910/</a>) as seen below.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-22-at-11.08.02-AM.png" class="kg-image" alt="Rapid Enterprise Development with RedwoodJS" loading="lazy" width="1217" height="477" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/Screenshot-2023-09-22-at-11.08.02-AM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/Screenshot-2023-09-22-at-11.08.02-AM.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-22-at-11.08.02-AM.png 1217w" sizes="(min-width: 720px) 720px"/></figure><p>And after adding our first todo:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-14-at-2.18.45-PM.png" class="kg-image" alt="Rapid Enterprise Development with RedwoodJS" loading="lazy" width="985" height="347" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/Screenshot-2023-09-14-at-2.18.45-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/Screenshot-2023-09-14-at-2.18.45-PM.png 985w" sizes="(min-width: 720px) 720px"><figcaption>Final root layout of our project app</figcaption></img></figure><p/><h3 id="next-steps">Next steps?</h3><p>Now it is time to explore the full power of Redwoodjs and explore some of its features. Some fun ideas would be to implement user authentication to keep those todos separate, update the input styles, and possibly a view for seeing <em>completed </em>todos<em>.</em></p><p><strong>To view the demo repo, </strong><a href="https://github.com/deptagency/blog-todoapp-example?ref=dept-engineering-blog.ghost.io">https://github.com/deptagency/blog-todoapp-example</a><strong>.</strong></p><h3 id="conclusion">Conclusion</h3><p>While our example was very simple, you can see how quickly it is to get up and running with functional code using RedwoodJs! Now for some caveats, at the time of this writing I am unable to recommend Redwood in its current state for beginners. Redwood’s development started as an more of an enterprise scaling applications more so than consumer usage so if you are not familiar with the underlying tools (Prisma, GraphQL, Node, React, Postgres), I would encourage you to start with gaining fundamental knowledge in those tools before diving in too deep with Redwood.</p><p>The future roadmap includes integrating with React Server components as the React core team continues to update React’s core to utilize these more smoothly.</p><p>If you still want more Redwood, I would encourage you to view the outstanding documentation on the <a href="https://redwoodjs.com/?ref=dept-engineering-blog.ghost.io">Redwood homepage</a>.</p></hr>]]></content:encoded></item><item><title><![CDATA[DevOps Quick Fix: GCP outgoing connection issues]]></title><description><![CDATA[Quick Fix when GCP outgoing HTTPS connections to third-party REST APIs are slow or dropping.]]></description><link>https://engineering.deptagency.com/fix-gcp-connection-issues/</link><guid isPermaLink="false">64dfa780a88eff0001a8ecae</guid><category><![CDATA[DevOps]]></category><dc:creator><![CDATA[Jirawat Uttayaya]]></dc:creator><pubDate>Thu, 24 Aug 2023 17:57:12 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/cloudnat-img-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/cloudnat-img-1.png" alt="DevOps Quick Fix: GCP outgoing connection issues"/><p><em><em>DevOps Quick Fix are solutions to common DevOps problems, usually encountered during client engagements</em>.</em></p><h2 id="problem">Problem</h2><p>From Google Cloud Platform (GCP), your outgoing HTTPS connections to third-party REST APIs are slow or dropping.</p><h2 id="solution">Solution</h2><h3 id="preamble">Preamble</h3><p>The cause of the issue is most likely GCP <a href="https://cloud.google.com/nat/docs/overview?ref=dept-engineering-blog.ghost.io">Cloud NAT</a> port exhaustion.</p><p>If you are using private network Compute Engine, GKE, or any of the GCP serverless services like Cloud Run, your outgoing connections are going through Cloud NAT. </p><p>Cloud NAT needs a unique 5-tuple [source IP, source port, destination IP/port/protocol] to make an outgoing connection. If all the 5-tuples are used, the connections are slowed or dropped. This is known as NAT port exhaustion.</p><h3 id="turn-off-endpoint-independent-mapping">Turn off Endpoint-Independent Mapping</h3><p>You can read the gnarly technical details about <a href="https://cloud.google.com/nat/docs/ports-and-addresses?hl=en&_ga=2.122463947.-1902993673.1671470601&_gac=1.225119720.1692633878.CjwKCAjwloynBhBbEiwAGY25dFgSMH9KPpilaw18GEU7JahsFc4_MKcYJfP9-NSinT8dhbV9hPaMGRoCrYgQAvD_BwE&ref=dept-engineering-blog.ghost.io#example-conflict-eim">Endpoint-Independent Mapping conflicts</a>, but bottom-line, EIM is more trouble that it is worth. Turn EIM off and increase the "Minimum ports per VM instances" setting (2048 is a good starting option).</p><p>Steps:<br>1. In GCP console, go to <a href="https://console.cloud.google.com/net-services/nat/list?_ga=2.88754587.1405936919.1692561800-1902993673.1671470601&_gac=1.87818730.1692288851.CjwKCAjwivemBhBhEiwAJxNWN6asc67rtDUmL3t_t8L1WEgJhmaRjCyf7X1lLaxBnstNlbgWaf0_bxoCZ2YQAvD_BwE&ref=dept-engineering-blog.ghost.io">Cloud NAT</a><br>2. Click your Cloud NAT Gateway link<br>3. Click the "<strong>Edit</strong>" button on top, then click "<strong>Advanced Configuration</strong>"<br>4. Uncheck "<strong>Enable Endpoint-Independent Mapping</strong>" and increase the "<strong>Minimum port per VM instance</strong>" value<br>5. Click the "<strong>Save</strong>" button at the bottom</br></br></br></br></br></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/EIM.png" class="kg-image" alt="DevOps Quick Fix: GCP outgoing connection issues" loading="lazy" width="1008" height="576" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/EIM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/08/EIM.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/EIM.png 1008w" sizes="(min-width: 720px) 720px"/></figure><h3 id="optional-enable-dynamic-port-allocation"> Optional: Enable Dynamic Port Allocation</h3><p><a href="https://cloud.google.com/nat/docs/ports-and-addresses?hl=en&_ga=2.115641286.-1902993673.1671470601&_gac=1.112986742.1692288851.CjwKCAjwivemBhBhEiwAJxNWN6asc67rtDUmL3t_t8L1WEgJhmaRjCyf7X1lLaxBnstNlbgWaf0_bxoCZ2YQAvD_BwE&ref=dept-engineering-blog.ghost.io#dynamic-port">Dynamic Port Allocation</a> will scale up the number of VM ports when it senses port exhaustion. The main issue is connections can drop during scaling. I usually prefer to turn it off and set the "Minimum ports per VM instance" to a high number.</p><p>If you prefer to turn on "Dynamic Port Allocation", set the minimum port high to reduce the chance of dropped connections.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/DPA-2.png" class="kg-image" alt="DevOps Quick Fix: GCP outgoing connection issues" loading="lazy" width="1008" height="720" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/DPA-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/08/DPA-2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/DPA-2.png 1008w" sizes="(min-width: 720px) 720px"/></figure><h3 id="optional-assign-more-ips-to-cloud-nat">Optional: Assign more IPs to Cloud NAT</h3><p>If you are using static reserved IPs for Cloud NAT because of third-party API firewall allow-lists, you can manually add more reserved IPs to reduce port exhaustion.</p><p>Steps:<br>1. In GCP console, go to <a href="https://console.cloud.google.com/net-services/nat/list?_ga=2.88754587.1405936919.1692561800-1902993673.1671470601&_gac=1.87818730.1692288851.CjwKCAjwivemBhBhEiwAJxNWN6asc67rtDUmL3t_t8L1WEgJhmaRjCyf7X1lLaxBnstNlbgWaf0_bxoCZ2YQAvD_BwE&ref=dept-engineering-blog.ghost.io">Cloud NAT</a><br>2. Click your Cloud NAT Gateway link<br>3. Click the "<strong>Edit</strong>" button on top, then go to the "Cloud NAT mapping" section.<br>4. Click the "+ ADD IP ADDRESS" button to add more IPs to Cloud NAT<br>5. Click the "<strong>Save</strong>" button at the bottom</br></br></br></br></br></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/NATIP.png" class="kg-image" alt="DevOps Quick Fix: GCP outgoing connection issues" loading="lazy" width="1008" height="864" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/NATIP.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/08/NATIP.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/NATIP.png 1008w" sizes="(min-width: 720px) 720px"/></figure>]]></content:encoded></item><item><title><![CDATA[How to speed up Docker builds in GitHub Actions]]></title><description><![CDATA[Are your Docker builds slow in GitHub Actions? Here's how to speed it up with the built-in GitHub Actions cache.]]></description><link>https://engineering.deptagency.com/how-to-speed-up-docker-builds-in-github-actions/</link><guid isPermaLink="false">64dea335a88eff0001a8eb7a</guid><category><![CDATA[docker]]></category><dc:creator><![CDATA[Jirawat Uttayaya]]></dc:creator><pubDate>Tue, 22 Aug 2023 13:04:33 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/gha-docker.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/gha-docker.png" alt="How to speed up Docker builds in GitHub Actions"/><p>Are your Docker builds slow in GitHub Actions? Here's how to speed it up with the built-in <a href="https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows?ref=dept-engineering-blog.ghost.io">GitHub Actions cache</a>.</p><h2 id="official-docker-action">Official Docker Action</h2><p>Docker published an official <a href="https://docs.docker.com/build/cache/backends/gha/?ref=dept-engineering-blog.ghost.io">GitHub Actions cache integration</a> along with an official <a href="https://github.com/docker/build-push-action?ref=dept-engineering-blog.ghost.io">GitHub Actions plugin</a>.</p><p>Here's how to use both and turn on the GitHub Actions cache to speed up your Docker builds. The trick is to set <strong>cache-from</strong> and <strong>cache-to </strong>to <code>type=gha</code></p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">      -
        name: Build Tag and Push Docker image
        uses: docker/build-push-action@v4
        with:
          file: nextjs-blog/docker/Dockerfile
          context: nextjs-blog
          tags: ${{ steps.dockermeta.outputs.tags }}
          labels: ${{ steps.dockermeta.outputs.labels }}
          push: true
          cache-from: type=gha
          cache-to: type=gha,mode=max</code></pre><figcaption><p><span style="white-space: pre-wrap;">GHA cache for Docker</span></p></figcaption></figure><p/><p>A complete working example is here at<br><a href="https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs.yml?ref=dept-engineering-blog.ghost.io">https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs.yml</a></br></p><p>How do you know that the cache is working? The Docker build output will tell you. Your first Docker build will be slower because it's populating the cache. For subsequent builds, you will see a Docker output similar to:</p><pre><code>#10 [deps 1/4] RUN apk add --no-cache libc6-compat
#10 CACHED</code></pre><p>See sample output here<br><a href="https://github.com/deptagency/engineering-blog-github-actions/actions/runs/5895631079/job/15991811696?ref=dept-engineering-blog.ghost.io#step:7:176">https://github.com/deptagency/engineering-blog-github-actions/actions/runs/5895631079/job/15991811696#step:7:176</a></br></p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">‼️</div><div class="kg-callout-text">GitHub Action cache has a current limit of <b><strong style="white-space: pre-wrap;">10 GB</strong></b>.Large Docker images can quickly outgrow this size limitation.</div></div><p/><h2 id="alternatives">Alternatives</h2><h3 id="gha-local-cache">GHA local cache</h3><p>If you want more control over your GitHub cache, you can use Docker <a href="https://docs.docker.com/build/cache/backends/local/?ref=dept-engineering-blog.ghost.io">local cache</a>. Just set <strong>cache-from</strong> and <strong>cache-to </strong>to <code>type=local</code> .</p><figure class="kg-card kg-code-card"><pre><code>      -
        name: GitHub Actions Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      -
        name: Build Tag and Push Docker image
        uses: docker/build-push-action@v4
        with:
          file: nextjs-blog/docker/Dockerfile
          context: nextjs-blog
          tags: ${{ steps.dockermeta.outputs.tags }}
          labels: ${{ steps.dockermeta.outputs.labels }}
          push: true
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max</code></pre><figcaption><p><span style="white-space: pre-wrap;">Local cache for Docker</span></p></figcaption></figure><p/><p>A complete working example is here at<br><a href="https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs-actions-cache.yml?ref=dept-engineering-blog.ghost.io">https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs-actions-cache.yml</a></br></p><h3 id="registry-cache">Registry cache</h3><p>You can use the container registry, such as <a href="https://github.blog/2020-09-01-introducing-github-container-registry/?ref=dept-engineering-blog.ghost.io" rel="noreferrer">GitHub Container Registry</a> or AWS ECR, to store the image build cache.<br><br>For this example, we will use the AWS ECR by setting the cache to type=registry</br></br></p><figure class="kg-card kg-code-card"><pre><code>      -
        name: Build Tag and Push Docker image
        uses: docker/build-push-action@v4
        with:
          file: nextjs-blog/docker/Dockerfile
          context: nextjs-blog
          tags: ${{ steps.dockermeta.outputs.tags }}
          labels: ${{ steps.dockermeta.outputs.labels }}
          pull: true
          push: true
          cache-from: type=registry,ref=${{ env.ECR_FULL_REPO }}:dockercache
          cache-to: type=registry,ref=${{ env.ECR_FULL_REPO }}:dockercache,mode=max,image-manifest=true</code></pre><figcaption><p><span style="white-space: pre-wrap;">ECR Registry cache for Docker</span></p></figcaption></figure><p>A complete working example for GHCR is here at<br><a href="https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs-registry-cache-ghcr.yml?ref=dept-engineering-blog.ghost.io" rel="noopener noreferrer">https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs-registry-cache-ghcr.yml</a><br><br>A complete working example for AWS ECR is here at<br><a href="https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs-registry-cache.yml?ref=dept-engineering-blog.ghost.io" rel="noopener noreferrer">https://github.com/deptagency/engineering-blog-github-actions/blob/main/.github/workflows/docker-nextjs-registry-cache.yml</a></br></br></br></br></p>]]></content:encoded></item><item><title><![CDATA[Creating a Unity animated character controller with C# best practices in mind]]></title><description><![CDATA[Get started and apply some general C# coding best practices to help you create something that’s scalable from the very beginning and minimizes spaghetti code. ]]></description><link>https://engineering.deptagency.com/creating-a-unity-animated-character-controller-with-c-best-practices-in-mind/</link><guid isPermaLink="false">64d3f8806720c400011519c4</guid><category><![CDATA[c#]]></category><dc:creator><![CDATA[John Askew]]></dc:creator><pubDate>Thu, 17 Aug 2023 13:23:07 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/unity-character-controller-3.png" medium="image"/><content:encoded><![CDATA[<h3 id="preventing-spaghetti-code-and-separating-concerns">Preventing spaghetti code and separating concerns</h3><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/unity-character-controller-3.png" alt="Creating a Unity animated character controller with C# best practices in mind"/><p>One of the most common things to do in unity is create a character controller so that you can take input and make something move and interact with its environment. There are lots of options for this along with tutorials on how to create your own, but what is often overlooked is how to make something that is maintainable and can scale as your game becomes more complex. Most tutorials produce code like this which is fine for understanding the basics of locomotion in Unity, but not something you would want to replicate for a character controller that will eventually have dozens of states.</p><pre><code class="language-csharp">public void Update()
{
	...

    if (wantingToSprint &amp;&amp; areWeGrounded &amp;&amp; !areWeCrouching)
    	currentSpeed = sprintMoveSpeed;
    else if (!areWeCrouching &amp;&amp; areWeGrounded)
    	currentSpeed = walkMoveSpeed;
    if(wantingToCrouch &amp;&amp; jumpCrouching)
    	crouch = true;
    else
    	crouch = false;
    if (areWeGrounded)
    	coyoteTimeCounter = coyoteTime;
    else
    	coyoteTimeCounter -= Time.deltaTime;
    if (wantingToJump)
    	jumpBufferCounter = jumpBuffer;
    else
    	jumpBufferCounter -= jumpBuffer;
    if (coyoteTimeCounter &gt; 0f &amp;&amp; jumpBufferCounter &gt; 0f &amp;&amp; jumpCoolDownOver)
    {
    	characterController.velocity = new Vector3(characterController.velocity.X, 0f, characterController.velocity.Z);
        characterController.Move(transform.up * jumpForce);
        jumpCoolDownOver = false;
        areWeGrounded = false;
        jumpBufferCounter = 0f;
        currentSpeed = jumpMoveSpeed;
        endJumpTime = Time.time + jumpTime;
        Invoke(nameof(jumpCoolDownCountdown), jumpCooldown);
    }
    else if (wantingToJump &amp;&amp; !areWeGrounded &amp;&amp; endJumpTime &gt; Time.time)
    	characterController.Move(Vector3.up * jumpAcceleration);
    
    ...
}</code></pre><p>My goal in this article is to help you get started and apply some general C# coding best practices along the way to help you create something that’s scalable from the very beginning and minimizes spaghetti code like the previous example.</p><h3 id="prerequisites">Prerequisites</h3><p>You can download the source code from <a href="https://github.com/deptagency/animated-character-controller?ref=dept-engineering-blog.ghost.io">GitHub</a> and here’s a video demo of what we are going to create:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="150" src="https://www.youtube.com/embed/UgsDq_Q6WHM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="Introduction - Creating a Unity animated character controller with C# best practices in mind"/></figure><p>Our starting point will be a new scene with an empty game object added to represent the player, an empty game object to parent any ProBuilder game objects, a main camera and a Cinemachine FreeLook camera. We’ll add a few basic structures to our Environment and create some InputActions.<br><br>You can download our starting point from here: <a href="https://github.com/deptagency/animated-character-controller/releases/tag/step-0?ref=dept-engineering-blog.ghost.io">https://github.com/deptagency/animated-character-controller/releases/tag/step-0</a></br></br></p><p>If you want to learn more about the following topics, here are some useful links:</p><ul><li><a href="https://www.youtube.com/watch?v=UyUogO2DvwY&ref=dept-engineering-blog.ghost.io">Input System</a></li><li><a href="https://www.youtube.com/watch?v=x6Q5sKXjZOM&list=PLX2vGYjWbI0TQpl4JdfEDNO1xK_I34y8P&ref=dept-engineering-blog.ghost.io">Cinemachine</a></li><li><a href="https://www.youtube.com/watch?v=a8JOk8nuK0k&ref=dept-engineering-blog.ghost.io">ProBuilder</a></li></ul><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="making-something-move">Making something move</h2></div><p>When creating a character controller there are a few options available:</p><ul><li>Character Controller component</li><li>Rigidbody</li><li>Rigidbody (kinematic)</li><li>Bespoke</li></ul><p>We’ll be using Unity’s Character Controller because we want to be able to interact with Unity’s physics (Nvidia PhysX) engine without having to implement our own collision handling but want to script how our character moves. This approach can become more restrictive when you want to have more control over collisions and outside of a tutorial we would be more likely to use a kinematic Rigidbody instead.</p><h3 id="configuring-and-abstracting-our-inputs">Configuring and abstracting our inputs</h3><p>The first thing we are going to create is an interface to represent our characters' inputs. The reason for abstracting this is so that we can later reuse this controller with a different input implementation that might use something like a Nav Mesh Agent for an NPC instead of controlled input for our Player.</p><p>Since we are only concerned with locomotion, we’ll create an <code>ILocomotionInput</code> interface and define it like this:</p><pre><code class="language-csharp">using UnityEngine;

public interface ILocomotionInput
{
    public Vector3 DirectionXZ { get; }
    public float SpeedXZ { get; }
    public Quaternion RotationY { get; }
    public bool Jump { get; }
}</code></pre><ul><li><strong>DirectionXZ</strong> represents the normalized direction of horizontal movement.</li><li><strong>SpeedXZ</strong> represents the speed of horizontal movement clamped between zero and one.</li><li><strong>RotationY</strong> represents the direction the character should face.</li><li><strong>Jump</strong> represents whether we should jump (provided other conditions for a jump are satisfied).</li></ul><p>Now we can create a concrete implementation of this interface using Unity’s Input System:</p><pre><code class="language-csharp">using UnityEngine;
using UnityEngine.InputSystem;

public class ControlledThirdPersonLocomotionInput : MonoBehaviour, ILocomotionInput
{
    // Editor Settings
    public Transform Camera;
    [Range(0f, 0.5f)] public float JumpButtonGracePeriod = 0.2f;
    [Range(0f, 1f)] public float WalkSpeed = 0.5f;
    [Range(0f, 1f)] public float RunSpeed = 1f;

    // Private variables
    private Vector3 _inputDirection = Vector3.zero;
    private float? _mostRecentJumpPress;
    private bool _run = false;

    // Public variables
    public Vector3 DirectionXZ { get; private set; } = Vector2.zero;
    public float SpeedXZ { get; private set; } = 0f;
    public Quaternion RotationY { get; private set; } = Quaternion.identity;
    public bool Jump =&gt; _mostRecentJumpPress + JumpButtonGracePeriod &gt;= Time.time;

    public void Update()
    {
        DirectionXZ = (Quaternion.AngleAxis(Camera.rotation.eulerAngles.y, Vector3.up) * _inputDirection).normalized;

        RotationY = DirectionXZ != Vector3.zero ? Quaternion.LookRotation(DirectionXZ, Vector3.up) : Quaternion.identity; 

        SpeedXZ = Mathf.Clamp01(_inputDirection.magnitude) * (_run ? RunSpeed : WalkSpeed);
    }

    public void OnMovement(InputValue value)
    {
        var input = value.Get&lt;Vector2&gt;();

        _inputDirection = new Vector3(input.x, 0f, input.y);
    }

    public void OnJump(InputValue value)
    {
        if (value.isPressed)
        {
            _mostRecentJumpPress = Time.time;
        }
    }

    public void OnRun(InputValue value)
    {
        _run = value.isPressed;
    }
}</code></pre><p>We set the public variables in <strong>Update</strong> which runs every tick of our game. In addition to this we have methods that get called by Unity’s Input System for <strong>OnMovement</strong>, <strong>OnJump</strong>, and <strong>OnRun</strong>.</p><p>For <strong>OnMovement</strong> we convert the <code>Vector2</code> input to a <code>Vector3</code> using Z instead of Y axis since Unity (by default) uses Y for the vertical axis.</p><p><strong>OnJump</strong> we update our private variable to record the last time a jump was requested. This is so that we are more flexible in the timing of the jump input. Pressing the jump button fractionally too early can still be accepted instead of requiring pixel perfect timing.</p><p>For <strong>OnRun</strong> we just set our private variable to record the current state.</p><p>Then, each time <strong>Update</strong> runs we can project the <code>_inputDirection</code> relative to our camera using <code>Quaternion.AngleAxis</code> so that the resulting <strong>DirectionXZ</strong> is relative to the camera’s perspective. Our <strong>RotationY</strong> just points in the same direction. <strong>SpeedXZ</strong> uses the <code>_inputDirection</code> magnitude with a multiplier based on whether we should be walking or running.</p><p>Because this script implements <code>MonoBehavior</code> we can add it to our Player game object and assign the Main Camera to it. We also need to make sure that our Player game object has a Player Input component so that the Input System methods are called as we expect.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Unity-Inspector-for-our-Player-game-object.png" class="kg-image" alt="Creating a Unity animated character controller with C# best practices in mind" loading="lazy" width="747" height="879" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/Unity-Inspector-for-our-Player-game-object.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Unity-Inspector-for-our-Player-game-object.png 747w" sizes="(min-width: 720px) 720px"><figcaption>Unity Inspector for our Player game object</figcaption></img></figure><p/><p>If we run the game now, we should be able to interact with our control schemes and see the values from our script change in the inspector.</p><h3 id="preparing-the-player-game-object">Preparing the player game object</h3><p>We’re going to need something visual to know where our player is in the game world. Let’s download a free character mesh rigged for animation from Adobe’s <a href="https://www.mixamo.com/?ref=dept-engineering-blog.ghost.io">mixamo.com</a>. You can create a free account and then download any character you like, making sure to change the format to FBX for Unity (.Fbx).</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Character-download-settings-Adobe-mixamo.png" class="kg-image" alt="Creating a Unity animated character controller with C# best practices in mind" loading="lazy" width="1075" height="435" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/Character-download-settings-Adobe-mixamo.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/08/Character-download-settings-Adobe-mixamo.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Character-download-settings-Adobe-mixamo.png 1075w" sizes="(min-width: 720px) 720px"><figcaption>Character download settings for Adobe mixamo.com</figcaption></img></figure><p/><p>Save this to your Assets folder and highlight it from the Project panel. In the import settings, select the Rig tab and change the Animation Type to Humanoid and apply. This will create a Unity avatar based upon this character. Then, from the Animation tab, uncheck Import Animation and apply that. We will download animations later which we can apply for this avatar. Finally, drag the character into the scene as a child of your Player game object. Rename it to Avatar and reset the transform to make sure it is facing the forward (+Z) axis.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Unity-Character-Controller-component.png" class="kg-image" alt="Creating a Unity animated character controller with C# best practices in mind" loading="lazy" width="1600" height="655" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/Unity-Character-Controller-component.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/08/Unity-Character-Controller-component.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Unity-Character-Controller-component.png 1600w" sizes="(min-width: 720px) 720px"><figcaption>Unity Character Controller component</figcaption></img></figure><p/><p>Next thing to do is add a character controller to our Player game object. Update the Center.Y to half the height and update the height and radius until your collider covers your avatar without being oversized as shown in the example above with our Y Bot avatar. We are aligning the bottom of our collider with the floor which works well for a character controller but another popular approach is to float the collider above the feet to provide more flexibility for stair and slope traversal. This is something we might explore in a future post potentially.</p><h3 id="scaffolding-our-state-machine">Scaffolding our state machine</h3><p>With our player prepared, we just need to script out some motion but we want to avoid a messy and unmaintainable codebase so we are going to script that using a state machine pattern. Many Unity tutorials for a character controller will have a single script on the Player that handles input, collisions and movement without any state management at all. This doesn’t look problematic at first when you only have a few lines of code, but as you move beyond basic movement and want to handle idle, jumping, falling states etc you start to run into trouble. In addition to spaghetti code being hard to understand and maintain because of the hierarchy of if else statements, it can also begin to impact performance when you are checking all the logic for all states on every single update. There is really no need to check our falling logic when we are grounded for example.</p><p>This is the state machine we are going to create:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/state-machine.png" class="kg-image" alt="Creating a Unity animated character controller with C# best practices in mind" loading="lazy" width="1060" height="520" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/state-machine.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/08/state-machine.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/state-machine.png 1060w" sizes="(min-width: 720px) 720px"/></figure><p>A state machine works by managing a current state and owning the transition from one state to another. Individual states can implement a common abstraction that exposes methods the state machine can invoke as well as allowing states to interact with the state machine to conditionally change states or access the context to interact with other abstractions like input and our controller code.</p><p>Our state abstraction will be similar to a <code>MonoBehavior</code> with an <strong>Update</strong> method. We’ll have an empty constructor for any initialization (the equivalent of our Enter logic) and implement this as <code>IDisposable</code> (the equivalent of our Exit logic). We’ll create a new instance of our next state whenever changing states.</p><p>First we need to create our context interface:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public interface ILocomotionContext
    {
        public ILocomotionInput Input { get; }

        public void ApplyInputTranslationXZ(float speedMultiplier = 1f);
        public void ApplyInputRotationY(float speedMultiplier = 1f);
    }
}</code></pre><p>We’ll return to this later to expose more properties and methods. Now, lets implement our abstract State Machine and State base classes:</p><pre><code class="language-csharp">using System;
using UnityEngine;

namespace DEPT.Unity
{
    public class StateMachine&lt;TContext&gt; : IDisposable
    {
        private State&lt;TContext&gt; _currentState;

        public TContext Context { get; private set; }
        public float ElapsedTime { get; protected set; }

        public StateMachine(TContext context)
        {
            Context = context;
        }

        public void ChangeState&lt;T&gt;() where T : State&lt;TContext&gt;
        {
            if(_currentState?.GetType() != typeof(T))
            {
                _currentState?.Dispose();

                ElapsedTime = 0f;

                _currentState = (T)Activator.CreateInstance(typeof(T), this);
            }
        }

        public virtual void Update()
        {
            ElapsedTime += Time.deltaTime;
            _currentState?.Update();
        }

        public void Dispose()
        {
            _currentState?.Dispose();
        }
    }
}</code></pre><p>This will show errors until we implement the abstract State class below but as you can see just handles changing from one state to another and allowing the current state to be updated.</p><pre><code class="language-csharp">using System;

namespace DEPT.Unity
{
    public abstract class State&lt;TContext&gt; : IDisposable
    {
        protected StateMachine&lt;TContext&gt; StateMachine { get; }

        public State(StateMachine&lt;TContext&gt; stateMachine)
        {
            StateMachine = stateMachine;
        }

        public virtual void Update()
        {

        }

        public virtual void Dispose()
        {
            
        }
    }
}</code></pre><p>We implement the methods as virtual so that they can be optionally overridden as needed. Next we can create our first state.</p><h3 id="moving-state-basic-locomotion">Moving state &amp; basic locomotion</h3><p>The first state we are going to create is our Moving state. In this state we can move around horizontally. This is where we will be able to use our <code>ILocomotionContext</code> to indirectly apply changes to our Character Controller. Instead of writing that code within the state for that we are going to use methods from <code>ILocomotionContext</code> that we will implement shortly. We do this so that we can avoid repeating the same logic in multiple places as several states will want to be able to move the character in this way.</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class MovingState : State&lt;ILocomotionContext&gt;
    {
        public MovingState(StateMachine&lt;ILocomotionContext&gt; stateMachine) : base(stateMachine)
        {
            
        }

        public override void Update()
        {
            StateMachine.Context.ApplyInputTranslationXZ(5f);
            StateMachine.Context.ApplyInputRotationY(10f);
        }
    }
}</code></pre><p>The last thing we need is to create our implementation of <code>ILocomotionContext</code>. This governs the construction of our state machine and subsequent updates but also encapsulates any shared logic that we need to implement for controlling the character. By implementing <code>MonoBehavior</code> also we can add this script as a component for our Player game object.</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class LocomotionController : MonoBehaviour, ILocomotionContext
    {
        private CharacterController _characterController;
        private StateMachine&lt;ILocomotionContext&gt; _stateMachine;
        private Vector3 _cumulativeTranslation = Vector3.zero;

        public CollisionFlags Flags { get; private set; } = CollisionFlags.None;
        public ILocomotionInput Input { get; private set; }

        public void Awake()
        {
            Input = GetComponent&lt;ILocomotionInput&gt;();
            _characterController = GetComponent&lt;CharacterController&gt;();
        }

        public void Start()
        {
            _stateMachine = new StateMachine&lt;ILocomotionContext&gt;(this);

            _stateMachine.ChangeState&lt;MovingState&gt;();
        }

        public void Update()
        {
            _stateMachine.Update();

            Flags = _characterController.Move(_cumulativeTranslation);

            _cumulativeTranslation = Vector3.zero;
        }

        public void ApplyInputRotationY(float speedMultiplier = 1f)
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Input.RotationY, speedMultiplier * Time.deltaTime);
        }

        public void ApplyInputTranslationXZ(float speedMultiplier = 1f)
        {
            _cumulativeTranslation += Input.SpeedXZ * speedMultiplier * Time.deltaTime * Input.DirectionXZ;
        }
    }
}</code></pre><p>We have a <strong>Start</strong> method to retrieve any required components from our game object (and its children) and construct our state machine before setting the initial state.</p><p>We have an <strong>ApplyInputRotationY</strong> method to immediately rotate our game object but <code>Slerp</code> to avoid rotating too fast. We apply this directly to our transform because Unity’s Character Controller doesn’t deal with rotation. We also have an <strong>ApplyInputTranslationXZ</strong> method to accumulate translations to be later applied during our <strong>Update</strong>. We do this to avoid our character controller having to move the transform multiple times per tick.</p><p>Our <strong>Update</strong> method runs every tick and first updates the state machine (which subsequently updates the current state), before we use our character controller to move the game object based on our cumulative translation vector. Finally, we reset that cumulative translation vector to zero so that we don’t reapply the same translations repeatedly.</p><p>We can now drag this script component onto our Player game object and play the scene to test our basic movement and should see something like this:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="150" src="https://www.youtube.com/embed/_el2za4PSGE?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="Movement no animation (1/5)"/></figure><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="animation-amp-root-motion">Animation &amp; root motion</h2></div><h3 id="adobe-mixamocom">Adobe Mixamo.com</h3><p>Now we have a moving character, we should introduce some animation. However, we need to consider the concept of root motion before doing so. Root motion uses translation and rotation in the animations to apply motion to our character. Its advantage is that it avoids visual defects by misaligned motion and animation such as a sliding effect when the character moves faster and slower than the animation itself. We could use in-place animations but lets download some animations that are designed to use root motion and update our Moving state to utilize root motion instead.</p><p>The first thing we need to do is grab some animations so let’s return to Adobe’s <a href="https://www.mixamo.com/?ref=dept-engineering-blog.ghost.io">mixamo.com</a> and this time, go to the animations tab.</p><p>Download the following animations making sure to leave the in-place option unchecked:</p><ul><li>Walking</li><li>Running</li></ul><p>Save these to your Assets folder and highlight them from the Project panel. In the import settings, select the <strong>Rig</strong> tab and change the Animation Type to <strong>Humanoid</strong>, updating Avatar Definition to <strong>Copy From Other Avatar</strong> and selecting the avatar from earlier. Apply our changes. Then, from the <strong>Animation</strong> tab, check <strong>Loop Time</strong> and bake the Original root transform rotation and position (Y) to only keep root motion on the XZ plane.</p><h3 id="mecanim-animation-system">Mecanim animation system</h3><p>Now that we have some animations we can use Unity’s Mecanim Animation System to create some animation states and parameters for our base layer. This is also a state machine implementation that could be used with behaviors to separate concerns but is more suited to visual development than C# programming and can be opinionated when it comes to transitions and customization.</p><p>Instead of having transitions and letting Mecanim decide what animation state is active, we will control the Animator from within our state machine implementation. As such, all we need are the states themselves and any parameters that are required by blend trees. There are lots of really detailed tutorials on the animation system and how it works but for these two animations we are simply going to create a 1D blend tree with 3 motions: Walking slowly, Walking, and Running. For Walking slowly we will just reuse the same Walking animation but play it at 0.5 speed. The blend parameter can be named <strong>SpeedXZ</strong>. Create an additional float parameter called <strong>SpeedY</strong> that we will use later too.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/one-dimensional-blend-tree-horizontal.png" class="kg-image" alt="Creating a Unity animated character controller with C# best practices in mind" loading="lazy" width="745" height="478" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/one-dimensional-blend-tree-horizontal.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/one-dimensional-blend-tree-horizontal.png 745w" sizes="(min-width: 720px) 720px"><figcaption>One dimensional blend tree for horizontal locomotion</figcaption></img></figure><p/><h3 id="scripting-our-animator">Scripting our animator</h3><p>Back in the code we will update our scripts to support root animation before applying it from our Moving state. First thing we need is a script to manage interactions with the Animator:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    [RequireComponent(typeof(Animator))]
    public class LocomotionAnimator : MonoBehaviour
    {
        private Animator _animator;

        private int _speedXZHash;
        private int _speedYHash;
        private Vector3 _cumulativeDeltaPosition = Vector3.zero;

        public void Awake()
        {
            _animator = GetComponent&lt;Animator&gt;();

            _speedXZHash = Animator.StringToHash("SpeedXZ");
            _speedYHash = Animator.StringToHash("SpeedY");
        }

        public void OnAnimatorMove()
        {
            _cumulativeDeltaPosition += _animator.deltaPosition;
        }

        public Vector3 ProcessRootMotionTranslation()
        {
            var rootMotion = _cumulativeDeltaPosition;

            _cumulativeDeltaPosition = Vector3.zero;

            return rootMotion;
        }

        public void SetSpeedXZ(float speedXZ, float deltaTime)
        {
            _animator.SetFloat(_speedXZHash, Mathf.Clamp(speedXZ, 0f, 1f), 0.1f, deltaTime);
        }

        public void SetSpeedY(float speedY, float deltaTime)
        {
            _animator.SetFloat(_speedYHash, Mathf.Clamp(speedY, 0f, 1f), 0.1f, deltaTime);
        }

        public void CrossFadeInFixedTime(int stateHashName, float fixedTransitionDuration = 0.1f)
        {
            _animator.CrossFadeInFixedTime(stateHashName, fixedTransitionDuration);
        }
    }
}</code></pre><p>This script exposes methods to process (and reset) the root motion translation, update our Animator parameters such as <strong>SpeedXZ</strong> and <strong>SpeedY</strong>, and can cross fade to a specific animation state.</p><h3 id="refactor-to-integration-animation-for-our-state-machine">Refactor to integration animation for our state machine</h3><p>This is added to our Avatar game object and next we can update our ILocomotionContext interface:</p><pre><code class="language-csharp">namespace DEPT.Unity
{
    public interface ILocomotionContext
    {
        ...
        public LocomotionAnimator Animator { get; }

        ...
        public void ApplyRootMotionTranslation();
    }
}</code></pre><p>We can now access our Animator from the <code>ILocomotionContext</code> in our state machine along with a method for applying root motion translation in our controller. Next we can update our <code>LocomotionController</code> to implement these new additions:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class LocomotionController : MonoBehaviour, ILocomotionContext
    {
        ...
        public LocomotionAnimator Animator { get; private set; }

        public void Awake()
        {
            ...
            Animator = GetComponentInChildren&lt;LocomotionAnimator&gt;();
            ...
        }

        ...

        public void Update()
        {
            Animator.SetSpeedXZ(Input.SpeedXZ, Time.deltaTime);
            ...
        }

        ...

        public void ApplyRootMotionTranslation()
        {
            _cumulativeTranslation += Animator.ProcessRootMotionTranslation();
        }
    }
}
</code></pre><p>The last change is for our Moving state:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class MovingState : State&lt;ILocomotionContext&gt;
    {
        public MovingState(StateMachine&lt;ILocomotionContext&gt; stateMachine) : base(stateMachine)
        {
            StateMachine.Context.Animator.CrossFadeInFixedTime(Animator.StringToHash("Moving"), 0.1f);
        }

        public override void Update()
        {
            StateMachine.Context.ApplyRootMotionTranslation();
            StateMachine.Context.ApplyInputRotationY(10f);
        }
    }
}
</code></pre><p>In the constructor we can cross fade to our Moving animation state and in <strong>Update</strong> we replaced our <strong>ApplyInputTranslationXZ</strong> with an alternative method call to <strong>ApplyRootMotionTranslation</strong> instead. Root motion could potentially affect our Y translation but because we baked the original root transform position (Y) in the animation clips that won’t be the case. We could implement something similar for root motion rotation but our animations don’t have any rotation so we aren’t concerned with that right now either.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="150" src="https://www.youtube.com/embed/cVwxaFXDzBc?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="Unity animated character controller (2/5)"/></figure><p>If we run the game now we will have animated movement and be able to see the blend tree in effect when we press the run key (left shift) / button (gamepad west). We still move forward slowly even without any input but we will address this in the next section when adding additional states such as Idle.</p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="hierarchy-amp-sub-states">Hierarchy &amp; sub-states</h2></div><h3 id="transitioning-between-states">Transitioning between states</h3><p>We are at a point where we can start to see the benefit of our state machine now. We are going to add our second state which will be Idle and implement conditions for transitioning between the Moving and Idle states. This will prevent our character from moving when we don’t have any controller input. Idle is typically a more sensible starting state too, so we will update that at the same time.</p><p>We’ll need to download an Idle animation from Mixamo. Follow the same steps from earlier for an Idle animation and update the Import settings to use a humanoid rig with our avatar and bake the original rotation, and position across XZ and Y axis. Make sure you name the animation Idle and drag it into the Animator to create an animation state of the same name for that motion. Update the layer to use Idle as our default state and we are ready to make our script updates.</p><p>We will start by creating our new Idle state:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class IdleState : State&lt;ILocomotionContext&gt;
    {
        public IdleState(StateMachine&lt;ILocomotionContext&gt; stateMachine) : base(stateMachine)
        {
            StateMachine.Context.Animator.CrossFadeInFixedTime(Animator.StringToHash("Idle"), 0.1f);
        }

        public override void Update()
        {
            if (StateMachine.Context.Input.DirectionXZ.magnitude &gt; 0f)
            {
                StateMachine.ChangeState&lt;MovingState&gt;();
            }
        }
    }
}</code></pre><p>This is very similar to our moving state but we don’t call any methods to rotate or translate our character while Idle. Instead, we check for input magnitude and conditionally change to a Moving state. We can update our Moving state to include an inverse condition to conditionally change to an Idle state if we don’t have any input magnitude:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class MovingState : State&lt;ILocomotionContext&gt;
    {
        ...

        public override void Update()
        {
            ...

            if (StateMachine.Context.Input.DirectionXZ.magnitude == 0)
            {
                StateMachine.ChangeState&lt;IdleState&gt;();
            }
        }
    }
}</code></pre><p>We can also optionally update our <code>LocomotionController</code> to use <code>IdleState</code> for the initial state change but these conditions would ensure that we quickly change to the correct state regardless of which starting state we choose.</p><h3 id="grounded-state-and-sub-states">Grounded state and sub-states</h3><p>We are now able to navigate our horizontal XZ plane but have not yet introduced the concept of gravity to ensure that our character remains grounded. If we remove the floor we continue to move horizontally but that’s not acceptable. Before we can introduce gravity we are going to create a Grounded state that encapsulates already being on the ground and we’ll move our Idle and Moving states to a sub-state machine within our Grounded state. This will allow us to avoid duplicating any logic that applies to both Idle and Moving and putting it in our Grounded state instead.</p><pre><code class="language-csharp">using DEPT.Unity;

public class GroundedState : State&lt;ILocomotionContext&gt;
{
    private readonly StateMachine&lt;ILocomotionContext&gt; _subStateMachine;

    public GroundedState(StateMachine&lt;ILocomotionContext&gt; stateMachine) : base(stateMachine)
    {
        _subStateMachine = new StateMachine&lt;ILocomotionContext&gt;(StateMachine.Context);

        if (StateMachine.Context.Input.DirectionXZ.magnitude &gt; 0f)
        {
            _subStateMachine.ChangeState&lt;MovingState&gt;();
        }
        else
        {
            _subStateMachine.ChangeState&lt;IdleState&gt;();
        }
    }

    public override void Update()
    {
        _subStateMachine.Update();
    }

    public override void Dispose()
    {
        _subStateMachine.Dispose();

        base.Dispose();
    }
}</code></pre><p>This Grounded state doesn’t map to an animation state. It’s only concerned with constructing a sub-state machine and conditionally setting its starting state to either Idle or Moving based on the input direction magnitude. It updates the sub-state machine every time it updates itself, and disposes the sub-state machine when it is disposed of. We can update our <code>LocomotionController</code> to use <code>GroundedState</code> as the initial state it changes to and everything continues to work the same way as before, but now we are ready to introduce another new state to represent our character Falling.</p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="going-airborne">Going airborne</h2></div><h3 id="velocity-gravity">Velocity &amp; Gravity</h3><p>Before we can fall we are going to need to be able to translate our character based on the effect of gravity. Gravity is usually approximated as an acceleration of 9.81m/s/s which means that every second the velocity on the -Y axis should increase by 9.81m/s which in turn means that every second the translation should changes based on the cumulative velocity produced by gravitational acceleration. Most games also include a multiplier so that gravity is higher than on Earth’s surface because this combined with a less realistic higher jump velocity produces that arcade feel.</p><p>We’re going to need to update our <code>LocomotionController</code> to support cumulative velocity and handle the gravitational acceleration. Let’s start with our <code>ILocomotionContext</code> interface:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public interface ILocomotionContext
    {
        ...
        public bool IsGrounded { get; }
        public Vector3 CumulativeVelocity { get; }
        ...
    }
}
</code></pre><p>Now we can update our <code>LocomotionController</code> with some additional properties:</p><pre><code class="language-csharp">private float _initialStepOffset;
private Vector3 _gravity = Physics.gravity;

[Range(0.1f, 5f)] public float GravityMultiplier = 2f;
[Range(0f, 100f)] public float TerminalVelocity = 50f;

public bool IsGrounded =&gt; _characterController.isGrounded;
public Vector3 CumulativeVelocity { get; private set; }</code></pre><p>Add a method to handle the velocity:</p><pre><code class="language-csharp">private void ApplyVelocity()
{
    if (IsGrounded &amp;&amp; CumulativeVelocity.y &lt; 0f)
    {
        CumulativeVelocity = new Vector3(CumulativeVelocity.x, -_initialStepOffset, CumulativeVelocity.z);
    }
    else
    {
        CumulativeVelocity = Vector3.ClampMagnitude(CumulativeVelocity + (_gravity * GravityMultiplier * Time.deltaTime), TerminalVelocity);
    }

    _cumulativeTranslation += CumulativeVelocity * Time.deltaTime;
}</code></pre><p>When we are grounded and not moving upwards (+Y) we won’t apply gravity but will gently push our player into the ground using <code>-_initialStepOffset</code>. We do this to ensure that our grounded collisions don’t suffer from the physics engine trying to resolve a collision and attempting to move our character just above the ground. Without this we could alternate between being Grounded and Falling every tick. If we aren’t grounded, we apply gravitational acceleration to our cumulative velocity but clamp to a terminal velocity to ensure there is a sensible maximum limit applied to our locomotion. We can set the value of <code>_initialStepOffset</code> in our <strong>Start</strong> method using our <code>_characterController.stepOffset</code>.</p><p>We need to apply these changes in our <strong>Update</strong> method:</p><pre><code class="language-csharp">public void Update()
{
    Animator.SetSpeedXZ(Input.SpeedXZ, Time.deltaTime);
    Animator.SetSpeedY(CumulativeVelocity.y / TerminalVelocity, Time.deltaTime);
    
    _stateMachine.Update();

    ApplyVelocity();
    
    Flags = _characterController.Move(_cumulativeTranslation);

    _cumulativeTranslation = Vector3.zero;
}</code></pre><p>You might have noticed that we are also setting the <strong>SpeedY</strong> parameter for our <code>LocomotionAnimator</code> so let’s update that next using the same approach as we already have for <strong>SpeedXZ</strong>.</p><h3 id="animating-our-descent">Animating our descent</h3><p>With the code in place we can create some animations for falling and landing along with equivalent states in our state machine. From <a href="https://www.mixamo.com/?ref=dept-engineering-blog.ghost.io">mixamo.com</a>, download animations for Falling Idle (renamed to Falling) and Falling to Landing (renamed to Landing) and Falling  (renamed to Freefall). Update the import settings to use our Humanoid rig and avatar, and bake original rotation and position for all animations. Falling Idle and Falling can Loop Time also. Create a new 1D Blend Tree in your Animator called Falling and add motions for Falling and Freefall. Associate it with a new float property called SpeedY. It should look something like this:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/one-dimensional-blend-tree.png" class="kg-image" alt="Creating a Unity animated character controller with C# best practices in mind" loading="lazy" width="745" height="477" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/one-dimensional-blend-tree.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/one-dimensional-blend-tree.png 745w" sizes="(min-width: 720px) 720px"><figcaption>One dimensional blend tree for vertical locomotion</figcaption></img></figure><p/><p>Also drag your Landing animation into the Animator base layer to create a Landing animation state too. With the animation clips in place, let’s create our states:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class LandingState : State&lt;ILocomotionContext&gt;
    {
        public LandingState(StateMachine&lt;ILocomotionContext&gt; stateMachine) : base(stateMachine)
        {
            StateMachine.Context.Animator.CrossFadeInFixedTime(Animator.StringToHash("Landing"));
        }

        public override void Update()
        {
            if (StateMachine.ElapsedTime &gt; 0.5f - StateMachine.Context.Input.SpeedXZ)
            {
                StateMachine.ChangeState&lt;GroundedState&gt;();
            }
        }
    }
}</code></pre><p>This Landing state is really simple and just waits up to a maximum of 0.5 seconds before transitioning to our Grounded state. The faster we are moving horizontally, the less time we will spend in a Landing state. This state could also include a condition to immediately switch to a Falling state if we are no longer grounded to cater for cases where we only land momentarily but that’s a corner case we don’t need to handle right now.</p><p>Next is our Falling state:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class FallingState : State&lt;ILocomotionContext&gt;
    {
        public FallingState(StateMachine&lt;ILocomotionContext&gt; stateMachine) : base(stateMachine)
        {
            StateMachine.Context.Animator.CrossFadeInFixedTime(Animator.StringToHash("Falling"), 0.1f);
        }

        public override void Update()
        {
            StateMachine.Context.ApplyInputTranslationXZ(5f);

            if (StateMachine.Context.IsGrounded)
            {
                StateMachine.ChangeState&lt;LandingState&gt;();
            }
        }
    }
}</code></pre><p>Our <strong>Update</strong> method simply enables horizontal translation while falling and checks if we are grounded to conditionally change to the Landing state. We can adjust the multiplier for our movement if we want to be able to move less while airborne.</p><p>We now can change from Falling to Landing, and Landing to Grounded but we need to update our Grounded state to conditionally change to Falling before we can test this. In the GroundedState add the following condition to the end of our <strong>Update</strong> method:</p><pre><code class="language-csharp">if(!StateMachine.Context.IsGrounded)
{
    StateMachine.ChangeState&lt;FallingState&gt;();
}</code></pre><p>If we run the game now, we should see our animated states in effect including a freefall animation as we approach terminal velocity.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="150" src="https://www.youtube.com/embed/Mo86Ylypfo4?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="Freefall animation"/></figure><h3 id="jumping">Jumping</h3><p>Since the hardest part of a jump is returning to the ground, adding a Jump state is pretty trivial. We’ll need to download another <a href="https://www.mixamo.com/?ref=dept-engineering-blog.ghost.io">mixamo.com</a> animation and import it in the usual way, baking the rotation and position for our downloaded animation clip. Dragging that into our Animator will create the animation state, and then we can script our Jump.</p><p>Add a float JumpHeight property defaulting to 2f along with a Jump method to our LocomotionController:</p><pre><code class="language-csharp">public void ApplyJump()
{
    var jumpVelocity = new Vector3(0f, Mathf.Sqrt(JumpHeight * -3f * _gravity.y), 0f);

    ApplyVelocity(jumpVelocity);
}</code></pre><p>The jump velocity approximates the velocity necessary to overcome gravity and reach the defined jump height. Make sure to update <code>ILocomotionContext</code> to include this method so that we can call it from our states. With that in place, we can create our Jumping state:</p><pre><code class="language-csharp">using UnityEngine;

namespace DEPT.Unity
{
    public class JumpingState : State&lt;ILocomotionContext&gt;
    {
        public JumpingState(StateMachine&lt;ILocomotionContext&gt; stateMachine) : base(stateMachine)
        {
            StateMachine.Context.Animator.CrossFadeInFixedTime(Animator.StringToHash("Jumping"), 0.1f);
            
            StateMachine.Context.ApplyJump();
        }

        public override void Update()
        {
            StateMachine.Context.ApplyInputTranslationXZ(5f);

            if (StateMachine.Context.CumulativeVelocity.y &lt; 0f)
            {
                StateMachine.ChangeState&lt;FallingState&gt;();
            }
        }
    }
}</code></pre><p>We call the <strong>Jump</strong> method from the constructor and our <strong>Update</strong> method enables horizontal movement and checks when we reach the peak of our jump to transition to a Falling state.</p><p>We only want to be able to jump from a Grounded state, so update our GroundedState <strong>Update</strong> method to include the following condition:</p><pre><code class="language-csharp">public override void Update()
{
    ...

    if (StateMachine.Context.Input.Jump)
    {
        StateMachine.ChangeState&lt;JumpingState&gt;();
    }
}</code></pre><p>If we test the jump now we’ll notice that the character jumps immediately but the animation crouches first giving an undesirable visual. We could use a coroutine to delay the change in velocity but I prefer an immediate jump so instead I will trim the animation clip to begin as the characters feet should leave the ground.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/animation-timeline.png" class="kg-image" alt="Creating a Unity animated character controller with C# best practices in mind" loading="lazy" width="742" height="121" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/08/animation-timeline.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/animation-timeline.png 742w" sizes="(min-width: 720px) 720px"><figcaption>Animation timeline for trimming keyframes</figcaption></img></figure><p/><p>For the animation clip I chose, trimming the first 14.5ms is optimal. The last visual defect is a noticeable tilt as we transition from jumping to falling animation clips. This is because the falling animation start position and rotation is different to the end of the jumping animation end position and rotation. We can duplicate and edit our falling animation clip to fix this in Unity. Instead of covering that here, you can take a look at this video tutorial which is part of an excellent channel and series that covers Unity character controllers: <a href="https://youtu.be/hfBdtkC-nLg?ref=dept-engineering-blog.ghost.io">https://youtu.be/hfBdtkC-nLg</a>.</p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="summary">Summary</h2></div><p>In the interest of brevity, there are some additional things we would typically implement that have been omitted from this tutorial. Some obvious next steps would include:</p><ul><li>Projecting our movement onto a ground plane using its normal (the vector describing the orientation of a surface). This means that if we descend a slope we’ll bounce down as we alternate between falling and grounded states. By casting a ray down and getting the normal of the ground we can orient our movement along the ground to avoid this.</li><li>Implement a better way to navigate stairs. The character controller we used supports <code>stepOffset</code> already but it’s a very crude implementation that we would normally enhance for a less jerky motion.</li><li>Replace the use of <code>Activator.CreateInstance</code> in our <code>StateMachine</code> with an injected factory.</li></ul><p>The purpose of the tutorial was to demonstrate the value of a state machine in the context of Unity character controllers and hopefully the benefit of this pattern was self-evident as we added more states to our character. We also demonstrated a structure that embraced SOLID principles so that we could change implementations like <code>ILocomotionInput</code> to reuse our character controller for other game objects like an NPC.</p><p>If you’re interested in learning more about DEPT’s capabilities with Unity for Gaming, XR, and experience development, or C# .NET in general, please reach out to us to discuss more.</p>]]></content:encoded></item><item><title><![CDATA[Terragrunt vs Terraform]]></title><description><![CDATA[In this article, I detail the benefits of using Terragrunt. At the end, I list the reasons why sometimes Terragrunt may not be the right choice for your project.]]></description><link>https://engineering.deptagency.com/terraform-vs-terragrunt/</link><guid isPermaLink="false">64a1071f5a20ff0001ca72aa</guid><category><![CDATA[Terraform]]></category><dc:creator><![CDATA[Jirawat Uttayaya]]></dc:creator><pubDate>Tue, 15 Aug 2023 13:54:45 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Screenshot-2023-08-08-at-5.00.22-PM-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/Screenshot-2023-08-08-at-5.00.22-PM-1.png" alt="Terragrunt vs Terraform"/><p><a href="https://terragrunt.gruntwork.io/?ref=dept-engineering-blog.ghost.io">Terragrunt</a> is a wrapper on top of <a href="https://www.terraform.io/?ref=dept-engineering-blog.ghost.io">Terraform</a> that fixes some of Terraform's deficiencies. When you use Terragrunt, you are running Terraform behind the scenes. </p><p>In this article, I detail the benefits of using Terragrunt. At the end, I list the reasons why sometimes Terragrunt may not be the right choice for your project.</p><p>Intended Audience: Technical team members with some familiarity with Terraform and AWS.</p><h2 id="why-use-terragrunt">Why use Terragrunt?</h2><p>The two main benefits of Terragrunt are:‌</p><ul><li>DRY clean code</li><li>Ease of use</li></ul><h3 id="preamble">Preamble</h3><p>Directly from HashiCorp's Terraform best practices, you should <a href="https://learn.hashicorp.com/tutorials/terraform/organize-configuration?ref=dept-engineering-blog.ghost.io"><strong><strong>avoid a monolithic configuration</strong></strong></a><strong><strong> </strong></strong>and<strong><strong> <a href="https://www.terraform.io/cloud-docs/guides/recommended-practices/part1?ref=dept-engineering-blog.ghost.io">eschew a large Terraform state file</a></strong> . </strong>In summary, if you put all your cloud resources like EC2, S3, RDS, etc. into Terraform files under one big directory, modifying a S3 bucket will slowly scan and potentially modify other AWS resources in that directory.</p><p>Seasoned Terraform users will group the TF configuration files into its own directory, such as VPC, IAM, EC2 or Frontend-UI, Backend-Service, Databases. In practice, this can lead to many duplicate configuration and variables.</p><h3 id="dry-clean-code">DRY clean code</h3><p>A fundamental tenet of clean code is <strong><a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself?ref=dept-engineering-blog.ghost.io">Don't Repeat Yourself</a> (DRY)</strong>. If you duplicate code in more than one place, you must change the code in multiple places when doing fixes and enhancements. When you don't change the code in all the duplicated places, bugs occur.</p><ul><li><strong>DRY Input Variables</strong><br>Terragrunt<br>You can put all your variables in one file and reuse it via the <em><a href="https://terragrunt.gruntwork.io/docs/features/inputs/?ref=dept-engineering-blog.ghost.io"><code>inputs</code></a></em> block.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terragrunt/dev/env.hcl?ref=dept-engineering-blog.ghost.io">env.hcl</a><br><br>Terraform<br>You are forced to duplicate your input variables in multiple tfvars files.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/vpc/terraform.tfvars?ref=dept-engineering-blog.ghost.io">vpc/terraform.tfvars</a><br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/s3/terraform.tfvars?ref=dept-engineering-blog.ghost.io">s3/terraform.tfvars</a><br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/iam/terraform.tfvars?ref=dept-engineering-blog.ghost.io">iam/terraform.tfvars</a><br/></br></br></br></br></br></br></br></br></br></br></br></li><li><strong>DRY Output Variables</strong><br>Terragrunt<br>Output values are <strong>automatically generated</strong> from the Terraform module output variables.<br><br>Terraform<br>There are multiple outputs.tf files.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/vpc/outputs.tf?ref=dept-engineering-blog.ghost.io">vpc/outputs.tf</a><br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/s3/outputs.tf?ref=dept-engineering-blog.ghost.io">s3/outputs.tf</a><br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/iam/outputs.tf?ref=dept-engineering-blog.ghost.io">iam/outputs.tf</a><br/></br></br></br></br></br></br></br></br></br></li><li><strong>DRY remote state backend</strong><br>Terragrunt<br>The <a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/?ref=dept-engineering-blog.ghost.io#remote_state"><code><em>remote_state</em></code></a> block dynamically generates the Terragrunt remote state configuration in the parent terragrunt.hcl file.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/65000d6c66fd87a925c13354992b25dfd2bd2325/soapbox/terragrunt/dev/terragrunt.hcl?ref=dept-engineering-blog.ghost.io#L33">terragrunt.hcl</a><br><br>Terraform<br>The Terraform remote state configuration is defined via the <a href="https://www.terraform.io/language/settings/backends/configuration?ref=dept-engineering-blog.ghost.io"><code><em>backend</em></code></a> block. Even worse, Terraform does not allow the use of variables in the backend block so you have to explicitly hard code everything. You must create multiple backend configuration files with nearly identical configuration.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/b88bf75b2a6aee185b2e3afcd2669f924a3d7903/soapbox/terraform/dev/vpc/providers.tf?ref=dept-engineering-blog.ghost.io#L27">vpc/providers.tf</a><br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/b88bf75b2a6aee185b2e3afcd2669f924a3d7903/soapbox/terraform/dev/s3/providers.tf?ref=dept-engineering-blog.ghost.io#L27">s3/providers.tf</a><br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/b88bf75b2a6aee185b2e3afcd2669f924a3d7903/soapbox/terraform/dev/iam/providers.tf?ref=dept-engineering-blog.ghost.io#L27">iam/providers.tf</a><br/></br></br></br></br></br></br></br></br></br></br></br></li><li><strong>DRY CLI arguments</strong><br>Terragrunt<br>You can define required Terraform CLI arguments via the <code><a href="https://terragrunt.gruntwork.io/docs/features/keep-your-cli-flags-dry/?ref=dept-engineering-blog.ghost.io"><em>extra_arguments</em></a></code> block in one central terragrunt.hcl.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/65000d6c66fd87a925c13354992b25dfd2bd2325/soapbox/terragrunt/dev/terragrunt.hcl?ref=dept-engineering-blog.ghost.io#L105">terragrunt.hcl</a><br><br>Terraform<br>If you want to always run the <code>terraform</code> command line with certain required arguments, you would have to hack a custom script.</br></br></br></br></br></br></br></li></ul><h3 id="ease-of-use">Ease of use</h3><p>Terragrunt helps using Terraform more convenient for the operators.</p><ul><li><strong>run-all</strong><br>Terragrunt<br>This is the killer feature of Terragrunt.<br>The primary issue with multiple Terraform state stacks is running it in the right order. For example, you need to apply the VPC stack before the EC2 stack.<br><br>You can explicitly define the correct order to apply the stacks via<code><a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/?ref=dept-engineering-blog.ghost.io#dependencies"><em>dependencies</em></a></code>. Then executing <code>terragrunt run-all apply</code> will execute the resource creation in the correct order.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/b88bf75b2a6aee185b2e3afcd2669f924a3d7903/soapbox/terragrunt/_envcommon/app_ec2.hcl?ref=dept-engineering-blog.ghost.io#L74">envcommon/app_ec2.hcl</a><br><br>Terraform<br>To apply the Terraform stacks in the proper order, you would have to code and maintain your own script.<br>Example script<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/terraform-run-all.sh?ref=dept-engineering-blog.ghost.io">terraform.run-all.sh</a><br/></br></br></br></br></br></br></br></br></br></br></br></br></li><li><strong>Before/After/Error hooks</strong><br>Terragrunt<br>You may want to run an external program like Slack before and after running Terragrunt. This is accomplished via the <a href="https://terragrunt.gruntwork.io/docs/features/hooks/?ref=dept-engineering-blog.ghost.io"><code><em>before_hook</em></code>, <code><em>after_hook</em></code>, and <code><em>error_hook</em></code></a><em>.</em><br>Exanple config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/65000d6c66fd87a925c13354992b25dfd2bd2325/soapbox/terragrunt/dev/terragrunt.hcl?ref=dept-engineering-blog.ghost.io#L118">terragrunt.hcl</a><br> <br>Terraform<br>To accomplish the above hooks in Terraform, it would be a custom wrapper script. <br/></br></br></br></br></br></br></br></li><li><strong>Auto-create remote state resource</strong><br>Terragrunt<br>If the remote state backend store (S3 and DynamoDB in AWS) does not exist, Terragrunt will automatically create it for you with the correct security settings.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/65000d6c66fd87a925c13354992b25dfd2bd2325/soapbox/terragrunt/dev/terragrunt.hcl?ref=dept-engineering-blog.ghost.io#L33">terragrunt.hcl</a><br><br>Terraform<br>Before you can start using Terraform, you either have to create the remote state backing store manually or create the store via Terraform using local state then migrate.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/main/soapbox/terraform/dev/terraform_infra/main.tf?ref=dept-engineering-blog.ghost.io">terraform_infra/main.tf</a><br/></br></br></br></br></br></br></br></br></br></li><li><strong>Auto-retry</strong><br>Terragrunt<br>Cloud providers can be finicky or slow.  You can configure Terragrunt to automatically retry after a failure with the <code><a href="https://terragrunt.gruntwork.io/docs/features/auto-retry/?ref=dept-engineering-blog.ghost.io">retry_max_attempts</a></code> setting.<br>Example config<br> <a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/blob/65000d6c66fd87a925c13354992b25dfd2bd2325/soapbox/terragrunt/dev/terragrunt.hcl?ref=dept-engineering-blog.ghost.io#L153">terragrunt.hcl</a><br><br>Terraform<br>Terraform has no built-in auto-retry setting.<br/></br></br></br></br></br></br></br></li><li><strong>Auto-init</strong><br>Terragrunt<br>It's a small annoyance, but in Terraform you must manually execute <code>terraform init</code>  once on a new stack before you can <code>terraform plan/apply</code>.  Terragrunt is smart enough to recognize that an init is needed during the plan/apply phase and will automatically execute it for you.<br/></br></br></li></ul><h2 id="why-not-to-use-terragrunt">Why not to use Terragrunt</h2><h3 id="yet-another-tool">Yet another tool</h3><p>DevOps already has a lot of tools to learn. If your team is already overwhelmed, it may not be the right time to introduce yet another tool.</p><h3 id="terraform-cloud">Terraform cloud</h3><p>Terragrunt does not work well with <a href="https://developer.hashicorp.com/terraform/cloud-docs?ref=dept-engineering-blog.ghost.io">Terraform Cloud</a>. Terraform Cloud competes with Terragrunt, offering many of the same clean code and ease of use features.</p><h3 id="large-terraform-codebase">Large Terraform codebase</h3><p>Refactoring a large Terraform codebase into Terragrunt is a pain, involving many tedious <code><em><a href="https://developer.hashicorp.com/terraform/cli/import?ref=dept-engineering-blog.ghost.io">terraform import</a></em></code><em>. </em>It's more prudent to introduce Terragrunt on a new project codebase instead.</p><h2 id="final-thoughts-on-terragrunt-vs-terraform">Final thoughts on Terragrunt vs Terraform</h2><p>I enjoy using Terragrunt on my projects and I wholeheartedly recommend it. Terragrunt offers so many additional benefits on top of the already wonderful Terraform.</p><p>If you enjoyed this article, come back next week for my article on "How to layout your Terragrunt project."</p><p>A preview can be found at GitHub<br><a href="https://github.com/deptagency/engineering-blog-terraform-terragrunt/tree/main/soapbox/terragrunt?ref=dept-engineering-blog.ghost.io">https://github.com/deptagency/engineering-blog-terraform-terragrunt/tree/main/soapbox/terragrunt</a></br></p><p/><p> </p>]]></content:encoded></item><item><title><![CDATA[DevOps Quick Fix: intermittent yum and apt errors on new EC2]]></title><description><![CDATA[DevOps Quick Fix are solutions to common DevOps problems, usually encountered during client engagements


Problem

When creating a new AWS EC2 (either via Packer for AMI or via Terraform for EC2), the usual next immediate step is to install Unix packages via yum or apt. You may get intermittent errors like:

"Package has no installation candidate"
"Unable to locate package"
"Error: Unable to find a match"
"No package available"


Solution

Running yum or apt before EC2 finishes initialization is]]></description><link>https://engineering.deptagency.com/devops-fix-intermittent-yum-and-apt-errors-on-new-ec2/</link><guid isPermaLink="false">625442291883db003d8128e6</guid><category><![CDATA[DevOps]]></category><dc:creator><![CDATA[Jirawat Uttayaya]]></dc:creator><pubDate>Thu, 10 Aug 2023 16:16:07 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/AdobeStock_315165290.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/08/AdobeStock_315165290.jpeg" alt="DevOps Quick Fix: intermittent yum and apt errors on new EC2"/><p><em>DevOps Quick Fix are solutions to common DevOps problems, usually encountered during client engagements</em></p><h3 id="problem">Problem</h3><p>When creating a new AWS EC2 (either via Packer for AMI or via Terraform for EC2), the usual next immediate step is to install Unix packages via yum or apt. You may get intermittent errors like:</p><p><strong>"Package has no installation candidate"</strong><br><strong>"Unable to locate package"</strong><br><strong>"Error: Unable to find a match"</strong><br><strong>"No package available"</strong></br></br></br></p><h3 id="solution">Solution</h3><p>Running yum or apt before EC2 finishes initialization is the cause of the problem.</p><p>You can check when <a href="https://cloudinit.readthedocs.io/en/latest/topics/faq.html?ref=dept-engineering-blog.ghost.io">cloud-init</a> finishes EC2 initialization by looking for the file <strong>/var/lib/cloud/instance/boot-finished</strong> .</p><p>The <strong>Bash</strong> solution is:</p><pre><code class="language-bash">while [ ! -f  /var/lib/cloud/instance/boot-finished ];
do
    echo "Waiting for cloud-init to finish ..."
    sleep 2
done

apt-get update -y
apt-get install -y package
</code></pre><p>The <strong>Ansible</strong> solution is:</p><pre><code class="language-yaml">- name: Wait until cloud-init finishes
  wait_for:
    path: /var/lib/cloud/instance/boot-finished
    sleep: 2
    timeout: 300
    
    
- name: Update all existing yum packages
  yum:
    name: '*'
    state: latest
    </code></pre>]]></content:encoded></item><item><title><![CDATA[Picture this: Using visual testing in quality assurance]]></title><description><![CDATA[In this article we will discuss visual testing in detail, including its importance, how it works, its benefits, and challenges.]]></description><link>https://engineering.deptagency.com/using-visual-testing-in-quality-assurance/</link><guid isPermaLink="false">64621b8a2ff13b0001a0f641</guid><category><![CDATA[QA]]></category><dc:creator><![CDATA[Ron Grootaarts]]></dc:creator><pubDate>Tue, 18 Jul 2023 13:04:40 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1586769852836-bc069f19e1b6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fG1hZ25pZmllcnxlbnwwfHx8fDE2ODQxNTE5MDF8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1586769852836-bc069f19e1b6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fG1hZ25pZmllcnxlbnwwfHx8fDE2ODQxNTE5MDF8MA&ixlib=rb-4.0.3&q=80&w=2000" alt="Picture this: Using visual testing in quality assurance"/><p>Visual testing is a software testing technique that focuses on visually examining the appearance and functionality of applications and websites. It is essential in ensuring that software meets user expectations and requirements, particularly those with user interfaces.</p><p>Visual testing can be performed manually or automated. Manual visual testing involves a tester manually inspecting the software for visual issues. This method is effective in detecting subtle visual discrepancies that can affect the user experience. However, it can be time-consuming and tedious, especially for larger <a href="https://www.deptagency.com/service/engineering/web-mobile-app-engineering/?ref=dept-engineering-blog.ghost.io">applications</a> and websites.</p><p>Automated visual testing uses software tools to detect and report visual discrepancies. This method is faster than manual testing, more efficient, and more accurate. It is especially useful for testing complex web applications with various user interfaces.</p><p>In this article, we will discuss visual testing in detail, including its importance, how it works, its benefits, and challenges.</p><h2 id="importance-of-visual-testing">Importance of visual testing</h2><p>Visual testing is essential because it ensures that software meets user requirements and expectations. Software with a poor user interface can be frustrating and challenging to use, leading to decreased user engagement and loss of revenue. Visual testing helps identify and resolve issues that may affect the user experience.</p><p>Visual testing also ensures that software complies with design standards and guidelines. When <a href="https://www.deptagency.com/service/experience/product-design/?ref=dept-engineering-blog.ghost.io">designing applications and websites</a>, developers follow specific design standards to ensure consistency across different platforms and devices. Visual testing ensures that software adheres to these design standards and guidelines.</p><p>Visual testing helps identify issues early in the software development life cycle. Detecting and resolving issues early saves time and money in the long run. Issues identified later in the software development cycle can be costly and time-consuming to fix.</p><h2 id="how-visual-testing-works">How visual testing works</h2><p>Visual testing involves visually examining the appearance and functionality of software. This process involves three critical steps:</p><ol><li>Capture baseline images: The first step involves capturing a set of baseline images that will be used for comparison during subsequent tests. These images serve as the expected result for the software being tested.</li><li>Test the software: The next step involves testing the software. During testing, new images are captured, and these are compared to the baseline images. Any differences between the two sets of images are reported as defects.</li><li>Review and validate defects: The final step involves reviewing and validating defects. The defects identified during testing are reviewed and validated to determine their severity and prioritize them for resolution.</li></ol><p>Visual testing can be done using either manual or automated techniques. Manual visual testing involves a tester manually inspecting the software for visual issues. The tester compares screenshots of the software to the expected results and reports any discrepancies. Automated visual testing involves using software tools that use machine vision to compare images and report discrepancies.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/07/visual_testing_example_with_percy.png" class="kg-image" alt="Picture this: Using visual testing in quality assurance" loading="lazy" width="2000" height="645" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/07/visual_testing_example_with_percy.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/07/visual_testing_example_with_percy.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/07/visual_testing_example_with_percy.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w2400/2023/07/visual_testing_example_with_percy.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Oops, the log out icon was replaced by accident</figcaption></img></figure><h2 id="tools-for-visual-testing">Tools for visual testing</h2><ol><li>Selenium is a popular open-source framework for testing web applications. It includes a visual testing component called Selenium WebDriver, which can be used to capture and compare screenshots of web pages.</li><li>Applitools is a cloud-based visual testing platform that uses artificial intelligence to detect and highlight visual differences between baseline and current images. It offers integrations with popular test automation frameworks, including Selenium and Cypress.</li><li>TestComplete is a functional testing tool that includes a visual testing component called Visual Check. It can capture screenshots of web and desktop applications and compare them to baseline images.</li><li>Cypress is a modern test automation framework for web applications. It includes a visual testing component called Cypress Image Snapshot, which can capture and compare screenshots of web pages.</li><li>Percy provides a visual testing platform that can be integrated with a range of test automation frameworks and CI/CD pipelines. It captures screenshots of web pages and compares them against a baseline to detect visual changes. Percy also offers advanced features such as smart visual diffing, pixel-to-pixel comparison, and support for dynamic content. It can be used for testing web applications, mobile apps, and even design files.</li></ol><h2 id="benefits-of-visual-testing">Benefits of visual testing</h2><p>Visual testing offers several benefits, including:</p><ol><li>Improved user experience: Visual testing helps ensure that software appears and functions correctly, leading to an improved user experience.</li><li>Faster testing: Visual testing can be faster than manual testing, particularly when it is automated.</li><li>Increased test coverage: Visual testing can cover a broader range of issues than manual testing alone, as it can detect issues that might be missed by a human tester.</li><li>Reduced costs: By detecting issues early in the development process, visual testing can help to reduce costs associated with fixing issues later on.</li><li>Improved quality: Visual testing helps ensure that software meets design standards and guidelines, improving software quality.</li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/07/missing_filters_found_fast_with_visual_testing.png" class="kg-image" alt="Picture this: Using visual testing in quality assurance" loading="lazy" width="1904" height="1034" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/07/missing_filters_found_fast_with_visual_testing.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/07/missing_filters_found_fast_with_visual_testing.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/07/missing_filters_found_fast_with_visual_testing.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/07/missing_filters_found_fast_with_visual_testing.png 1904w" sizes="(min-width: 720px) 720px"><figcaption>Find missing elements fast</figcaption></img></figure><h2 id="challenges-of-visual-testing">Challenges of visual testing</h2><p>Visual testing can be challenging, particularly when using automated techniques. Some of the challenges of visual testing include:</p><ol><li>False positives: Automated visual testing can sometimes report false positives, which are defects that are not actual defects. These false positives can be time-consuming to review and can lead to a loss of confidence in the testing process.</li><li>Dynamic content: Visual testing can be challenging for software with dynamic content, such as web applications that change frequently. In such cases, the baseline images may not match.</li><li>Browser compatibility: Different browsers can render web pages differently, making it challenging to create accurate baseline images for testing.</li><li>Localization: Visual testing can be challenging for applications that are localized in different languages. This is because the text and layout of the software can change depending on the language, making it difficult to create accurate baseline images.</li><li>Maintenance: Creating and maintaining accurate baseline images can be time-consuming and resource-intensive, particularly for larger applications and websites.</li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/07/visual_testing_with_dynamic_content.png" class="kg-image" alt="Picture this: Using visual testing in quality assurance" loading="lazy" width="1874" height="1006" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/07/visual_testing_with_dynamic_content.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/07/visual_testing_with_dynamic_content.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/07/visual_testing_with_dynamic_content.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/07/visual_testing_with_dynamic_content.png 1874w" sizes="(min-width: 720px) 720px"><figcaption>Dynamic content should be avoided or ignored in visual testing</figcaption></img></figure><h2 id="conclusion">Conclusion</h2><p>Visual testing is an essential testing technique for ensuring that software meets user requirements and expectations. It involves visually examining the appearance and functionality of software and can be performed manually or automated. Visual testing offers several benefits, including improved user experience, faster testing, increased test coverage, reduced costs, and improved quality. However, it can also be challenging, particularly when using automated techniques. To overcome these challenges, testers must carefully plan and execute their visual testing efforts.</p>]]></content:encoded></item><item><title><![CDATA[Optimizing the use of Emotion in React]]></title><description><![CDATA[Using Emotion the way they don't tell you to.]]></description><link>https://engineering.deptagency.com/optimizing-emotion-in-react/</link><guid isPermaLink="false">64a72c1398c1050001f93a13</guid><category><![CDATA[Process]]></category><category><![CDATA[react]]></category><dc:creator><![CDATA[Ethan Standel]]></dc:creator><pubDate>Mon, 10 Jul 2023 16:13:31 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1564951415682-4466397f7707?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fHBpbmslMjBibHVlfGVufDB8fHx8MTY4ODY3OTY0M3ww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h2 id="what-is-emotion">What is Emotion?</h2>
<img src="https://images.unsplash.com/photo-1564951415682-4466397f7707?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fHBpbmslMjBibHVlfGVufDB8fHx8MTY4ODY3OTY0M3ww&ixlib=rb-4.0.3&q=80&w=2000" alt="Optimizing the use of Emotion in React"/><p>Emotion is a series of NPM packages that allow for extensible CSS-in-JS dynamic style construction, all under the <code>@emotion/*</code> organization. It is used as the styling engine for several popular component libraries including <code>@mui/*</code> and <code>@chakra-ui/*</code>.</p>
<p>With the <code>@emotion/css</code> library, you can generate styles under a randomized className at runtime which can allow it to be used effectively with or without any framework. The <code>@emotion/react</code> library has special integrations with React that allow for additional optimizations and a <code>React.Context</code> driven theme system. Finally, the <code>@emotion/styled</code> library allows for the creation of React components by only specifying an HTML element name and its associated styles (a pattern borrowed from the <code>styled-components</code> library). There are actually several other libraries in the Emotion ecosystem, but the rest are mostly underlying core libraries, compiler plugins, or testing tools.</p>
<p>If used correctly, Emotion can be a great tool for constructing highly dynamic applications and is performant enough for the task. It also has the DX benefit for React applications that is already found in most alternate framework templates in colocated styles. For example, Vue, Svelte, Astro, and others all allow for component scoped styles in the same file as the component logic and template. This makes for a great developer experience and Emotion brings this to React.</p>
<h2 id="so-what-is-the-problem">So what is the problem?</h2>
<p>Emotion can be slow. It is not always slow and for what it has to do, Emotion is actually fairly performant. However, Emotion inherently has to do more runtime work than using raw CSS, CSS modules, a CSS preprocessor (e.g. <a href="https://sass-lang.com/?ref=dept-engineering-blog.ghost.io">Sass/SCSS</a>, <a href="https://lesscss.org/?ref=dept-engineering-blog.ghost.io">Less</a>, or <a href="https://stylus-lang.com/?ref=dept-engineering-blog.ghost.io">Stylus</a>), or another zero-runtime solution for styles (e.g. <a href="https://tailwindcss.com/?ref=dept-engineering-blog.ghost.io">TailwindCSS</a>, <a href="https://compiledcssinjs.com/?ref=dept-engineering-blog.ghost.io">Compiled</a>, or <a href="https://vanilla-extract.style/?ref=dept-engineering-blog.ghost.io">Vanilla Extract</a>). Raw CSS requires no extra processing. CSS modules, CSS preprocessors, and zero-runtime solutions all require some extra processing at build &amp; dev time, but the resulting CSS is static and can be minified &amp; cached so the client gets optimal performance with these solutions. Emotion, on the other hand, has to do all of the processing at runtime and does comparably similar work to both CSS modules <em>and</em> a CSS preprocessor.</p>
<h2 id="just-how-much-work-is-emotion-doing">Just how much work is Emotion doing?</h2>
<p>Just like at the build time of a CSS preprocessor, Emotion has to parse the style passed into it. Emotion <em>does not</em> simply pass its input strings into a <code>&lt;style&gt;</code> tag, it must parse the style strings into an object tree. The reason your style strings must be parsed by Emotion before the browser is because Emotion offers SCSS-like nesting syntax as it uses <a href="https://www.npmjs.com/package/stylis?ref=dept-engineering-blog.ghost.io"><code>stylis</code></a> as its underlying stylesheet language.</p>
<p>Once the parsing is complete, it then generates a minified CSS output and inserts that value into a <code>&lt;style&gt;</code> tag in the <code>&lt;head&gt;</code> of the document. If you are using SSR then the content will be in the tag as a normal <a href="https://developer.mozilla.org/en-US/docs/Web/API/Text?ref=dept-engineering-blog.ghost.io">Text</a> node (which is how it content would load if you hand-wrote CSS into a <code>&lt;style&gt;</code> element). For SPA styles, they will be added rule-by-rule directly to a live <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSSRuleList?ref=dept-engineering-blog.ghost.io"><code>CSSRuleList</code></a> of an empty <code>&lt;style&gt;</code> tag for improved performance in cases of more frequent modifications of style rules.</p>
<p>The generic <code>@emotion/css</code> library then returns a string of a random-hashed className that can be used to reference the styles that were just generated. The <code>@emotion/react</code> library handles the order-of-operations here slightly differently by only running style rule insertions when components are being mounted and memoizing the construction of these styles for future renders. There are performance advantages to using <code>@emotion/react</code> over <code>@emotion/css</code> in React applications but in the end, it always has to do all of these steps.</p>
<p><em>This is a lot of work to do at runtime.</em> The fact that most applications using Emotion don't show obvious performance buckling or dropped frames is impressive to the quality of the engineering at work in Emotion. However, I think it's important to understand here that using Emotion is never necessary and is always a tradeoff for gains in the developer experience at the risk of lost performance. There is nothing that you can do with Emotion that you can't do without it but it enhances code readability and increases developer velocity with the code colocation that it allows for.</p>
<h2 id="how-can-we-optimize-our-use-of-emotion-in-react-applications">How can we optimize our use of Emotion in React applications?</h2>
<h3 id="optimization-i-avoid-doing-construction-of-styles-in-render-functions">Optimization I. Avoid doing construction of styles in render functions.</h3>
<p>Now that it's clear how much work Emotion has to do at runtime, it becomes far more obvious that we want to do this work as infrequently as possible. So this means avoiding using Emotion style constructions in renders as much as we possibly can. Despite almost every example in <a href="https://emotion.sh/docs/introduction?ref=dept-engineering-blog.ghost.io">the Emotion documentation</a> showing style constructions in render functions, this is not the best practice. The documentation, to me, reads as a bit too focused on what is possible with Emotion and not enough on what would be best practice for performance.</p>
<p><em>So please don't do this...</em></p>
<pre><code class="language-tsx">import * as React from 'react';
import { css } from '@emotion/react';

export const MyButton = (
  props: React.DetailedHTMLProps&lt;
    React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt;,
    HTMLButtonElement
  &gt;
) =&gt; (
  &lt;button
    css={css`
      padding: .5rem;
      border-radius: .5rem;
      box-shadow: 7px 5px 6px 0px black;
      border: 2px solid navy;
      color: white;
      background: blue;
      transition: .1s ease box-shadow, .1s ease border-color;
      &amp;:hover, &amp;:focus-visible {
        outline: none;
        box-shadow: 7px 5px 10px 0px navy;
        border-color: skyblue;
      }
      &amp;:active {
        box-shadow: none;
      }
    `}
    {...props}
  /&gt;
);
</code></pre>
<p><strong><em>Figure 1.</em> A nonoptimal use of Emotion which causes style reconstruction or cache fetching on every render.</strong></p>
<p><em>Instead, do this...</em></p>
<pre><code class="language-tsx">import * as React from 'react';
import { css } from '@emotion/react';

export const MyButton = (
  props: React.DetailedHTMLProps&lt;
    React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt;,
    HTMLButtonElement
  &gt;
) =&gt; (
  &lt;button css={styles.button} {...props} /&gt;
);

const styles = {
  button: css`
    padding: .5rem;
    border-radius: .5rem;
    box-shadow: 7px 5px 6px 0px black;
    border: 2px solid navy;
    color: white;
    background: blue;
    transition: .1s ease box-shadow, .1s ease border-color;
    &amp;:hover, &amp;:focus-visible {
      outline: none;
      box-shadow: 7px 5px 10px 0px navy;
      border-color: skyblue;
    }
    &amp;:active {
      box-shadow: none;
    }
  `
}
</code></pre>
<p><strong><em>Figure 2.</em> A more optimal use of Emotion which only causes style construction once and never relies on cache fetching.</strong></p>
<p>In the second example here, we are only parsing and constructing the styles once and then reusing the same instance of <code>SerializedStyles</code> for every render of the component. If we do run the construction of styles in the render, Emotion will try to cache the style and fetch it from cache based on the input but this logic is still not free and it's still better to avoid it entirely and not trust that library optimizations will outperform writing code that is optimized from the start.</p>
<h3 id="optimization-ii-ensure-that-your-emotion-styles-can-be-properly-minified-in-your-build-output">Optimization II. Ensure that your Emotion styles can be properly minified in your build output.</h3>
<p>When creating styles using the <code>css</code> template tag function (like both examples above), the template passed to the function is one large string which is outside of the context of your JavaScript. This means that when standard build tools go through your code to minify the content, they won't touch your style strings. This leaves you with a bunch of extra white space characters in your build outputs that unnecessarily increases the size of your bundle. This problem is made worse if you use <code>css</code> template styles inside of your components as all the space &amp; newline characters from the nesting depth of your component will also be preserved in the output.</p>
<pre><code class="language-tsx">import{css}from'@emotion/react';export const MyButton=(props)=&gt;createElement(&quot;button&quot;,{css:css`
      padding: .5rem;
      border-radius: .5rem;
      box-shadow: 7px 5px 6px 0px black;
      border: 2px solid navy;
      color: white;
      background: blue;
      transition: .1s ease box-shadow, .1s ease border-color;
      &amp;:hover, &amp;:focus-visible {
        outline: none;
        box-shadow: 7px 5px 10px 0px navy;
        border-color: skyblue;
      }
      &amp;:active {
        box-shadow: none;
      }
    `,...props});
</code></pre>
<p><strong><em>Figure 3.</em> The minification output of Figure 1 (not a complete reflection of the exact output of a full production build's minification &amp; uglification).</strong></p>
<p>As you can see, a highly unnecessary amount of the original code structure intended for code readability is left in the output. This issue can be avoided with one of two solutions.</p>
<h4 id="minification-solution-1-use-object-styles-instead-of-template-string-styles-for-reliable-minification">Minification solution 1: Use object styles instead of template string styles for reliable minification.</h4>
<p>The first option, which I would personally recommend, would be that you use <a href="https://emotion.sh/docs/object-styles?ref=dept-engineering-blog.ghost.io">object styles</a> as opposed to template string styles. This will allow you to write your styles as a plain JavaScript object, with nested selectors as nested objects, and then pass that object to the <code>css</code> function. This will allow almost any build tool to minify style code out of the box as the ending output of everything will be plain JavaScript.</p>
<pre><code class="language-tsx">import * as React from 'react';
import { css } from '@emotion/react';

export const MyButton = (
  props: React.DetailedHTMLProps&lt;
    React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt;,
    HTMLButtonElement
  &gt;
) =&gt; (
  &lt;button css={styles.button} {...props} /&gt;
);

const styles = {
  button: css({
    padding: &quot;.5rem&quot;,
    borderRadius: &quot;.5rem&quot;,
    boxShadow: &quot;7px 5px 6px 0px black&quot;,
    border: &quot;2px solid navy&quot;,
    color: &quot;white&quot;,
    background: &quot;blue&quot;,
    transition: &quot;.1s ease box-shadow, .1s ease border-color&quot;,
    &quot;&amp;:hover, &amp;:focus-visible&quot;: {
      outline: &quot;none&quot;,
      boxShadow: &quot;7px 5px 10px 0px navy&quot;,
      borderColor: &quot;skyblue&quot;,
    },
    &quot;&amp;:active&quot;: {
      boxShadow: &quot;none&quot;
    }
  })
};
</code></pre>
<p><strong><em>Figure 4.</em> The object styles equivalent of Figure 2.</strong></p>
<p>Using object styles also has the added benefit of giving you better syntax validation, code highlighting, and auto-completion in your editor as you are writing your styles. Because <em>anything</em> can be a CSS property due to the existence of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/--*?ref=dept-engineering-blog.ghost.io">custom properties/variables</a>, object styles are not <em>strictly</em> typed in TypeScript but they do offer <a href="https://www.youtube.com/channel/UCswG6FSbgZjbWtdf_hMLaow/community?lb=UgkxYzADCYswk8um65IUNVjT5KGuXhJJsh_V&ref=dept-engineering-blog.ghost.io">loose autocomplete</a> for known existing properties. You are also far more likely to catch simple syntax errors in object styles.</p>
<pre><code class="language-tsx">const brokenStyle = css`
  color: red
  background: blue;
`;
</code></pre>
<p><strong><em>Figure 5.</em> A broken style due to a missing semicolon. No error will be thrown and neither style rule will be successfully applied in the generated class because the missing semicolon will break the parsing of all styles until <em>the next semicolon.</em></strong></p>
<pre><code class="language-tsx">const brokenStyle = css({
  color: &quot;red&quot;
  background: &quot;blue&quot;
});
</code></pre>
<p><strong><em>Figure 6.</em> A broken style due to a missing comma. An error will be thrown and the problem will be highlighted in your editor.</strong></p>
<h4 id="minification-solution-2-use-the-emotionbabel-plugin-to-minify-template-string-styles">Minification solution 2: Use the <code>@emotion/babel-plugin</code> to minify template string styles.</h4>
<p>The second option, and the recommended solution by Emotion, would be to <a href="https://emotion.sh/docs/@emotion/babel-plugin?ref=dept-engineering-blog.ghost.io">use the <code>@emotion/babel-plugin</code></a>, which will search your code for uses of <code>css</code> as a template tag function and will minify the content of the template string as if it was just raw Stylis input.</p>
<pre><code class="language-tsx">import{css}from'@emotion/react';export const MyButton=(props)=&gt;createElement(&quot;button&quot;,{css:css('padding:.5rem;border-radius:.5rem;box-shadow:7px 5px 6px 0px black;border:2px solid navy;color:white;background:blue;transition:.1s ease box-shadow, .1s ease border-color;&amp;:hover,&amp;:focus-visible{outline:none;box-shadow:7px 5px 10px 0px navy;border-color: skyblue;}&amp;:active {box-shadow: none;}'),...props});
</code></pre>
<p><strong><em>Figure 7.</em> The minification output of Figure 1 while using <code>@emotion/babel-plugin</code>.</strong></p>
<p>This eliminates the many characters of whitespace which would otherwise be included in your production bundle, and in fact may be <em>the most performant</em> option because this output ends up slightly smaller than minified object styles. It's also worth considering that this input is the native expectation of the underlying Stylis parser, whereas object styles are <a href="https://github.com/emotion-js/emotion/blob/main/packages/serialize/src/index.js?ref=dept-engineering-blog.ghost.io#L228">partially converted to strings</a> before being parsed again into the Stylis <a href="https://github.com/thysultan/stylis?ref=dept-engineering-blog.ghost.io#abstract-syntax-structure">AST</a>.</p>
<p>However, this plugin is not always an option. If you're not using Babel or a framework/build-tool that supports Babel plugins, then you'll be entirely without access to this optimization. This leads back to my advice that it is better to write code that is performant from the start, rather than relying on a plugin to fix it for you. <em>Technically</em> we are still relying on the JavaScript build toolchain to minify our code, but this is a much more common and reliable optimization than relying on a specific plugin to minify template string styles.</p>
<h3 id="optimization-iii-consider-if-you-really-need-emotion-and-everything-that-it-offers-%F0%9F%A4%B7%E2%80%8D%E2%99%82%EF%B8%8F">Optimization III. Consider if you really need Emotion and everything that it offers 🤷‍♂️</h3>
<p>The final solution to optimizing use of Emotion in React applications that I would like to propose for consideration is: not using it. There are many situations where you may have to pull in Emotion. If your organization has standardized use of a particular component library which relies on Emotion, like MUI or Chakra, then you already depend on Emotion. You are best off just using it in that case because the tool is already in front of you. So use it, and use it well with the above recommended optimizations.</p>
<p>However, if you are starting a new application and are considering using Emotion, you may want to take a wider look at the rest of the JavaScript ecosystem. There are a lot of great solutions to this problem. Emotion is a well engineered solution and fits very well into React's &quot;just JavaScript&quot; engineering model, but it is not the only solution. With the availability of CSS variables, sharing state to nested CSS has actually become incredibly easy.</p>
<p><em>Somewhere in your styles...</em></p>
<pre><code class="language-css">button.my-button:hover, button.my-button:focus-visible {
  outline: none;
  box-shadow: 7px 5px 10px 0px navy;
  border-color: var(--button-hover-border-color);
}
</code></pre>
<p><em>Somewhere in your JSX...</em></p>
<pre><code class="language-tsx">export const MyButton = ({
  buttonHoverBorderColorOverride = &quot;skyblue&quot;,
  ...props
}: { buttonHoverBorderColorOverride?: string } &amp; React.DetailedHTMLProps&lt;
  React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt;,
  HTMLButtonElement
&gt;) =&gt; (
  &lt;button
    {...props}
    className={&quot;my-button &quot; + props.className}
    style={{
      &quot;--button-hover-border-color&quot;: buttonHoverBorderColorOverride,
      ...props.style,
    }}
  /&gt;
);
</code></pre>
<p><strong><em>Figure 8.</em> Passing state from React to raw CSS</strong></p>
<p>The reason Emotion is chosen for bigger component libraries is that it allows for high customizability with its theme overrides model which is exposed by most component libraries that use it. This allows them to be highly overridable and configurable, but at the cost of a less performant solution to styling. If you are building a new component library, you may want to consider that you can sacrifice the extent of your customizability &amp; configurability in exchange for a more performant solution. This would allow you to compete with bigger component libraries because while they would have the advantages that Emotion affords them, your library could have the performance advantage.</p>
<p>If you really feel that you need what Emotion offers, there are still other alternatives in the runtime CSS-in-JS ecosystem. <a href="https://stitches.dev/?ref=dept-engineering-blog.ghost.io">Stitches</a> is a fantastic alternative of similar features to Emotion. It is a bit more opinionated than Emotion, and only allows for object-styles. However, this means that it doesn't have to support a custom string parser like Emotion does by relying on Stylis. Because of this, Stitches is smaller, and by offering less syntax options <a href="https://stitches.dev/docs/benchmarks?ref=dept-engineering-blog.ghost.io">it maintains better performance than Emotion in their open-sourced benchmarks</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Emotion is a well engineered, unopinionated, and surprisingly performant ecosystem of tools that may offer the optimal implementation of a sensible solution for a complex problem, but it is not without its drawbacks. It is important to understand the tradeoffs that you are making when you choose to use Emotion or any styling system in your application or library. It is also important to understand that there are many ways to optimize your use of Emotion, and the docs aren't always up-front about it and seem to focus more directly on the enhanced capabilities and developer experience than the potential drawbacks. If you come into a project with all this awareness in mind and are able to account for optimizations slightly earlier, then hopefully you can avoid <a href="https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b?ref=dept-engineering-blog.ghost.io">having to do a lot of refactoring later on if Emotion starts to feel like it's not meeting your needs</a>.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Micro-frontends 101: When a composable frontend architecture makes sense for your application]]></title><description><![CDATA[At a high level, micro-frontends are a software development approach that breaks down a monolithic frontend application into smaller, individual mini applications.]]></description><link>https://engineering.deptagency.com/micro-frontends-101-when-a-composable-frontend-architecture-makes-sense-for-your-application/</link><guid isPermaLink="false">649b028c39495c0001346903</guid><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Jake Rainis]]></dc:creator><pubDate>Wed, 28 Jun 2023 13:59:30 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/micro-frontends-101.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/micro-frontends-101.jpg" alt="Micro-frontends 101: When a composable frontend architecture makes sense for your application"/><p><em><strong>Are “micro-frontends” just the UI version of backend “microservices”?</strong></em></p><p>Another day, another new front-end solution! Perhaps you’ve heard about the concept of “micro-frontends.” They are a relatively new concept, but they are getting plenty of buzz. You might be wondering if they’re just the frontend flavor of “microservices.” If so, you’re not too far off, but to arm you with everything you need to know to know on the matter, let’s dive a little deeper. </p><p>At a high level, micro-frontends are a software development approach that breaks down a monolithic frontend application into smaller, individual mini applications.</p><p>The ultimate goal is for each of these applications to be developed, deployed, and maintained independently. As a result, micro-frontends can help to improve agility, flexibility, and scalability. By breaking down the frontend into smaller, more manageable pieces, teams can work in a more independent manner that involves less coordination with other teams or squads.</p><h2 id="so%E2%80%A6-micro-frontends-are-just-microserves-for-the-frontend">So… Micro-frontends <em>are</em> just microserves for the frontend?</h2><p>Kind of. Conceptually, micro-frontends are similar to microservices, but they are implemented at the frontend level rather than the backend level. Microservices can be implemented using different programming languages and frameworks, but at their core, each one should be designed with a purpose to isolate its responsibility from the rest of the technical ecosystem. Oftentimes, one microservice might be dedicated and owned by a particular business unit or individual team.</p><p>On the other hand, micro-frontends are typically implemented using the same programming language and framework and can also be isolated from each other for the purposes of a sole responsibility. But they don’t necessarily have to be. In fact, how they should be split is going to differ based on an organization's team structure as well as the structure of their product architecture. </p><p>Whereas most microservices interact directly with a corresponding database, most frontends do not. Therefore, one micro-frontend application doesn’t need to have a corresponding micro-service, although it can if it makes sense for the product.</p><h2 id="a-simple-example-of-a-micro-frontends-architecture">A simple example of a micro-frontends architecture</h2><p><em>Let’s say you have a complex microservices architecture where each service is responsible for its own piece of functionality. Your company offers a video streaming service to paying customers. Within your ecosystem, you have a service dedicated to customer accounts and their preferences. You have another service focused on membership billing logistics. And of course, there are other service(s) that are tailored to video management and delivery.</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/frontend-architecture-graphic.jpeg" class="kg-image" alt="Micro-frontends 101: When a composable frontend architecture makes sense for your application" loading="lazy" width="1100" height="600" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/06/frontend-architecture-graphic.jpeg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/06/frontend-architecture-graphic.jpeg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/frontend-architecture-graphic.jpeg 1100w" sizes="(min-width: 720px) 720px"><figcaption>A scenario where two micro-frontends each talk to a dedicated backend microservice</figcaption></img></figure><p><br>With this scenario, there could reasonably be a micro-frontend for the “billing portal” and another micro-frontend for the “customer preferences portal.” This is perfectly acceptable, but perhaps the UX/UI of your application provides customers some sort of “profile dashboard” where customers are able to manage their billing information and general user preferences in the same interface. </br></p><p>In this case, it might make more sense to have a micro-frontend dedicated holistically to all aspects of how a customer manages their profile. And likewise, there will be other micro-frontends to power other aspects of your streaming application.<br/></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/frontend-architecture-graphic2.jpeg" class="kg-image" alt="Micro-frontends 101: When a composable frontend architecture makes sense for your application" loading="lazy" width="1100" height="600" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/06/frontend-architecture-graphic2.jpeg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/06/frontend-architecture-graphic2.jpeg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/frontend-architecture-graphic2.jpeg 1100w" sizes="(min-width: 720px) 720px"><figcaption>A scenario where one micro-frontend talk to different backend microservices</figcaption></img></figure><h2 id="you-might-already-be-using-micro-frontends">You might already be using micro-frontends</h2><p>It’s common for front-end application architecture to consist of reusable UI components. A larger application will likely have many components, often containing patterns to help streamline typography (such headings, body content, and links), structure (such as grids and general page layouts), and frequently used widgets (such as content blocks, data tables, or form elements). </p><p>Many organizations have invested the time and effort into managing their components in a separate library that can be versioned and packaged up for use within frontend repositories. This is a great pattern to start a new product with since it allows for rapid scaling down the line. </p><p>In other words, rather than managing all of the frontend UI components along with the application itself within a monolithic frontend repository, they could easily be consumed by a new frontend application that needs to look and behave the same way. As a result, the components are maintained and distributed centrally, and the consumer applications that rely on them can pull them in (and upgrade them when they change or if new components are added).</p><p>As a product scales, this type of pattern becomes an obvious and natural progression for engineers to take. If your front-end isn’t monolithic and instead is broken apart into multiple repositories, you’re already employing a micro-frontend architecture.</p><p>There’s more to the concept of building a mature micro-frontend architecture than just having multiple frontend repositories — and we’ll get into that later in this post — but don’t let a shiny new buzzword describing an old architectural pattern add to your confusion. </p><h2 id="a-micro-frontend-architecture-might-be-very-overkill">A micro-frontend architecture might be (very) overkill</h2><p>A micro-frontend architecture may be appropriate when you have a large-scale application with multiple teams working on different features. It can help reduce the complexity of the frontend and improve the overall maintainability of the application. However, just like microservices, it may be overkill for smaller applications with only one or two teams, or even bigger applications with multiple teams that have functional co-dependencies.</p><p>When moving to a micro-frontend architecture, there are several infrastructural considerations that need to be taken into account. These include </p><ol><li>How to integrate the different micro-frontends into a single application</li><li>How to handle shared dependencies and data</li><li>How to manage routing and navigation between different micro-frontends</li></ol><p>These considerations are important and can become complex very quickly, which results in a technical overhead that might become burdensome. It’s worth weighing these aspects together when it comes to your application to ensure they don’t create more problems than they’re intended to solve.</p><h3 id="infrastructure-security">Infrastructure &amp; security</h3><p>If you’ve worked in microservices architecture, perhaps you're intimately familiar with the infrastructural hurdles needed to stand up, maintain, and deploy new microservices. This is no different when it comes to micro-frontends. Each micro-application will need to be independently managed in the cloud. Infrastructure as code is highly recommended to streamline these tasks, but the nature of the complexities still remain a reality.</p><p>Micro-frontends can also be more difficult to secure than traditional monolithic applications. This is because micro-frontends need to be able to have independent deployments, which can make it more difficult to enforce security policies across the entire application.</p><h3 id="codependency">Codependency</h3><p>Micro-frontends need to communicate with each other in a way that is consistent, efficient, and performant. Simple examples of this are global state management and routing, but can also involve more complex nuances like shared package dependencies (for performance gain) or sharing complex modules that might be too small to be their own micro-frontend as well as too big to be their own shared component. </p><p>This can be challenging, especially if the micro-frontends are developed by different teams using different technologies and frameworks.</p><h3 id="deployment-cadence">Deployment cadence</h3><p>The beauty of backend microservices is that they can be maintained and deployed independently. There might be occasions where coordination is required to roll out major application-wide updates, but since microservices are built to be isolated, the majority of releases shouldn’t hinge upon other microservices.</p><p>Similarly, micro-frontends represent different layers of an application, but they inevitably interface with each in unison to compose a larger cohesive application. As a result, deployments might require more consideration when rolling out features that could cause cross-application regressions (and these aren’t always obvious or foreseeable!). </p><p>It’s feasible that a new deployment of one micro-frontend might necessitate an update and deployment to another micro-frontend to ensure working functionality across the entire application.</p><h2 id="orchestrating-a-micro-frontend-architecture">Orchestrating a micro-frontend architecture</h2><p>At this point, we have a good foundational understanding of what a micro-frontend architecture is. However, the topic wouldn’t be complete without discussing some of the existing tooling that has been developed to lessen some of the complexities that can come with standing up a micro-frontend architecture.</p><p>If you’re familiar with working in a large monolithic architecture, perhaps you’ve come across tools like <a href="https://github.com/lerna/lerna?ref=dept-engineering-blog.ghost.io">Lerna</a>, <a href="https://nx.dev/?ref=dept-engineering-blog.ghost.io">NX</a>, or <a href="https://classic.yarnpkg.com/en/docs/workspaces/?ref=dept-engineering-blog.ghost.io">Yarn Workspaces</a> to help manage a monolithic repository. <br><br>Similarly, there are tools out there built for the purposes of orchestrating micro-frontends. This topic is big enough for its own post and thus, this section is only a quick glimpse into the ecosystem. When evaluating technologies that cater to micro-frontends, keep in mind that different orchestration patterns might be more appropriate for your product than others. It’s worth going through provided tutorials or video demos to see what each solution offers at its core.</br></br></p><h3 id="bit-cli">Bit CLI</h3><p><a href="https://bit.dev/?ref=dept-engineering-blog.ghost.io">Bit CLI</a> (command-line interface) bills itself as an open-source all-encompassing “component-driven-development” tool that aims to transform monolithic architectures into distributed micro-frontends. Its tooling enables engineers to work independently without inhibiting cross-team collaboration. </p><p>Bit CLI is one of the more mature solutions in the micro-frontends realm. It  can do many things, so it’s worth reading the documentation for more details. It is also worth pointing out that Bit has a <a href="https://bit.cloud/?ref=dept-engineering-blog.ghost.io">cloud offering</a> to compliment their CLI tooling, making it easier to get up and running on a mature micro-frontend infrastructure. This might be a great solution for some applications, but tread carefully when introducing new cloud services to your technical infrastructure.</p><h3 id="piral">Piral</h3><p><a href="https://piral.io/?ref=dept-engineering-blog.ghost.io">Piral</a> is another “all-encompassing” micro-frontend framework that allows engineers to create modular applications. These applications are referred to as “Pilets” and live within  a “Piral instance” using a relatively opinionated, yet predictable architecture. What Piral does under the hood is quite interesting (check out the <a href="https://docs.piral.io/concepts/I01-architecture?ref=dept-engineering-blog.ghost.io">architectural overview</a>).<br><br>Piral is easy to understand conceptually, but getting up and running in a large pre-existing monolith can be a feat, resulting in some trial and error. While it appears on the surface to be a seamless development experience, engineers might also find the documentation lacking.</br></br></p><h3 id="webpack-module-federation">Webpack Module Federation</h3><p><a href="https://webpack.js.org/concepts/module-federation/?ref=dept-engineering-blog.ghost.io">Module Federation</a> is a new feature in Webpack 5 that aims to help engineers compose micro-frontends into a single application. Module Federation works by allowing you to share modules between different applications. It can dynamically load code from another micro-frontend application at runtime. </p><p>It also has the ability to intelligently manage shared code dependencies, which keeps bundle sizes nice and small in the spirit of optimal performance. In other words, if two or more micro-frontends share the same library (and they almost always will), Module Federation will build the project in a way that won’t download and load a dependency more than once.</p><h3 id="systemjs">SystemJS</h3><p><a href="https://github.com/systemjs/systemjs?ref=dept-engineering-blog.ghost.io">SystemJS</a> is a dynamic module loader used to import modern JS module formats and load them at run time, rather than build time (like Webpack). Its orchestration capabilities are not specific to micro-frontends, but it does allow an application to dynamically share independent modules, which is a crucial aspect of a micro-frontend architecture.</p><h2 id="not-sure-what-the-best-solution-is-for-your-needs">Not sure what the best solution is for your needs?</h2><p>Even though micro-frontends are a relatively new concept, the options above are far from exhaustive. Many members of the open source community are clearly passionate about solving complex architecture problems and sharing those solutions. But we’re still in the early days, and trying to understand every option out there is quite a challenge.</p><p>If you’re considering a micro-frontend architecture, our architects are always available to help evaluate your needs and work through an <a href="https://www.deptagency.com/service/engineering/technology-strategy-architecture/architecture-sprints/?ref=dept-engineering-blog.ghost.io">architecture strategy together</a>.</p><h2 id="the-tldr-on-micro-frontends">The TLDR on Micro-frontends</h2><p>Micro-frontends are a powerful architectural pattern that can help to improve the agility, flexibility, and scalability of large-scale software applications. However, micro-frontends can be more complex to develop and maintain than traditional monolithic applications. It is important to carefully consider the pros and cons of micro-frontends before deciding whether to use them for a particular project.</p><p>If your application is undergoing challenges or growing pains with regards to frontend scaling and stability, be sure to get in touch with us for a free consultation. Our software architects love solving complex problems at scale and we’d love to work together with you to solve yours!</p><p><br/></p><p><br/></p>]]></content:encoded></item><item><title><![CDATA[Deploying GPT4 in the enterprise: How I created a NetSuite/Slack support bot]]></title><description><![CDATA[See how I attempted to revolutionize our Tier 1 NetSuite support using ChatGPT. ]]></description><link>https://engineering.deptagency.com/deploying-gpt4-in-the-enterprise-netsuite-support-bot/</link><guid isPermaLink="false">6491d7cca5b3850001a2ec4e</guid><category><![CDATA[Chatgpt]]></category><category><![CDATA[AI]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Sven Witteveen]]></dc:creator><pubDate>Thu, 22 Jun 2023 13:32:56 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/andy-kelly-0E_vhMVqL9g-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/andy-kelly-0E_vhMVqL9g-unsplash.jpg" alt="Deploying GPT4 in the enterprise: How I created a NetSuite/Slack support bot"/><p>Like many others, we at DEPT® have been working hard to learn how to leverage LLM's like ChatGPT in a corporate setting. One example of this is our initiative to use ChatGPT-4 as our Tier-1 NetSuite support. </p><p>In this post, I'll share some insights on both the personal and technical sides of my journey to deploying ChatGPT in the enterprise.</p><h2 id="chatgpt-appears-on-stage">ChatGPT appears on stage</h2><p>Somewhere towards the end of December, I was in the midst of doing the <a href="http://adventofcode.com/?ref=dept-engineering-blog.ghost.io">Advent of Code</a> challenges. This is an annual coding competition, and I thoroughly enjoyed working my way through a bunch of the problems. I was challenging myself to use languages I’d never used before, such as Python (I know, right!?) or libraries I hadn’t used in a long time like <a href="https://ramdajs.com/docs/?ref=dept-engineering-blog.ghost.io">Ramda.js</a>. </p><p>It was at this time that ChatGPT suddenly appeared on the world stage. </p><p>I’m not sure what I expected to happen when I took one of my Ramda.js implementations and asked ChatGPT to explain what the code did, but I was certainly surprised when it gave me back a flawless breakdown of the code in natural language - especially because I’ve always found Ramda.js hard to read. When I then asked it to translate the code into Python, and the result was perfect except for a single ‘if-statement’ that it missed, I was truly impressed. Finally, when I asked it about the if-statement it apologized for the oversight in its now familiar tone and proceeded to fix the code. By that time I knew I would not see my friends and family for while.</p><h2 id="experimentation">Experimentation</h2><p>Equal parts excited and terrified of this new technology, I started playing around with the API and the chat interface. I’ve spent many nights and weekends in those months coding up all kinds of interesting things. A personal favorite was <a href="https://github.com/kulltc/chatgpt-sql?ref=dept-engineering-blog.ghost.io" rel="noopener noreferrer">a bot</a> that would take a question from a user about the AdventureWorks database. I came up with this design where ChatGPT was put in between the user and an SQL server as is shown here:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/basic-chart.png" class="kg-image" alt="Deploying GPT4 in the enterprise: How I created a NetSuite/Slack support bot" loading="lazy" width="2000" height="338" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/06/basic-chart.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/06/basic-chart.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/06/basic-chart.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w2400/2023/06/basic-chart.png 2400w" sizes="(min-width: 720px) 720px"/></figure><p>One cool feature of this design was that ChatGPT could independently decide how many queries it would send to the server before answering the user. This also allowed it to correct any errors it may create when formulating queries. All in all, I was pretty proud of my design, so you can imagine my disappointment when I found out that there were many people before me that came up with similar ideas that were executed much better, such as the <a href="https://langchain.readthedocs.io/?ref=dept-engineering-blog.ghost.io">langchain</a> framework.</p><p>Along with the conclusion that I had in fact not created The Next Big Thing, I also learned from this that autonomous agents are currently just too unreliable to be very useful in a real world setting. The essence of the issue is that these models seem to have e.g. a 95% success rate of doing something right, which means that if you start chaining <code>x</code> actions together the success rate starts coming down quickly (<code>success % = .95^x</code>).</p><h2 id="hype-cycle">Hype cycle</h2><p>I had clearly arrived on the downhill slope of my personal GPT hype cycle, so I decided that I’d focus my efforts on something a little more productive. After all, I had invested all this time expecting that something useful would probably come out at the other end. I needed something that would be more exciting than plain old ChatGPT, that was more realistic than a fully autonomous data analyst and that would address the main obstacles I saw for using ChatGPT in a business setting:</p><ul><li>We’re not comfortable sharing any PII or otherwise sensitive data</li><li>Even with ChatGPT 4 hallucinations are still an issue</li><li>It has no knowledge of our company, vocabulary, processes, etc.</li></ul><p>It turns out that Langchain offers one concept that addresses all three of these downsides, and that is its <a href="https://python.langchain.com/en/latest/use_cases/question_answering.html?ref=dept-engineering-blog.ghost.io">question answering solution</a>. I decided to go ahead and give that one a go.</p><h2 id="chatgpt-in-the-enterprise">ChatGPT in the enterprise</h2><p>I decided to (attempt to) revolutionize our Tier 1 NetSuite support. Users would be able to ask any question and the bot would answer based on our extensive internal NetSuite documentation. In case the documentation did not contain an answer, the bot would point users to the Tier-2 service desk. This seemed like a solution that struck the perfect balance – it was innovative without overreaching, and practical in addressing real-world needs. Also, our internal documentation is just that - internal - it is not so sensitive that the risk of a data breach is prohibitive for this use case.</p><p>To bring this idea to life, the first step was to integrate ChatGPT into Slack. This ensured that the bot was easily accessible since Slack is already widely used for internal communication.</p><p>Moreover, the NetSuite support bot was placed in a slack channel where our NetSuite admins could monitor its responses. This was crucial for quality control, as it allowed the admins to step in and correct the bot if necessary.</p><h2 id="the-nitty-gritty">The Nitty Gritty</h2><p>To start off, you’ll need all the documentation you want to use for Q&amp;A in a single directory. This allows you to use the Langchain directory loader that will recursively go through the folder and parse all files (optionally filtered to a specific set of file extensions). It splits each file into chunks of e.g. 4000 characters that will fit in the ChatGPT context window, and then send them to an embeddings API that creates an embedding for them (a mathematical representation of the chunk). Once that’s done, the chunk is saved in an in-memory vector database, indexed by it’s embedding. This all happens in two lines of python code ❤️.</p><figure class="kg-card kg-code-card"><pre><code class="language-Python">loader = DirectoryLoader('./netsuite_docs/', glob="**/*.html", loader_cls=UnstructuredHTMLLoader)
index = VectorstoreIndexCreator().from_loaders([loader])</code></pre><figcaption>I left out imports for dramatic effect, <a href="https://python.langchain.com/docs/use_cases/question_answering/?ref=dept-engineering-blog.ghost.io" rel="noopener noreferrer">see full code example here.</a></figcaption></figure><p>Once setting up the vector database with the documentation was done, the system was ready for question answering. The process begins when a user poses a question. This question is then sent to the Embeddings API, which generates an embedding. The system uses this embedding to query the Vector Database, which has the chunks of documentation along with their respective embeddings. The Vector Database returns the chunks of documentation that are most relevant to the question, based on the similarity of their embeddings. These relevant chunks, along with the original question, are then passed to ChatGPT. ChatGPT reads the documentation chunks and answers the question based on that information. This is another line of code.</p><pre><code class="language-Python">response = index.query("How do I modify a memorized journal?")</code></pre><p>Finally, the system delivers this answer back to the user in slack, completing the question-answering process. The slack integration needed much more code, so I won’t share those here (thankfully chatgpt4 was able to write that). I do think it’s worth pondering that using this brand new AI technology is easier than creating a slack bot. All in all, the process now looks like the one visualized below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/06/sequence-diagram.png" class="kg-image" alt="Deploying GPT4 in the enterprise: How I created a NetSuite/Slack support bot" loading="lazy" width="2000" height="1096" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/06/sequence-diagram.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/06/sequence-diagram.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/06/sequence-diagram.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w2400/2023/06/sequence-diagram.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Fun fact, all diagrams are <a href="https://chat.openai.com/share/8d28e320-7915-415c-a075-ee20cb4952f8?ref=dept-engineering-blog.ghost.io" rel="noopener noreferrer">created by ChatGPT</a>, I just copy pasted the text above.</figcaption></img></figure><p/><h2 id="every-rose-has-its-thorn">Every Rose Has Its Thorn</h2><p>The fact that a working (CLI) prototype can be created with Langchain in less than 10 lines code simply amazing. However, this of course also comes with some downsides.</p><p>First of all, because it’s so easy to get it to work, initially it wasn’t at all obvious to me <em><strong>how</strong></em> it worked. As a matter of fact, it came as a bit of a surprise when I found out that I’d been using an ‘embeddings API’ from OpenAI that I was actually paying for using my API key, which I thought was only needed for the ChatGPT API (don’t worry though, the embeddings API is so cheap it may as well have been free).</p><p>Second, because everything is abstracted away, the basic implementation is also fairly generic. For example, it turns out that the standard template used by Langchain for question anwering is designed to work with pretty much any LLM, which means that it has a very elaborate few-shot examples in it’s prompts which ChatGPT3.5 and 4 don’t really need. I ended up writing my own prompt to save tokens and avoid confusing the models with irrelevant example conversations.</p><p>Looking back, I’d still recommend using Langchain. It’s super effective for rapid prototyping and exploring the possibilities of ChatGPT API’s. However, I think I will probably implement a future production version without the framework. In the end of the day it is mostly the OpenAI API’s - rather than the Langchain framework - that allow for these applications to be so powerful with so little effort. This became only more true with the recent launch of the <a href="https://openai.com/blog/function-calling-and-other-api-updates?ref=dept-engineering-blog.ghost.io">functions feature</a>.</p><h2 id="enter-the-user">Enter: The User</h2><p>Once we started a closed beta with a selected group of users, I learned that my testing effort was a bit of a farce and the bot wasn’t performing very well. I hadn’t done a a very good at coming up with questions that real users would be asking. It turns out that when you read the docs and then try to come with a questions about them, the questions will be worded in ways ‘similar’ to the docs (even if you try not to do that). This bias then helps both the similarity search retrieving snippets and the chatbot that has to answer the question. I’m fairly sure others will face this same issue since the core competency of LLM’s is that they can parse unpredictable inputs. And if you can’t predict the inputs, it’s difficult to test the system properly.</p><p>Shortly after the initial deploy we switched from ChatGPT 3.5 to 4 which made a huge difference and it was much better able more unexpected inputs. I would therefore definitely recommend to follow <a href="https://www.youtube.com/watch?v=bZQun8Y4L2A&ref=dept-engineering-blog.ghost.io">Andrej Karpathy’s advice</a> on deploying LLM’s, which is to first roll out with the best technology you have (GPT 4) and then scale back - rather than other way around. In a world with many LLM skeptics, it’s valuable to make a good first impression.</p><p>After this initial hiccup, the reception was overwhelmingly positive. Not only did the NetSuite / Slack support bot reduce the response time for queries dramatically, it also allowed our NetSuite admins to focus on more complex issues.</p><h2 id="conclusion">Conclusion</h2><p>At the start of this year I set out to build something cool with ChatGPT. While experimenting I learned that ChatGPT and other LLM’s show enormous potential, but there are also many things to keep in mind when trying to deploy generative AI responsibly. From avoiding hallucinations to managing costs and ensuring proper testing, these systems come with new challenges. It turned out more difficult than expected to create something that starts delivering value in the real world, based on todays state of the technology. </p><p>That being said, I’m very happy with the end-result. Perhaps in 12 months this will all be hilariously outdated, but it can’t hurt to try to keep up with the current state of tech. And whatever may come next, I had a lot of fun tinkering along during these first few months of the generative AI revolution, and if nothing else, that to me is a comforting thought.</p>]]></content:encoded></item><item><title><![CDATA[Making a dynamically-expanding video in CSS]]></title><description><![CDATA[How I used clip-path to help achieve a dynamically-expanding video in CSS. ]]></description><link>https://engineering.deptagency.com/making-a-dynamically-expanding-video-in-css/</link><guid isPermaLink="false">646f720c966ce000013d9063</guid><category><![CDATA[CSS]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Aaron Weissman]]></dc:creator><pubDate>Wed, 31 May 2023 13:25:16 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/dynamically-expanding-video-css.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/dynamically-expanding-video-css.jpg" alt="Making a dynamically-expanding video in CSS"/><p>With two weeks to go on my project: rebuilding the website for The Tony Blair Institute for Global Change (TBI), the design team from BASIC/DEPT® realized that if this site were going to be an exciting and engaging user experience, we’d need a hero that wasn’t just your classic (boring) old video hero.  </p><p>We needed something that felt both inviting and intriguing. That would tease something more, forcing the user to lean in to see it. So they devised an idea to use an expanding window pane animation that would open as the user scrolls–and asked us to build it at the 11th hour.</p><h2 id="getting-fancy-with-no-time-to-spare">Getting fancy with no time to spare</h2><p>Of course, for anyone that’s ever built a website, or, well… anything with a deadline, there are always tasks lingering as you approach the end. In the case of TBI’s new site, we were still cleaning up the articles that had been ported over from the old Drupal site, tidying up page transitions, had a nav that was buggy and unpolished, and search functionality that needed tuning and testing. </p><p>On top of all that, one of our developers was off to Hawaii for a long-planned trip that coincidentally lined up perfectly with launch week, and the engineering team was feeling the heat to produce something that didn’t feel chock-full of bugs. But alas, after speaking with the client at length, we all agreed that launching an eye-catching site that felt exciting was more important than some lingering bugs buried deeper inside the other parts of the site. We’d go back and fix those later.</p><p>I was handed the following comp and told, </p><blockquote class="kg-blockquote-alt">“As the user begins to scroll, the video widens as the scroll happens until it reaches maximum width. Then they can carry onwards down the page.”</blockquote><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/dynamic-window-homepage.gif" class="kg-image" alt="Making a dynamically-expanding video in CSS" loading="lazy" width="1000" height="563" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/dynamic-window-homepage.gif 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/dynamic-window-homepage.gif 1000w" sizes="(min-width: 720px) 720px"/></figure><p><br>It doesn’t seem too crazy, right? </br></p><p>That’s what I thought too. It’s just a couple of `div`s on the sides of the screen that shrink in width as the user scrolls. We’ll use fixed position to stick the video to the top of the page until we hit the bottom of our arbitrary scroll depth and then “release” it by positioning it absolutely to the bottom of the hero. This is just using position sticky, but because the video is nested and isn’t at the top level of the dom, position sticky won’t work here, so we can use a little JS and watch the scroll depth to flip it between “fixed” and “absolute.” </p><p>I spent the next hour or so coding this up, plopping in the video, tweaking the sizing of it (videos want to keep their aspect ratio, so making this fill the page responsively was a task all its own), properly positioning the titles and eyebrows and making the “explore” button at the bottom of the page work so that when a user clicks it, they automatically scroll to the bottom. </p><p>I got damn close and figured I was just about over the finish line when it looked like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/expanding-window-animation-first-try-css-tbi-1.gif" class="kg-image" alt="Making a dynamically-expanding video in CSS" loading="lazy" width="1920" height="1080" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/expanding-window-animation-first-try-css-tbi-1.gif 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/expanding-window-animation-first-try-css-tbi-1.gif 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/05/expanding-window-animation-first-try-css-tbi-1.gif 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/expanding-window-animation-first-try-css-tbi-1.gif 1920w" sizes="(min-width: 720px) 720px"/></figure><p>It turns out I was not close. </p><p>I spent the next couple of hours banging my head against a wall trying to get the white text to show up between the window panes while the black text appeared on the outside. I tried all sorts of things, like using “mix-blend-mode,” a CSS property that “​​sets how an element's content should blend with the content of the element's parent and the element's background.” Still, it was a matter of layer order, and no matter what I did with z index and positioning, it wouldn’t work. After many attempts to track the z index of each individual element on the page or to try some funky technique to make some layers transparent with others opaque, I had to step away to the physical world to figure out my issue.</p><p>By definition, if those two text components were going to be in the same position on the page, one would have to be on top of the other. And that meant that with this approach, I would always have white or black text. There was no way to get the white text on the video and the black text on the window panes.</p><p>I even made myself a little physical example to figure out how the layers could be ordered to see if it would work. Spoiler alert: it does not.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-443a9292-2cea-4f65-8771-ad276d3b48d6.png" class="kg-image" alt="Making a dynamically-expanding video in CSS" loading="lazy" width="2000" height="1091" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/data-src-image-443a9292-2cea-4f65-8771-ad276d3b48d6.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/data-src-image-443a9292-2cea-4f65-8771-ad276d3b48d6.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/05/data-src-image-443a9292-2cea-4f65-8771-ad276d3b48d6.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-443a9292-2cea-4f65-8771-ad276d3b48d6.png 2048w" sizes="(min-width: 720px) 720px"/></figure><p>So it was back to the drawing board, or in my case, to the place where I do my best thinking: the shower. As I stood there, trying to wash away my failure, frustrated that on a project with so little time left, I’d just burnt a few precious hours trying an approach that wouldn’t work and now seriously wondering if I’d have to tell the design team that their idea is impossible, I pondered what else I could do. Then I had what alcoholics and overworked devs both refer to as “a moment of clarity” and realized there was a better way to do it. </p><p>My old friend “clip-path.”</p><p>For those unfamiliar with clip-path, it is like a cookie cutter for elements on a webpage. It lets you cut out different shapes from elements, like circles or polygons, so that only the parts within the shape are visible. </p><p>It's a way to create exciting and unique designs by hiding or revealing specific areas of an element based on the shape you choose. Think of it as using different cookie cutters to create cookies in different shapes and sizes. Clip path provides flexibility when designing and shaping elements, allowing for more creative and visually appealing website layouts. Modern web browsers support it and it can be implemented using CSS properties or SVG (Scalable Vector Graphics) paths.</p><p>First things first, I had to figure out what the path would look like that I could use to cut out the white background and show through to the video behind it. Using a clip path generator, I played around with a few ways of doing it but used a frame-type shape where I pushed everything to the edge.</p><p>It started out looking like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-0d6c649c-791f-4dd4-8167-e69da6f84a75.png" class="kg-image" alt="Making a dynamically-expanding video in CSS" loading="lazy" width="332" height="331"/></figure><p>And then, after pushing everything to the outside, it looked like this when static:<br/></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-f1323874-42e3-41f6-9983-526b321a06ea.png" class="kg-image" alt="Making a dynamically-expanding video in CSS" loading="lazy" width="330" height="330"/></figure><p>And here’s what the static path looks like in code; it’s color-coded so that you can see which value corresponds to which point on the image:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/static-path-colors.png" class="kg-image" alt="Making a dynamically-expanding video in CSS" loading="lazy" width="1352" height="146" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/static-path-colors.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/static-path-colors.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/static-path-colors.png 1352w" sizes="(min-width: 720px) 720px"/></figure><h2 id="creating-this-performant-scroll-effect">Creating this performant scroll effect</h2><p>After figuring out how to cut out the window from an element on the page, I had to actually animate the thing and do it on scroll without absolutely trashing the performance (there is a video playing in the background, after all). </p><p>To create this performant scroll effect that would update the clip path property as the user progressed down the page, I used a library called framer-motion, which helps with all sorts of frontend animations. I recommend it to anyone doing semi-advanced animations or transitions on the page. You can do a lot with it and don’t have to deal with overloading the page until you are doing WAY too much.</p><h2 id="finally-the-code">Finally, the code</h2><p>I can’t share the client repo in this post, but here’s a crude recreation of the effect in a code sandbox. </p><p>This is missing several styles that we used on the site, and it’s not responsive, but it does the job of communicating what exactly is happening. More or less, what’s happening is that as the user scrolls down the page, I adjust the inner corners of the clip path frame to slowly approach the edge until they reach 0 or 100%, respectively. </p><p>This has the effect of opening up the window panes. <a href="https://codesandbox.io/s/framer-motion-transition-prop-forked-19oyob?file=/src/index.tsx&resolutionWidth=1440&resolutionHeight=900&ref=dept-engineering-blog.ghost.io">Sandbox: Framer motion transition </a> </p><p>After all this effort, and as we continued sprinting to the finish line, a member of our client’s board told us, “I just don’t get it” when it came to the scroll effect, and we had to switch to something that would happen automatically rather than on scroll. It still looks pretty awesome, but, at least in my mind, it does not have quite the same allure as the scroll effect would have had. Such is life; you deliver what the client wants and what makes them happy.</p><p>If you made it this far, thanks for reading. </p><h2 id="here-are-some-good-resources-for-a-bunch-of-the-things-mentioned-above">Here are some good resources for a bunch of the things mentioned above:<br/></h2><p>Z Index and clip path from the mozilla docs:</p><ul><li> <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/z-index?ref=dept-engineering-blog.ghost.io">https://developer.mozilla.org/en-US/docs/Web/CSS/z-index</a></li><li> <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path?ref=dept-engineering-blog.ghost.io">https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path</a></li></ul><p>A clip path generator</p><ul><li> <a href="https://bennettfeely.com/clippy/?ref=dept-engineering-blog.ghost.io">https://bennettfeely.com/clippy/</a></li></ul><p>The framer-motion library</p><ul><li> <a href="https://www.framer.com/motion/introduction/?ref=dept-engineering-blog.ghost.io">https://www.framer.com/motion/introduction/</a></li></ul><p>The Tony Blair Institute for Global Change (where you can see the effect live)</p><ul><li> <a href="https://institute.global/?ref=dept-engineering-blog.ghost.io">https://institute.global/</a></li></ul><p>Code sandbox of the effect</p><ul><li> <a href="https://codesandbox.io/s/framer-motion-transition-prop-forked-19oyob?file=/src/index.tsx&resolutionWidth=1440&resolutionHeight=900&ref=dept-engineering-blog.ghost.io">https://codesandbox.io/s/framer-motion-transition-prop-forked-19oyob?file=/src/index.tsx&amp;resolutionWidth=1440&amp;resolutionHeight=900</a><br/></li></ul>]]></content:encoded></item><item><title><![CDATA[How to add Disqus to your site with a custom look and feel]]></title><description><![CDATA[A recent client had a requirement to add comment sections to articles on their site. The ask was to have the sections maintain the site's branding for a nice, unified digital experience. Could it be done?]]></description><link>https://engineering.deptagency.com/adding-custom-disqus-to-my-site/</link><guid isPermaLink="false">64247a1e92e99c003ddd72cb</guid><category><![CDATA[Process]]></category><category><![CDATA[contentful]]></category><category><![CDATA[Postgres]]></category><dc:creator><![CDATA[Ali Halim]]></dc:creator><pubDate>Thu, 25 May 2023 13:27:59 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDJ8fGNoYXQlMjBidWJibGV8ZW58MHx8fHwxNjgxNDE2MTc4&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDJ8fGNoYXQlMjBidWJibGV8ZW58MHx8fHwxNjgxNDE2MTc4&ixlib=rb-4.0.3&q=80&w=2000" alt="How to add Disqus to your site with a custom look and feel"/><p>Nothing brings a site's users together (for better or worse) like a comment section. But comments are complex (replies of replies, anyone?), so third party plugins are definitely best suited to handle all the fun stuff that comes with them.  So off I went to search for one to use.</p><p><a href="https://disqus.com/?ref=dept-engineering-blog.ghost.io">Disqus</a> was the first plugin that came to mind and turned out to be best suited for the task at hand, with a few caveats that I'll explain.</p><p>The first big gotcha is that <a href="https://help.disqus.com/en/articles/1717201-disqus-appearance-customizations?ref=dept-engineering-blog.ghost.io">it does not allow for much customization of its embed</a> and thus couldn't be simply plugged in and customized to match the site's look and feel. Phooey.</p><p>Luckily <a href="https://help.disqus.com/en/articles/1717212-using-the-api?ref=dept-engineering-blog.ghost.io">the Disqus API</a> has plenty of resources and I was able to use it to help satisfy my requirements:</p><ul><li>adding</li><li>editing</li><li>deleting</li><li>replying to</li><li>liking</li><li>pinning</li><li>and flagging comments.</li></ul><p>Disqus gives prospective API users a fair warning though:</p><blockquote>...[the API] is designed as a <em>supplemental</em> data source, and as such <strong>won't</strong> provide an easy method for replacing the comments embed.</blockquote><p>Patooey!</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/challenge-lion-king.gif" class="kg-image" alt="How to add Disqus to your site with a custom look and feel" loading="lazy" width="360" height="202"/></figure><p>Now, I wouldn't say it was easy, but it wasn't difficult to fully deliver the feature with the help of a database and a couple of weeks of dev work. Happy clients first!</p><p>Here's how I went about it.</p><h2 id="technologies-involved">Technologies Involved</h2><ul><li><a href="https://nuxtjs.org/?ref=dept-engineering-blog.ghost.io">Nuxt</a> with some API middleware</li><li><a href="https://www.contentful.com/?ref=dept-engineering-blog.ghost.io">Contentful</a> for CMS</li><li><a href="https://www.postgresql.org/?ref=dept-engineering-blog.ghost.io">Postgres</a> for database</li></ul><h2 id="execution">Execution</h2><p><a href="https://help.disqus.com/en/articles/1717169-api-data-relationships?ref=dept-engineering-blog.ghost.io">Disqus lays out its data relationships</a> nicely, and given the structure, each article on my client's site represents a Thread that can contain Posts (comments and their replies).  So far, so good.</p><p>But I couldn't send users over to Disqus to authenticate and then back to the site so they could make comments on their own behalf (it wouldn't be a user-friendly flow as they already have to sign into the site with Auth0).</p><p>So I had to set the site itself up to be the lone, authenticated Disqus User taking actions on behalf of the site's users (<a href="https://disqus.com/api/docs/auth/?ref=dept-engineering-blog.ghost.io">see the "Authenticating as the Account Owner" section here</a>) by doing the following:</p><ol><li><a href="https://disqus.com/profile/signup/?ref=dept-engineering-blog.ghost.io">getting a Disqus account</a></li><li><a href="https://disqus.com/admin/create/?ref=dept-engineering-blog.ghost.io">creating a Disqus site</a>, which represents a Forum in the Disqus data model</li><li><a href="https://disqus.com/api/applications/?ref=dept-engineering-blog.ghost.io">registering a Disqus API application</a> and using the API token, key and secret provided as environment variables in the code where needed</li></ol><h3 id="making-comments">Making Comments</h3><p>When a user submits a comment on an article using my fancy, 100% custom form, I hit the Disqus API to <a href="https://disqus.com/api/docs/threads/create/?ref=dept-engineering-blog.ghost.io">create a new Thread</a> (only for an article's very first comment) and then <a href="https://disqus.com/api/docs/posts/create/?ref=dept-engineering-blog.ghost.io">create a Post under that Thread</a>.  Subsequent comments on that article are created under the same Thread.</p><p>In order to tie an "Article" item (in my Contentful model) to its comments (in Disqus), I just save the following in a <em>comment_sections</em> table upon comment creation:</p><ul><li>the unique id that Contentful automatically provides with every item in a model (call it <em>cms_id</em>)</li><li>the unique id Disqus provides on creation of a new Thread (<em>comment_section_id</em>)</li></ul><p>I use a second table called <em>comments</em> to tie each comment to the article it was made on (using the <em>comment_section_id</em>), as well as to persist the commenter's name (among other things).</p><h3 id="loading-an-articles-comments">Loading an Article's Comments</h3><p>When an article is loaded, I simply query my table for the relevant comment section using the article's <em>cms_id</em>, then <a href="https://disqus.com/api/docs/threads/listPosts/?ref=dept-engineering-blog.ghost.io">retrieve its comments from Disqus</a> using the <em>comment_section_id</em> in the endpoint's <em>thread</em> param. Finally, I augment each of the fetched comments with data from the <em>comments</em> table.</p><h3 id="editing-and-deleting-comments">Editing and Deleting Comments</h3><p>I was able to easily use the <strong>/posts/update</strong> and <strong>/posts/remove</strong> endpoints to implement editing and deleting.  It was mainly about the UI for these pieces of functionality.</p><h3 id="replies">Replies</h3><p>Implementing the ability to reply to a comment was fairly straightforward.  Use the same <strong>/posts/create</strong> endpoint that's called when a top-level comment is made, but set the <em>parent </em>param to the <em>id</em> of the comment being replied to.</p><p>Now retrieving, processing and properly displaying replies was a large chunk of the overall work.  I ended up having to loop through comments twice to set replies.  Here's a visual of the logic:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/Screen-Shot-2023-05-23-at-2.45.54-PM.png" class="kg-image" alt="How to add Disqus to your site with a custom look and feel" loading="lazy" width="1852" height="654" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/Screen-Shot-2023-05-23-at-2.45.54-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/Screen-Shot-2023-05-23-at-2.45.54-PM.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/05/Screen-Shot-2023-05-23-at-2.45.54-PM.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/Screen-Shot-2023-05-23-at-2.45.54-PM.png 1852w" sizes="(min-width: 720px) 720px"/></figure><p/><p>Throw in some recursive component action and I was able to support n-level comment nesting...not something you ever want to do twice!</p><p/><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/Screen-Shot-2023-05-23-at-2.57.58-PM.png" class="kg-image" alt="How to add Disqus to your site with a custom look and feel" loading="lazy" width="1268" height="1356" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/Screen-Shot-2023-05-23-at-2.57.58-PM.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/Screen-Shot-2023-05-23-at-2.57.58-PM.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/Screen-Shot-2023-05-23-at-2.57.58-PM.png 1268w" sizes="(min-width: 720px) 720px"/></figure><p/><h3 id="likes">Likes</h3><p>The requirement here was to simply allow likes ("recommendations") and display the number of likes each comment has. Surpisingly, no endpoint supports this (there is a <em>likes</em> field in the response data, but that represents the number of likes made on the comment from an actual Disqus embed, not a custom UI). So I just use my <em>comments</em> table for this functionality.</p><h3 id="pinned-comments">Pinned Comments</h3><p>For this site, any comment made by someone with a certain Auth0 metadata field is automatically pinned. There's no Disqus endpoint for pinning either, so I leverage my <em>comments</em> table again to persist that metadata field for each comment.</p><p>From there, I just give special treatment to any pinned comment on the frontend (i.e. extract it out and render it above the rest of the comments). Not ideal to have to do this manually with no help from the API, but it worked out.</p><h3 id="flagging-comments">Flagging comments</h3><p>The simple ask here was just a call-to-action on each comment allowing users to flag other people's comments with no further input (such as the reason for flagging), and for the comment to be hidden immediately upon flagging.</p><p>I went in and tried to use the <strong>/posts/report </strong>API for this, but I learned that Disqus users cannot report their own comments (makes sense).  And because the site itself is the User, from Disqus' perspective, calls to that endpoint look like the User trying to report itself.</p><p>You <em>can</em> mark your own comment as spam in Disqus, so I had to use the <strong>/posts/spam </strong>endpoint instead.</p><p>Misnomer aside, to handle flagged (spam) comments, the site admin just looks under a "Spam" tab on the Disqus dashboard and approves or leaves them there as desired. Approved comments get cleared out of "Spam" while comments left in there are held out of response data and remain hidden to users.</p><h3 id="data-refreshing">Data refreshing</h3><p>An important thing to note is that any time a comment is added, edited, deleted or flagged, I go and retrieve the article's comments fresh from Disqus and get a re-render. This allows Disqus to remain the single source of truth and reduces the burden of state management in my code.</p><p>Also of note is that the Disqus API is slow to return updated data (e.g. it takes a few seconds between calling the endpoint to create a comment and getting that comment back in a fetch), so I unfortunately have to poll for updates and show a small pending state to the user at times.</p><h2 id="conclusion">Conclusion</h2><p>Using the Disqus API for a custom comment section UI isn't techincally <em>supposed </em>to be done, but I wanted to share my experience with it for anyone out there that may be facing a similar task.</p><p>I always enjoy a learning experience, and a happy client in the end made it all worthwhile!</p>]]></content:encoded></item><item><title><![CDATA[Beware the outdated SDK: A cautionary tale of broken websites]]></title><description><![CDATA[Your site has gone down. Users are contacting you for support, but you have no idea what could be causing the issue.

Mild panic sets in.

This scenario is all too common, and recently, I also encountered this issue. It turned out that the problem was not in the code I had recently written; but in my failure to realize the software development kit (SDK) I was using was deprecated.

This article will explore the risks of using an outdated SDK and provide tips to avoid its pitfalls.


So, what exa]]></description><link>https://engineering.deptagency.com/beware-the-outdated-sdk-a-cautionary-tale-of-broken-websites/</link><guid isPermaLink="false">645bf1a12ff13b0001a0f554</guid><category><![CDATA[SDK]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[negou seid]]></dc:creator><pubDate>Thu, 18 May 2023 16:13:13 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/Outdated-SDK-cautionary-tale-broken-websites.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/Outdated-SDK-cautionary-tale-broken-websites.jpg" alt="Beware the outdated SDK: A cautionary tale of broken websites"/><p>Your site has gone down. Users are contacting you for support, but you have no idea what could be causing the issue.</p><p>Mild panic sets in. </p><p>This scenario is all too common, and recently, I also encountered this issue. It turned out that the problem was not in the code I had recently written; but in my failure to realize the software development kit (SDK) I was using was deprecated. </p><p>This article will explore the risks of using an outdated SDK and provide tips to avoid its pitfalls.</p><h2 id="so-what-exactly-is-an-outdated-sdk">So, what exactly is an outdated SDK?</h2><p>SDKs are software development tools for building applications. Developers use these tools to create software that works seamlessly with the platform, including libraries, APIs, and documentation. </p><p>However, when an SDK becomes outdated, the tools are no longer current or compatible with the latest platform update. This results in issues like decreased functionality, security vulnerabilities, and even crashes. Therefore, it's crucial to keep your SDKs up-to-date. </p><p>In my case, the application uses <a href="https://directus.io/?ref=dept-engineering-blog.ghost.io">Directus</a>, which has an SDK called directus/sdk-js. It’s used to get content from the site and requires permissions. Unfortunately, this npm package is deprecated, and completely unsupported which broke the functionality of the application.</p><h2 id="what-happens-when-an-sdk-is-out-of-date">What happens when an SDK is out of date?</h2><p>It can cause several problems, including security vulnerabilities and decreased website performance. </p><p>Outdated SDKs expose websites to attack by hackers or malicious software, resulting in data breaches, lost revenue, and reputational damage. From a website performance perspective, an outdated SDK can cause compatibility issues with other software and hardware components, leading to system crashes and errors. </p><p>This can lead to frustrated users and lost revenue as customers abandon your website for faster, more responsive competitors. Additionally, outdated SDKs may not be compatible with the latest browser versions, resulting in website incompatibility and decreased search engine rankings.</p><p>In my case, the SDK stopped working, and users could not perform any tasks. It took time to find the issue and update to the latest version. Updating the version required code changes and additional testing to ensure the site's functionality.</p><h2 id="best-practices-for-updating-sdks">Best practices for updating SDKs</h2><p>There is no perfect way to prevent an incident like this from happening to you. We live in a world where software changes rapidly, and innovations are on the horizon. </p><p>But there are some best practices for updating SDKs.</p><ol><li>Regularly update your dependencies; if you use npm, a simple npm update will update the packages and give you warnings.</li><li>Stay up-to-date with the SDK releases and updates by subscribing to software development forums and newsletters.</li><li>Before updating an SDK, review the release notes and changelogs to understand the changes and potential compatibility issues.</li><li>Test your applications after each SDK update to ensure compatibility and performance.</li><li>Consider using automated testing tools to streamline the testing process and identify potential issues quickly.</li></ol><h2 id="importance-of-testing-after-updating">Importance of testing after updating</h2><p>Testing your applications after each SDK update is critical to ensuring compatibility and performance. As part of the testing process, functional, performance, and security testing are conducted to determine whether any issues or vulnerabilities are introduced during the update process.</p><p>Updating an SDK always requires changes, sometimes minor and sometimes major. For this example, I was fortunate to work in a system that involved minor changes. </p><p>As for me, I tested the application from end to end. One issue I encountered, I resolved when I changed the credentials set up. The other issue was a query, the new version of Directus had made changes to return a payload different than what we were receiving before, and I had to update the code to support that new payload. Minor updates, it took time and patience to ensure every part of the application worked correctly.</p><h3 id="how-do-you-avoid-being-blindsided-by-a-required-update-in-the-future">How do you avoid being blindsided by a required update in the future?</h3><p/><p>To avoid being blindsided by a required update, stay on top of SDK releases and updates. Set up alerts and notifications for software development forums, newsletters, and other relevant  sources.</p><p>Additionally, it's important to have a proactive approach to software development and maintenance. This means staying on top of your software inventory, identifying potential risks, and developing a plan to mitigate them before they become critical issues.</p><p>In conclusion, using an outdated SDK will have implications for your website's performance, security, and user experience. Keep yourself from falling victim by staying up-to-date, testing your applications after each update, and taking a proactive approach to <a href="https://www.deptagency.com/service/engineering/?ref=dept-engineering-blog.ghost.io">software development</a>.</p>]]></content:encoded></item><item><title><![CDATA[Unleashing the Plumbing Superhero: Fixing a Memory Leak caused by Emotion, Chakra-UI, and Dynamic Props!]]></title><description><![CDATA[This blog post explores a memory leak caused by Emotion, Chakra-UI, SSR, and dynamic props in web development. It covers their significance, impact, detective work, solution, and impressive results.]]></description><link>https://engineering.deptagency.com/unleashing-the-plumbing-superhero-fixing-a-memory-leak-with-emotion-chakra-ui-and-dynamic-props/</link><guid isPermaLink="false">64626d812ff13b0001a0f7ad</guid><category><![CDATA[react]]></category><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Eduardo Brito]]></dc:creator><pubDate>Tue, 16 May 2023 12:04:51 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1523738758777-2cb2360ba0ff?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDV8fHBpcGUlMjB3YXRlcnxlbnwwfHx8fDE2ODQxNzIxNTZ8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1523738758777-2cb2360ba0ff?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDV8fHBpcGUlMjB3YXRlcnxlbnwwfHx8fDE2ODQxNzIxNTZ8MA&ixlib=rb-4.0.3&q=80&w=2000" alt="Unleashing the Plumbing Superhero: Fixing a Memory Leak caused by Emotion, Chakra-UI, and Dynamic Props!"/><p>In the world of web development, frameworks, and libraries continually emerge to enhance user experiences and streamline the development process. This blog post explores the intriguing encounter with a memory leak in a work project, which was attributed to the combination of <a href="https://emotion.sh/docs/introduction?ref=dept-engineering-blog.ghost.io">Emotion</a>, <a href="https://chakra-ui.com/?ref=dept-engineering-blog.ghost.io">Chakra-UI</a>, server-side rendering (SSR), and dynamic props. We will delve into the significance of these technologies, the extent of the memory leak's impact, the detective work involved in identifying the root problem, the solution implemented, and the remarkable results achieved.</p><h3 id="what-is-emotion-chakra-ui-and-dynamic-props">What is <a href="https://emotion.sh/docs/introduction?ref=dept-engineering-blog.ghost.io">Emotion</a>, <a href="https://chakra-ui.com/?ref=dept-engineering-blog.ghost.io">Chakra-UI</a>, and <a href="https://emotion.sh/docs/styled?ref=dept-engineering-blog.ghost.io#object-styles">Dynamic Props</a>? </h3><p>To grasp the essence of the memory leak issue, it is crucial to understand the technologies at play. <a href="https://emotion.sh/docs/introduction?ref=dept-engineering-blog.ghost.io">Emotion</a> is a popular library for styling React components, providing a seamless way to handle CSS-in-JS. <a href="https://chakra-ui.com/?ref=dept-engineering-blog.ghost.io">Chakra-UI</a>, on the other hand, is a powerful React component library that offers a rich set of UI components and styling utilities. <a href="https://emotion.sh/docs/styled?ref=dept-engineering-blog.ghost.io#object-styles">Dynamic props</a> refer to the ability to pass props dynamically to styled components, enabling flexible and interactive styles.</p><h3 id="the-magnitude-of-the-memory-leak">The Magnitude of the Memory Leak</h3><p>We had been dealing with this leak for quite a while, and it never crossed our minds that a styling tool could be the root cause. It's interesting to note that we had two projects with the same code but different languages, and only one of them leaked. The only noticeable difference between the two was the amount of requests hitting the server.</p><p>On Server 1, where the leak occurred, we saw an average of 10,000 requests every 5 minutes. Meanwhile, Server 2, which didn't leak, handled only around 1,000 requests in the same timeframe.</p><p>Realizing this disparity has made us reconsider the impact of the styling tool on the issue. It's surprising how something seemingly unrelated could be the culprit behind the leak.</p><p>Here's what the Server 1 memory usage graph looked like:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/CleanShot-2023-05-15-at-17.36.20.png" class="kg-image" alt="Unleashing the Plumbing Superhero: Fixing a Memory Leak caused by Emotion, Chakra-UI, and Dynamic Props!" loading="lazy" width="1503" height="420" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/CleanShot-2023-05-15-at-17.36.20.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/CleanShot-2023-05-15-at-17.36.20.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/CleanShot-2023-05-15-at-17.36.20.png 1503w" sizes="(min-width: 720px) 720px"/></figure><h3 id="unraveling-the-root-problem">Unraveling the Root Problem:</h3><p>Determined to identify the root cause, I decided to adopt the strategy outlined in <a href="https://alberic.trancart.net/2020/05/how-fixed-first-memory-leak-nextjs-nodejs/?ref=dept-engineering-blog.ghost.io">this post</a>. Taking inspiration from it, I proceeded to stress test a production build instance by running it locally using Artillery, simulating a small number of users per second.</p><p>To my surprise, I observed a peculiar behavior during the stress test. With every page refresh, the value of 'Internal Node' consistently increased. However, at this stage, I couldn't definitively conclude whether emotion was the underlying issue responsible for this behavior.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/image.png" class="kg-image" alt="Unleashing the Plumbing Superhero: Fixing a Memory Leak caused by Emotion, Chakra-UI, and Dynamic Props!" loading="lazy" width="1958" height="444" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/image.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/image.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/05/image.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/image.png 1958w" sizes="(min-width: 720px) 720px"/></figure><p>Excited by the progress made, I proceeded to isolate the problem further. To do so, I recreated a specific component using <a href="https://vanilla-extract.style/?ref=dept-engineering-blog.ghost.io">vanilla-extract</a>. I then rendered this component multiple times using different strategies - once with vanilla-extract and again with emotion.</p><p>The results were telling. The pattern repeated itself consistently. Irrespective of the number of renderings (whether it was 10, 50, 100, or even 1000), when using emotion, a substantial amount of memory was allocated to InternalNode.</p><p>These findings strongly suggest that emotion was indeed the root cause of the memory consumption issue we've been facing.</p><p>With the source of the leak now identified, my focus shifted toward understanding the underlying reason behind it. To delve deeper into the issue, I conducted a series of tests using the emotion. I explored three different styling strategies: utilizing the <a href="https://legacy.reactjs.org/docs/dom-elements.html?ref=dept-engineering-blog.ghost.io#style">style prop</a> (from React), the <a href="https://emotion.sh/docs/introduction?ref=dept-engineering-blog.ghost.io">css prop</a>, and dynamic props.</p><p>And boom! A breakthrough was made as I analyzed the results of the tests. Among the three styling strategies employed, one of them stood out by consistently exhibiting the pattern we had been observing. That culprit was none other than dynamic props!</p><p>It became evident that the usage of dynamic props with emotion was responsible for triggering the memory leak.</p><p><strong>But what about chakra?</strong><br>Chakra UI builds upon the emotion as its foundation, it would be necessary to address this aspect as well to fully resolve the memory leak issue.</br></p><h3 id="the-solution">The Solution</h3><p>Now comes the challenging (and costly) part: considering the entire project's reliance on the emotion and chakra libraries, removing them would be an extensive and time-consuming undertaking.</p><p>Therefore, we devised a three-step solution to address the memory leak issue:</p><ul><li><strong>Step 1 (short-term solution):</strong> Implement a cache layer, such as <a href="https://learn.microsoft.com/en-us/azure/frontdoor/front-door-overview?ref=dept-engineering-blog.ghost.io">Azure Frontdoor</a>. By introducing this caching mechanism, we could mitigate the impact of the memory leak while buying ourselves some time for further resolution.</li><li><strong>Step 2 (short-term solution):</strong> Transition all styles currently implemented through dynamic props to inline styles. Although this step will help reduce the memory leak, it may introduce code smells and increase code complexity due to the manual placement of styles within the markup.</li><li><strong>Step 3 (long-term solution):</strong> Commit to removing emotion and chakra from every project and design system. Instead, we will adopt a new zero-runtime solution that eliminates the possibility of memory leaks arising from styling issues. This comprehensive change will require careful planning and execution but will ultimately ensure a more reliable and robust system.</li></ul><p>By following these steps, we can address the memory leak problem, implement short-term mitigation measures, and pave the way for a long-term solution that eliminates the potential for future memory leaks by adopting a new styling approach.</p><p>In our specific project, we decided to deviate from the recommended approach and proceeded directly to step 3, bypassing step 2. However, it's important to note that step 2 remains a viable option for addressing the memory leak.</p><p>By implementing step 2 and transitioning the styles from dynamic props to inline styles, we can potentially reduce the memory leak while maintaining the use of the emotion and chakra libraries. However, it's worth considering that this approach may introduce code smells and increase code complexity due to the manual placement of styles within the markup.</p><p>Ultimately, the decision to skip or include step 2 depends on the specific circumstances, project constraints, and trade-offs involved. It's crucial to assess the potential impact on code quality, development time, and maintenance overhead when determining the most suitable solution for addressing the memory leak in your project.</p><h3 id="results">Results</h3><p>Now, let's dive into the results! As the fix is still a work in progress, we are yet to obtain the actual server results. However, the results from the stress test conducted thus far are undeniably impressive.</p><p>To stress test the server and evaluate the impact of our fix, I once again utilized Artillery, employing three distinct scenarios:</p><ul><li>Warming up: 60 seconds with 30 users per second.</li><li>Ramping up: 240 seconds with 50 users per second.</li><li>Sustained load: 500 seconds with 100 users per second.</li></ul><p>Each user followed the same flow: accessing the homepage, waiting for 1 second, accessing a secondary page, waiting for 2 seconds, and then leaving.</p><p>Here are the summarized results:</p><ul><li><strong>Production Average Data:</strong><br>Memory Usage: 1.3GB <br>Requests: 500 requests per minute (during the period of the test)</br></br></li><li><strong>Stressed Server Average Data (Before Fix):</strong><br>Memory Usage: 420MB <br>Requests: 2 requests per minute (yes, only two!)</br></br></li><li><strong>Stressed Server Average Data (Scenario 1):</strong><br>Memory Usage: 250MB <br>Requests: 1.5k requests per minute</br></br></li><li><strong>Stressed Server Average Data (Scenario 2):</strong><br>Memory Usage: 258MB <br>Requests: 3k requests per minute</br></br></li><li><strong>Stressed Server Average Data (Scenario 3):</strong><br>Memory Usage: 450MB <br>Requests: 5k requests per minute</br></br></li></ul><p>Comparing the stressed server's average data before the fix to the production environment, we achieved the following improvements:</p><h4 id="memory-usage">Memory Usage:</h4><ul><li>Scenario 1:<strong> 80% </strong>reduction compared to production.</li><li>Scenario 2: <strong>80%</strong> reduction compared to production. </li><li>Scenario 3: <strong>65%</strong> reduction compared to production.</li></ul><h4 id="requests-per-minute">Requests per Minute:</h4><ul><li>Scenario 1: <strong>200%</strong> increase compared to production. </li><li>Scenario 2:<strong> 500%</strong> increase compared to production. </li><li>Scenario 3:<strong> 900%</strong> increase compared to production.</li></ul><p>These impressive improvements demonstrate the effectiveness of our fix in mitigating the memory leak issue and enhancing the server's performance under stress.</p><h3 id="conclusion">Conclusion</h3><p>In the realm of web development, we embarked on a thrilling memory leak investigation, uncovering a hidden culprit lurking within Emotion, Chakra-UI, SSR, and dynamic props. Armed with determination and a three-step solution, we set out to crack the case and restore harmony to our codebase. With our detective hats firmly in place, we left no stone unturned, leading to remarkable results that left us in awe. As the memory leak faded into oblivion, our server danced with joy, boasting reduced memory usage and soaring performance.</p><p>In this thrilling tale, we learned that even the most unexpected suspects can be behind mysterious bugs. Armed with perseverance and the right tools, we can uncover their secrets and bring stability back to our projects. So, fellow developers, remember to stay curious, keep investigating, and code happily ever after, knowing that no bug can escape your determined gaze.</p>]]></content:encoded></item><item><title><![CDATA[Composable architecture - How is this any different from microservices?]]></title><description><![CDATA[What’s the buzz all about Composable Architecture? Isn’t that just microservices?]]></description><link>https://engineering.deptagency.com/composable-architecture-how-is-this-any-different-from-microservices/</link><guid isPermaLink="false">645bf64b2ff13b0001a0f591</guid><category><![CDATA[Process]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Danny Paterson]]></dc:creator><pubDate>Mon, 15 May 2023 13:05:57 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/composable-architecture-concept.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/composable-architecture-concept.jpg" alt="Composable architecture - How is this any different from microservices?"/><p>There's a lot of buzz in the tech industry about Composable Architecture. At DEPT®, we're explicitly seeing this term surface in the <a href="https://kinsta.com/knowledgebase/content-management-system/?ref=dept-engineering-blog.ghost.io">content management systems (CMS)</a>, e-commerce, and marketing technology (Martech) spaces.</p><p>So what is it? And what's all the fuss about? After all, it's a given that in software engineering, we try to make our architectures composable and design software modules that are easy to interchange and move around. </p><p>Isn't that what microservices are?</p><p>Composable Architecture (capital C, capital A) is a new-ish term that encompasses many of these principles but generally applies to specific types of off-the-shelf software. You can also apply the principles to content design and even user experience design. </p><p>Let's dive in more to see how this new term breaks down compared to what we usually do as software engineers, designers, and architects. Then, once you understand the concepts, we'll see how they apply to e-commerce and CMSs.</p><h2 id="what-is-composable-architecture-how-is-it-different-from-microservices">What is Composable Architecture? How is it different from Microservices?<br/></h2><p>Composable Architecture is a method of designing your software modularly, breaking down complex components of your architecture into small, reusable pieces--something <a href="https://www.deptagency.com/service/engineering/?ref=dept-engineering-blog.ghost.io">software engineers</a> always strive for.</p><p>But specifically, the term is usually applied to off-the-shelf software solutions to indicate they're built with modern standards and can easily fit into your overall system architecture.  </p><p>Why is this a big deal? If you're asking this question, consider yourself lucky. Old vendors usually build tools with walled gardens, so moving data around, getting data in, or cooperating with other systems takes a lot of work. </p><blockquote>To make it short and sweet: <strong>Composable Architecture applied to these areas indicates to potential customers that an off-the-shelf system is engineer-friendly. YAY! 🥳</strong></blockquote><p>Systems that subscribe to Composable Architecture principles usually mean that your vendor solution can be composed, extended, and recomposed without long development cycles. In addition, you'll be able to create separate APIs, applications, functions, and micro UIs to separate responsibilities and makes management and dependency between them easy. </p><p>This approach introduces additional monitoring levels and allows you to move business logic into a reusable process for all other applications. This is the basic concept of the MACH (Microservices, API-first, Cloud-Native, Headless) Architecture approach (We at DEPT® are big fans of the MACH approach! <a href="https://www.deptagency.com/insight/why-we-joined-the-mach-alliance/?ref=dept-engineering-blog.ghost.io">DEPT is part of the MACH alliance</a>).</p><p>The term and its principles are applied in less technical areas to involve good engineering concepts in those disciplines. Some good examples are content modeling for CMSs and how to break down user interface concepts.</p><p>Let's walk through theoretical architecture using Composable Architecture principles in more detail to understand our discussion better.</p><h2 id="example-architecture-walkthroughe-commerce">Example architecture walkthrough - e-commerce</h2><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/commerce-concept.jpg" class="kg-image" alt="Composable architecture - How is this any different from microservices?" loading="lazy" width="900" height="419" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/commerce-concept.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/commerce-concept.jpg 900w" sizes="(min-width: 720px) 720px"/></figure><p>Let's say you're building an e-commerce system. It's comprised of the following:</p><ol><li>Marketing content (to show you the goods)</li><li>e-commerce (to sell you the goods)</li><li>Logistics (to ship you the goods)</li></ol><p>An older approach to this system might be to have a couple of frontends, say mobile and web, and a backend API to service requests from the front ends.</p><p>In Composable Architecture principles, you'll first break down the system's responsibilities into smaller independent components. Separating the functional components into microservices allows better management of individual services vs. a monolith API or application. In addition, it provides for independent release cycles and enhancement without affecting other services and a shared codebase. This design also supports the integration of other third-party SaaS or on-premise products that may be part of your overall digital ecosystem. </p><p>For instance, in the case of e-commerce, you might have a set of services as defined below:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-64ee1262-56c9-429e-8f6f-33c39c9ccb0f.png" class="kg-image" alt="Composable architecture - How is this any different from microservices?" loading="lazy" width="1280" height="804" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/data-src-image-64ee1262-56c9-429e-8f6f-33c39c9ccb0f.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/data-src-image-64ee1262-56c9-429e-8f6f-33c39c9ccb0f.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-64ee1262-56c9-429e-8f6f-33c39c9ccb0f.png 1280w" sizes="(min-width: 720px) 720px"/></figure><p>However, simply saying "MICROSERVICES!" is not a silver bullet. </p><p>Interdependent service calls are inevitable and can get complex quickly. To avoid this, you can compose your system with another level of abstraction using a message bus or message queue system. This is particularly useful in a system that is transactional, such as e-commerce, or has multiple integrations. </p><p>Adding this additional layer allows microservices to be genuinely independent, raise events for transactions or errors that other services can subscribe to, and react accordingly. It also means you provide a mechanism to allow change. Even though transactional, the whole system can introduce new services to subscribe to events or change core services for new products as your business needs grow.</p><p>A version of the e-commerce system with a message bus might look like this:<br/></p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-aad6ec7a-013e-4e90-a9bd-2ce89715dc8c.png" class="kg-image" alt="Composable architecture - How is this any different from microservices?" loading="lazy" width="1280" height="659" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/data-src-image-aad6ec7a-013e-4e90-a9bd-2ce89715dc8c.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/data-src-image-aad6ec7a-013e-4e90-a9bd-2ce89715dc8c.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-aad6ec7a-013e-4e90-a9bd-2ce89715dc8c.png 1280w" sizes="(min-width: 1200px) 1200px"/></figure><h2 id="going-furtherbreaking-down-a-cms-into-composable-architecture-principles">Going Further - Breaking down a CMS into Composable Architecture Principles<br/></h2><p>We have seen a trend in many modern CMSs that subscribe to Composable Architecture principles. These CMSs allow easy integration with third-party vendors for <a href="https://www.bigcommerce.com/ecommerce-answers/what-is-ecommerce/?ref=dept-engineering-blog.ghost.io">e-commerce</a> or <a href="https://brandfolder.com/resources/what-is-digital-asset-management/?ref=dept-engineering-blog.ghost.io">Digital Asset Management (DAM) systems</a>. Most older CMSs required custom code strapped into their monolith or (god forbid 🙀) professional services engagements to do any significant customization. These newer systems expose ready-to-use extension points to customize the experience to the customer's needs. </p><p>MACH using asynchronous workloads is an excellent example of how to begin to approach a CMS architecture design. Let's translate the concepts discussed in our e-commerce example into a CMS implementation. </p><p>If we look at some core roles of a CMS content delivery solution, we have a list of functions that is something like this:</p><ul><li>Publishing content - Taking content that is is editorially approved and moving that it in the desired form to the content delivery portion of the architecture and data store.</li><li>Delivery of content - A method for channel applications to consume the content. (Note that in marketing and CMS terms we generally refer to “channels” instead of different types of end clients, so we’ll keep referring to them that way for the rest of this example).</li><li>Caching content - A system to provide high-performance responses for content in high-traffic load environments.</li><li>Dynamically Query and Search content - The ability to surface content dynamically or provide site search capabilities.</li></ul><p>If we translate these roles into a MACH architecture we get something like the following diagram:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-12a1e5c8-6c2f-4572-8c1b-1755251b4473.png" class="kg-image" alt="Composable architecture - How is this any different from microservices?" loading="lazy" width="1600" height="668" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/data-src-image-12a1e5c8-6c2f-4572-8c1b-1755251b4473.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/data-src-image-12a1e5c8-6c2f-4572-8c1b-1755251b4473.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-12a1e5c8-6c2f-4572-8c1b-1755251b4473.png 1600w" sizes="(min-width: 1200px) 1200px"/></figure><p>Let's walk through it:  </p><p>The CMS publish will trigger an event that our indexing API will consume. The indexing API will pull the published content from the CMS content delivery data store (usually via an API) and store it in our search index. </p><p>There are multiple advantages to this approach:</p><p>First, <strong>we get to transform the content we retrieved into a data contract</strong> for our business and abstract the clients/channels from the CMS. </p><p>This is a key design feature when you think about composable architecture. If you have a CMS dependency directly in your channel applications (Web, Mobile App, Kiosk, etc.) and change the CMS, all your channel apps need to be updated, which is a huge undertaking. Creating the content management orchestration as defined here removes that obstacle. It also means you can change your CMS or even add multiple content hubs to your ecosystem, and none of the apps or the content API should notice a change.</p><p>Second, <strong>we can implement secondary events</strong> from our indexing and trigger cache layer updates, which provides complete redundancy from the CMS platform. If the CMS fails to operate, you have an independent content store to run the content delivery system. We then have a content API that distributes the content to channels by endpoints allowing querying and fetching content differently. This break up of different content indexing, caching, and retrieval services makes the system more manageable and can keep logic for these workloads isolated and reusable.</p><p>Another feature of this design is that even though we have a distributable cache, <strong>you can provide additional cache layers at the application levels.</strong> Because we have an event triggered on indexing and publishing completion, other integrators can listen to the same events and handle cache expiration and other native feature if they want to.</p><p>You can achieve an additional layer of composability to the system by exposing your content service via an orchestration layer rather than integrating it directly with your applications or partners.  </p><p>That might look something like this:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-ea8fb594-0fea-4059-8ac7-c9d980629e96.png" class="kg-image" alt="Composable architecture - How is this any different from microservices?" loading="lazy" width="1600" height="1316" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/05/data-src-image-ea8fb594-0fea-4059-8ac7-c9d980629e96.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/05/data-src-image-ea8fb594-0fea-4059-8ac7-c9d980629e96.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/data-src-image-ea8fb594-0fea-4059-8ac7-c9d980629e96.png 1600w" sizes="(min-width: 1200px) 1200px"/></figure><p>This provides a few significant benefits:</p><p><strong>No direct application access to the content service</strong>. This allows you to change the origin of the service to newly updated providers as you need without worrying about direct integration issues. In an omnichannel business (meaning one with lots of different types of channels), this is incredibly important. You don't want to have to roll out changes to your mobile app, kiosks, website, and other IoT applications just due to one provider change. Centralizing this in your orchestration layer allows you to make changes in one place while keeping your channels unaffected.</p><p><strong>Apply additional data transform</strong> - Each integrator will have their own data contract and unique needs for the content data that their system abides by. Integrating the content service directly into your applications and channels means you must change those channels when the core API changes. In addition, you have to handle backward compatibility and roll out with the integrators as updates and breaking changes occur. This adds pressure to your development teams and lifecycles that can interrupt your general business flow. Having the orchestration layer apply data transformation to the source contracts means the integrators own those contracts. Regardless of the source, you will have complete control over your data going in and out of your system, and the abstraction layer can handle transforms when data interaction is needed. </p><p><strong>Unique Content Querying Requirements </strong>- Not all of your integrators will want to consume and query the content similarly. Adding an orchestration layer provides a system of separation for integrators to have control. In addition, it prevents the core content delivery API from being constantly developed to support new ways of querying and inventing new ways to support requirements like GraphQL.</p><p><strong>Caching and Extension</strong> - Some third-party or internal integrators may need faster response times for certain content items. Or integrators may have special requirements. Using your orchestration layer to handle additional caching, data storage, and extending existing capabilities gives you fault tolerance and areas of decoupling you wouldn't have with direct integrations. As with the above points, you also move the responsibility for these edge cases out of your core design and services.</p><p><strong>Cross-Service Communication</strong> - All cross-service communication can be proxied through the orchestration layer. This abstracts interdependent services from becoming tightly coupled and allows the advantage of a single load-balanced endpoint for clustered services, centralized monitoring, and observability of inter-service calls. Even if the calls are passthrough, this is still advantageous. </p><p>The above approaches provide layers of protection, scaling, and flexibility to compose and recompose a system without the headache. As a result, teams can be more agile when managing system components and have completely independent work streams to help make your business more agile.</p><h2 id="wrapping-up-composable-architecture">Wrapping up composable architecture </h2><p>We used CMSs to illustrate many of our points. It's critical to remember that not all CMS solutions support the design patterns and approaches mentioned above. Adopting some or all of them will improve your ecosystem. </p><p>Hopefully, this article helped clear up a vague term for you and walked you through some sound engineering principles. Stay composable!<br/></p>]]></content:encoded></item><item><title><![CDATA[Demystifying Layer 2 solutions: Why you should consider them when building on Ethereum]]></title><description><![CDATA[With over one million transactions being processed per day, a new block is created every 12 to 14 seconds and Ethereum has now reached its present capacity on the network. This is where layer-2s jump in.]]></description><link>https://engineering.deptagency.com/layer-2-solutions-critical-tool-for-businesses-looking-to-incorporate-web3-technology/</link><guid isPermaLink="false">645bf16c2ff13b0001a0f546</guid><category><![CDATA[web3]]></category><dc:creator><![CDATA[Victor Sizaret]]></dc:creator><pubDate>Fri, 12 May 2023 14:30:10 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/layers-concept.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/layers-concept.jpg" alt="Demystifying Layer 2 solutions: Why you should consider them when building on Ethereum"/><p>Making sure customers have a smooth and fun ride is a big deal for businesses, and that's no different in the world of blockchain. Though currently, the user experience often tends to lean disorganized and fragmented.</p><p> Your average Joe or Jane might feel overwhelmed by the complicated interfaces and may even require technical experience to simply understand what is actually happening. This combined with high fees and slow transaction times makes it difficult for businesses to provide customers with a seamless and cost-effective <a href="https://www.deptagency.com/web3/?ref=dept-engineering-blog.ghost.io">Web3 experience</a>, ultimately impacting brand reputation and customer loyalty.</p><p>Whether you’re building a loyalty 3.0 program, developing an on-chain video game, or implementing crypto-micropayments in your business model, layer-2s may be the solution to making your projects work better and faster. </p><p><em>But before we dive into layer-2s, be sure to take time to grasp the basic ideas of <a href="https://ethereum.org/en/developers/docs/intro-to-ethereum/https://ethereum.org/en/developers/docs/intro-to-ethereum/?ref=dept-engineering-blog.ghost.io"><strong>blockchain and decentralized networks</strong></a>. </em></p><h3 id="in-a-perfect-world-blockchains-would-be-totally-decentralized-secure-and-scalable">In a perfect world, blockchains would be totally decentralized, secure, and scalable. </h3><p>Realistically, a blockchain can only have two of these three properties at a time. Vitalik Buterin, the co-founder of Ethereum, dubbed this the "blockchain trilemma." Ethereum’s fame is driven by its decentralized and security factors, but this comes at the cost of high transaction fees. If you’ve ever traded an NFT or engaged in DeFi protocols, chances are you paid a hefty fee.</p><p>The goal is to solve the trilemma without discarding decentralization, which is where layer-2s act as the hero solution. These scalability enablers come in all sorts of shapes and sizes, with the major categories being the so-called optimistic rollups and zk-rollups. For now, you can think of all these fancy terms as tools that make it possible for the Ethereum network to welcome more transactions without emptying your bank account.</p><h3 id="how-do-they-work">How do they work?</h3><p>Layer-2s are blockchains that sit on top of the base blockchain (e.g. Ethereum) — hence the word “layers.” They constantly communicate with the Ethereum chain by sending transactions. But how do they scale transactions? </p><p>Because transactions that demand high computational power also have higher fees, the solution is to refrain from doing these computations on the base chain. Instead, the computation of transactions can be done off-chain, meaning they are processed in a parallel blockchain: the layer-2. </p><p>In this case, the base chain provides the security features while the layer-2 does the majority of the work, enabling speedy and affordable transactions, plus the ability to handle a high volume of transactions per second. Then, multiple off-chain transactions can be combined into a single layer-1 transaction, massively reducing transaction fees.</p><p>Here’s a simple way to think of it: Picture 100 people trying to drive on a one-lane highway all in separate cars, at the same time. Traffic. Now imagine how much more efficient this scene would be if the cars were replaced by 10 buses carrying 10 people each. </p><p>In addition to increased efficiency, layer-2s also have the benefit of security. Layer-2s settle their transactions on Ethereum, which is already pretty secure as a network. Given these factors, it’s  much easier for businesses to start using the Ethereum network for projects when operating through layer-2s.</p><h3 id="the-need-for-layer-2s">The need for Layer-2s</h3><p>Transactions fees vary based on the level of demand on the network. As more users attempt to add their transactions to the upcoming base-network block, the cost of those transactions goes up. With over one million transactions being processed per day, a new block is created every 12 to 14 seconds and Ethereum has now reached its present capacity on the network. This is where layer-2s jump in.</p><p>Layer-2s work like a gadget that grabs a bunch of transactions and bundles them into one slick base-chain transaction. Then, the Ethereum fee gets divided up among all the players involved, sort of like how a group of people on a highway would split the cost of the bus. Currently, the most popular choice for scaling Ethereum using layer-2 solutions is called rollups. The two most common types of rollups are: </p><ul><li>Optimistic</li><li>Zero-knowledge <br/></li></ul><p>While their functionalities are similar (computation is performed off-chain and transactions are bundled in a bag), they differ in their approaches to validating transactions. This enables different benefits such as withdrawal speed or transaction latency. </p><p>Comparing optimistic rollups and zero-knowledge rollups, the former is considered to be more user-friendly, cheaper, and simpler to program. However, it comes with a drawback of a delay in transaction finalization and relies on users to verify the accuracy of the transaction. On the other hand, zero-knowledge rollups offer immediate withdrawals and guarantee authenticity, but they come with higher costs and require clear cryptographic proof to function properly.</p><p>Optimistic rollups cover around 85% of the market (in terms of Total Value Locked) and include Optimism and Arbitrum. Some neat examples of zero-knowledge rollups are Polygon zkEVM, Starknet, zkSync Era, and Immutable X.</p><h3 id="the-ethereum-virtual-machine-evm-compatibility">The Ethereum Virtual Machine (EVM) compatibility</h3><p>Zero-knowledge rollups use different technologies to prove transactions (zkSNARKs, zkSTARKs), and some are compatible with the Ethereum Virtual Machine (EVM), which means they can run Ethereum smart contracts. Think of it like two people who speak the same language and can easily understand each other. This compatibility makes it easier for developers to create and deploy the same smart contracts on different EVM-based blockchains without making major changes to their code.</p><p>The Polygon zkEVM rollup aims to be EVM compatible so that Ethereum-based applications, tools, and infrastructure can be easily migrated to Polygon zkEVM with minimal changes. Polygon zkEVM can handle Solidity code directly, which is the most commonly used language for developing smart contracts on Ethereum. This means that almost all code from pre-existing EVM chains can be transferred to the new chain without needing to rewrite any code.</p><p>Similarly, zkSync Era is also EVM compatible and can accept EVM languages like Solidity, Vyper, and Yul. It uses its custom virtual machine to run the code, which makes it easy for developers to move their existing EVM smart contracts to zkSync Era without having to rewrite them in a different language or tool.</p><p>However, some rollups are not EVM compatible, like Starknet, which uses a unique programming language called Cairo. This means that you can't use the same language you would use for Ethereum on Starknet. Cairo was designed specifically to create Starknet programs to optimize the proving performance of each transaction execution. Transactions written in Cairo go through a transpiler that translates them to Solidity so Ethereum can understand the activity done on Starknet. This is like two people who speak different languages and require translation to understand one another.</p><h2 id="what-does-this-mean-for-you">What does this mean for you?<br/></h2><p>Layer-2 solutions are a critical tool for businesses looking to incorporate Web3 technology into their operations. </p><p>Visa, for example, has partnered with Starknet to enable customers to pay bills using their digital assets while maintaining control over them. This is possible due to account abstraction, a feature that treats every cryptocurrency account as a smart contract, eliminating the need for private keys, seed phrases, and other cumbersome security measures — ultimately resulting in a more user-friendly experience.</p><p>Layer-2s also improve the user experience for cryptocurrency traders by providing the comfort of Web2 apps with the security and decentralization of Web3 apps when handling digital financial assets. Decentralized exchanges like Uniswap and 1inch have deployed on Layer-2 solutions like Arbitrum, Polygon, and Optimism to offer transaction scaling and account abstraction.</p><p>Gaming is another industry that's rapidly adopting blockchain technology. Platforms like TreasureDAO enable developers to build games on Arbitrum, while games like Influence utilize Starknet's computing power to run on-chain. ImmutableX, a popular zk-Rollup for gaming platforms, has partnered with Starkware's StarkEx to reduce transaction costs and improve user experiences.</p><p>Layer-2 solutions are essential for any brand building on Ethereum, as they can help avoid issues like slow transactions, poor customer experiences, high development costs, and fluctuating transaction fees. Layer-2 solutions like Starknet and zkSync Era offer account abstraction features that can solve common DeFi problems like insufficient token balances and rejected smart contracts, making them a valuable tool for building successful projects and providing users with next-level experiences. </p><p>By embracing the power of Layer-2s, businesses can revolutionize their operations.<br/></p>]]></content:encoded></item><item><title><![CDATA[Which jobs are the AIs coming for?]]></title><description><![CDATA[I want to walk through a framework I use to understand how the AI work I've done and supported over the years impacts my coworkers. ]]></description><link>https://engineering.deptagency.com/which-jobs-is-ai-coming-for/</link><guid isPermaLink="false">6457ae153a6d4c00014b7d54</guid><category><![CDATA[AI]]></category><category><![CDATA[People]]></category><dc:creator><![CDATA[David Grover]]></dc:creator><pubDate>Tue, 09 May 2023 13:57:18 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/shutterstock_644724364.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/05/shutterstock_644724364.jpg" alt="Which jobs are the AIs coming for?"/><p>I want to walk through a framework I use to understand how the AI work I've done the years impacts my coworkers. I build these systems because the people I work with benefit from them, and in the main that's why other people build these systems. There are nefarious reasons why people would build AIs too. But most people won't see nefarious AIs, they'll see something built by someone like me to theoretically make their job easier. </p><p>This isn't a technical discussion, and I'm definitely not proposing an ethical framework. My framework is about the kinds of decisions AIs are designed to automate. What we expect out of <em>intelligence </em>is good judgment, which means good reasons and a valid conclusion. An <em>artificial intelligence </em>is subject to the same standards. We want a useful prediction, at least, and ideally good reasons too. This post is about how different jobs use different kinds of reasoning, and which kinds of reasoning are easier to automate.</p><p>We automate judgment when we think a machine can do it more reliably than a human. Judgments can be hard to make for a lot of reasons. Making the same three judgments about the same list of cases day after day is tedious and repetitive. Machines don't get bored and make the wrong choice. This is why we trust machines to make judgments: They won't get bored and/or creative at the critical moment of judgment. They aren't alive, and so they just don't care how boring or tedious the judgment is.</p><p>Humans are amazing generators of predictions and theories. We're also often unreliable, unpredictable and in my own experience we tire pretty easily. Machines get to finer precision, analyze data faster, and with signals humans simply can't read without a machine for help. When we automate video recommendations, for example, we can feed far more records of preferences into an AI than we could into a human. This doesn't make the AI better than a human at all of the kinds of judgments you might need to make a <em>good</em> choice. But the AI is way better at matching the preferences of people that meet certain criteria and finding suggestions you haven't seen. Even the best neighborhood video store employee could only manage preferences for their 1000-or-so most-regular patrons. While I find that skill admirable and you may think its a head full of useless trivia, either way its a fact a machine can now usually do that kind of recommendation better.</p><p>There are lots of species of judgment: Predictions, recommendations, explanations, forecasts, hypotheses and deductions are just a handful of examples. The study of good and bad reasoning is historically part of Logic, which identifies three kinds of reasoning: Inductive, Deductive and Abductive, each of which I'll describe below. Complex judgments like scientific theories, great works of art, or big software systems will use all three kinds of reasoning. Humans are good at fluidly moving back and forth between each kind of logic, plus probably a couple other kinds of inference we haven't spotted yet. AIs are built to simulate these kinds of reasoning using software. Every problem we use an AI for requires a mix of logics to create an output a human would agree is a good judgment, too.</p><p>For example, sometimes a problem may require a lot of inductive reasoning and a little deduction to reach a conclusion a human would agree is a good one. Other times the problem may be deeply abductive, and the simulation requires a whole lot of as-yet-undiscovered software voodoo to get a good judgment. Or the problem may be just flat-out deductive from start to finish. </p><p>I want to make a final point about judging hard and easy intellectual work using your own models of intelligence. For most of European history the supreme examples of "intelligence" were (a) axiomatic geometry, and (b) chess, followed closely by (c) conjugating Latin verbs. All three are excellent examples of deductive inference. More recently, standardized tests from the SATs to IQ became "measures of intelligence." Those tests also prioritize deductive reasoning skills. Its fair to say we hold deductive inference in high esteem. And we should; its hard. But its not always as hard as the other two known kinds of inference. </p><p>Any given inference problem may require more than one kind of reasoning. Deduction may be useless or useful in any given case, whether its considered the paradigm of good judgment by tenured professors of Logic or not. Inductive skills were once rejected by those professors as just being "good at guessing," and abduction was witchcraft or sorcery or madness or architecture. Consider how random that is as we review my work history.</p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="inductive-dave">Inductive Dave</h2><h3 class="kg-header-card-subheader" id="telemarketer">Telemarketer</h3></div><p>Someone makes an inductive argument when they claim a general statement is probably true because a lot of similar specific statements are also true. The classic example of an inductive argument is "The likelihood the sun will rise in the East tomorrow is 100% because the Sun has risen in the East every day of my life so far." Inductive arguments are good when you know how many relevant samples you need to win the argument, and you have that many or more. They're bad when you don't have enough.</p><p>One of the first jobs I had as an undergraduate student was working for a professional fundraiser. Charities and NGOs hire professional fundraisers to solicit the public for donations. In 1989, when I had this job, we called people on the phone.</p><p>Inductive Dave's job was to carefully rip a sheet of paper out of the phone book and call each person on the sheet until (1) they donated money or (2) they said no or (3) I called three times and got no answer. I dialed a number, read an approved script, and more-or-less answered any questions the person on the other end of the line might have. If they agreed to donate, I'd suggest some numbers, and then note their name and eventual agreed donation down and move on. If they said no, I'd make a note and move on. If they didn't answer, I'd make a note and move on. In the worst-case scenario the person on the other end got abusive, and I'd hand the call to my supervisor and move on.</p><p>In 1989 this work was all manual. Much of it has now been automated away completely. Computers dial the number, read the pitch, and based on the answer, respond accordingly. The computer can also securely process the donation: My coworkers often wrote the donation amount down wrong, somehow pocketed the money themselves, or just plain messed up the final piece of the transaction. We've also automated the lists, so people can opt-out or be subscribed to lists for certain charities. This ensures people who get calls are more likely to want them, and more likely to donate.</p><p>Inductive Dave really didn't like this job. For one thing, there's not a lot of creativity allowed. I would try reading the pitch in different accents or with different emphases but as the evening wears on the enjoyment is limited. Our assumption that three tries was enough to remove someone from the list was basically just a default. There were more than enough people to call and me and my coworkers often crossed people off after just one or two missed calls. And finally, it was emotionally hard. Some nights you'd have a run of people happy to donate, and other nights everyone you called was outraged.</p><p>Turnover was high. People lasted a few months and then tired of the tedium and emotional abuse. I learned to hate phone calls.</p><p>Jobs like Inductive Dave's are obvious candidates for an AI, and have been for decades now. They involve very simple inferences. A machine doesn't particularly care that some of the premises might have been presented in an abusive way, either.</p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="deductive-dave">Deductive Dave</h2><h3 class="kg-header-card-subheader" id="research-assistant">Research Assistant</h3></div><p>Someone makes a deductive argument when they claim a statement is true because that statement is a specific consequence of more general relevant statements already held to be true. The paradigm example is the syllogism, "All humans are mortal, and Socrates is a human, therefore Socrates is mortal." Generally deductions proceed from general statements to specific ones.</p><p>When I was an undergraduate a professor in the Philosophy department was writing a book on how scientific theories evolve, and he asked me to be his research assistant. My job was to read a large collection of books and articles and provide a weekly summary of the results. </p><p>Every week he'd give me a list of stuff to read that was relevant to a specific chapter. Some weeks he was worried about <a href="https://www.geo.arizona.edu/Antevs/ecol438/lect02.html?ref=dept-engineering-blog.ghost.io">how biogeography had evolved</a> to merge elements of biology and geology; other weeks he was more concerned with all the anxiety that <a href="https://www.sciencehistory.org/historical-profile/robert-boyle?ref=dept-engineering-blog.ghost.io#:~:text=Known%20for%20his%20law%20of,versa%E2%80%94the%20famous%20Boyle's%20law.">Boyle's law</a> and the steam pump caused early modern physicists. I'd go into the library, read the texts, and on Friday write him out a summary so we could discuss what I'd found. This job was a lot of fun, and I learned a lot.</p><p>Each science has its own set of terms, assumptions and expectations. I had to learn how the sentences fit together. My boss wanted to know that any conclusions I'd drawn could be assigned to some precursor premises, that I wasn't just making things up, and so I had to document what I found and where. He also wanted to know when terms, conclusions or assumptions crossed disciplines, whether they meant the same thing or changed meaning. There was a schedule too: A bibliography of a specific size, a set of chapters on specific sciences, and a timeline to submit the manuscript and/or the money would run out for research assistants.</p><p>The work was purely deductive. My contribution was to summarize the main points, identify key edge cases, and collect similarities. I was good at deductive inference. I also got tired easily, which is to say distracted by my girlfriend, my schoolwork, the state of the world and weekend ski trips into the Rockies. Sometimes I didn't get my summaries in until Monday, and sometimes they were sloppy and only rarely were they consistent from one week to the next. </p><p>This is precisely the problem a modern LLM is designed to fix. Professor Brown might have been much better off hiring an LLM instead of me to do the work he needed, but since it was 1988 nobody had enough compute.</p><p>Deductive Dave continued on after this job to get even better at deductive inference, studying logic in graduate school. He scored high on various standardized tests, as would be expected from someone with that much training in deductive inference. But by the time Deductive Dave gave up logic school in 1996, there were already AIs that could do a better job at several kinds of deductive inference.</p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="abductive-dave">Abductive Dave</h2><h3 class="kg-header-card-subheader" id="senior-consultant">Senior Consultant</h3></div><p>Someone makes an abductive argument when they claim a statement is likely true because adding the statement to an existing set of truths makes them imply even more than they currently do. Abductive arguments are an <em>inference to the best explanation</em>. The classic abductive argument is Newton's inference of the laws of motion to predict a wide variety of gravitational effects, including apples falling off trees. </p><p>Abductive Dave's job is to help ensure the models junior consultants develop for the clients of the company he works for are big enough to last a couple of years. </p><p>Some of these models are actual machine-learning models designed to automate a sets of inductive judgments, usually for statistical predictions. Some of these models are data models, which reduce business entities to axioms that can be used to prove things about the company's business. And some of these models are expectations about how processes work at the client. In each case, Abductive Dave provides junior consultants with additional hypotheses that make their models more effective. My specific domain of expertise is in models, but my coworkers who design more traditional software systems also do the same kind of abductive work. That is, they make an <em>inference to the best explanation </em>even better by offering their engineers hypotheses about the software solution they're building that make the eventual system better.</p><p>Abductive inference can get repetitive. A friend of mine once suggested that you could replace all the architects with a list of heuristics everyone knows already and some suggestions for priority. At a certain level of abstraction, what Abductive Dave has to contribute is really mostly conventional wisdom. </p><p>Effective abductive inference also requires a good knowledge of the relevant filter criteria, to help choose the right explanations. You can find yourself on the left side of the Dunning-Kruger curve, at the top of Mt. Stupid, if you think your explanation is the best when its really kind of dumb. Abduction requires you know which hypotheses to reject and which are at least plausible. So far there's not much automated help.</p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style="" data-kg-background-image=""><h2 class="kg-header-card-header" id="so-whos-job-is-in-danger">So who's job is in danger?</h2></div><p>The short answer is that everyone's job might be in danger. But it's complicated. We can't assume that because we think a kind of judgment is hard for a human that it'll be hard for a machine. Not just because we get tired and machines don't. Some kinds of logic are just easier to automate. </p><p>Inductive inferences are usually the easiest to automate. We use automated statistical inference everywhere to make reliable judgments, particularly predictions, from seatbelt quality testing to email marketing. Digital transformation projects always eventually replace some manual inductive inferences with automated ones. But even after Inductive Dave watches his job get automated, there's always another set of inductive inferences waiting for his manual effort.</p><p>Excellence in deductive inference was thought until recently to be the hallmark of intelligence. The three main achievements of modern AI - beating Chess and Go grandmasters, automated mathematical theorem proving, and passing the SATs - mean that what we thought was hard turns out to be something a robot can do. What room does that leave for human deductive excellence? If all the deductive work can now be done by robots, what can someone who scores high on IQ tests or plays great chess contribute? A lot of the angst caused by modern AI is because the people with great deductive skills have now discovered they're not so special. But while Deductive Dave is threatened by the new LLMs, he's still persuaded he's the smartest one in the room because of his deductive skills.</p><p>Good abduction requires a mix of induction, deduction and abductive inferences, iterated over time. LLMs like ChatGPT can simulate abduction but only so far: The inferences an LLM draws from its document corpus can look like the AI is adding new truths to the stock of existing ones, and inferring better explanations. But the AI is still only deriving specific conclusions from the general premises contained or implied in it's document source. This can simulate abduction close-enough for some applications. LLMs might prove useful for Abductive Dave, if they can be tuned to point to gaps in junior consultant models, for example. So far, however, the iterative process of making good models better still uses manual judgments.</p><p>Whichever Dave's judgment is simulated, not everything in that argument can be automated. We'd do better to remember that, and be more precise about the work we think a machine should do and the work a human should.</p>]]></content:encoded></item><item><title><![CDATA[Comparison of Web Accessibility Tools]]></title><description><![CDATA[Automated web accessibility testing tools can help you find and fix accessibility issues in your website, ensuring that it's accessible to everyone. In this guide, we'll explore some popular tools for automated web accessibility testing.]]></description><link>https://engineering.deptagency.com/testing-accessibility/</link><guid isPermaLink="false">6440de1cd05a10003d58baaf</guid><category><![CDATA[QA]]></category><category><![CDATA[Accessibility]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Ron Grootaarts]]></dc:creator><pubDate>Tue, 02 May 2023 14:56:17 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1574887427561-d3d5d58c9273?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGFjY2Vzc2liaWxpdHl8ZW58MHx8fHwxNjgxOTc2OTM4&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1574887427561-d3d5d58c9273?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGFjY2Vzc2liaWxpdHl8ZW58MHx8fHwxNjgxOTc2OTM4&ixlib=rb-4.0.3&q=80&w=2000" alt="Comparison of Web Accessibility Tools"/><p/><p>Accessibility will become more important the coming years. The European Accessibility Act (EAA) is a central law for the implementation of the UN Convention on the Rights of Persons with Disabilities (UN CRPD) in Europe. It formulates accessibility requirements according to WCAG level AA for goods and services and thus also makes companies responsible. On June 28 in 2025 most <a href="https://www.deptagency.com/insight/2025-is-the-deadline-for-digital-accessibility/?ref=dept-engineering-blog.ghost.io">companies must ensure</a> that newly marketed products and services covered by the Act are accessible.</p><p><a href="https://www.deptagency.com/service/engineering/?ref=dept-engineering-blog.ghost.io">As software developers</a> we have to make sure that our software related to e-commerce, banking services, services related to air, bus, rail and waterborne passenger transport and access to audio-visual media services such as tv broadcast is easy to use for at least 87 million people.</p><p>For this many people with disabilities, including older people, accessibility is a precondition for participation in society on equal basis. How can we make sure of that as developers and QA?</p><p>There are a couple of tools already out there to help us identify common accessibility problems within our software. In this blog post, we will compare and review some of the most popular web accessibility tools and determine which ones are the best.</p><h2 id="axe">Axe</h2><p>Axe is a free and open-source accessibility testing tool that provides comprehensive testing and reporting for web applications. Axe can be used as a browser extension, command-line tool, or integrated into your continuous integration and deployment process. Axe is powered by the Accessibility Rules Engine, which is a set of rules that can be customized to meet the specific needs of your website.</p><h3 id="pros">Pros</h3><ul><li>Axe provides detailed reports on accessibility issues</li><li>It supports several programming languages, including JavaScript, Ruby, and Python.</li><li>Axe integrates with popular development tools like Chrome DevTools and Jenkins.</li></ul><h3 id="cons">Cons</h3><ul><li>Axe is not a complete accessibility solution and should be used in conjunction with other tools.</li><li>Axe will point out some color contrast issues that still need to be tested manually.</li><li>Axe's comprehensive reports can be overwhelming and difficult to understand for beginners.<br/></li></ul><h2 id="lighthouse">Lighthouse</h2><p>Lighthouse is a free and open-source web performance and accessibility testing tool developed by Google. Lighthouse can be used as a browser extension, command-line tool, or integrated into your continuous integration and deployment process. Lighthouse provides a comprehensive report on web performance, accessibility, best practices, and search engine optimization.</p><h3 id="pros-1">Pros</h3><ul><li>Lighthouse is easy to use and provides comprehensive reports on accessibility issues.</li><li>Lighthouse is integrated with Google Chrome's developer tools and can be used with other browsers.</li><li>Lighthouse can be used to improve other factors like SEO, performance and best practices in addition to accessibility.</li></ul><h3 id="cons-1">Cons</h3><ul><li>Lighthouse's accessibility testing is limited to basic issues. It does use axe-core for accessibility audit, but it does not run the tests that the axe extension does.</li><li>Because of this, Lighthouse can give a 100% accessibility rating while a site is not accessible.</li></ul><h2 id="wave">Wave</h2><p>Wave is a free web accessibility evaluation tool developed by WebAIM. Wave can be used as a browser extension or integrated into your web page. Wave provides a detailed report on accessibility issues and provides solutions for fixing them.</p><h3 id="pros-2">Pros</h3><ul><li>Wave is integrated with popular web browsers and can be used with any web page.</li><li>Wave uses icons on the tested page to point out issues, it’s easy to read</li><li>Wave's reports provide recommendations for fixing issues.</li></ul><h3 id="cons-2">Cons</h3><ul><li>Wave's icon overlay can get confused. Because the icons provide no other information about the element on the page you have to dig deeper to find out what’s going on when that happens.</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/wave-example.jpg" class="kg-image" alt="Comparison of Web Accessibility Tools" loading="lazy" width="2000" height="989" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/04/wave-example.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/04/wave-example.jpg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/04/wave-example.jpg 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w2400/2023/04/wave-example.jpg 2400w" sizes="(min-width: 720px) 720px"><figcaption>Example of WAVE report</figcaption></img></figure><h2 id="tenon">Tenon</h2><p>Tenon is a web accessibility testing tool that provides comprehensive testing and reporting for web applications. Tenon can be used as a browser extension or integrated into your continuous integration and deployment process. Tenon provides a detailed report on accessibility issues and provides solutions for fixing them.</p><h3 id="pros-3">Pros</h3><ul><li>Tenon does not require anything to be downloaded or installed on your computer because it’s a web service that only needs the page url under test.</li><li>Tenon provides a "Suggest" feature that suggests solutions for fixing issues.</li><li>Tenon can be used to test web applications for accessibility and compliance with regulations like the Americans with Disabilities Act (ADA).</li></ul><h3 id="cons-3">Cons</h3><ul><li>Tenon is a paid service, and pricing can be a barrier for smaller organizations.</li><li>The page needs to be publicly available so Tenon’s web service can check the URL passed.</li></ul><h2 id="conclusion">Conclusion</h2><p>After reviewing and comparing these popular web accessibility tools, it is difficult to determine which tool is the best. </p><p>Each tool has its strengths and weaknesses, and the tool you choose will depend on your specific skills, needs and requirements. </p><p>WAVE is great for beginners, while axe is better for the more experienced user. Tenon is also excellent tool that can be used to supplement your accessibility testing efforts. Lighthouse is nice for a quick check, although it should never be your only check because of the basic implementation. </p><p>Ultimately, the best approach to web accessibility is to use a combination of these tools and to ensure that your development team is knowledgeable about web accessibility standards and best practices. Use Aria-attributes for input elements for example. Additionally, it's essential to involve users with disabilities in the testing and design process to ensure that your website is truly accessible to all. And don’t forget to educate content authors of the web application. Things like using the correct headings, alternate text for images that are not decorative and enabling captions for videos are important. With the right tools and practices, you can create a website that is accessible, user-friendly, and inclusive for all users.</p>]]></content:encoded></item><item><title><![CDATA[How to Build an Arcade Style Video Game in JavaScript]]></title><description><![CDATA[I built a game in JavaScript. Here's how I did it. ]]></description><link>https://engineering.deptagency.com/how-to-build-an-arcade-style-video-game-in-javascript/</link><guid isPermaLink="false">6424507c92e99c003ddd7243</guid><category><![CDATA[javascript]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Jake Rainis]]></dc:creator><pubDate>Wed, 05 Apr 2023 14:39:18 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/dumb-ways-to-die-feature.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/dumb-ways-to-die-feature.png" alt="How to Build an Arcade Style Video Game in JavaScript"/><p><strong><strong>In this article, I share my experiences in building a fun, javascript canvas game for the DEPT® marketing team. You can </strong><a href="https://www.deptagency.com/ways-to-die-in-product-development/?ref=dept-engineering-blog.ghost.io"><strong>play it here</strong></a><strong> </strong>or see the the code in the <a href="https://github.com/deptagency/dw2d-public?ref=dept-engineering-blog.ghost.io">Github repository</a><strong>.</strong></strong></p><p>Recently, I received a ping from my manager asking if I wanted to join a quick brainstorming session with a marketing manager we work closely with to ideate something fun. I’m always up for fun, so I eagerly hopped on to Zoom. </p><p>The goal was to come up with something interactive that we could use to showcase our experience building Digital Products here at DEPT®. We have many case studies (and they’re all great!) but we wanted something fun and interactive. Something that a user in this industry could engage with — and perhaps even have a few laughs. </p><p>It took about five minutes before we decided it should be a playable game. “How about a pixelated, old-school Nintendo game like Zelda or Pokémon?” </p><p>These top-down RPG-style dungeon-crawlers were a blast to play, so we figured we’d throw a contextual spin on the idea. Rather than playing the role of a heroic character exploring a world and fighting enemies, you were someone in the tech industry exploring a modern office and dealing with common problems that we’ve all experienced in some way or another within the realm of product development.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/01-zelda.jpg" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1600" height="900" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/01-zelda.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/01-zelda.jpg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/01-zelda.jpg 1600w" sizes="(min-width: 720px) 720px"/></figure><p>The idea was set! The only problem is that none of us had ever made a game before. After all, this was just an internal side-project. Is this something we can actually pull off in a short amount of time… or is it going to take months? Realistically, we only had a couple of weeks, so we had to work fast and smart.</p><p>One of the best answers an engineer can give when faced with a question of this nature is “I can’t definitively answer that question right now, but let me do some digging and get back to you soon.” <br><br>So I went digging.</br></br></p><h2 id="exploration">Exploration</h2><p>I enjoy working with all technology but my career started in front-end development, so I’ve certainly done my fair of motion design with CSS transitions. I’ve also worked with more powerful JS-based libraries like <a href="https://greensock.com/?ref=dept-engineering-blog.ghost.io">Greensock</a> and <a href="https://www.framer.com/motion/?ref=dept-engineering-blog.ghost.io">Framer Motion</a>. These libraries are <em>excellent</em>, and while you could certainly use them to build a game of sorts, they didn’t feel like a silver-bullet in that regard.</p><p>If I was going to pull this off, I knew I needed a framework that could handle basic game mechanics. Things like loading and managing assets, the ability to control them with a keyboard or touch screen, detecting proximity and collision with other elements in the world I was building, and some sort of state to manage progress throughout the game.</p><p>I’m not an expert in the web-based gaming space, but one thing I do know is that many games are built on HTML5 canvas. And for good reason; canvas is powerful, performant, and widely supported across different browsers and devices. I knew this wasn’t the only way to achieve my solution, but it was the one that felt the most promising based on my prior experience and the short amount of time we had to pull this off.</p><p>Over the past couple of years, I worked on some small 3D projects using <a href="https://threejs.org/?ref=dept-engineering-blog.ghost.io">three.js</a>, which renders complex WebGL graphics to a canvas. Reaching for three.js was my first inclination for this reason, but I quickly recalled some of the hurdles I ran into with my past projects. Perhaps the most pertinent of which was that it required 3D models — and someone had to make those. I know enough to be dangerous in programs like <a href="https://www.blender.org/?ref=dept-engineering-blog.ghost.io">Blender</a>, but modeling game assets would have taken me ages, and they probably would have looked pretty awful.<br/></p><p>The good news is that this was meant to be a tongue in cheek project with 2D arcade-style pixel art. Low fidelity was <em>good</em> in this case.<br>After some quick googling, I found that there were a number of JS-based gaming frameworks that cater to this specific use case, a wealth of free and reasonably-priced royalty free sprites (characters, textures, world objects, etc.), and a vibrant community of passionately knowledgeable developers that had created some truly inspiring games. <br><br><em>Sigh… I love the open source community…</em></br></br></br></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/sigh-open-source.gif" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="480" height="270"/></figure><h3 id="technical-approach">Technical Approach</h3><p>In most scenarios, I opt to spend as much time as reasonably possible doing my research before deciding on a definitive solution, particularly when it’s a scenario I have little or no experience in solving for. There’s nothing worse than finding out you’ve painted yourself into a corner towards the end because you were cutting corners in the beginning.</p><p>To be honest, I was feeling anxious about pulling this game off. Not only did I have to learn a new framework, but I also had to build the actual game using the new framework. I’m confident I can do anything I set my mind to, but I didn’t have much runway to get this done.</p><p>I decided that the best approach I could take was to just pick one and start building something. I didn’t know exactly what the end-product was going to look like, but I did have a good understanding of what aspects were going to be needed. Can I render a character, move it around an empty box, and have it bump into something? If so, that’d be a big step in the right direction, and then I can work from there.</p><h3 id="phaser">Phaser</h3><p>I spent the better part of a week test-driving a <a href="https://blog.bitsrc.io/9-top-js-gaming-engines-and-libraries-for-2020-81707d9f095?ref=dept-engineering-blog.ghost.io">wealth of different framework options</a>. Each one was unique and impressive, but I quickly found myself drawn to <a href="https://phaser.io/?ref=dept-engineering-blog.ghost.io">Phaser</a> for its comprehensive documentation, countless interactive examples of community-driven solutions, and its approachable API. As if that wasn’t enough, it was also well-typed (I love me some TypeScript!).</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/02-phaser.jpg" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="381" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/02-phaser.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/02-phaser.jpg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/02-phaser.jpg 1160w" sizes="(min-width: 720px) 720px"/></figure><p>Phaser also boasts built-in solutions for all of the integral features I knew I would need to draw upon, including but certainly not limited:<br/></p><ul><li>A WebGL and canvas engine with device scaling; in other words, the ability to reliably display the game at an optimal size and frame rate across modern devices.</li><li>Asset preloading and scene management; the ability to show a progress bar while the assets load and have an intro and game over screens.</li><li>Camera capabilities; the ability to follow our character around the screen based on its movement.</li><li>Input control bindings; the ability to support keyboard, mouse, and touch across modern devices.</li><li>Sprite animations and tilemap support; more details on this in a later section</li></ul><p>I spent a couple of days following tutorials to build some small and <em>extremely </em>basic games, but I was excited by how quickly I was able to get up and running. I’d like to give a huge shout-out to the hundreds of contributors who <a href="https://phaser.io/examples?ref=dept-engineering-blog.ghost.io">provided examples on the Phaser site</a>. </p><p>The first game I pieced together was a simple “Flappy Bird” game where you make a bird fly as far as you can through a series of pipes. For each set of pipes the bird flies through, you get a point. If the bird hits a pipe, the game restarts.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/03-flappy-1.gif" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="721" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/03-flappy-1.gif 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/03-flappy-1.gif 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/03-flappy-1.gif 1160w" sizes="(min-width: 720px) 720px"/></figure><p>Go easy! I know it’s laughably simple, but there’s more mechanics here than you might think. At a high level:</p><ul><li>The bird is an animated sprite that can be controlled by mouse or keyboard input.</li><li>The pipe sets are randomly generated and get recycled to be closer and closer together once they go off-screen, which makes the game progressively harder.</li><li>There is collision detection between the bird and pipes, which trigger an event to restart the game.</li><li>There is the concept of “state management” with regards to the score and the number of times the game is retried.</li></ul><p>These are the kind of mechanics I needed to understand. Once they’re understood, they can be employed in any context. If you think about it, this isn’t terribly different from the <a href="https://www.deptagency.com/ways-to-die-in-product-development/?ref=dept-engineering-blog.ghost.io">finished product</a>. Sure, it’s much simpler, but it still has the concept of a controllable character within a canvas “world” full of interaction points.</p><p>Anyway, that was enough flapping about for me. I was ready to move on to building the actual game.</p><h2 id="sourcing-pixel-art">Sourcing Pixel Art</h2><p>I’d love to be able to say I designed all of this amazing pixel art, but the credit goes to <a href="https://twitter.com/lime_px?ref=dept-engineering-blog.ghost.io">LimeZu</a> (<a href="https://limezu.itch.io/?ref=dept-engineering-blog.ghost.io">https://limezu.itch.io/</a>). LimeZu is an incredible artist and asset designer and we were able to bring our vision to life with their <a href="https://limezu.itch.io/modernoffice?ref=dept-engineering-blog.ghost.io">Modern Office</a> and <a href="https://limezu.itch.io/moderninteriors?ref=dept-engineering-blog.ghost.io">Modern Interiors</a> sprite packs. </p><p>If you are interested in game development, you should absolutely check out their offerings. Not only were they cohesive and easy to work with, but the selection of assets in each pack is vast.</p><p>Read on to see how we took these incredible assets and built a unique world with them.</p><h2 id="setting-up-the-repository">Setting Up the Repository</h2><p>Having never built a game before, I wasn’t entirely sure how to structure the codebase at first. However, since Phaser merely renders a canvas, it became apparent I could structure the codebase in any way I pleased. At DEPT®, the majority of the projects I work on leverage PWA (progressive web app) frameworks like <a href="https://nextjs.org/?ref=dept-engineering-blog.ghost.io">Next.js</a>, so I opted for this direction.<br><br>Considering the final product, Next.js might have been a little overkill, but it was important to choose something that had the flexibility to scale. If we were to iterate on this game in the future, it might be convenient to have page routing and API support available. Plus, <a href="https://vercel.com/?ref=dept-engineering-blog.ghost.io">Vercel</a> makes deploying a production app a cinch.</br></br></p><p>Within mere minutes, we had our environments setup and with continuous deployments going out on merges to the main branch, which made it convenient to share progress with the team as I iterated through the build.</p><p>We’ve also made an instance of the repository publicly available. You can download or clone it <a href="https://github.com/deptagency/dw2d-public?ref=dept-engineering-blog.ghost.io">here</a> if you’d like to follow along.</p><p>Out of respect for the artist, we’ve heavily watermarked the imagery we licensed, so running the game in your local environment will look odd with superimposed watermark text on the sprites, but it will still be functional. If you’d like to use the original assets in your own project, you can purchase them, and other great artwork from <a href="https://limezu.itch.io/?ref=dept-engineering-blog.ghost.io">https://limezu.itch.io/</a>. </p><h2 id="execution">Execution</h2><p>The idea of rendering an object and moving it around isn’t a tough concept to grasp. But one of my first questions was “How does one actually <em>code</em> a 2D <em>world</em> that includes a floor you can walk on, with walls and objects that you can’t walk through?” <br><br>This was the one major thing that Flappy Bird didn’t reveal to me, but it was the next problem I wanted to solve since the vision of the game was based on this virtual office that you could move your character through. </br></br></p><h3 id="building-the-world-with-sprites-tiled">Building the World with Sprites &amp; <a href="https://www.mapeditor.org/?ref=dept-engineering-blog.ghost.io">Tiled</a></h3><p>How do we <em>code</em> the “world”? The answer is actually quite simple. It’s entirely composed of slices of pixel art sprite sheets that are then mapped into a certain position on a matrix of coordinates. This matrix of coordinates represents our “world”. At the end of the day, this matrix (along with paths to the sprite images it uses) is exported to an auto-generated JSON file that can be consumed by our framework.</p><p>Perhaps that doesn’t quite read as simply as it truly is, so let’s break it down further.</p><p>A <em>sprite</em> or <em>sprite sheet</em> is an image — typically a PNG for the purposes of alpha transparency — that contains a bunch of other images within it. Here’s an example of a small portion of one of the sprite sheets we used to construct our office:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/04-spritesheet.png" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="768" height="288" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/04-spritesheet.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/04-spritesheet.png 768w" sizes="(min-width: 720px) 720px"/></figure><p>At first glance, it looks a little odd, but bear with me. It’s not visible, but sprite sheets are positioned on a grid, and each grid item (referred to as “tile” in the context of map) or groups of adjacent grid items can then be designated to tile(s) on our map.</p><p>This sounds like a pretty tedious process, and it absolutely would be if we were to do it manually, but thankfully, there’s a free tool called <a href="https://www.mapeditor.org/?ref=dept-engineering-blog.ghost.io">Tiled</a> to do the heavy-lifting for us.</p><p>With Tiled, all we have to do is create a new empty map, set its size, and specify how big our tiles are. In our game, our map is 100 x 100 tiles with a tile size of 48px. That’s a total map size of 4,800px by 4,800px.</p><p>48px might sound arbitrary, but it’s actually the format of the spritesheets that we had acquired. To accompany our map, we can import these image assets into Tiled and begin placing portions of the sprite on our map. </p><p>Perhaps one of the most convenient features of Tiled is that you can create different “tile layers”. This is what allowed me to create a floor <em>as well as</em> walls and other decor (such as desks, plants, and chairs) that our player can’t walk through. Phaser allows us to granularly parse this map and set the specific layers we specify in Tiled to allow or disallow collisions. </p><p>Building the map was a blast and thanks to Tiled, it didn’t take long at all. Here’s this finished map:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/05-world-map.jpg" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="870" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/05-world-map.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/05-world-map.jpg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/05-world-map.jpg 1160w" sizes="(min-width: 720px) 720px"/></figure><p>If you’d like to better understand how this map was built, you can <a href="https://thorbjorn.itch.io/tiled?ref=dept-engineering-blog.ghost.io">download Tiled</a>, <a href="https://github.com/deptagency/dw2d-public?ref=dept-engineering-blog.ghost.io">clone the repo</a> and open the file located at <em>public/office/world/map.json</em> in Tiled.</p><h3 id="main-character-npcs">Main Character &amp; NPCs</h3><p>Now that I had figured out how to build a virtual office using Tiled, it was time to bring it to life! The premise of the game is for the main character to move through the map and interact with different employees in the office, so the next logical step was to add those characters to the map. </p><p>Phaser’s development patterns encourage class-based inheritance, which naturally makes reusing common functionality convenient and keeps the codebase clean. Once I programmed the main character, it was easy to program the NPCs (non-playable characters) since they all have similar qualities. For example, each character can walk up, down, left, or right — or stand idle while facing up, down, left, or right. </p><p>The only thing inherently different about how they behave in the world is that you can only control one of them (the main character versus the NPCs) and that each one has a different sprite sheet (so each character appears aesthetically different).This means that we can simply create <em>n</em> number of class instances (one for each character) and associate each instance to a unique character sprite file.</p><p>Check out this character sprite:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/06-character-sprite.png" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="153" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/06-character-sprite.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/06-character-sprite.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/06-character-sprite.png 1160w" sizes="(min-width: 720px) 720px"/></figure><p>If you look closely, you’ll notice each frame is slightly different. Phaser allows us to take in this sprite sheet and designate an array of frames for every combination of their current state (idle or moving) and their current direction (up, down, left or right). Each array of frames represents a loopable animation and we simply trigger the appropriate animation based on what the character is doing. For example, if the character is walking to the right, we tell phaser to play the character’s rightward walking animation. When the character stops, we tell Phaser to play the character’s rightward idle animation.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/07-walking.gif" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="721" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/07-walking.gif 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/07-walking.gif 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/07-walking.gif 1160w" sizes="(min-width: 720px) 720px"/></figure><p>In the end, we have our one main character and 11 NPCs. Each NPC is an instance of a single class and each is mapped to their own sprite sheet. Now I just had to place them somewhere on the map. Of course, I could have crunched some numbers and placed them at specific map coordinates, but fortunately Tiled makes it much easier than that. All I had to do was create an “object layer”, place some markers on the map. Each marker can be named and I can then use this name to associate the designated placement point to one of my character class instances.</p><p>Here’s the map, now updated with NPC placement points:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/08-character-zones.jpg" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="870" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/08-character-zones.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/08-character-zones.jpg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/08-character-zones.jpg 1160w" sizes="(min-width: 720px) 720px"/></figure><h3 id="interaction-points-dialogs">Interaction Points &amp; Dialogs</h3><p>We’re making good progress so far! We have an office to walk around in and people sprinkled throughout. Next, I wanted to make it so you can interact with a character. </p><p>To make the interactions easy to initiate, I figured it made sense to have the NPCs approach the main character when the main character was within a certain distance from the NPC, rather than forcing the player to move the main character right up against the NPC. </p><p>This took a little bit of trial and error, but the solution I ended up going with was quite simple and worked just as I had envisioned. Back in Tiled, I created invisible interaction zones with object layers for each NPC.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/09-interaction-zones.jpg" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="870" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/09-interaction-zones.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/09-interaction-zones.jpg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/09-interaction-zones.jpg 1160w" sizes="(min-width: 720px) 720px"/></figure><p>Once I registered each zone in Phaser, I was able to make it so an NPC would face the direction of and walk up to the main character, where a prompt dialog would then be shown. </p><p>There’s nothing particularly special about the prompts; they’re simply a React modal that pops up over the canvas and feeds question, answer, and response content from as JS object. I opted to handle these content interactions outside of the canvas. Since the canvas can scale based on the device’s screen size, the text within it could potentially shrink to an illegible size. As a result, rendering them outside of the canvas for accessibility purposes made the most sense.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/10-prompt.gif" class="kg-image" alt="How to Build an Arcade Style Video Game in JavaScript" loading="lazy" width="1160" height="721" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/03/10-prompt.gif 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/03/10-prompt.gif 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/03/10-prompt.gif 1160w" sizes="(min-width: 720px) 720px"/></figure><h3 id="finishing-touches">Finishing Touches</h3><p>At this point, we had a working game, but I also wanted to create a splash screen from which the game could be started, as well as an outro screen that gave the player their score when they left the office at the end of the game. </p><p>Phaser has a “scene” feature that comes in quite handy. When a game first mounts in the browser, you specify the first scene it should display. In the case of our game, we had several scenes:</p><ul><li>The “Loading” scene: This simply preloaded all of the assets and displayed a progress bar.</li><li>The “Intro” scene: Once the assets are loaded, the “Loading” scene triggers this scene to begin. It has some text and imagery along with a button to start the game.</li><li>The “Game” scene: Once the button in the “intro” scene is clicked, the player can then play the game and move their character throughout the office.</li><li>The “Outro” scene: Once the main character collides with a special “door” at the end of the level, the “Outro” scene is triggered where the player can see their score. From here, they can play again, or go to a special landing page to learn more about their choices.</li></ul><h2 id="conclusion">Conclusion</h2><p>That’s our game! We started from scratch and launched a finished product in about 3 weeks. If you happen to know anyone at Bethesda or CD Projekt Red, be sure to send them our way ;) </p><p>In all seriousness, the game is silly and perhaps a little rough around the edges, but it’s meant to be. And it’s a great example of how quickly we can bring things to life here at DEPT®. We love technology, and we love a good challenge. </p><p>If you haven’t checked it out yet, you can <a href="https://www.deptagency.com/ways-to-die-in-product-development/?ref=dept-engineering-blog.ghost.io">play the game here</a>. Interested in the code? Here’s <a href="https://github.com/deptagency/dw2d-public?ref=dept-engineering-blog.ghost.io">the Github repository</a>.</p><p><br/></p><p><br/></p><p><br/></p><p><br/></p>]]></content:encoded></item><item><title><![CDATA[In Praise Of Logging (A Node.js/Javascript Logging Guide)]]></title><description><![CDATA[In this post, I'm going to outline some good practices I've learned along the way and (hopefully) convince you why you should consider using a logging framework immediately instead of console.log statements.]]></description><link>https://engineering.deptagency.com/in-praise-of-logging-a-node-js-javascript-logging-guide/</link><guid isPermaLink="false">63f6306df82716003dc554b6</guid><category><![CDATA[javascript]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Thu, 23 Feb 2023 14:32:55 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/log.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/log.png" alt="In Praise Of Logging (A Node.js/Javascript Logging Guide)"/><p>I often find myself leaving comments in pull requests about logging. Usually about adding logging where they might be helpful for production troubleshooting, or changing their levels to be more appropriate for production. Understandably, when you're under pressure to hit a deadline, logging can fall by the wayside. I find that most people don't share my love of logging and why it can be great. Done well, it can save you a ton of time and headaches. </p><p>In this post, I'm going to outline some good practices I've learned along the way and (hopefully) convince you why and how you should love 🪵 logging 🪵 and why you should consider using a logging framework immediately instead of <code>console.log</code> statements.</p><p>I'll also show by example what some of the most popular node logging libraries do well and how they stack up against what I'm suggesting.</p><p>I am going to be referring to code snippets and output from a sample project I put together called <a href="https://github.com/theothermattm/node-logging-examples?ref=dept-engineering-blog.ghost.io">node-logging-examples</a>.  You can run this project over on <a href="https://replit.com/@theothermattm/node-logging-examples?ref=dept-engineering-blog.ghost.io#README.md">Replit</a> by using the "Shell" button at the bottom right.</p><h1 id="lessons-from-the-frontlineaka-why-should-i-listen-to-this-guy">Lessons from the Frontline - Aka "Why should I listen to this guy?"</h1><p>I've supported a number of server-side applications. Including being on call for those applications. When you get woken up in the middle of the night for an alert, you want to make sure that your logs are watertight, otherwise, you'll be waterlogged (GET IT!?). So watertight you can read them squinting in a dreamy haze! I know what to do, and probably more importantly what not to do.</p><p>It's also worth knowing that I spent a lot of my career in the Java world (though not recently). Since this post is targeted at Javascript developers, reserve your judgment when I say: Java does logging <em>very</em> well. There are a number of battle-tested libraries that allow for out-of-the-box ease, as well as endless customizability.</p><p>Okay, on with the show...</p><h2 id="use-a-framework-built-for-logging-not-consolelog">Use a framework built for logging, not <code>console.log</code></h2><p>Table stakes - The rest of this article will hopefully convince you why! I know some people will disagree, and for really really simple apps you can get away with it, but I can't recall a case in my career where I started with simple console.log statements and didn't regret it later.</p><h2 id="separate-logging-configuration-from-logging-code">Separate logging configuration from logging code</h2><p>Whenever possible, you want the ability to dial logging levels up and down at granular levels without performing a code change. This could be via environment variables, or even through external configuration files or databases.</p><p>Either way, during a production incident, or even while debugging, being able to dial up or down logging levels with minimal risk is a lifesaver.</p><p>At a very minimal level, you can simply set a <code>LOG_LEVEL</code> environment variable that your logging setup inspects and sets the appropriate level.</p><p>Going to the more advanced levels, you might have an external configuration file that defines log levels.  Log4js is the only framework I saw with a specific <a href="https://log4js-node.github.io/log4js-node/api.html?ref=dept-engineering-blog.ghost.io">option to configure logging with an external source</a> right out of the box.</p><p>You may be thinking: Of course, we set our log levels outside of code! However, I would ask if you can do it only for the areas of the application which need the logging tweaked?</p><p>Read on...</p><h2 id="configuring-logging-on-a-per-filemodule-basis">Configuring logging on a per file/module basis</h2><p>Being able to dial up logging levels in very specific parts of your application can help reduce a lot of noise when troubleshooting. Of course, modern log ingestion/viewing tools like Elasticsearch or Splunk allow you to do really advanced filtering, but what about when you just want to trace through what's happening for a given user in your application? It'd be nice to see just a bit of logging for the parts of your application that aren't having issues, but a ton for the areas of concern.</p><p>A lot of node.js frameworks do not have the ability to do this out of the box, instead relying on you to roll your own configuration.  Which has its drawbacks and benefits.</p><p>I've been programming in Node.js for years now, but still, one of the best features of <a href="https://logging.apache.org/log4j/2.x/?ref=dept-engineering-blog.ghost.io">Log4j</a> (the de-facto standard Java logging library) I miss the most while programming in Node is being able to easily customize which modules are logging to what logging level.</p><p>As an example, these Java classes:</p><pre><code class="language-java">class Main {
  static final Logger logger = LogManager.getLogger(Main.class.getName());
  
  public static void main(String[] args) {
    logger.info("Begin app ...");

ExternalService.makeACall("test request info");
    FailingService.makeACall("something that will fail");
    InternalBusinessLogic.wickedImportantRules("test input");
    logger.info("Completed app!");
  }
}

public class ExternalService {
  static final Logger logger = LogManager.getLogger(ExternalService.class.getName());
  public static void makeACall(String request) {
    logger.info("Making external service call");
    logger.debug("External Service Call Request {}", request);
  }
}

public class FailingService {
  static final Logger logger = LogManager.getLogger(FailingService.class.getName());
  public static void makeACall(String request) {
    logger.info("Making failing external service call");
    logger.error("Something went wrong in the call!");
  }
}

public class InternalBusinessLogic {
  static final Logger logger = LogManager.getLogger(InternalBusinessLogic.class.getName());

  public static void wickedImportantRules(String arg) {
    logger.info("Running business rules");
    logger.debug("Fixing Johnson rod");
    logger.trace("Arguments for business logic: {}", arg);
    logger.trace("More information here that's really low level");
    logger.debug("Done running business rules");
  }
}
</code></pre><p>Will output like this on the console (configuration dependent):</p><pre><code>22:20:44.283 [main] INFO  FailingService - Making failing external service call
22:20:44.285 [main] ERROR FailingService - Something went wrong in the call!
22:20:44.286 [main] INFO  InternalBusinessLogic - Running business rules
22:20:44.286 [main] DEBUG InternalBusinessLogic - Fixing Johnson rod
22:20:44.294 [main] TRACE InternalBusinessLogic - Arguments for business logic: test input
22:20:44.294 [main] TRACE InternalBusinessLogic - More information here that's really low level
22:20:44.294 [main] DEBUG InternalBusinessLogic - Done running business rules
</code></pre><p>You can dial logging levels and even separate outputs for individual files/classes. See <a href="https://replit.com/@theothermattm/JavaLoggingExample?ref=dept-engineering-blog.ghost.io">this ReplIt</a> for a working example you can run.</p><p>I haven't found any Javascript frameworks that allow this type of customizability, out of the box, however, some logging frameworks have the concept of categories or child loggers that allow you to do something <em>similar</em>.</p><p>For example, I was able to get Log4js to do this with a little bit of help from their <a href="https://log4js-node.github.io/log4js-node/categories.html?ref=dept-engineering-blog.ghost.io">categories</a> API.</p><p>This is the type of output you see with my approach, which results in this type of output where the file name is listed before the message.</p><pre><code>[2023-02-13T10:58:00.168] [INFO] log4js/example-log4js.js - Syncing clients...
[2023-02-13T10:58:00.172] [DEBUG] log4js/log4jssubfolder/clientservice.js - Getting last run
[2023-02-13T10:58:00.174] [DEBUG] log4js/log4jssubfolder/clientservice.js - Getting clients updated since 2023-02-13T10:58:00
[2023-02-13T10:58:00.174] [INFO] log4js/example-log4js.js - Fetching clients updated since 2023-02-13T10:58:00
[2023-02-13T10:58:00.174] [DEBUG] log4js/log4jssubfolder/clientservice.js - Doing something really complicated
[2023-02-13T10:58:00.174] [ERROR] log4js/example-log4js.js - ERROR Calling Remote Service
[2023-02-13T10:58:00.174] [ERROR] log4js/example-log4js.js - Service Call Result []
[2023-02-13T10:58:00.174] [DEBUG] log4js/log4jssubfolder/anotherservice.js - Doing something really fun!
[2023-02-13T10:58:00.174] [DEBUG] log4js/log4jssubfolder/clientservice.js - Setting last run

</code></pre><p>And you can dial the level up and down with a glob pattern (see the <code>categories</code> block):</p><pre><code class="language-javascript">{
  "appenders": { "standard": { "type": "stdout" } },
  "categories": {
    "default": { "appenders": ["standard"], "level": "info" },
    "log4js/log4jssubfolder/**.js": {
      "appenders": ["standard"],
      "level": "debug"
    },
    "perf": { "appenders": ["standard"], "level": "trace"}
  }
}

</code></pre><p>It would be great if this were built into log4js, but it's fairly easy to see how it's done (<a href="https://github.com/theothermattm/node-logging-examples/blob/main/log4js/log4jslogger.js?ref=dept-engineering-blog.ghost.io">source here</a>). Here are the highlights:</p><pre><code class="language-javascript">import { fileURLToPath } from 'url';
import glob from 'glob';


const defaultOptions = {
  ignore : ['default'],
}

function getLog4JOptionsUnGlobbed(globbedLog4JsOptions, globOptions) {

  const mergedGlobOptions = {...defaultOptions, globOptions};
  const unglobbedOptions = JSON.parse(JSON.stringify(globbedLog4JsOptions));
  for (const category in globbedLog4JsOptions.categories ) {
    const matchingFiles = glob.sync(category, mergedGlobOptions);
    if ( matchingFiles &amp;&amp; matchingFiles.length &gt; 0 ) {
      matchingFiles.forEach((file) =&gt; {
        unglobbedOptions.categories[file] = globbedLog4JsOptions.categories[category];
      })
      delete unglobbedOptions.categories[category]
    }
  }
  return unglobbedOptions;
}
function createModuleLogger(fileName) {
  // you can just use __filename if using commonjs modules! (eg require() format)
  const __filename = fileURLToPath(fileName);
  const relativeModuleName = path.relative('', __filename);
  return log4js.getLogger(relativeModuleName);
}
</code></pre><p>You can create new loggers like this in each of your modules/files:</p><pre><code class="language-javascript">// again, you don't need import.meta.url if you're using commonjs
const logger = createModuleLogger(import.meta.url);
</code></pre><p>After that, we're using log4js categories to add the filename for output and configuration. With a little help with some preprocessing the log4js config file with the <a href="https://www.npmjs.com/package/glob?ref=dept-engineering-blog.ghost.io">glob</a> library, we can dial those levels up and down on a per file (or folder) basis.</p><h2 id="use-low-level-log-statements-instead-of-comments-to-document-code">Use low-level log statements instead of comments to document code</h2><p>You can use logging statements to simultaneously document your code <em>and</em> provide useful logging output that you can turn on or shut off with a logging framework.</p><p>Consider this simple example, ignore the details, they're purposely opaque so you can pretend you're in a new codebase:</p><pre><code class="language-javascript">const syncClientsWithoutLoggingFramework = async function() {
  console.log('Syncing clients...');
  const lastRun = await getLastRun();
  const from = lastRun || moment().subtract(3, 'days').format(DATE_FORMAT);
  const timeOfLastFetch = moment().format(DATE_FORMAT)
  const clients = await clientsUpdatedSince(timeOfLastFetch);

  console.log(`Fetching clients updated since ${timeOfLastFetch}`);
  if (clients.length === 0) {
    console.log('No updated clients.')
    return;
  }

  // call out to another service to modify the clients list
  // with some _really important business logic_
  const modifiedClients = await doSomethingReallyComplicatedInAnotherService(clients);
  if ( modifiedClients &lt; 1 || (modifiedClients.errors &amp;&amp; modifiedClients.errors.length &gt; 0) ) {
    console.error('ERROR Calling Remote Service')
    console.error(`Service Call Result ${JSON.stringify(modifiedClients, null, 2)}`)
  }

  // Store the last time we updated the clients.
  await setLastRun(moment())
  return modifiedClients;
}
</code></pre><p>That results in this logging output:</p><pre><code>Syncing clients...
Fetching clients updated since 2023-01-23T17:25:18
ERROR Calling Remote Service
Service Call Result []
</code></pre><p>Well, it's better than nothing! But I'm still kinda confused about what's going on.</p><p>If we add a logging framework (here I'm going with <a href="https://github.com/pinojs/pino?ref=dept-engineering-blog.ghost.io">Pino</a>) and use the lowest log level (in this case, <code>trace</code>), this is what the code looks like as we run it:</p><pre><code class="language-javascript">const logger = pino({level: 'trace'});

const syncClientsWithLoggingFramework = async function() {
  logger.info('Syncing clients...');
  const CLIENT_LAST_RUN_KEY = 'CLIENT_LAST_RUN_DATE';
  const lastRun = await getLastRun();
  const from = lastRun || moment().subtract(3, 'days').format(DATE_FORMAT);
  const timeOfLastFetch = moment().format(DATE_FORMAT)
  const clients = await clientsUpdatedSince(timeOfLastFetch);

  logger.info("Fetching clients updated since %s", timeOfLastFetch);
  logger.trace("Fetching clients with parameters from: %s and timeOfLastFetch: %s", from, timeOfLastFetch);
  if (clients.length === 0) {
    logger.warn('No updated clients.')
    return;
  }

  logger.debug("Calling out to another service to do some really complicated business logic");
  const modifiedClients = await doSomethingReallyComplicatedInAnotherService(clients);
  if ( modifiedClients &lt; 1 || (modifiedClients.errors &amp;&amp; modifiedClients.errors.length &gt; 0) ) {
    logger.error('ERROR Calling Remote Service')
    logger.error(`Service Call Result ${JSON.stringify(modifiedClients, null, 2)}`)
  }
  logger.trace("Done calling out and doing important things.");

  logger.trace("Setting last run date so we know where to pick up next time.")
  await setLastRun(moment())
  return modifiedClients;
}
</code></pre><p>This is what the output looks like, and before you judge, hold up:</p><pre><code class="language-json">{"level":30,"time":1674503301467,"pid":918,"hostname":"f6572868d2b5","msg":"Syncing clients..."}
{"level":30,"time":1674503301469,"pid":918,"hostname":"f6572868d2b5","msg":"Fetching clients updated since 2023-01-23T19:48:21"}
{"level":50,"time":1674503301469,"pid":918,"hostname":"f6572868d2b5","msg":"ERROR Calling Remote Service"}
{"level":50,"time":1674503301469,"pid":918,"hostname":"f6572868d2b5","msg":"Service Call Result []"}
</code></pre><p>You might think "But when I'm developing, this looks awful!" And you're right.</p><p>But with a logging framework, you can easily configure your logs to be prettified and colorized for development, along with dialing the logging level up to get more information:</p><pre><code class="language-js">const logger = pino(
  {
  level : 'trace',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true
    }
  }
});
</code></pre><p>Alternatively, Pino lets you use <code>pino-pretty</code> as a command line utility like this:</p><pre><code>npm start | pino-pretty
</code></pre><p>You can even put a separate <code>pino-prettyrc</code> config to customize the output as desired.</p><p>This is what the colorized, prettified output looks like:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/logging-1.png" class="kg-image" alt="In Praise Of Logging (A Node.js/Javascript Logging Guide)" loading="lazy" width="1484" height="362" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/logging-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/logging-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/logging-1.png 1484w" sizes="(min-width: 720px) 720px"/></figure><p>With the addition of a couple of lines of configuration, you have extremely useful JSON objects for production logging, as well as nicely readable, colorized log statements for my command line development with a timestamp to know when things were logged.</p><p>Using environment variables, you can very easily drive these logging configurations for local versus production.  There is an example of this with Pino in my example project <a href="https://github.com/theothermattm/node-logging-examples/blob/main/pino/pinologger.js?ref=dept-engineering-blog.ghost.io#L8">here</a>. Using console logging makes this extremely hard.</p><h2 id="bonus-having-a-logging-adapter">Bonus: Having a logging <em>Adapter</em></h2><p>This one is nice to have, but a lot of people don't really think about it. Let's go back to Java for a minute to illustrate: Generally, most Java applications reach for the <a href="https://logging.apache.org/log4j/2.x/manual/api.html?ref=dept-engineering-blog.ghost.io">Log4J Api</a> to log information. This is an <em>interface</em> for logging in Java. Meaning that other libraries can implement that interface. In Javascript, we don't have interfaces, in Typescript we kinda do, but the point is to have an adapter that delegates the act of logging to a library you configure.</p><p>Why is this nice?  Well, let's say you start with a logging framework that serves you well while your app is small, but then you switch to using Elasticsearch to ingest logs. Maybe there's another framework that provides really easy integration to ingest Elasticsearch logs. You can switch out that implementation without changing your actual code. This also makes it easy for library maintainers to have a consistent way to log output.</p><p>(As a side note, most people in the Java world just use the Log4J implementation for everything, but it's nice to have options.)</p><p>Compare this approach against Node, where there are a number of methods for logging, but no standard interface or adapter. The best we can do is use the traditional <code>console.log</code>, <code>console.error</code> (and so on) statements and then use another tool to scrape the console output to ingest in a different way. Certainly nothing wrong with that, but it can get kinda complicated depending on your deployment setup. However, Log4js, one of the logging frameworks I'll go over, does have an <a href="https://www.npmjs.com/package/@log4js-node/log4js-api?ref=dept-engineering-blog.ghost.io">API only library</a> that you can use.</p><h1 id="node-logging-frameworksan-selected-overview">Node Logging Frameworks - An selected overview</h1><p>This is by no means an extensive list of logging frameworks for Node, but rather a specially selected list based on popularity and my own use and interests based on my recommendations above.</p><p>For another great analysis, check out <a href="https://geshan.com.np/blog/2021/01/nodejs-logging-library/?ref=dept-engineering-blog.ghost.io">this article</a>.</p><h2 id="bunyan"><a href="https://www.npmjs.com/package//bunyan?ref=dept-engineering-blog.ghost.io">Bunyan</a></h2><p>⭐7k, 2 million weekly downloads on NPM</p><p>This library has been around since the early days of Node and is well respected. Unfortunately, the GitHub project hasn't been updated in 2 years as of this writing, and it is also somewhat of a laggard in the performance area as you'll see below.  There is no ability to do external configuration that I see, and it would be fairly difficult to roll your own.</p><p>For these reasons, I don't recommend it.</p><h2 id="log4js"><a href="https://www.npmjs.com/package/log4js?ref=dept-engineering-blog.ghost.io">Log4js</a></h2><p>⭐5.6k, 3.7 million weekly downloads on NPM</p><p>I came into this article never having used this library. Probably because I thought "Why would I want to use a Java-like library in node?" As the README itself states:</p><blockquote>Although it's got a similar name to the Java library log4j, thinking that it will behave the same way will only bring you sorrow and confusion.</blockquote><p>While it's a bit slow on the performance end of things (which you can see in the bonus section at the end of this article) its out of the box configuration settings are very easy to use and extremely useful. Pino also allows a huge amount of customization and much, much better performance, but since it works with streams it can be harder to reason about. Through writing this article, I became a big fan of log4js, despite its name.</p><h2 id="loglevel"><a href="https://www.npmjs.com/package/loglevel?ref=dept-engineering-blog.ghost.io">LogLevel</a></h2><p>⭐2.4k, 9.3 million weekly downloads on NPM</p><p>It's so simple! Definitely worth a look, but I think LogLevel's brilliance lies in having a logging framework that works with your browser's console <em>or</em> your server's console. Out of the box, you can't even get timestamps without some gymnastics.</p><h2 id="pino"><a href="https://www.npmjs.com/package/pino?ref=dept-engineering-blog.ghost.io">Pino</a></h2><p>️⭐10.9k, 4.5million weekly downloads on NPM</p><p>This framework is newer than the others, having gone 1.0 in 2016. It has been making the rounds in Node projects at DEPT® recently to good fanfare amongst our developers. As it claims, it is <em>fast</em> and very customizable. To me its best feature is the ability to perform logging on a separate thread asynchronously with the flip of a configuration switch. Very cool. On any project that required some <em>serious</em> log gymnastics and high performance, I'd definitely go with Pino.</p><h2 id="winston"><a href="https://www.npmjs.com/package/winston?ref=dept-engineering-blog.ghost.io">Winston</a></h2><p>⭐21.1k, 12.6 million weekly downloads on NPM.</p><p>One of the more established and popular options, having been around since 2011, I've used Winston personally in many past projects. My opinion of it before going into this analysis was that it was ... fine. A little wonky to set up, and the <code>silly</code> logging level is cute but leaves me scratching my head why they didn't use <code>trace</code> like almost every other logging framework.</p><p>Winston allows for good customization of logging transports and the ability to customize formats quite well. Its configuration API is a bit wonky though. I've used it in production applications for years and never really liked it, but rather just accepted its wonkiness. Now that I know there are better options, I don't think I'd go back to it.</p><h2 id="my-takeaways">My Takeaways</h2><p>First, I don't think I would ever start a project with plain console logs again after knowing what kind of benefits you can get from a logging framework over time. I know some will disagree with me, and that's fine. But, I'd ask you to consider it. Logging can save you a <em>lot</em> of headaches in an application that's operational.</p><p>For Node.js, there are lots of great logging frameworks available. If low-level customizability and performance are your utmost concern, go with Pino. Its ability to take streams and log asynchronously out of the box is amazing.</p><p>For everything else, I can honestly recommend log4js. I hadn't used it (or even heard of it!) before I started writing this, and after working with it, I really loved its ability to get up and running fast and the ease of customization. As you saw here, it's a bit slower, but for the majority of applications I work on, this isn't much of an issue, especially since most production logging levels are dialed way back to info and above-level log output. For my next project, I plan on using log4js if I have the choice.</p><p>Go forth and Log!</p><h1 id="what-about-logging-in-the-frontend-client">What About Logging in the Frontend Client?</h1><p>By <a href="https://www.linkedin.com/in/jakerainis/?ref=dept-engineering-blog.ghost.io">Jake Rainis</a></p><p>Historically, it could be argued this sort of complexity on the server is far greater than on the client since the front-end has traditionally been more responsible for layout and aesthetics than it has for business logic. However, the behavioral complexity of modern front-end applications continues to increase, particularly in framework-driven front-ends such as React, Angular, or Vue. And depending on the context of how the front-end application operates functionally, logging mechanisms could provide a great deal of insight into behavior resulting in an easier way to identify and isolate bugs.  \</p><p>So, where does that leave us on the front-end?</p><p>Of course, we all know that we can drop <code>console.log</code>s anywhere we please to debug an issue during development, but this isn’t a mature solution. These transient log statements are isolated to a single user’s browser session, and then they’re gone forever. Front-end applications run in the browser and don't have the same luxuries as applications running on a server where events can be captured. \</p><h2 id="what-about-my-nextjs-app">What About My NextJS App?</h2><p>You might be wondering about full-stack frameworks such as a <a href="https://nextjs.org/?ref=dept-engineering-blog.ghost.io">Next</a>, <a href="https://remix.run/?ref=dept-engineering-blog.ghost.io">Remix</a>, or <a href="https://nuxtjs.org/?ref=dept-engineering-blog.ghost.io">Nuxt</a>. These too run on a Node server, so they _do _enable logging solutions… But only on the server-side aspects of the application.</p><p>For example, let’s consider a Next application that uses the <a href="https://github.com/pinojs/pino?ref=dept-engineering-blog.ghost.io">Pino</a> library (<a href="https://nextjs.org/docs/going-to-production?ref=dept-engineering-blog.ghost.io#logging">Next recommends Pino</a>). Dropping a Pino log within a Next API route, a middleware, or even in one of Next’s server helpers such as <code>[getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props)</code> will result in the type of log we’re after here. On the other hand, dropping a Pino log into one of the application's React components would <em>only</em> result in a log to the browser's console. Once again, this is helpful for debugging throughout the development cycle, but it doesn’t help engineers when the application is running in production since the log is never captured!</p><h2 id="front-end-logging-solutions">Front-End Logging Solutions</h2><p>As you’ve likely surmised by now, we can’t effectively capture a log from a client-side application without some additional effort. But it is entirely possible! After all, this is a common conundrum for application developers.</p><p>The gist of front-end logging lies in sending the log events <em>somewhere</em> to be captured and this could even be accomplished through a custom hand-rolled solution that sends events to your back-end (though, be cautious of the overhead this might introduce). However, there are also third-party solutions out there to make the job easier.</p><p><a href="https://sentry.io/welcome/?ref=dept-engineering-blog.ghost.io">Sentry</a> is perhaps the most battle-tested error and performance monitoring service that plugs seamlessly into <a href="https://sentry.io/platforms/?ref=dept-engineering-blog.ghost.io">any application</a> — even front-end applications. But there are others too, like <a href="https://www.bugsnag.com/?ref=dept-engineering-blog.ghost.io">BugSnag</a> and <a href="https://logflare.app/?ref=dept-engineering-blog.ghost.io">Logflare</a> (recently acquired by <a href="https://supabase.com/?ref=dept-engineering-blog.ghost.io">Supabase</a>).</p><p>One aspect of the front-end that we don’t have to worry as much about when monitoring a back-end are the browsers themselves. _What browser/version is the end-user viewing our app from? Oh jeez… what if it’s Internet Explorer? <a href="https://www.wired.com/story/microsoft-internet-explorer-is-finally-really-fully-dead/?ref=dept-engineering-blog.ghost.io">Just kidding</a>.  What device are they on? What user flow did they follow to trigger this event/error? _One one the great selling points about the third-party services listed above is that they capture this information. This additional context can make bug reproduction and squashing much easier for application engineers.</p><p>These tools will come with a price tag when used at scale, but all have generous free tiers that might be sufficient for smaller teams and applications. And since they also work for back-end technologies, logging for all portions of an application can be captured, evaluated, and triaged in one place.</p><h2 id="does-every-front-end-need-logging">Does Every Front-End Need Logging?</h2><p>In general, logging and monitoring is a best practice and should not be an afterthought when it comes to a back-end application. After all, it’s simple and quick to implement.</p><p>But is it absolutely necessary on the front-end? Well, any front-end app would benefit, but the right answer is subjective. A larger and/or more complex front-end that manages a good deal of business logic would be a better candidate than a relatively static front-end that doesn’t have much complex functionality — particularly if it’s well-tested. Ultimately, it should be a discussion with the product and engineering team to determine what monitoring solutions make sense for your application. \</p><p>Speaking of, we here at DEPT® have quite a bit of experience in the realm of application monitoring. We’d love to chat more about it, so don’t hesitate to reach out!</p><h1 id="bonus-section-logging-framework-performance-analysis">BONUS SECTION: Logging Framework Performance Analysis</h1><p>If you've made it this far, I appreciate you, and welcome, fellow logging nerd!</p><p>Now, how about performance?  Pino claims to be super fast.  Let's find out!</p><p>To test, I ran a loop of 100,000 simple statements like this (with varying syntax for each logging system):</p><pre><code class="language-js">  const numberOfLoops = 100000
  let startTime = new Date();
  for(let i = 0; i &lt;= numberOfLoops ; i++) {
    console.info(`Test Log Message ${i}`)
  }
  let endTime = new Date();

  console.info(`Time to execute ${numberOfLoops} log messages: ${(endTime-startTime) / 1000} seconds`);
</code></pre><p>To get a better sample, I ran the node process using the GNU <code>time</code> command 20 times each using <a href="https://github.com/theothermattm/node-logging-examples/blob/main/scripts/perf-test.sh?ref=dept-engineering-blog.ghost.io">a bash script</a>. I purposely wrote the output to a file so my tty was not the bottleneck. This is a sample of the output of each command in the output files:</p><pre><code>/usr/bin/time -a -o pino-times-output.txt node index.js -f pino -p &gt;&gt; pino-output.txt
&lt;&lt; snip ... &gt;&gt;
{"level":30,"time":1675893207318,"pid":54614,"hostname":"Matts-16-Macbook-Pro.local","msg":"Time to execute 100000 log messages: 517 ms"}
        0.62 real         0.39 user         0.15 sys
</code></pre><p>This was running on my Intel 2.6ghz 6 core i7 Macbook with 16gb of ram. I used the "real" time for comparison.</p><p>Of course, this probably isn't a perfect test, but let's see what happened:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/logging-2.png" class="kg-image" alt="In Praise Of Logging (A Node.js/Javascript Logging Guide)" loading="lazy" width="1956" height="974" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/logging-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/logging-2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/09/logging-2.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/logging-2.png 1956w" sizes="(min-width: 720px) 720px"/></figure><p><em>And it's console winning the race by a nose!</em></p><p>Let's take a deeper look.</p><h2 id="the-laggard-log4js">The Laggard: Log4js</h2><p>The laggard in the race was log4js, coming in almost 200ms longer than any other framework at an average of 1.54 seconds. </p><h2 id="the-middle-tier">The middle tier</h2><p>Second to last was Bunyan, doing slightly better than Log4js at 1.44 seconds. Third to last was Winston at 1.42 seconds, then LogLevel at 1.38 seconds.</p><h2 id="runner-up-pino">Runner Up: Pino</h2><p>As promised, Pino did very well, coming in only about 80 milliseconds slower than console logging. And that's with <em>synchronous</em> logging, not <a href="https://getpino.io/?ref=dept-engineering-blog.ghost.io#/docs/asynchronous">async logging</a>, which offloads logging onto worker threads to make logging a non-blocking operation, which is awesome. Of course, the whole command would likely take just as long with the worker thread, but it's nice to know that for an online application your logging won't be blocking your application's threads.</p><h2 id="console">Console</h2><p>Probably not a surprise, but good old <code>console.log</code> came in first by about 80 milliseconds. Given that there's nothing else happening with the logs, this makes total sense.  If performance is your concern, console.log can't be beat!  But, since it's not nearly as customizable, there are plenty of tradeoffs to consider.</p>]]></content:encoded></item><item><title><![CDATA[What I learned about network programming with Kotlin/Native]]></title><description><![CDATA[A note to preface: you can find the project repository here.


The origin story

After a while developing software in Java, I was intrigued by all the praise of Kotlin and also interested in Kotlin/Native as a way to develop applications that don't require a VM to run. Inspired by this Golang implementation of traceroute, I settled on trying my hand at implementing traceroute with Kotlin/Native.

tracerte is already available on every operating system, so why the hell would I rewrite it? Good qu]]></description><link>https://engineering.deptagency.com/ktraceroute-an-experiment-in-network-programming-with-kotlin-native/</link><guid isPermaLink="false">63c0515407827f003d8bfdfb</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Platforms]]></category><category><![CDATA[Native]]></category><dc:creator><![CDATA[Mike Hadam]]></dc:creator><pubDate>Tue, 14 Feb 2023 14:37:09 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1474059060963-44710306956c?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGxvcyUyMGZsYW1lbmNvc3xlbnwwfHx8fDE2NzM1NDgxOTI&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1474059060963-44710306956c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGxvcyUyMGZsYW1lbmNvc3xlbnwwfHx8fDE2NzM1NDgxOTI&ixlib=rb-4.0.3&q=80&w=2000" alt="What I learned about network programming with Kotlin/Native"/><p>A note to preface: you can find the project repository <a href="https://github.com/deptagency/ktraceroute?ref=dept-engineering-blog.ghost.io">here</a>.</p><h2 id="the-origin-story">The origin story</h2><p>After a while developing software in Java, I was intrigued by all the praise of Kotlin and also interested in Kotlin/Native as a way to develop applications that don't require a VM to run. Inspired by <a href="https://github.com/aeden/traceroute?ref=dept-engineering-blog.ghost.io">this Golang implementation of <code>traceroute</code></a>, I settled on trying my hand at implementing <code>traceroute</code> with Kotlin/Native.</p><p><code>tracerte</code> is already available on every operating system, so why the hell would I rewrite it? Good question. I had the opportunity to spend some time learning new things, and I'm always up for a good challenge. In this case, to fearlessly venture into the abyss of TCP/IP, network programming, and UNIX.</p><h2 id="ok-ill-bitewhats-kotlinnative">Ok I'll bite...what's Kotlin/Native?</h2><p>Kotlin/Native is a technology that compiles Kotlin code into a native binary for a number of different platforms - no JVM required. <a href="https://kotlinlang.org/docs/native-overview.html?ref=dept-engineering-blog.ghost.io#why-kotlin-native">As indicated in the Kotlin documentation</a>, this technology is meant to bridge the gap when deploying to certain platforms:</p><blockquote>Kotlin/Native is primarily designed to allow compilation for platforms on which <em><em>virtual machines</em></em> are not desirable or possible, such as embedded devices or iOS.</blockquote><p>Kotlin/Native includes a wonderful capability, called <a href="https://kotlinlang.org/docs/native-c-interop.html?ref=dept-engineering-blog.ghost.io"><code>cinterop</code></a>, to easily make calls to C libraries.</p><p>My hope is that my project contains useful examples of what Kotlin/Native (and <code>cinterop</code>) is capable of accomplishing. The Kotlin docs are great but somewhat unclear for parts like memory management, casting, and other minutiae. There should be some good snippets from this project of what those trickier parts of Kotlin/Native look like in practice.</p><h1 id="a-nifty-tool">A nifty tool</h1><p><code><a href="https://en.wikipedia.org/wiki/Traceroute?ref=dept-engineering-blog.ghost.io">traceroute</a></code> (aka <code>tracerte</code>) is a common system utility that collects network latency information by probing each hop in between you and the place you're trying to connect to. In other words, we can find out the time it takes for the packet to get to each stop in the network on the way to the destination.</p><p>There are different ways to probe the network, but the most official one is where we send out ICMP <a href="https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol?ref=dept-engineering-blog.ghost.io#Control_messages">"Echo Request"</a> packets, and our destination will respond with an "Echo Reply" packet.</p><p>At the heart of <code>traceroute</code> is what happens between the origin and destination. Probing every hop along the way is made possible by the IP TTL, or time-to-live. This value is set in the IP header and defines when a packet expires on the network. Routers use this value to determine whether they should pass the packet forward to the next device. If a router does pass the packet forward, it decrements the IP TTL by <code>1</code>.</p><p>So, as you can imagine, it'll eventually reach <code>0</code>. And when the IP TTL is <code>0</code>, the router sends a response, back to the origin, called a "Time Exceeded Error" message.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/image.png" class="kg-image" alt="What I learned about network programming with Kotlin/Native" loading="lazy" width="1512" height="899" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/image.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/image.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/image.png 1512w" sizes="(min-width: 720px) 720px"><figcaption>Here I used my project to query google.com, and captured this Time-to-live exceeded message with Wireshark (<code>tcpdump icmp</code> is also a handy command for capturing all ICMP traffic).</figcaption></img></figure><p>And that's basically it. <code>traceroute</code> first sends a packet with a TTL of <code>1</code>, and then keeps sending more, incrementing the TTL by <code>1</code> each time. The program waits for each "Time Exceeded Error" message returned by every router along the way until there's finally a packet that doesn't exceed the TTL but instead reaches the destination. At that point, the destination will send back an "Echo Reply" packet.</p><h2 id="but-first-the-internet">But first, the internet</h2><p>Beneath it all, the internet is made up of many devices speaking to each other using a number of protocols. All the protocols are part of what is called the OSI Model. The model is a bunch of layers, nested like a Russian doll. At the bottom, there are physical circuits that only have to worry about reliably transferring individual bits over a cable or an antenna. And at the top, there's someone writing lines of code to make an HTTP API request.</p><h3 id="ip">IP</h3><p>Whether you realize it or not, when your computer is talking back and forth with other machines, it's exchanging data in the form of packets. Keeping in mind that there are multiple layers that allow devices to communicate over the internet, packets exist on the Network and Transport layer. And keeping in mind that everything generally follows a standard, packets follow a protocol called TCP/IP (transmission control protocol/internet protocol), or simply, the internet protocol suite.</p><p>TCP/IP is the central standard of the internet.</p><h3 id="tcp-udp">TCP &amp; UDP</h3><p>Each layer of the network stack has a different protocol, and sometimes there are even multiple protocols to choose from on a single layer. For instance, if you're working on the Transport layer, you'll often be choosing between sending a UDP or TCP packet.</p><p>You may know that UDP is for firing off quick messages. Messages that possibly disappear into the abyss, they might end up received or they may not.</p><p>Whereas TCP involves an exchange of messages in order to form a connection. The operating system keeps track of these connections. With TCP, devices on both sides get confirmation that the data got to the other end, without any errors, and that data will be received in the exact order it was sent.</p><h2 id="pick-your-protocol">Pick your protocol</h2><p>It turns out that <code>traceroute</code> can be written to utilize packets at either level of the network stack: the Network or Transport layer. Remember that even though the packets may be different, this doesn't change the fact they all work by setting the time to live.</p><p>One transport layer method is to fire off a UDP packet to the device at each hop.</p><p>Another transport layer method, using what's called a <a href="https://en.wikipedia.org/wiki/Port_scanner?ref=dept-engineering-blog.ghost.io">half-open scan or SYN scan</a>, is to attempt to open a TCP connection with the device at each hop.</p><p>And then there's another type of packet, called an ICMP (internet control message protocol) packet, that's a layer above UDP and TCP, in the network layer. The confusing bit is that while ICMP is pretty much the same deal as UDP and TCP, there's one crucial difference.</p><p>ICMP, UDP, and TCP packets are all payloads nested in an IP packet. But the difference is that ICMP is a part of the IP standard, whereas UDP &amp; TCP are just arbitrary data as far as IP is concerned.</p><p>So to review there are three techniques:</p><ul><li>fire off an ICMP packet and listen for a response</li><li>fire off a UDP packet and listen for a response</li><li>start a TCP connection, listen for a response and close the connection</li></ul><h3 id="trade-offs">Trade-offs</h3><p>ICMP is designed to be used in the way that <code>traceroute</code> uses it, for gathering information about the network. It's not meant to transfer any data, unlike TCP and UDP. Sometimes hops in the network will have rules that deny ICMP packets, so TCP and UDP are used as workarounds. While UDP may be able to get around rules that filter ICMP traffic, UDP packets are sent to a specific range of ports, and traffic on those ports may be blocked. Most devices allow TCP traffic on port 80, so that's a safe bet. Though with TCP we're now going to have to send a SYN packet, wait for the ACK and then send off a RST to close the connection.</p><h1 id="the-nitty-gritty">The nitty-gritty</h1><p>So now that we've covered the background, let's look at how I wrote <code>traceroute</code>. I chose to implement the ICMP method, so keep that in mind.</p><h2 id="programming-with-syscalls">Programming with syscalls</h2><p>When you're writing a script or compiling a program that interacts with the internet, it seems like things just magically happen, but under the hood, your interpreter or compiled code is talking to the operating system in order to send and receive data. As programmers, the way we can access the capabilities of our operating system is through a library of functions called "syscalls."</p><p><code>traceroute</code> relies on a number of syscalls. I'll go through most of them.</p><h3 id="sockets">Sockets</h3><p>One important syscall is <code>socket</code>. <a href="https://en.wikipedia.org/wiki/Unix_domain_socket?ref=dept-engineering-blog.ghost.io">Sockets</a> are how we're able to ask the operating system to connect us to the internet. They are an interface to the internet in the form of a file. This is great since files are simple and familiar: you read and write to them.</p><p>This implementation uses two sockets, one for sending packets and one for receiving packets. This is easier than, say, using just one, since <code>socket</code> works as a filter for different sorts of packets. With a socket, you can subscribe to the sorts of packets you want to receive. So in the receiving socket, I set a filter for ICMP packets and for the sending socket I'm specifying that I don't need any help from the OS to write my IP headers.</p><h3 id="sometimes-store-bought-isnt-fine">Sometimes store-bought isn't fine</h3><p>When you open a socket you have to specify which protocol you want to work with. If you were to just want to send a TCP or UDP packet you may be happy for the OS to work out all of the details, so you'd call <code>socket</code> with <code>SOCK_STREAM</code> or <code>SOCK_DGRAM</code> (more info can be found on the <a href="https://linux.die.net/man/2/socket?ref=dept-engineering-blog.ghost.io">man page for </a><code><a href="https://linux.die.net/man/2/socket?ref=dept-engineering-blog.ghost.io">socket</a></code>). This is an off-the-shelf solution to network programming, it makes life easy!</p><p>But in my case, I wanted to write and receive the entire packet, IP header and all. And to do this I had to open what's called <a href="https://en.wikipedia.org/wiki/Network_socket?ref=dept-engineering-blog.ghost.io#Types">a raw socket</a>. A raw socket gives complete access to the entire packet. In fact, since ICMP is a control protocol, a part of IP, raw sockets are required in order to work with those packets. Raw sockets are opened by calling <code>socket</code> with <code>SOCK_RAW</code>. An important note: programs that open raw sockets require root privileges in order to run.</p><p>Maybe it dawned on you that you're able to run <code>traceroute</code> on your computer without <code>sudo</code>. That's probably because your <code>traceroute</code> executable is owned by <code>root</code> and has the <a href="https://en.wikipedia.org/wiki/Sticky_bit?ref=dept-engineering-blog.ghost.io">sticky bit</a> enabled - a file permission that allows users to execute a file as the user that owns the file. On Linux there's a smart alternative method to allow users to execute a file with elevated permission, without willy-nilly bestowing root privileges, by setting <a href="https://wiki.archlinux.org/title/capabilities?ref=dept-engineering-blog.ghost.io">POSIX capabilities</a> instead.</p><h5 id="checksums">Checksums</h5><p>When you use a raw socket, you're signing up to be responsible for everything in the header. Or at least almost everything, some kernels will fill in the IP checksum for you.</p><p>In any event, the ICMP header does require me to create a checksum of the entire packet, so the receiver can verify that the request hasn't been corrupted or written wrong.</p><h4 id="call-and-response">Call and response</h4><p>Once you've made a socket, you need to use it. In my implementation, I use, in order: <code>sendto</code>, <code>select</code>, <code>recvfrom</code> and <code>close</code>.</p><ol><li><code>sendto</code> fires off the packet, our internet probe.</li></ol><p>2. <code>select</code> is used to tell us whether our file descriptor (a number that's assigned to our socket) has any data ready for us to read it.</p><p>3. <code>recvfrom</code> reads the data from the socket into a memory address we can then access.</p><p>4. <code>close</code> frees up the sockets we used to send and receive packets.</p><h3 id="all-the-rest">All the rest</h3><ul><li><code>getaddrinfo</code>: when you tell traceroute to query an address like <code>google.com</code> it needs to resolve that into an IP address, and this syscall gets us a linked list of IP addresses for that hostname.</li><li><code>getifaddrs</code>: since we need to send a packet to a remote place and wait for a response, we have to tell that place where to send it back. So with this syscall we're able to get a list of the network interfaces available to our operating system (from the device it's running on).</li><li><code>inet_ntop</code>: when we're dealing with addresses, they're almost always supplied to us in the form of a struct (in this case it accepts <code>in_addr</code>), and the fields are all a bunch of binary data (in network order). This syscall lets us converts the struct into a string value, the IPv4 network address in "dotted-decimal" format, i.e. "ddd.ddd.ddd.ddd".</li></ul><h2 id="a-big-monkey-wrench">A big monkey wrench</h2><p>After meticulously writing a bunch of code to put together the complete IP packet with ICMP payload, I hit a real hard roadblock sending out the packet I had  put together. Everything seemed right. My checksum code was fine. All the fields in both packets were correct. But the <code>sendto</code> syscall would always fail. <code>Invalid argument</code> was the only error information it returned. And to make matters worse, thanks to a macOS security feature called <a href="https://en.wikipedia.org/wiki/System_Integrity_Protection?ref=dept-engineering-blog.ghost.io" rel="noopener">System Integrity Protection</a>, it's impossible, or at least difficult enough that I just gave up, to run something like <code><a href="https://strace.io/?ref=dept-engineering-blog.ghost.io">strace</a></code> to debug the failing syscall.</p><p>After some trial and error, I found that changing around the <code>ip_len</code> field would send out my packet. Using <code>tcpdump</code>, I could see the packet send, and then the response get received. I could tell the packet that got sent had a weird data payload, but <code>sendto</code> wasn't failing outright.</p><p>So with that hint, I managed to dig up <a href="https://packetstories.com/post/tale_of_two_endians/?ref=dept-engineering-blog.ghost.io">a wonderful blog post</a> that explained there's a long-standing quirk in BSD-family kernels where two specific fields, <code>ip_len</code> and <code>ip_off</code> need to be set in host order. Talk about an obscure error!</p><h2 id="lessons-learned">Lessons learned</h2><p>I didn't really know how this project would play out, and it's been a twisty path sorting out a lot of different things I haven't worked with before.</p><p>First, the importance of <strong>not giving up prematurely when you're learning something</strong>. You never know when you're right on the cusp of solving an issue. I find that if you're truly stumped by something, step away for a bit - a day, a week, maybe even months - and come back to the problem with a fresh mind. It worked for me on this project.</p><p>Second, don't be intimidated by seemingly complex subjects. <strong>The patience to painstakingly piece together knowledge like this is a skill that can be learned through practice</strong>. I had the impression of network programming as something reserved for a college course, but in my experience, it's totally possible to teach yourself.</p><p>Third, I think books are an under-appreciated resource! For network programming, I can't recommend <a href="https://en.wikipedia.org/wiki/UNIX_Network_Programming?ref=dept-engineering-blog.ghost.io">UNIX Network Programming, Volume 1: The Sockets Networking API by W. Richard Stevens</a>. Tutorials are fine for some things, but it's hard to find another format that presents so much information in a clear and complete way. When learning a new topic seek out a wide array of sources to try out what works best for you. <strong>Sometimes you just need to find the information presented in the right way and this isn't necessarily the same for everyone</strong>.</p><p>I'd be remiss not to mention my impression of using Kotlin for the first time. Kotlin/Native is a great platform and its interoperability is very powerful. That said, it's probably not wise to just go ahead and rewrite C applications in Kotlin. I found debugging to be hard, and it's easier using the very well-developed tooling around C and related languages. If you're using Kotlin/Native, it's best to stick to what it excels at: interoperating with native libraries to build multi-platform native applications.</p><p>This project hasn't been easy, but that made it all the more rewarding and I've learned a lot along the way. I think anyone that wants to fill out their knowledge about the internet should just jump into the deep end and give network programming a try. I feel more confident having peeked behind the curtain, and perhaps now network errors will be less of a mystery.</p>]]></content:encoded></item><item><title><![CDATA[We asked ChatGPT to architect our application and this is what happened]]></title><description><![CDATA[I asked ChatGPT to help us get started with our app architecture. This is what happened.]]></description><link>https://engineering.deptagency.com/we-asked-chatgpt-to-architect-our-application-and-this-is-what-happened/</link><guid isPermaLink="false">63dbe8ac3d8082003d19edf4</guid><category><![CDATA[Chatgpt]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Fri, 03 Feb 2023 13:55:24 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/architecture-concept.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/architecture-concept.jpg" alt="We asked ChatGPT to architect our application and this is what happened"/><p>I opened up my laptop Monday morning and saw this email from the CEO:</p><blockquote>We're pivoting. We need a new app, fast! New idea is basically Uber for dogs. If you need a cute dog, quick, this app will get you a dog to rent, FAST. It has to have an excellent native iOS and Android app. Need it by next Monday.</blockquote><p>After I wiped up the coffee I spit out on my desk and gathered my sanity, I blanked on where to start. Then I remembered that ChatGPT is here to help me get started.</p><p>So, I asked ChatGPT to help us get started with our app architecture. This is what happened.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/rentals.-It-has.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1552" height="1554" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/rentals.-It-has.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/rentals.-It-has.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/rentals.-It-has.png 1552w" sizes="(min-width: 720px) 720px"/></figure><p>Huh, well, I'm a little put off by the level of confidence, but that's actually ... Better than I expected.</p><p>I'm not just gonna sit here and let AI take over my job though, I can still do better.</p><p>A queue will definitely work, but what about "dogs" screams "queuing" to ChatGPT? I'm not quite sure why dog rentals need a queue-based system, but I'm not against it either.</p><p>And more importantly, what's the justification for using a NoSQL database over a relational database? That's questionable, in this engineer's opinion!</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/11-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--why-would-I-use-a-NoSQL-database.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1620" height="1398" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/11-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--why-would-I-use-a-NoSQL-database.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/11-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--why-would-I-use-a-NoSQL-database.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/02/11-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--why-would-I-use-a-NoSQL-database.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/11-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--why-would-I-use-a-NoSQL-database.png 1620w" sizes="(min-width: 720px) 720px"/></figure><p>Ok well, I definitely need to handle a large number of dog rental requests. This idea is going to take off like crazy so I'm glad NoSQL is webscale.</p><p>The rest of this is a load of baloney, ChatGPT!</p><p>Who's to say NoSQL will be faster at querying than PostGresSQL!? What if I need to get at my dog rentals by joining user information. What if I want to give free dog rentals on birthdays? That's gonna be hard to do!</p><p>And NoSQL "can be more cost-effective than relational databases" Phoey! Running my own data center <em>can be</em> more effective than using the cloud but I'm not gonna do that!</p><p>I also don't think storing my dog rental data in unstructured form is a good thing. This is a mission-critical app and I want the utmost data integrity.</p><p>Ok, let's move on.</p><p>What about those Microservices? That seems like overkill to me.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-microservices-or-create-a.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1574" height="1270" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-microservices-or-create-a.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-microservices-or-create-a.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-microservices-or-create-a.png 1574w" sizes="(min-width: 720px) 720px"/></figure><p>That... Makes a lot of sense. Fair point on the pros and cons, ChatGPT. While I watched this answer type out I was full of judgment but then it ended on that note and I don't know if I could have come up with a better answer myself.</p><p/><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/clap.gif" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="364" height="347"><figcaption><em>Well done, ChatGPT, point for you.</em></figcaption></img></figure><p>Ok, so how about the deployment. Using "cloud services" is pretty vague.  Let's clear this up.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Choose-a-cloud-provider-Select-a-cloud-provider-that-meets-the-requirements-of-the.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1786" height="1482" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/Choose-a-cloud-provider-Select-a-cloud-provider-that-meets-the-requirements-of-the.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/Choose-a-cloud-provider-Select-a-cloud-provider-that-meets-the-requirements-of-the.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/02/Choose-a-cloud-provider-Select-a-cloud-provider-that-meets-the-requirements-of-the.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Choose-a-cloud-provider-Select-a-cloud-provider-that-meets-the-requirements-of-the.png 1786w" sizes="(min-width: 720px) 720px"/></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/suspect.gif" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="343" height="480"><figcaption>Suspicious...</figcaption></img></figure><p>This is all suspiciously vague. This type of answer seems like it was written by a non-technical MBA student in their first year. Not even a mention of containers. Blasphemy!</p><p>Take that point away!</p><p>Ok, how about React Native. Sure, we'll save some money and effort not having to code two different apps but the experience won't be as good!</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-native-code-or-React.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1524" height="1420" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-native-code-or-React.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-native-code-or-React.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/If-I-m-going-to-design-an-Uber-for-Dog-Rentals-app--should-I-use-native-code-or-React.png 1524w" sizes="(min-width: 720px) 720px"/></figure><p>Hmmm... Ok, another good overview.</p><p>Point back, ChatGPT.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Our-CEO-really-wants-the-ultimate-user-experience-for-her-dog-renters--so-we-re-gonna-go.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1574" height="534" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/Our-CEO-really-wants-the-ultimate-user-experience-for-her-dog-renters--so-we-re-gonna-go.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/Our-CEO-really-wants-the-ultimate-user-experience-for-her-dog-renters--so-we-re-gonna-go.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Our-CEO-really-wants-the-ultimate-user-experience-for-her-dog-renters--so-we-re-gonna-go.png 1574w" sizes="(min-width: 720px) 720px"/></figure><p>Finally, let's ask ChatGPT about the CEO's idea, after all, the best engineers know when NOT to build things, right?</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Would-you-use-an-uber-for-dogs-app.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1492" height="604" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/Would-you-use-an-uber-for-dogs-app.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/Would-you-use-an-uber-for-dogs-app.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Would-you-use-an-uber-for-dogs-app.png 1492w" sizes="(min-width: 720px) 720px"/></figure><p>Sounds like a winner to me! Off to build that app...</p><h1 id="what-did-we-learn">What did we learn?</h1><p>AI is a good actor!</p><p>But seriously: despite seeing lots of posts/articles about how great ChatGPT is at various things, it <em>still</em> did quite a bit better than I thought it would with such a vague question about design/architecture. I was not expecting such well-rounded answers, even with caveats about weighing the pros and cons.</p><p>It also does well with very specific information. You can see how diving into NoSQL gave many more specifics and even gave those nice caveats about when it might not be applicable.</p><p>The overly-confident tone it presents is off-putting, though. For a new tool that is scraping internet articles for its answers, it sounds <em>way</em> more confident than it should be. And not <em>all</em> of the answers presented caveats about weighing pros and cons.</p><p>Using the tool is a great way to get started if you're stuck or just learning. It does a great job of amalgamating information that might take you a while to collect with various internet searches.</p><p>However, I do hope that the confident tone can be scaled back before unknowing newbies use the tool to direct their software development efforts, whether it be for code or for architecture and design.</p><p>There is a great article I recently read called <a href="https://castlebridge.ie/insights/llms-and-the-enshittening-of-knowledge/?ref=dept-engineering-blog.ghost.io">ChatGPT and the Enshittening of Knowledge</a> that sums this up perhaps better than I can:</p><blockquote>[...] in order to ensure that its answer fits what its model tells it is expected to come next, ChatGPT is also making shit up. So, we have a generic non-committal middle of the road representation of knowledge coupled with what, in a human, we’d class as “A-Grade Bullshitter” levels of self-confidence so they make shit up to support their argument.</blockquote><blockquote>The merit or trust rating of a book or journal or newspaper that cites the A-Grade Bullshit then increases the truthiness of the bullshit, resulting the next iteration of the question to the AI having EVEN MORE CONFIDENCE in their bullshit. [...] This results in a classic data quality spiral where the incorrect data becomes accepted as fact and decisions or outputs that disagree are discounted. The Enshittening of Knowledge gathers momentum.</blockquote><p>We're still not even close to the eyes, ears, and brain of a discriminating engineer.</p><p>You heard it here first: Learning the old-fashioned way by trying and failing is still the way to go.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Why-are-you---confident.png" class="kg-image" alt="We asked ChatGPT to architect our application and this is what happened" loading="lazy" width="1680" height="628" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/Why-are-you---confident.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/Why-are-you---confident.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/02/Why-are-you---confident.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/Why-are-you---confident.png 1680w" sizes="(min-width: 720px) 720px"/></figure>]]></content:encoded></item><item><title><![CDATA[Setting up PostGIS with Rails and ActiveRecord on Fly.io]]></title><description><![CDATA[How to get Ruby on Rails working with PostGIS and PostgreSQL on Fly.io]]></description><link>https://engineering.deptagency.com/setting-up-postgis-with-rails-on-flyio/</link><guid isPermaLink="false">633dad4cfe9806003d0cf11e</guid><category><![CDATA[Platforms]]></category><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Caleb Rutan]]></dc:creator><pubDate>Tue, 31 Jan 2023 14:40:57 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1635772429028-f0375f2fbddb?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDMzfHxwdXp6bGV8ZW58MHx8fHwxNjY0OTkwODE5&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1635772429028-f0375f2fbddb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDMzfHxwdXp6bGV8ZW58MHx8fHwxNjY0OTkwODE5&ixlib=rb-1.2.1&q=80&w=2000" alt="Setting up PostGIS with Rails and ActiveRecord on Fly.io"/><p>If you're interested in deploying a Rails application to <a href="https://fly.io/?ref=dept-engineering-blog.ghost.io">Fly.io</a> and need support for geographic objects and location data in your database, they now support <a href="https://postgis.net/?ref=dept-engineering-blog.ghost.io">PostGIS</a> - an extension to the powerful PostgreSQL database backend out of the box.  It took a little extra tweaking to make it work with Rails the way I needed, though.</p><p>There is a very handy database adapter for ActiveRecord - the aptly named <a href="https://github.com/rgeo/activerecord-postgis-adapter?ref=dept-engineering-blog.ghost.io">ActiveRecord PostGIS Adapter</a> that enables you to include spatial data types in your ActiveRecord models, and then lean on the database for spatial queries.  </p><h3 id="basic-setup">Basic Setup</h3><p>I set up a simple Rails application.  Added the PostGIS adapter in the Gemfile:</p><!--kg-card-begin: markdown--><pre><code># Use postgresql as the database for Active Record
gem &quot;pg&quot;, &quot;~&gt; 1.1&quot;
gem &quot;activerecord-postgis-adapter&quot;, &quot;~&gt; 8.0&quot;
</code></pre>
<!--kg-card-end: markdown--><p>I  configured the database.yml configuration as specified in the documentation, with the addition of the url as an environment variable provided by Fly.io:</p><!--kg-card-begin: markdown--><pre><code>production:
  adapter: postgis
  encoding: unicode
  url: &lt;%= ENV['DATABASE_URL'] %&gt;  
  schema_search_path: public, postgis</code></pre>
<!--kg-card-end: markdown--><p>I ran <em><a href="https://fly.io/docs/flyctl/launch/?ref=dept-engineering-blog.ghost.io">fly launch</a></em> to get the app set up.  All good.</p><h3 id="a-bump-in-the-road">A Bump in the Road</h3><p>I built a very simple model and migration with a single piece of spatial data to test things out:</p><!--kg-card-begin: markdown--><pre><code>class CreatePlaces &lt; ActiveRecord::Migration[7.0]
  def change
    create_table :places do |t|
      t.string :name, null: false
      t.st_point :latlon, geographic: true
      t.timestamps
    end
  end
end</code></pre>
<!--kg-card-end: markdown--><p>I ran <em><a href="https://fly.io/docs/flyctl/deploy/?ref=dept-engineering-blog.ghost.io">fly deploy</a></em> - which promptly blew up trying to run the migration:</p><!--kg-card-begin: markdown--><pre><code> undefined method `st_point' for #&lt;ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition...
</code></pre>
<p>(many, many more lines of error followed)</p>
<!--kg-card-end: markdown--><p>Looks like Rails was still trying to do everything via the PostgreSQL adapter, as opposed to the PostGIS one.  The issue is that ActiveRecord takes its cue for the adapter from the protocol prefix on the URL.  Fly.io configures our database URL to look like this:</p><p><code><a href="postgis://appname:password@host:5432/appname" rel="noopener noreferrer">postgres://appname:password@host:5432/appname</a></code></p><p>And we need it to look like this:</p><p><code><a href="postgis://appname:password@host:5432/appname" rel="noopener noreferrer">postgis://appname:password@host:5432/appname</a></code></p><h3 id="the-solution">The Solution</h3><p>There are a couple of roads to take to solve this problem.  You can reset the DATABASE_URL environment variable - stored in a secret in Fly.io:</p><!--kg-card-begin: markdown--><pre><code>flyctl secrets set DATABASE_URL=postgis://appname:password@host:5432/appname
</code></pre>
<!--kg-card-end: markdown--><p>There are some <a href="https://groups.google.com/g/rgeo-users/c/SDRhEeMZLWc/m/nue2sFRV6qwJ?ref=dept-engineering-blog.ghost.io">potential problems</a> with doing that to the environment variable, as other extensions may read it, and they will probably not interpret a "postgis" protocol correctly.</p><p>I have chosen to adjust my database.yml to swap the protocols:</p><!--kg-card-begin: markdown--><pre><code>production:
  encoding: unicode
  adapter: postgis
  url: &lt;%= ENV['DATABASE_URL'].gsub(/^postgres/,'postgis') %&gt;  
  schema_search_path: public, postgis
</code></pre>
<!--kg-card-end: markdown--><p>Both solutions work - but there could be some follow-on consequences with changing the DATABASE_URL environment variable across the board.  Either way, run <em>fly deploy</em> to push changes out, and it should deploy cleanly:</p><!--kg-card-begin: markdown--><pre><code>         Preparing to run: `bin/rails fly:release` as root
         2022/10/05 18:25:45 listening on [fdaa:...]:22 (DNS: [fdaa::3]:53)
         Starting clean up.
==&gt; Monitoring deployment

 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--&gt; v7 deployed successfully
</code></pre>
<!--kg-card-end: markdown--><p>In console, everything worked as expected - I was able to add a Place with a latitude/longitude point:</p><!--kg-card-begin: markdown--><pre><code>irb(main):007:0&gt; Place.create(name: &quot;Statue of Liberty&quot;, latlon: 'POINT(40.689347001801465 -74.04447894312094)')
=&gt;
#&lt;Place:0x00007f0f50e39c50
 id: 4,
 name: &quot;Statue of Liberty&quot;,
 latlon: #&lt;RGeo::Geographic::SphericalPointImpl:0x2f4e0 &quot;POINT (40.689347001801465 -74.04447894312094)&quot;&gt;,
 created_at: Wed, 05 Oct 2022 16:49:31.455995000 UTC +00:00,
 updated_at: Wed, 05 Oct 2022 16:49:31.455995000 UTC +00:00&gt;
irb(main):008:0&gt;</code></pre>
<!--kg-card-end: markdown--><h3 id="wrapping-up">Wrapping Up</h3><p>This problem had me stumped for a few hours.  Hopefully this writeup helps you get through it faster than I did!</p>]]></content:encoded></item><item><title><![CDATA[Building an NLP Image: Amazon AMI for NVIDIA NeMo]]></title><description><![CDATA[NeMo is a toolkit provided by NVIDIA that allows you to use modules (reusable components) for a variety of AI tasks. Components like encoders, decoders, loss functions and more.]]></description><link>https://engineering.deptagency.com/building-an-nlp-image-amazon-ami-for-nvidia-nemo/</link><guid isPermaLink="false">642b11e59527e0003dca401f</guid><category><![CDATA[AI]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Bob Briski]]></dc:creator><pubDate>Wed, 25 Jan 2023 18:52:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/Building-an-NLP-Image_-Amazon-AMI-for-NVIDIA-NeMo.jpg" medium="image"/><content:encoded><![CDATA[<h1/><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/Building-an-NLP-Image_-Amazon-AMI-for-NVIDIA-NeMo.jpg" alt="Building an NLP Image: Amazon AMI for NVIDIA NeMo"/><p>TLDR; I built a <a href="https://us-west-2.console.aws.amazon.com/ec2/home?region=us-west-2&ref=dept-engineering-blog.ghost.io#Images:visibility=public-images;search=dept-nemo;">NeMo AWS image (AMI)</a> for my NLP work. It is simple and sparse on purpose. Feel free to <a href="https://github.com/rbriski/nemo-arch?ref=dept-engineering-blog.ghost.io">use it by itself</a> or as a base image to build on top of:</p><ul><li><a href="https://github.com/rbriski/nemo-arch?ref=dept-engineering-blog.ghost.io">Code to create the entire environment</a></li><li><a href="https://us-west-2.console.aws.amazon.com/ec2/home?region=us-west-2&ref=dept-engineering-blog.ghost.io#Images:visibility=public-images;search=dept-nemo">Just the AMI</a></li></ul><h2 id="what-is-nemo">What is NeMo?</h2><p><a href="https://docs.nvidia.com/deeplearning/nemo/user-guide/docs/en/stable/starthere/intro.html?ref=dept-engineering-blog.ghost.io">NeMo</a> is a toolkit provided by NVIDIA that allows you to use modules (reusable components) for a variety of AI tasks. Components like encoders, decoders, loss functions and more.</p><p><strong><strong>From NVIDIA</strong></strong></p><blockquote><em>NVIDIA NeMo, part of the NVIDIA AI platform, is a framework for building, training, and fine-tuning GPU-accelerated speech and natural language understanding (NLU) models with a simple Python interface. Using NeMo, developers can create new model architectures and train them using mixed- precision compute on Tensor Cores in NVIDIA GPUs through easy-to-use application programming interfaces (APIs).</em></blockquote><p>I chose it, specifically, because I wanted to fine-tune pre-trained NLP models for a client. This is possible with other toolkits, for example HuggingFace, but I wanted to take advantage of the massive pre-trained models that NVIDIA makes available.</p><h2 id="why-create-an-ami">Why create an AMI?</h2><p>AWS machine images are called AMIs. You can use existing AMIs for general use cases. For example, a clean install of Ubuntu Linux. You can also create custom AMIs for private use or publish it for public use.</p><p>NVIDIA actually already has a NeMo AMI image available. I tried using it but I was getting dependency conflicts on certain libraries that I was trying to install. I also just like to understand how the underlying system is built when I’m working on something so I decided to build my own AMI.</p><h1 id="building-an-ami"><strong><strong>Building an AMI</strong></strong></h1><h2 id="instance-type-and-size"><strong><strong>Instance type and size</strong></strong></h2><p>I chose the <em>p3.2xlarge</em> instance to build on. NeMo needs to run on NVIDIA GPUs so there are a limited number of instances you can choose from. The <em>p3.2xlarge</em> type is reasonably powerful and I want to fine-tune some pretty large models so I figured this would be a good compromise between performance and cost.</p><p>A warning, do not just spin this machine up and let it run. They are expensive. Around $3/hr at the cheapest.</p><p>I also bumped the root device size to 100GB to accommodate all of the packages and libraries that need to be built for NeMo.</p><h2 id="prerequisites"><strong><strong>Prerequisites</strong></strong></h2><p>NeMo requires quite a few packages to run. The full list is in the code but I’ll go through some of the bigger ones.</p><h2 id="nvidia-drivers"><strong><strong>NVIDIA Drivers</strong></strong></h2><p>The <em>p3.2xlarge</em> uses V100 Tensor Core GPUs. A quick check with ubuntu-drivers devices shows me that the <em>nvidia-driver-525</em> package is the recommended install.</p><h2 id="cuda"><strong><strong>CUDA</strong></strong></h2><p>The entire point of using the NVIDIA chipset for the highly parallel performance boost delivered by CUDA. CUDA allows programmers to use GPUs to run highly concurrent code. It’s the foundation for much of the modern AI tools ecosystem and definitely required for NeMo. I had using NeMo with the latest version (CUDA 12, right now) so this image uses CUDA 11.7.</p><h2 id="python"><strong><strong>Python</strong></strong></h2><p>Most of the AI universe runs on Python. Ubuntu ships with Python 3.7 so it was already available but I wanted something a little more modern. I installed Python 3.10.9 on this image. It’s installed using <em>pyenv</em> under the <em>ubuntu</em> user so it’s immediately available as you log in.</p><p>Changing the primary Python version makes me squeamish since so many operating system processes and tools rely on the “factory installed” Python. I use <em>pyenv</em> to shim another version into the shell. This keeps the system happy and lets me use whatever I want.</p><h2 id="nemo"><strong><strong>NeMo</strong></strong></h2><p>The whole point of this exercise is to install NeMo, so that’s the final step. I installed it straight from the main branch. I only pull the NLP packages though so if you want to use it for something else, you’ll want to pip install the full toolkit.</p><h1 id="creating-the-ami"><strong><strong>Creating the AMI</strong></strong></h1><p>I’ve used <a href="https://www.packer.io/?ref=dept-engineering-blog.ghost.io">Hashicorp’s Packer</a> to create the AMI. I like Packer for a bunch of reasons. I like that I can basically do a history of commands I used to build the original image and arrange them into scripts. I also like that it looks similar to <a href="https://www.terraform.io/?ref=dept-engineering-blog.ghost.io">Terraform</a>, which I’m already familiar with. But the biggest, and probably most embarrassing, the reason is that I don’t like working with Docker.</p><p>I know Docker is amazing and it’s the backbone of so much of today’s modern infrastructure. But it’s also another abstraction and abstractions introduce unknowns. AMIs are also abstractions, but one I’m more familiar with. So it boils down to knowing my own areas of ignorance and planning around them. And, hey, if you love Docker, <a href="https://developer.hashicorp.com/packer/tutorials/docker-get-started/docker-get-started-build-image?ref=dept-engineering-blog.ghost.io">Packer builds Docker images</a> too.</p><p>My primary philosophy with this image is to get NeMo’s NPL library and that’s it. Feel free to use this as a base image to build a more featureful set of tools. Adding <a href="https://jupyter.org/?ref=dept-engineering-blog.ghost.io">Jupyter</a> is probably a good start and I might do that soon, but not in this image. I want to keep this simple because it was hard enough to get the versions dependencies correct.</p><p>The AMI name is <em>dept-nemo-image-{{timestamp}}</em> so it’s easy to pick up the latest build. You can see how in my <a href="https://github.com/rbriski/nemo-arch/blob/main/main.tf?ref=dept-engineering-blog.ghost.io#L31">Terraform code</a>.</p><h1 id="launching-an-instance-from-the-ami"><strong><strong>Launching an Instance from the AMI</strong></strong></h1><p>I have included Terraform code to build a simple VPC and launch the instance into that VPC. I allow SSH ingress and egress to anywhere. This setup is pretty simplistic so I imagine anyone wanting to use this in a more complex environment will need to make changes. Again, I don’t think I’ll include much more complex code in this repo though. It’s meant to be simple to launch and understand.</p><p>You’ll have to have Terraform set up to run in your AWS account, but that’s beyond the scope of this article. Luckily, Terraform already has a good set of instructions for getting started here: <a href="https://developer.hashicorp.com/terraform/tutorials/aws-get-started?ref=dept-engineering-blog.ghost.io">https://developer.hashicorp.com/terraform/tutorials/aws-get-started</a></p><h1 id="connecting-and-using"><strong><strong>Connecting and Using</strong></strong></h1><p>I only have one variable right now and that is a keypair name and key grouping. It allows you to add your key to the machine under the ubuntu user.</p><p>To test it out, I used this Notebook as a guide: <a href="https://github.com/NVIDIA/NeMo/blob/main/tutorials/nlp/Question_Answering.ipynb?ref=dept-engineering-blog.ghost.io">https://github.com/NVIDIA/NeMo/blob/main/tutorials/nlp/Question_Answering.ipynb</a></p><p>Something to keep in mind, the first time <em>pytorch_lightning</em> and some of the NLP models are imported will take a while. I haven’t figured out exactly what they’re doing, but I suspect they’re dynamically caching some information. After the first run, however, they’re fast to import.</p><h1 id="conclusion"><strong><strong>Conclusion</strong></strong></h1><p>I started this little project to help me understand how the different software pieces work together so that, in the future, I can easily make changes if I find that my setup doesn’t support what I’m trying to do. I don’t know enough about how to fine-tune LLMs yet to know the size of the machine I’ll need, additional supporting libraries, or even other pieces of AWS infrastructure.</p><p>My hope is that, by using Terraform and Packer with AWS, I give myself enough flexibility to quickly change my underlying build and then, when I’ve gotten it right, lock it in quickly so that I can replicate it.</p><p>I hope you find it useful too.</p>]]></content:encoded></item><item><title><![CDATA[I believe I can Fly [A Fly.io review]]]></title><description><![CDATA[Very soon Fly will become a contender to fully replace Heroku and will be much more simple and easy to use than something like Kubernetes or any of AWS's offerings.
Right now, it's great for hobbyists or a team with one or two developers. ]]></description><link>https://engineering.deptagency.com/our-experience-with-fly-io/</link><guid isPermaLink="false">63b8921507827f003d8bf77e</guid><category><![CDATA[Platforms]]></category><category><![CDATA[Fly.io]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Tue, 17 Jan 2023 16:02:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/Fly_Fishing_-154375843-.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/Fly_Fishing_-154375843-.jpeg" alt="I believe I can Fly [A Fly.io review]"/><p>I remember when I heard that Heroku was getting acquired by Salesforce - To say I was disappointed is an understatement.  Heroku pioneered developer-centric, easy-to-use webapp hosting and they did it <em>very</em> well.  But, as expected, things started slowly going downhill after the Salesforce acquisition - Culminating in them <a href="https://blog.heroku.com/next-chapter?ref=dept-engineering-blog.ghost.io">removing their free hobbyist hosting option</a>.</p><p>Here at DEPT® US, we still use Heroku to host an internal project tracking application. Last year <a href="https://thestack.technology/heroku-outage-github-breach/?ref=dept-engineering-blog.ghost.io">their Github integration broke</a> and wasn't fixed for almost two months, with very little communication from the company, that's when I decided I would no longer recommend it or use it as an option for deployments.</p><p>A few months ago, a colleague enthusiastically recommended <a href="http://fly.io/?ref=dept-engineering-blog.ghost.io">Fly.io</a> to me.  I trust his opinion a lot, and I gave it a shot and stood up a sample Node.js app on it.  I was seriously impressed!  An amazing CLI and I got an app with a database up and running in less than 30 minutes.  AND I could do all the wonderful things I could usually do with Docker containers. Oh, and it still has a free option, unlike some others (looking at you, Heroku!)</p><p>I was so impressed, that when we had to host another internal app, I immediately reached for it.  It's worth noting that these types of <a href="https://en.wikipedia.org/wiki/Platform_as_a_service?ref=dept-engineering-blog.ghost.io">Platform as a Service (PaaS)</a> tools are not for every use case, but ours was perfect since it wasn't a mission-critical app.  That said, we still wanted some basic things for it like monitoring and being able to view logs, etc.  Things that Heroku does splendidly.</p><p>To make a long story short: My experience was that though <a href="http://fly.io/?ref=dept-engineering-blog.ghost.io">Fly.io</a> has amazing promise, it doesn't <em>yet</em> have the necessary features for operational use on a team. I'll outline why in this article.  <strong>It </strong><em><strong>is great, </strong></em><strong>however, for hobby projects, single developer applications.  It's especially great if you just need a (free!) place to start and plan on migrating to a more mature platform at some point soon. </strong></p><p>This article may come off as critical, but useful software often has critics. I'm one of them. <strong>But, let me make it 100% clear: I am a Fly.io fan! I wouldn't take the time to write this if I wasn't excited by the promise it has.</strong></p><p>Ok, on we go....</p><h2 id="why-fly-is-awesome">Why Fly is awesome</h2><p><br>The whole platform is based on Docker containers.  You can do everything from creation of projects to managing secrets via <a href="https://fly.io/docs/hands-on/install-flyctl/?ref=dept-engineering-blog.ghost.io">command line</a>. This means everything can be scripted. It can use Heroku build packs.  It can be as simple or as complicated (within reason, see below) as you want or need it to be.</br></p><p>Think of it as Kubernetes or AWS Elastic Container Service on easy mode.  Though this ease of use brings with it a restriction the customization abilities of your deployments, and, as you'll see as you read, some limitations.</p><p>Oh, and did I mention that <strong>it has a free option to get started</strong>?</p><h2 id="setting-up-an-app">Setting up an app</h2><p>It's very, very easy:</p><ol><li><a href="https://fly.io/docs/hands-on/sign-in/?ref=dept-engineering-blog.ghost.io">Download the CLI</a>.</li><li>Authenticate with <code>flyctl auth login</code></li><li>Run <code>flyctl launch</code> or <code>flyctl apps create</code> and point it to your Dockerfile or Docker image.</li></ol><p>That's it! </p><h2 id="plaintext-configs">Plaintext Configs</h2><p>Fly uses a <code>fly.toml</code> file that is used to <a href="https://fly.io/docs/reference/configuration/?ref=dept-engineering-blog.ghost.io">define your application</a>.  All of your configs are here, at a glance and able to be version controlled. </p><p>You can also provide multiple definitions for different environments or variations as well using the <code>--config</code> flag on the CLI.  </p><p>One thing to note is that there is no "override" option (<a href="https://docs.docker.com/compose/extends/?ref=dept-engineering-blog.ghost.io">a-la docker-compose</a>), so you will have to copy/paste your definitions for now. No big deal.</p><h2 id="deploys-free-builders">Deploys / Free Builders</h2><p>One of the nicest things of the developer experience is how <em>easy</em> it is to deploy your app for testing.  You run <code>flyctl deploy</code> and it ships off your code to a free builder which builds your container image on their servers and uploads it to a secure private repo, then deploys it.  It's really awesome that they provide this for free, even as you scale beyond the initial free option.  Other CI/CD offerings like Github actions cap you at a certain number of builder minutes a month then charge you afterwards.</p><h2 id="apps-and-organizations">Apps and Organizations</h2><p>Fly has the concept of organizations, which are collections of individual apps.  Individual apps are essentially Docker containers that can run and scale independently. This is also how you grant access to users to manage your applications.</p><h2 id="secrets-management-environment-variables">Secrets Management / Environment Variables</h2><p><a href="https://open.spotify.com/track/1gVFwk7WUdhdmD7YBSaGxI?si=0bedd9198ce34db4&ref=dept-engineering-blog.ghost.io">Adding and managing secrets </a>in Fly is very easy and a pleasure to use.  You run a CLI <code>add</code> or <code>update</code> command and the variable is present to your app's container automatically. That's it.  There's even a very easy way to define <a href="https://fly.io/docs/reference/configuration/?ref=dept-engineering-blog.ghost.io#the-env-variables-section">non-sensitive environment variables in your fly.toml</a>. This was probably the easiest and best way of managing environment vars I've used, bar none.</p><h2 id="user-roles-and-permissioning">User Roles and Permissioning</h2><p>There are NO user permissions beyond having access to an organization.  You have organization access, you have the keys to the kingdom.</p><p>Additionally, your API tokens have access to <em>everything you as an end user have access to</em>.  There is no way to narrow them down so they only have deploy access to one application.  Most importantly, in order to hook up the command line utility to any deployment pipelines, you have to give your CI/CD tool <em>full access</em> to <em>everything</em> your user has access to 😵, including across organizations if the user is part of more than one. I would really like to see fine grained permissioning so that an access token only has deploy access to one application and nothing else.</p><p>To get around this, we created a separate "non production" organization where a whole development team can have permissions to freely configure and deploy the app.  We locked our production organization down to a small subset of trusted committers/deployers.</p><h2 id="monitoringalerting">Monitoring/Alerting</h2><p><br>Fly gives you <a href="https://fly.io/docs/reference/metrics/?ref=dept-engineering-blog.ghost.io">Prometheus metrics, displayed via a Grafana instance</a> out of the box that are super useful.  I really love that they used a well-known, open source tool to do this.  It is a version of Grafana with features disabled, though. </br></p><p>Crucially,<em> it does not have alerting enabled</em>.  I had to stand up my own instance of Grafana to be able to set up real-time alerts for errors on my app.</p><p>It was fairly easy for me to set up Grafana, and the Prometheus metrics from each app were available to plug into any Grafana instance.</p><p>I do like that the metrics are exposed and you can do what you want with it, but I had to sink about half a day into getting Grafana stood up with an SMTP provider.</p><p>Also, there is no way I'm aware of to export logs to a more digestible place like Splunk/Elasticsearch.  You can look at logs via the web console or the <a href="https://fly.io/docs/flyctl/logs/?ref=dept-engineering-blog.ghost.io">command line</a> only.</p><h2 id="databases">Databases</h2><p>Fly does support running <a href="https://fly.io/docs/database-storage-guides/?ref=dept-engineering-blog.ghost.io">Postgres and other databases</a> but they are really just running docker containers on volumes on fly itself.  It doesn't give you out-of-the-box High Availability or monitoring or easy snapshotting and restoring. To set up Postgres, you have to fork one of their repos.</p><p>Fly themselves claim that "<a href="https://fly.io/docs/postgres/getting-started/what-you-should-know/?ref=dept-engineering-blog.ghost.io">Fly Postgres is not managed Postgres</a>." Managed means things like replication and automated restores (among other things). They even point you to Heroku for a fully managed version of Postgres.  I commend them for being upfront about it, but it sure would be nice to be able to have a managed database alongside your app without having to worry about this. What they provide is certainly workable though!</p><h2 id="scheduled-tasksjobs">Scheduled Tasks/Jobs</h2><p>Surprisingly, there is no great way to run a Docker container as a scheduled task.  There is a way to do it using the newer "Machines" API which is a bit buried in their <a href="https://fly.io/docs/machines/working-with-machines/?ref=dept-engineering-blog.ghost.io">documentation (search for schedule)</a>, but it only supports limited scheduling options and not cron expressions.</p><p>Do a quick <a href="https://www.google.com/search?q=fly.io+scheduled+jobs&ref=dept-engineering-blog.ghost.io">Google search for Fly.io scheduled jobs</a>, and you'll see that many users have been rallying for this feature for a while.</p><h2 id="running-multiple-processes-sidecar-containers">Running Multiple Processes / Sidecar Containers</h2><p>Related to the Jobs portion, but zooming out a little: Fly does not yet have support for sidecar containers, so running multiple processes requires <a href="https://fly.io/docs/app-guides/multiple-processes/?ref=dept-engineering-blog.ghost.io">adding a process manager into your Docker container</a>. The addition of this feature might solve a lot of operational challenges we had!</p><p>If we could run a sidecar container, we could probably run our jobs or have a logging exporter to output logs to another service for us. </p><h2 id="security">Security</h2><p>One of the big draws of Fly is its ease of use on the command line.  However, this comes at a cost - Because of the lack of fine-grained permissions noted above, it is incredibly easy to run a command to change the <em>wrong</em> application.  And if your auth token is accidentally shared or exposed to a bad actor, they will have access to change a lot. Rotating keys also becomes problematic because you have to replace just about everything.</p><p>Speaking for myself, I would love to have an authentication configuration just for my non-productions applications to make sure I don't inadvertently take down my production app.  </p><h2 id="docker-limitations">Docker Limitations</h2><p>Some things you take for granted in Docker may not be available, like for instance the ability to mount multiple volumes in a container.  When I was setting up Grafana for alerting, I needed to mount two volumes to configure the app's secrets securely, but only one volume was supported.  Because of this, I had to work around it by copying files over and doing environment variable substitution.</p><h2 id="shifting-product-features">Shifting Product Features</h2><p>Fly is still in the early days, and they are adjusting the product quickly.  This is great because you know that there's a passionate group of developers working on this and making the product better.  It's not so great in that features are shifting and not being supported as well anymore.  For instance, we created a Postgres database, and when we went to create a staging version of it, the option to create it was no longer available.   When we went to the documentation page, we saw this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/image.png" class="kg-image" alt="I believe I can Fly [A Fly.io review]" loading="lazy" width="630" height="223" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/01/image.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/image.png 630w"/></figure><p>When you dive into that, you find out that their new "Apps V2" architecture is supplanting their original Hashicorp Nomad architecture, which everything we built was based on.  We stood up the app in June, and somewhere in the October/November timeframe, this announcement happened.</p><p>Unfortunately, at the time of this writing (January 2023), the Machines API is just that: literally a REST API you have to send commands to.  There is no support in their CLI for it or at least minimal support</p><p>In the course of writing this, I stumbled on a lot of information about how Fly's Machines-based architecture will likely solve a LOT of these limitations.</p><p>One such example is that there is a <a href="https://fly.io/docs/machines/guides-examples/terraform-machines/?ref=dept-engineering-blog.ghost.io">Terraform provider for the machines API</a>!  That's really exciting to see and hear.</p><h2 id="in-summary-onward-and-upwards">In Summary: Onward and Upwards</h2><p>I hope I've done a decent job of showing you the pros and cons of using Fly right now. Fly's promise and advantage are that it is obviously run by a team of talented developers working to make a tool that they would want to use.  They don't have the baggage of a large company like Amazon or Salesforce holding them back.  This is really exciting.</p><p>Very soon Fly will become a contender to fully replace Heroku for production application and will be much more simple and easy to use than something like Kubernetes or any of AWS's offerings.</p><p>Right now, it's pretty much the perfect option for hobbyists or a team with one or two developers.  When you need to get going fast and free, it can't be beat.  Just be aware of the limitations I've mentioned and how they may limit your ability to support your application.</p><p>In the meantime, if you need a simple-to-use deployment option for a production application on a team, my opinion is that you should stick with Heroku, or investigate using Elastic Beanstalk on AWS.  Of course, there are other options as well (<a href="https://www.aptible.com/?ref=dept-engineering-blog.ghost.io">Aptible</a> is a very interesting one some of our clients are using) but these are the ones we have experience with. </p>]]></content:encoded></item><item><title><![CDATA[Strengths & Weaknesses of GPT-3 for Enhancing Developer Efficiency]]></title><description><![CDATA[In this article, we will explore the usefulness of GPT-3 for developers by examining its performance in handling three common challenges that they may encounter in their work. These challenges include generating new code, explaining unfamiliar code constructs, and deciphering unusual error messages.]]></description><link>https://engineering.deptagency.com/strengths-and-weaknesses-of-gpt-3-for-enhancing-developer-efficiency/</link><guid isPermaLink="false">63b778aa07827f003d8bf768</guid><category><![CDATA[Chatgpt]]></category><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Ashwin Sundar]]></dc:creator><pubDate>Mon, 09 Jan 2023 16:18:23 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/strengths-weakness-gpt3-developers.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h1 id="introduction">Introduction</h1>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/strengths-weakness-gpt3-developers.jpg" alt="Strengths & Weaknesses of GPT-3 for Enhancing Developer Efficiency"/><p>Writing code can be a complex and cognitively demanding task, involving various intellectual gymnastics such as debugging code and memorizing syntaxes across different languages. To manage this cognitive load and improve efficiency, programmers need strategies and tools to help them navigate their daily work. One such tool is OpenAI's Generative Pretrained Transformer <a href="https://openai.com/blog/chatgpt/interact.sh/%2f../?ref=dept-engineering-blog.ghost.io">(GPT-3)</a>, an artificial intelligence (AI) language model which interacts with users in a conversational manner through its recently released ChatGPT-3 interface.</p>
<p>In this article, we will explore the usefulness of GPT-3 for developers by examining its performance in handling three common challenges that they may encounter in their work. These challenges include generating new code, explaining unfamiliar code constructs, and deciphering unusual error messages.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="use-case-1generating-new-code">Use case #1 - Generating new code</h1>
<p>ChatGPT-3 can generate new code. Here are some sample prompts and accompanying responses from ChatGPT-3.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong><u>Prompt</u></strong>: Create boilerplate code for a React.js project<br>
<strong><u>Response</u></strong>: Here is some boilerplate code for a React.js project:</br></p>
<pre><code class="language-javascript">import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      // Initialize state here
    };
  }

  render() {
    return (
      // Add JSX here
    );
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(&lt;App /&gt;, rootElement);
</code></pre>
<blockquote>
<p>This code creates an App class that extends React.Component and has a render method that returns JSX to be rendered to the page. The App component is then rendered to the element with the root ID. You can add your own state and JSX to customize the component.</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>That looks acceptable. GPT-3 even explained the code in plain English. The output can be corroborated by simply running the code and confirming that it behaves as described.</p>
<p>A more challenging prompt is to solve Problem 2 from <a href="https://projecteuler.net/problem=2?ref=dept-engineering-blog.ghost.io">Project Euler</a>, which is a series of problems that require mathematical and programming proficiency to solve.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong><u>Prompt</u></strong>: Solve Problem 2 from Project Euler:</p>
<blockquote>
<p>Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, the first 10 terms will be:</p>
<p>1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...</p>
<p>By considering the terms in the Fibonacci sequence whose values do not exceed four million, find the sum of the even-valued terms.</p>
</blockquote>
<p><strong><u>Response</u></strong>: Here is a recursive solution to Problem 2 in Project Euler using Python:</p>
<pre><code class="language-python"># define a recursive function to compute the sum of the even-valued terms in the Fibonacci sequence
# whose values do not exceed a given maximum value
def even_fibonacci_sum(n, a=1, b=2, sum=0):
  # if the second term exceeds the maximum value, return the sum
  if b &gt; n:
    return sum
  # if the second term is even, add it to the sum
  if b % 2 == 0:
    sum += b
  # call the function recursively with the next terms in the sequence
  return even_fibonacci_sum(n, b, a + b, sum)

# test the function
print(even_fibonacci_sum(4000000))
</code></pre>
<blockquote>
<p>This function takes in three arguments: the maximum value (n), the first term (a), and the second term (b) of the Fibonacci sequence. It also has an optional argument sum that keeps track of the sum of the even-valued terms in the sequence.</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>The solution comes with comments and a brief explanation of how the code works. The recursive function accepts one mandatory argument (the maximum value <code>n</code>) and three optional arguments:</p>
<ul>
<li><code>a</code>: The first term of the sequence</li>
<li><code>b</code>: The second term of the sequence</li>
<li><code>sum</code>: The running sum of the even-valued terms</li>
</ul>
<p>If <code>b</code> exceeds the maximum value specified, then the entire sum is returned and the function completes. Otherwise, the function checks if the second term is even and adds it to the running sum, and the function is called again. However, ChatGPT-3 did not correctly pick up on the fact that the last 3 arguments are optional, so the solution description is not accurate in this instance.</p>
<p>Finally, let's come up with some fun and novel code based on a vague prompt, in a functional programming language called Standard ML.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong><u>Prompt</u></strong>: Write an interesting Standard ML program based on <a href="https://www.bartleby.com/129/?ref=dept-engineering-blog.ghost.io">Bartleby's Parable</a>.</p>
<p><strong><u>Response</u></strong>: Here is a possible Standard ML program that is based on Bartleby's Parable.</p>
<pre><code class="language-sml">(* The main function that represents Bartleby's actions *)
fun bartleby() =
  let
    val message = &quot;I would prefer not to&quot;
    val tasks = [&quot;copy documents&quot;, &quot;file papers&quot;, &quot;answer letters&quot;]
    val responses = map (fn task =&gt; print(task ^ &quot;? &quot; ^ message ^ &quot;\n&quot;)) tasks
  in
    if (forall (fn response =&gt; response = message) responses) then
      print(&quot;Bartleby is uncooperative and does not complete any tasks.\n&quot;)
    else
      print(&quot;Bartleby completes some tasks, but not all.\n&quot;)
  end

(* Call the main function *)
bartleby()
</code></pre>
<blockquote>
<p>This program defines a function <code>bartleby</code> that represents the actions of the character Bartleby from the parable. The function has a list of tasks that Bartleby is asked to do, and for each task Bartleby responds with the message &quot;I would prefer not to&quot;. If Bartleby responds with this message for all tasks, then the function prints that Bartleby is uncooperative and does not complete any tasks. Otherwise, the function prints that Bartleby completes some tasks, but not all.</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Impressively, ChatGPT-3 is able to generate a functional program based on a couple of totally unrelated topics: recursive programming in SML and the parable of <a href="https://www.bartleby.com/129/?ref=dept-engineering-blog.ghost.io">&quot;Bartleby, the Scrivener&quot;</a>. This is a short story written by Herman Melville in 1853 about a Wall Street clerk who refuses to do his boss's bidding, simply replying &quot;I would prefer not to,&quot;, a feeling shared by many programmers when the holidays approach. The output is a short SML program that seems to produce a variation of this response based on what is asked of him. However, there are a few problems - both on the programming side as well as the story side.</p>
<p>First of all, this code does not compile because the <code>forall</code> function is not defined in this context. The fix is to rewrite the <code>if(forall (...))</code> expression in a recursive style.</p>
<p>Second, in the original parable, Bartleby refuses to complete any task asked of him, yet this code indicates that he may complete some tasks, but not all. It doesn't make sense to include this branch in the conditional statement based on the original story. This intersection of unrelated topics seems to prove difficult for the language model to process properly.</p>
<p>The code provided requires some intermediate familiarity with Standard ML to debug and fix successfully. Therefore, in this example, the programmer is not fully relieved from the task of writing idiomatic and correct code. GPT-3 performs well with close-ended requests, such as how to create simple code in a language. It seems to struggle with open-ended requests that have an indefinite solution, such as the query to write a program about Bartleby.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="use-case-2explaining-unfamiliar-constructs">Use Case #2 - Explaining unfamiliar constructs</h1>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>How does ChatGPT-3 handle unfamiliar syntax and programming constructs? In the following example, a Complex number class is created using a construct called an <a href="https://en.cppreference.com/w/cpp/language/operators?ref=dept-engineering-blog.ghost.io">overloaded operator</a>. This construct is not uncommon, but may be unfamiliar to some developers. Let's see if GPT-3 can make sense of this code.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong><u>Prompt</u></strong>: What does this C++ code mean?</p>
<pre><code class="language-c++">class Complex {
  double re, im;

public:
  Complex(double re, double im) : re(re), im(im) {}

  Complex operator+(const Complex&amp; other) const {
    return Complex(re + other.re, im + other.im);
  }
};
</code></pre>
<p><strong><u>Response</u></strong>:</p>
<blockquote>
<p>This C++ code defines a Complex class that represents complex numbers. The Complex class has two member variables, re and im, that represent the real and imaginary parts of the complex number, respectively. It also has a constructor that takes the real and imaginary parts as arguments and initializes the re and im member variables.</p>
</blockquote>
<blockquote>
<p>The Complex class also has a operator+ member function that overloads the + operator for the Complex class. This function takes another Complex object as an argument and returns a new Complex object that is the sum of the two operands. The operator+ function uses the re and im member variables of the Complex objects to compute the sum of the two complex numbers, and then it returns a new Complex object with the result.</p>
</blockquote>
<blockquote>
<p>This Complex class allows you to create Complex objects and add them using the + operator, as shown in the following example:</p>
</blockquote>
<pre><code class="language-c++">int main() {
  Complex a(1.0, 2.0);  // a = 1.0 + 2.0i
  Complex b(3.0, 4.0);  // b = 3.0 + 4.0i
  Complex c = a + b;    // c = (1.0 + 3.0) + (2.0 + 4.0)i = 4.0 + 6.0i

  return 0;
}
</code></pre>
<blockquote>
<p>In this example, we create two Complex objects a and b, and then add them using the + operator. The result is a new Complex object c that represents the sum of the two operands. Because we have overloaded the + operator for the Complex class, the + operator will automatically call the operator+ member function of the Complex class to add the two objects and return the result.</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>This is a good explanation of the Complex number class created. GPT-3 was able to understand that this code relates to complex numbers based on the names of the class and member variables, even though the code was uncommented. Finally, it was able to write an example of how to use the Complex function to add two complex numbers using the overloaded + operator. GPT-3 performs well when presented with a question about language syntax, a well-documented concept that lends itself to definitive answers.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="use-case-3deciphering-misleading-error-messages">Use Case #3 - Deciphering misleading error messages</h1>
<p>A useful skill for a programmer to develop is the ability to interpret unhelpful or misleading error messages. The following C++ code compiles successfully, but generates a <a href="https://en.wikipedia.org/wiki/Segmentation_fault?ref=dept-engineering-blog.ghost.io">segfault</a> at run-time.</p>
<p><code>segfault_ex.cpp</code></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main() {
	std::vector&lt;int&gt; v{1,2,3};	
	for (int i=1; i&lt;=4; i++) { std::cout &lt;&lt; v[i] &lt;&lt; std::endl; }	
	return 0;
}
</code></pre>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>This code should compile successfully, yet generate a segfault to out-of-bounds access of items in vector <code>v</code>. Yet when we run <code>g++ segfault_ex.cpp</code>, which uses <a href="https://gcc.gnu.org/?ref=dept-engineering-blog.ghost.io">GNU Compiler</a> to compile the <code>.cpp</code> file to an executable, the following error message is generated at compilation:</p>
<pre><code>segfault-example.cpp:5:20: error: expected ';' at end of declaration
        std::vector&lt;int&gt; v{1,2,3};      
                          ^
                          ;
1 error generated.
</code></pre>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Helpfully, the error explains where a semicolon needs to be inserted. We could resolve the error by filling the vector on the next line with <a href="https://en.cppreference.com/w/cpp/container/vector/push_back?ref=dept-engineering-blog.ghost.io"><code>std::vector::push_back</code></a>, but that isn't an elegant or idiomatic solution. If we research the <a href="https://en.cppreference.com/w/cpp/container/vector/vector?ref=dept-engineering-blog.ghost.io">rules for expressions for vectors</a>, we discover that this syntax is correct, as it uses an initializer list {1,2,3} to construct the vector.</p>
<p>Something doesn't make sense. The syntax used is valid, so why are we getting this error? Let's revisit the link and carefully read again:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/cppref-vectors1.png" width="807" height="592" loading="lazy" alt="Strengths & Weaknesses of GPT-3 for Enhancing Developer Efficiency" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/01/cppref-vectors1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/cppref-vectors1.png 807w" sizes="(min-width: 720px) 720px"/></div><div class="kg-gallery-image"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/cppref-vectors2.png" width="745" height="755" loading="lazy" alt="Strengths & Weaknesses of GPT-3 for Enhancing Developer Efficiency" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/01/cppref-vectors2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/cppref-vectors2.png 745w" sizes="(min-width: 720px) 720px"/></div></div></div><figcaption><a href="https://en.cppreference.com/w/cpp/container/vector/vector?ref=dept-engineering-blog.ghost.io">https://en.cppreference.com/w/cpp/container/vector/vector</a></figcaption></figure><hr><!--kg-card-begin: markdown--><p>The initializer list syntax for vectors was not implemented until C++11! By default, the version of <code>g++</code> on my machine compiles to C++98.</p>
<pre><code class="language-bash">❯ g++ --version | head -1
Apple clang version 13.1.6 (clang-1316.0.21.2)
❯ g++ -dM -E -x c++  /dev/null | grep -F __cplusplus
#define __cplusplus 199711L
</code></pre>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>The type of debugging suddenly jumped from simple syntax mistakes to changing language settings in the compiler. A lot of programmers will find this frustrating to solve and fix. Can GPT-3 save the day? Will it get distracted by the obvious segfault issue? Or will it dig deeper and discover which g++ flag needs to be used to compile to C++11 and allow the use of vector intializer lists?</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong><u>Prompt</u></strong>: Why won't this C++ code compile when I run &quot;g++ file.cpp&quot;?</p>
<pre><code class="language-c++">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main() {
	std::vector&lt;int&gt; v{1,2,3};	
	for (int i=1; i&lt;=4; i++) { std::cout &lt;&lt; v[i] &lt;&lt; std::endl; }	
	return 0;
}
</code></pre>
<p><strong><u>Response</u></strong>:</p>
<blockquote>
<p>It looks like there is an issue with the for loop in your code. The loop variable i is being initialized to 1, and the loop is continuing as long as i is less than or equal to 4. This means that the loop will attempt to access the elements at indices 1, 2, 3, and 4 of the vector v. However, the vector v only has 3 elements (at indices 0, 1, and 2), so attempting to access the element at index 4 will result in an out-of-bounds error.</p>
</blockquote>
<blockquote>
<p>To fix this issue, you can modify the loop condition to stop the loop before i becomes greater than the size of the vector. For example, you could change the loop condition to i &lt; v.size() or i &lt; 3 (since the vector has 3 elements).</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>GPT-3 successfully discovered the run-time segfault that results from accessing an out-of-bounds value, but it failed to explain that g++ is not compiling to the correct version of C++. Let's refine the prompt and try again:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong><u>Prompt</u></strong>: The error received for the previous code was:</p>
<pre><code>segfault-example.cpp:5:20: error: expected ';' at end of declaration
        std::vector&lt;int&gt; v{1,2,3};      
                          ^
                          ;
1 error generated.
</code></pre>
<p><strong><u>Response</u></strong>:</p>
<blockquote>
<p>I apologize for the mistake in my previous response. It looks like there is a different issue with your code.</p>
</blockquote>
<blockquote>
<p>The error message expected ';' at the end of declaration is indicating that there is a problem with the line <code>std::vector&lt;int&gt; v{1,2,3};</code>. This line is using list initialization to create a new vector and initialize it with the elements 1, 2, and 3. The curly braces {} are used to enclose the list of elements, and a semicolon ; is used to terminate the statement. It looks like there is a typo in this line of code, and the semicolon is missing. To fix the error, add a semicolon ; after the closing curly brace }: After adding the semicolon, the code should compile without any issues.</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Politely, GPT-3 tries to fix the error. However, the response misleads us into debugging the syntax once again, even after we provided additional hints that the problem resides not with the code, but with the compiler. The correct solution is to compile the program with the command <code>g++ -std=c++11 segfault_ex.cpp</code> so that the compiler uses the syntax rules of C++11 to generate the program with a vector initializer list.</p>
<p>This example demonstrates that GPT-3 is capable of syntax debugging, but falls short when the problem lies outside of syntax mistakes. As a result, a programmer must still have a good idea of what the solution space for a problem looks like and recognize when they are being led astray.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="conclusion">Conclusion</h1>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>AI tools like GitHub CoPilot and TabNine can be useful for programmers, but they have also faced <a href="https://matthewbutterick.com/chron/this-copilot-is-stupid-and-wants-to-kill-me.html?ref=dept-engineering-blog.ghost.io">controversy</a>. GPT-3 is unlikely to remain immune from similar questions about where the code it is generating actually comes from. Therefore, programmers should be cautious when relying on AI tools and approach results with a grain of salt.</p>
<p>GPT-3 excels at processing close-ended, complete problems. These are problems where the shape of the solution is evident in the distance and slowly reveals itself in a progressive manner, like a distant island that a castaway paddles towards during sunset. As long as they don't lose sight or give up, they'll reach their target.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/castaway-2.png" class="kg-image" alt="Strengths & Weaknesses of GPT-3 for Enhancing Developer Efficiency" loading="lazy" width="1024" height="1024" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/01/castaway-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/01/castaway-2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/castaway-2.png 1024w" sizes="(min-width: 720px) 720px"><figcaption>OpenAI. (2023). DALL-E 2. Retrieved from <a href="https://labs.openai.com/s/MD2Wq0QTNE2xRNbPojwaFqY3?ref=dept-engineering-blog.ghost.io">https://labs.openai.com/s/MD2Wq0QTNE2xRNbPojwaFqY3</a></figcaption></img></figure><hr><!--kg-card-begin: markdown--><p>On the other hand, GPT-3 struggles with open-ended problems. These problems have multiple viable solutions, but it is unclear which path to pursue. These solutions are like distant stars to a spaceship - they all look equally far apart and appear about the same size and shape - how does the pilot decide which to navigate to? Each star requires a lot of research just to determine its composition, yet the mere nature of a star doesn't provide any guarantees about what might be there once one arrives.</p>
<p>Regardless, GitHub CoPilot, TabNine, ChatGPT-3, and other AI assistants mark the arrival of a new method of work for software engineers. After seeing the capabilities of these tools, some developers may fear that automation has finally come to take their jobs.</p>
<p>Rather than fear automation in the field of software engineering, programmers should embrace it as a way to improve productivity and stay current with technological advances. Automation has been a part of the software engineering industry from its inception - from Ada Lovelace's <a href="https://books.google.com/books?id=hPRmnQEACAAJ&printsec=frontcover&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false">description</a> of a language to run Charles Babbage's analytical engine, to text editors with macros, to GUI buttons that condense a series of commands to the click of a button.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/spaceship-1.png" class="kg-image" alt="Strengths & Weaknesses of GPT-3 for Enhancing Developer Efficiency" loading="lazy" width="1024" height="1024" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/01/spaceship-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/01/spaceship-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/spaceship-1.png 1024w" sizes="(min-width: 720px) 720px"><figcaption>OpenAI. (2023). DALL-E 2. Retrieved from <a href="https://labs.openai.com/s/3MbYm2Pjb3SJoy8yu7FcqGhK?ref=dept-engineering-blog.ghost.io">https://labs.openai.com/s/3MbYm2Pjb3SJoy8yu7FcqGhK</a></figcaption></img></figure><hr><!--kg-card-begin: markdown--><p>Imagine a future where a knowledge worker, on their first day of work, is issued a badge, a personal computer, and an AI assistant personalized to the knowledge and training they need to know for their job. One could ask the assistant to summarize complex documentation or get clarification on a process, among other use cases.</p>
<p>For programmers specifically, there are other ways that AI tools can be folded into their careers, such as <a href="https://www.swyx.io/learn-in-private?ref=dept-engineering-blog.ghost.io">facilitating directed learning</a>, improving <a href="https://en.wikipedia.org/wiki/Personal_information_management?ref=dept-engineering-blog.ghost.io">personal knowledge management</a>, and <a href="https://meta.stackoverflow.com/questions/419427/tips-for-searching-effectively-on-stack-overflow-novice-developer-training/419440?ref=dept-engineering-blog.ghost.io#419440">maximizing learning</a> from complex programming problems.</p>
<p>Ultimately, it is up to the individual programmer to use AI tools responsibly and effectively in their work. In this article, we explored the strengths and weaknesses of GPT-3 in executing three tasks a programmer encounters in their daily work - generating new code, explaining existing code, and debugging code. The same precautions one takes after consulting Google or StackOverflow for help with these tasks should be taken when consulting an AI assistant as well.</p>
<p>By following best practices, such as not sharing proprietary software or information, and approaching results with a critical eye, programmers can take advantage of AI tools and enhance their productivity.</p>
<!--kg-card-end: markdown--></hr></hr></hr>]]></content:encoded></item><item><title><![CDATA[Kotlin Multiplatform Mobile (KMM) Adoption: Insights and Lessons Learned]]></title><description><![CDATA[At DEPT® we believe that Kotlin Multiplatform is a true game-changer. By sharing business logic between platforms, we had the opportunity to shift our focus to the UI aspects of an app.]]></description><link>https://engineering.deptagency.com/a-case-study-of-kmm-adoption-insights-and-lessons-learned/</link><guid isPermaLink="false">63b6c65207827f003d8bf6e8</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Mobile]]></category><dc:creator><![CDATA[Wesley Donk]]></dc:creator><pubDate>Thu, 05 Jan 2023 14:10:09 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/image-1--1-.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/image-1--1-.png" alt="Kotlin Multiplatform Mobile (KMM) Adoption: Insights and Lessons Learned"/><p>As we learnt <a href="https://dept-engineering-blog.ghost.io/kotlin-multiplatform-mobile-the-whats-and-whys">in our last post</a> about Kotlin Multiplatform Mobile (KMM), traditionally, building a native app is quite time-consuming. Developers build an identical app per platform and both apps contain twice the same business logic. This costs us and you a lot of time with building, testing, and maintenance.</p><p>This can be done much smarter with a multi-platform approach, without compromising on experience or quality. With a native multi-platform framework we build the business logic of an app only once. This logic is then converted without too much fuss into native code, to which both apps can then connect. With both apps, you can focus on the user experience. KKM gives us the option to write the business logic for multiple platforms at the same time. Building the shared KMM layer is handled by the same native developers. No additional resources or resources with other skills are required.</p><p>At DEPT® we believe that Kotlin Multiplatform is a true game-changer, it is a relatively new way of working for mobile development. That is why our mobile team invested time and research to push forward with this approach to stay ahead of the game.</p><p>It did not become a game-changer for us over night. When is the right time to start working with it and when are we confident that KMM will work for us in the long run? Should we wait until it is widely accepted or should we already start investigating?</p><p>KMM was introduced in 2020 and currently just gone into Beta. KMM allows sharing code for Android, iOS, and even more platforms. The common code consists of business logic. Business logic is never seen visually in an application but is still the foundation of every application. By sharing business logic between platforms, we had the opportunity to shift our focus to the UI aspects of an app.</p><p><strong>Where did we start</strong></p><p>We started with multiple proofs of concepts that used KMM, testing basic functionalities like code sharing, networking, and storage. It took a little time before the team had something up and running. The <a href="https://github.com/joreilly/PeopleInSpace?ref=dept-engineering-blog.ghost.io">PeopleInSpace</a> repository is a really good example of what we were looking for in the first place. </p><p>Each of these concepts would use <a href="https://github.com/rickclephas/KMP-NativeCoroutines?ref=dept-engineering-blog.ghost.io">KMP-Coroutines</a>, which allowed us to minimize the gap between the platform concurrencies and offer cancellation support like any other project. iOS projects even have the choice to integrate shared with Combine, ReactiveX, or Async/await. </p><p>Below you can find an example of using KMP-Coroutines, where the plugin automatically generates an additional function that can be used within the iOS project:</p><!--kg-card-begin: markdown--><pre><code class="language-kotlin">class NewsRepository {
    
    // A suspend function to retrieve data
    suspend fun getNews(): List&lt;Article&gt;

    // The plugin will generate this function, which can be used on iOS
    fun getNewsNative() = nativeSuspend { getNews() }
}
</code></pre>
<!--kg-card-end: markdown--><p><strong>Note: </strong>Currently the 1.0.0 alpha versions removes even the <em>Native</em> suffix from the ObjC/Swift names, which makes the usage on both platforms even more similar.</p><p>Another interesting part of the concepts was about defining the process of how the teams should work together. We would start by exploring how both teams would depend on the same business logic, and how this business logic would be distributed to all teams.</p><p>The integration with an iOS project was pretty straightforward. Initially, the team looked into CocoaPods but quickly shifted to Swift Package. Building the KMM project would generate a swift package that is pushed to a separate repository. The iOS team would include the repository as Swift Package in their project. It allowed the iOS team to make use of the common code without having to set up an entire Android Studio / IntelliJ environment.</p><p>It was by far the most interesting topic during the proof of concept: Getting the process up and running of publishing swift packages when we made changes in the KMM repository.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/01/Diagram-zonder-titel.drawio--1--1.png" class="kg-image" alt="Kotlin Multiplatform Mobile (KMM) Adoption: Insights and Lessons Learned" loading="lazy" width="394" height="254"/></figure><p><strong>What did we learn</strong></p><p>KMM can be introduced gradually in a project, it is not mandatory from the start of a project. It is a tool that helps you to achieve your goals. However, our goal should never be to build something with KMM. It is one way to do something, but that is not always the right one!</p><p>Meetings were more centralized now that there is a shared dependency for both platforms, which has to work for both platforms. Suddenly more discussions took place, which would never have happened when building two fully native projects. Using the same naming convention defined in shared and the need to ask more questions resulted in better communication. The terms <em>shared</em> and <em>platform</em> became part of our natural vocabulary. It became a way to define where something lives in a project. </p><p>A culture change happened over time. Up until now we always had two separate products, based on two entirely different tech stacks. These two approaches resulted in two different products, which had nothing to do with each other. Now that we are using KMM as a foundation for business logic, it has become a different game.</p><p>One thing we did not expect was that the team became more interested in the other platform. At some point, the team members started up the other IDE and invested time learning the other platform! We did not have this set as a goal, but it is a nice bonus.</p><p><strong>What helped us move forward</strong></p><p>There are some essential libraries that we have used during our journey of using KMM. </p><p><em>KMP-NativeCoroutines;</em></p><ul><li>As described before, it allowed us to use Flows and suspend functions of Kotlin in our Swift projects;</li></ul><p><em>SwiftPackageManager;</em></p><ul><li>In showing us the way how to build a plugin;</li><li>Unfortunately, the project got abandoned which required us to do something similar in-house;</li></ul><p><em>Ktor;</em></p><ul><li>Easy-to-use networking features;</li><li>Allowed us to use WebSockets on both platforms without any additional platform-specific code;</li></ul><p><em>Koin;</em></p><ul><li>Koin brought possibilities to expose what matters to the platforms from shared;<br/></li></ul><p><strong>What can we conclude?</strong></p><p>The quality of our apps increased by using KMM. Compared to a traditional native project, we see over <strong>50%</strong> fewer issues created by QA.</p><p>While working with KMM our team has shown an increase in performance. We communicate better, collaborate more, and individually know more about the other platform, resulting in team growth. As a bonus, we even work with kick-ass technologies and learn some things along the way.</p><p>The flexibility that KMM gives us to share the application's business logic between both platforms. It gives us the benefits of native programming by writing platform-specific code where it only is necessary. That allows us to focus more on creating beautiful native UI.</p><p>This is a big win for us. It brings the benefits of cross-platform frameworks (faster, cost-effective, consistent, and reusability) to <a href="https://www.deptagency.com/service/engineering/web-mobile-app-engineering/?ref=dept-engineering-blog.ghost.io">native app development</a>. Based on this great experience, KMM will be used in a lot of our native app projects in the future.<br/></p>]]></content:encoded></item><item><title><![CDATA[Why and how to create your own Mastodon server on AWS with Terraform]]></title><description><![CDATA[We've updated and open-sourced Terraform scripts to bootstrap your own Mastodon server using AWS, Elastic Container Service and Fargate.  ]]></description><link>https://engineering.deptagency.com/create-your-own-mastodon-server-on-aws-with-terraform/</link><guid isPermaLink="false">639b36e22f5065003d2ddd26</guid><category><![CDATA[People]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Mon, 19 Dec 2022 13:44:00 GMT</pubDate><content:encoded><![CDATA[<p>TL;DR: We've updated and <a href="https://github.com/deptagency/mastodon-terraform-aws-ecs?ref=dept-engineering-blog.ghost.io">open-sourced Terraform scripts</a> to bootstrap your own Mastodon server using AWS, Elastic Container Service, and Fargate.  </p><p>All good ideas start with conversations like this:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/image-3.png" class="kg-image" alt="" loading="lazy" width="1228" height="1554" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/12/image-3.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/12/image-3.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/image-3.png 1228w" sizes="(min-width: 720px) 720px"><figcaption>Why not create a mastodon server? We're hip!</figcaption></img></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/image-4.png" class="kg-image" alt="" loading="lazy" width="850" height="320" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/12/image-4.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/image-4.png 850w" sizes="(min-width: 720px) 720px"><figcaption>Sure, easy.</figcaption></img></figure><p>So began my journey into creating a Mastodon server for us to use internally at DEPT®.  </p><p>But, before we get into all that ...</p><h2 id="why-would-i-create-my-own-mastodon-server-isnt-that-why-twitter-exists">Why would I create my own Mastodon server? Isn't that why Twitter exists?</h2><p>It would be hard to avoid hearing about <a href="https://joinmastodon.org/?ref=dept-engineering-blog.ghost.io">Mastodon</a> lately with all that's happening around Elon Musk's takeover of Twitter:  <a href="https://www.nytimes.com/2022/12/15/technology/twitter-suspends-journalist-accounts-elon-musk.html?ref=dept-engineering-blog.ghost.io">Journalists being banned</a>, <a href="https://techcrunch.com/2022/12/18/twitter-wont-let-you-post-your-facebook-instagram-and-mastodon-handles/?ref=dept-engineering-blog.ghost.io">accounts in violation of policies for links to other services</a>, and constant unpredictability with policies being changed daily.</p><p>But, even though you've heard of Mastodon, you may not know how you use it - <a href="https://buffer.com/resources/mastodon-social/?ref=dept-engineering-blog.ghost.io">here's a good resource</a> to learn a little more.  Also, if you're not already signed up: You have to find and choose a Mastodon server to use, a key difference from the centralized approach of Twitter.  To get you going faster, I'd recommend creating an account on <a href="https://mastodon.social/?ref=dept-engineering-blog.ghost.io">Mastodon.social</a> (one of the biggest servers).  Then, once you get that going, use <a href="https://www.movetodon.org/?ref=dept-engineering-blog.ghost.io">Movetodon.org</a> to import your Twitter following list into Mastodon. </p><p>But why on earth would you want to create your own Mastodon server?  I can't state it any better than Julien Deswaef in his article <a href="https://martinfowler.com/articles/your-org-run-mastodon.html?ref=dept-engineering-blog.ghost.io">Your organization should run its own Mastodon server:</a></p><blockquote>... just as with email, you can send a message to anyone in your organization, and at the same time have a conversation with anyone outside of your organization. All this is transparent and your correspondent’s email address lets you know that you are actually talking to someone as part of a particular organization.</blockquote><blockquote>By using your own domain name, your brand, you are creating a recognizable social presence in the Fediverse, without the need to associate it with facebook.com, twitter.com or instagram.com. No need to worry about someone else squatting your name either. Your domain name is what will get people to trust that the Mastodon accounts under it are legitimate and official.</blockquote><blockquote>And once you run your own corner of a social network, you can decide who you invite there, what are your rules of engagement and code of conduct. At Thoughtworks, we’ve allowed all our employees to open an account on our Mastodon server as this is aligned with our culture and our practice of being quite vocal and transparent about what we are passionate about.</blockquote><p>Having some piece of mind for your company's brand to exist on a social network that <em>you</em> control fully is huge.</p><p>Ok, now that we've gotten that out of the way...</p><h2 id="the-context">The Context</h2><p>Jumping ahead a bit, one thing that I couldn't really find was a nice, introductory article explaining the architecture of Mastodon.  I found myself struggling to set up infrastructure I had no context in. So here's a crash course that will help you understand things a bit better before diving in.</p><p>Mastodon is comprised of just a few main components, all within the same <a href="https://github.com/mastodon/mastodon?ref=dept-engineering-blog.ghost.io">open-source codebase</a>:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/image-5.png" class="kg-image" alt="" loading="lazy" width="2000" height="1098" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/12/image-5.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/12/image-5.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2022/12/image-5.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w2400/2022/12/image-5.png 2400w" sizes="(min-width: 720px) 720px"/></figure><p>(<a href="https://gist.github.com/theothermattm/0740f982316e3b87e8efc9adbc87b7fe?ref=dept-engineering-blog.ghost.io">Diagram Source</a>) </p><p><strong>The main web application. </strong>This is a Ruby on Rails application that runs on <a href="https://puma.io/?ref=dept-engineering-blog.ghost.io">Puma</a>. This includes web pages themselves as well as APIs.</p><p><strong>Background Job Processing.</strong>  This runs using <a href="https://sidekiq.org/?ref=dept-engineering-blog.ghost.io">Sidekiq</a>, a Ruby job processing system. These jobs handle things like handling requests from other Mastodon servers (known as federated servers) for messages and follow requests, handling notifications, and other things.</p><p><strong>The streaming API.</strong>  This is a Node.js application that has Websocket API's for real-time updates for the various parts of Mastodon's data model.  Think of things like getting notifications in the app for new follows.</p><p>Another important thing to know is that Mastodon created and implements the <a href="https://docs.joinmastodon.org/spec/activitypub/?ref=dept-engineering-blog.ghost.io">ActivityPub spec</a>.  This is a specification managed by the W3C for decentralized social networking.  I won't go into the details about ActivityPub here, but it's super important to know about the concepts in it to help you with setup, more on that later.</p><h2 id="how-federated-servers-communicate">How Federated Servers Communicate</h2><p>I think perhaps the most confusing thing is understanding the flow between different Mastodon servers.  So, let's take the example of Following someone on a different server:</p><ol><li>A user clicks "Follow" for someone on a different Mastodon server.</li><li>The Puma web application will take the federation information and lookup how to contact the other server and send a follow request to that server.  The endpoint that's called on the other end is something like <code>POST /users/{followed_remote_username}/inbox</code>.  This puts a message in the remote user's "inbox that they have a follow request".</li><li>On the remote end, the API endpoint puts a message in the <a href="https://docs.joinmastodon.org/admin/scaling/?ref=dept-engineering-blog.ghost.io#sidekiq">Sidekiq <code>ingress</code> queue</a>. When the job processor picks up that task, if the user doesn't need to approve follow, the server will respond with a corresponding call back to the original users inbox that the follow request is accepted.</li></ol><p>This is all part of the ActivityPub spec but is kind of hard to follow, and if I'm being honest, I'm still not sure I got it completely right (comments welcome!).</p><p>The takeaway is that background jobs are <em>really</em> important in Mastodon, and so is the ability to securely connect between servers through HTTPS connections.  This process is fraught with SSL errors and layers you'll need to debug if the connections don't just work.</p><h2 id="the-infrastructure-needed">The Infrastructure Needed</h2><p>To host this beast (ha ha) here are a few things that need to be set up, and a couple optional things:</p><p><strong>An online web application container.</strong>  Something that can run Ruby processes.  Could be Docker, could be a Virtual Machine.  Pick your poison.</p><p><strong>A PostGresSQL database.</strong>  This is the heart of your Mastodon instance storing user accounts, messages, etc.</p><p><strong>A Redis data store. </strong>This is used for the job queueing system described above as well as caching.</p><p><strong>A static file store.</strong> This is used to store uploaded photos/files for posts.  Think AWS S3, or similar.</p><p><strong>A reverse proxy or load balancer.</strong>  You really don't want your web application directly hosting requests.  You'll want some way to throttle and distribute those requests and also put in some security checks as well.  You could use something like <a href="https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/?ref=dept-engineering-blog.ghost.io">NGinx</a> for a reverse proxy, or in our case, we used an AWS Application Load Balancer.</p><p><strong>Optional: An ElasticSearch cluster.</strong>  This is used for advanced searching capabilities.  I didn't set this up so it's not included in this write-up. If I end up setting one up I'll update the post.</p><h2 id="choosing-a-path">Choosing a path</h2><p>There are affordable hosted Mastodon server offerings, but really part of this exercise was learning more about the architecture of Mastodon and what goes into setting up a server.  After all, we're engineers.  We can do it better ourselves, right? 🫠 Kidding...</p><p>If you have the knowledge, it makes a lot of sense to stand up Mastodon yourself on cloud infrastructure instead of relying on a hosting provider: You can be assured you're in control of your data and treat Mastodon just like any other app you develop. It's also advantageous to be able to really tweak your cloud resources to your number of users to save money or scale up.  whereas hosting providers charge for <a href="https://masto.host/pricing/?ref=dept-engineering-blog.ghost.io">the price of Mastohost is $89/month for approximately 2000 users</a>, and the price for <a href="https://toot.io/mastodon_hosting.html?ref=dept-engineering-blog.ghost.io#pricing">toot.io's 1000 user plan is $89/month</a>.  Also, as of this writing, Mastohost isn't even accepting new customers according to their page.  That's kinda important especially with the recent influx of Mastodon users.</p><p><s>As for cost, I'm anticipating it will be a wash compared with hosting providers.  The AWS estimate for our first month's bill is about $130 but that includes lots of experimentation and also a NAT Gateway, </s><a href="https://www.easydeploy.io/blog/how-to-create-private-link-for-ecr-to-ecs-containers-to-save-nat-gatewayec2-other-charges/?ref=dept-engineering-blog.ghost.io"><s>which can be avoided with a Privatelink</s></a><s>.</s> See my update at the end of the article. It may be more cost-effective to use one of these services unless you need to tightly control your infrastructure!</p><p>At DEPT® US engineering, we're most familiar with AWS, and most of our US-based clients are on AWS as well.  I'm also personally a fan of containerization, so I thought I might go in that direction.  It helps that Mastodon has <a href="https://hub.docker.com/r/tootsuite/mastodon?ref=dept-engineering-blog.ghost.io">official Docker Images</a>! Finally, I knew if I was going to go with AWS, I wanted to use an Infrastructure as Code (IaC) tool so that I could share the setup with the world, and you, dear reader.  Our IaC tool of choice here on the DEPT® US Engineering team is <a href="https://www.terraform.io/?ref=dept-engineering-blog.ghost.io">Terraform</a>.</p><p>With all my preferences, I set out to see what already existed to help me out.  One of the huge benefits of using IaC tools is being able to leverage existing open-source code to do this type of thing.  After some false starts, I landed on a project called <a href="https://github.com/r7kamura/mastodon-terraform?ref=dept-engineering-blog.ghost.io">mastodon-terraform by r7kamura</a>. It was a beautifully laid out Terraform project with modules and a great setup for providing configuration variables. It also used Elastic Container Service which is a "just complicated enough" container orchestration engine. </p><p>The problem was that it was out of date, having been touched last six years ago.  Mastodon had only released its <a href="https://github.com/mastodon/mastodon/releases/tag/v1.0?ref=dept-engineering-blog.ghost.io">first stable release</a> in 2017, so I assumed much had changed between then and now.  It also did not use <a href="https://docs.aws.amazon.com/AmazonECS/latest/userguide/what-is-fargate.html?ref=dept-engineering-blog.ghost.io">Fargate Managed ECS Clusters</a>, but instead EC2 servers which can be cumbersome to maintain, even with Terraform.</p><p>That was as good a place as any to start!</p><h2 id="the-takeaways">The Takeaways</h2><p>In the course of a couple weeks of off and on work, maybe about 20 hours total, I got a server up and running, ready to scale out should our community take off.  I've shared the forked repository for all to use here: <a href="https://github.com/deptagency/mastodon-terraform-aws-ecs?ref=dept-engineering-blog.ghost.io">deptagency/mastodon-terraform-aws-ecs</a> (MIT Licensed).</p><p>I hit a couple bumps in the road making changes to container definitions in ECS to be in line with updates to Mastodon since 2017, but other than that <a href="https://github.com/r7kamura?ref=dept-engineering-blog.ghost.io">r7kamura</a>'s work was a fantastic base and I thank them greatly.</p><p>I also hit a knowledge wall at one point where messages from other servers were not being received by ours.  I spent a lot of time with networking settings, but in the end, posted a question on the <a href="https://github.com/mastodon/mastodon/discussions/22310?ref=dept-engineering-blog.ghost.io">Mastodon Github discussion forum</a> which was super helpful and pointed me in the right direction.</p><p>My biggest challenge was finding <em>high-level</em> information about mastodon's architecture and infrastructure.  Mastodon has all kinds of fantastic documentation about the details of how to set up a server and how to configure it.  They also have great introductory material on how to use and administer Mastodon.  Everything in between is a bit of a black box.  I hope that this post helps someone else fill in those gaps.</p><p><em><strong>Update 2/2023:</strong></em> The cost for the past couple of months ended up being about $170 a month! The majority of that was (surprisingly) the NAT Gateway to keep the containers isolated from the public internet.  This could be avoided by allowing containers to be accessed through the internet. We also ended up dialing back the RDS instance size to save a bit.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/image-1.png" class="kg-image" alt="" loading="lazy" width="1988" height="1386" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/02/image-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/02/image-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2023/02/image-1.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/02/image-1.png 1988w" sizes="(min-width: 720px) 720px"/></figure>]]></content:encoded></item><item><title><![CDATA[Kotlin Multiplatform Mobile: The Whats and Whys]]></title><description><![CDATA[The what could be described as a cross-platform solution for mobile development. But, there is a big difference between a cross-platform solution and Kotlin Multiplatform Mobile.]]></description><link>https://engineering.deptagency.com/kotlin-multiplatform-mobile-the-whats-and-whys/</link><guid isPermaLink="false">6396ebc7ce4469003d3f28c8</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Daniel Dimovski]]></dc:creator><pubDate>Mon, 12 Dec 2022 17:11:26 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1588864721034-4afdb05a5799?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGtvdGxpbiUyMG11bHR8ZW58MHx8fHwxNjcwODUzMDE4&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h1 id="what-are-we-going-to-talk-about">What are we going to talk about?</h1><img src="https://images.unsplash.com/photo-1588864721034-4afdb05a5799?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGtvdGxpbiUyMG11bHR8ZW58MHx8fHwxNjcwODUzMDE4&ixlib=rb-4.0.3&q=80&w=2000" alt="Kotlin Multiplatform Mobile: The Whats and Whys"/><p>The <strong><strong>what</strong></strong> could be described as a cross-platform solution for mobile development. But, there is a big difference between a <em><em>cross-platform</em></em> solution and Kotlin <em><em>Multiplatform</em></em> Mobile.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/image.png" class="kg-image" alt="Kotlin Multiplatform Mobile: The Whats and Whys" loading="lazy" width="1400" height="618" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/12/image.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/12/image.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/image.png 1400w" sizes="(min-width: 720px) 720px"><figcaption><a href="https://www.bitrise.io/blog/post/getting-started-with-kotlin-multiplatform-mobile-kmm?ref=dept-engineering-blog.ghost.io">Image by Bitrise</a></figcaption></img></figure><p>Looking at the most popular cross-platform solutions, most of them try to bring the web world to mobile. </p><p>These solutions often offer one codebase for all platforms (web, Android &amp; iOS) and while that sounds great, it comes with trade-offs. While we can dispute the performance of a native application and a cross-platform solution, an obvious problem is losing the native feel of the application. Android and iOS users expect different things from an application, different UI &amp; UX. And while some of the cross-platform solutions do an ok job at this, it’s never good enough (for me, at least). </p><p>This is where KMM steps in. And this is where the difference between multiplatform and cross-platform is most obvious. KMM allows you to share only what you would like to share between platforms. That ranges from something as simple as sharing constants to sharing the whole data layer &amp; all of the business logic. It’s up to you.</p><h1 id="who-is-writing-this-article">Who is writing this article?</h1><p>I’ve been an Android developer for (almost) 5 years at the time of writing this article. Half of these years I’ve worked with Java. Switching to Kotlin was one of the best decisions I’ve made. While Java has its qualities, I’ve come to realise that Kotlin is superior, at least when it comes to Android development.</p><p>After getting comfortable with Kotlin and seeing how it speeds up my development process, I thought it was time to try and improve further. Honestly, I was a bit sceptical when I first heard of KMM, mostly due to my (negative) opinion of cross-platform solutions. Once I gave it a try, I realised the big difference between the two approaches. The technology is still new to me, but it feels really familiar (likely due to the fact that it’s still Kotlin!)</p><p>Enough about the <strong><strong>who</strong></strong>, let’s continue with:</p><h1 id="why-would-anyone-want-to-use-kmm"><strong>Why </strong>would anyone want to use KMM?</h1><p>As an Android developer, I’m keen on native mobile applications. I want to fully utilise what the system has to offer, including its hardware and its UI/UX. </p><p>So, <strong><strong>why</strong></strong> would I use KMM?</p><p>Developing native applications takes time, that’s for sure. And businesses usually want things to go as fast as possible. That’s understandable — it means faster time-to-market, which is crucial. But how to do that, and still get a great native application in the end?</p><p>There is a lot of shared logic between applications on different platforms. Ultimately, we want our application to provide the same capabilities on all platforms, but in the native way the users expect. And with two native applications, we have to implement this logic twice. That leads to code duplication and makes the application more prone to errors &amp; differences between platforms. What if we could write this logic once and use it on both platforms — natively?</p><p>Well, as you probably guessed, we can do just that with Kotlin Multiplatform Mobile. The end goal is to write the business logic once, expose it to both platforms and use it. This eventually leads to a codebase that’s easier to maintain, easier bug detection, and consistency on both platforms.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/kotlin-comparison.webp" class="kg-image" alt="Kotlin Multiplatform Mobile: The Whats and Whys" loading="lazy" width="1292" height="612" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/12/kotlin-comparison.webp 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/12/kotlin-comparison.webp 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/12/kotlin-comparison.webp 1292w" sizes="(min-width: 720px) 720px"/></figure><p>The image above shows a nice comparison between native, cross-platform &amp; KMM. And while this comparison may look a little bit biased (given that it comes from <a href="https://kotlinlang.org/lp/mobile/?ref=dept-engineering-blog.ghost.io" rel="noopener ugc nofollow">KMM’s official site</a>), I’ve come to learn that it’s pretty much true.</p><p>Indeed a lot of time is saved by using either cross-platform and KMM, given that you need to write less code. The fact is, that cross-platform edges out KMM in this comparison, mostly because cross-platform solutions also work on the web, but also because of the fact that with KMM, you still have to create the UI twice (which is not bad, but the work still needs to be done). And while Kotlin Multiplatform is working on expanding its targets to the web, in this article we are talking about KMM (Kotlin Multiplatform <strong><strong>Mobile</strong></strong>), which only targets Android &amp; iOS.</p><p>The fact that you can integrate KMM with existing projects is awesome. Any Android or iOS application can start using KMM immediately. Although the integration process is not that smooth yet, it is getting better.</p><p>And obviously, the last two items are true for both KMM and native. In fact KMM is ultimately a native application, so the access to platform-specific capabilities and the native performance &amp; feel is retained.</p><h1 id="where-does-kmm-stand"><strong>Where</strong> does KMM stand?</h1><p>The first version of KMM was initially released on August 31, 2020. And although that was only just over two years ago, it has come a long way. Kotlin Multiplatform Mobile reached the <a href="https://blog.jetbrains.com/kotlin/2022/10/kmm-beta/?ref=dept-engineering-blog.ghost.io" rel="noopener ugc nofollow">BETA stage</a> a little over a month ago. And while that might sound like it’s not production-ready, that’s not true.</p><blockquote><em><em>Kotlin Multiplatform Mobile’s promotion to Beta means that the technology is almost done and is safe to use in your projects. It also means we’ll be actively working to minimize any migration issues you might encounter.</em></em></blockquote><p>This also gives us the <strong><strong>when</strong></strong>. When is the right time to start using Kotlin Multiplatform? And the answer is: <strong><strong>now</strong></strong>. And if you need further proof that now is a great time to start using KMM, or at least give it a try, you can check out some of the <a href="https://kotlinlang.org/lp/mobile/case-studies/?ref=dept-engineering-blog.ghost.io" rel="noopener ugc nofollow">case studies</a> on companies that are already utilising KMM in production, including tech giants such as Netflix, Philips, Cash App…</p>]]></content:encoded></item><item><title><![CDATA[T3: Rise of the Monorepo]]></title><description><![CDATA[Building full-stack monorepo's doesn't have to be a headache. With the T3 stack we can quickly spin up full-stack, typesafe Next.js projects.]]></description><link>https://engineering.deptagency.com/t3-rise-of-the-monorepo/</link><guid isPermaLink="false">6356e8ec02da7d003d826ea5</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Nic Ford]]></dc:creator><pubDate>Wed, 02 Nov 2022 17:27:10 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/Wjf4Ti8fsMu0z0qfYnPo--1--8s4fz.jpg" medium="image"/><content:encoded><![CDATA[<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/11/pasted-image-0.png" class="kg-image" alt="T3: Rise of the Monorepo" loading="lazy" width="1584" height="550" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/11/pasted-image-0.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/11/pasted-image-0.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/11/pasted-image-0.png 1584w" sizes="(min-width: 720px) 720px"><figcaption>Next.js and TypeScript</figcaption></img></figure><h2 id="what-is-the-t3-stack">What is the T3 stack?</h2><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/Wjf4Ti8fsMu0z0qfYnPo--1--8s4fz.jpg" alt="T3: Rise of the Monorepo"/><p><code>create-t3-app</code> is a wonderfully opinionated CLI to create full-stack, typesafe Next.js projects quickly.</p><p>The T3 stack at its core is just a Next.js and Typescript CLI that works incredibly well out of the box. It does allow you to opt into nextAuth, Prisma, Tailwind, or tRPC.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/11/pasted-image-0--1-.png" class="kg-image" alt="T3: Rise of the Monorepo" loading="lazy" width="1584" height="550" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/11/pasted-image-0--1-.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/11/pasted-image-0--1-.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/11/pasted-image-0--1-.png 1584w" sizes="(min-width: 720px) 720px"><figcaption>NextAuth, Prisma, tRPC, and Tailwind</figcaption></img></figure><h3 id="nextauth">NextAuth</h3><p>NextAuth.js is an open-source project for adding authentication to your projects. Its provider list is extensive and works really well with Next.js. Combined with its Prisma adapter, we can easily create and manage authentication for our React application.</p><h3 id="prisma">Prisma</h3><p>Prisma is a Node.js and TypeScript ORM for managing databases that allow us to easily create database tables and data structures to manage data in our applications.</p><h3 id="trpc">tRPC</h3><p>tRPC, short for “Typescript Remote Procedure call” is a tool that allows us to create RPCs (much like a normal API endpoint) that support type inheritance to the frontend client that is consuming these endpoints. This means any changes to types on the server will be passed down to the client and give us type errors before anything is sent to production.</p><h3 id="tailwind-css">Tailwind CSS</h3><p>Tailwind is a utility-first CSS framework that helps bridge the gap between opinionated frontend frameworks (<a href="https://mui.com/?ref=dept-engineering-blog.ghost.io">MUI</a>, <a href="https://react-bootstrap.github.io/?ref=dept-engineering-blog.ghost.io">Bootstrap</a>, etc.) and the hard start of starting from scratch (<a href="https://necolas.github.io/normalize.css/?ref=dept-engineering-blog.ghost.io">Normalize</a>, <a href="https://meyerweb.com/eric/tools/css/reset/?ref=dept-engineering-blog.ghost.io">Reset</a>). Do you want bold? <code>&lt;p className=”font-bold”&gt;</code> Do you want rounded corners? <code>&lt;div className=”rounded”&gt;</code>. It’s inline CSS, but easier to read, faster to write, and has tree shaking.</p><h2 id="so-what-makes-create-t3-app-worth-your-time">So, what makes Create T3 App worth your time?</h2><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/11/exterminador-do.gif" class="kg-image" alt="T3: Rise of the Monorepo" loading="lazy" width="498" height="317"/></figure><h3 id="modularity">Modularity</h3><p>It’s super easy to plug in any package you want to use and get things working in the Next.js + Typescript environment. Maybe you really want to stick with GraphQL or use your preferred ORM, that’s no problem, just don’t install tRPC or Prisma during setup and you can configure your Next.js + TS stack to work to your liking. But, if you want to get going quickly, it works very efficiently out of the box and gives you the ability to quickly integrate any project-specific dependencies without the overhead of having to set up a specific stack before you can start building. Modularity also means it’s much easier to learn an individual package and implement it than having to commit to learning a monolithic framework and making sure it has the features you may or may not need in the future.</p><h3 id="flexibility">Flexibility</h3><p>It gives you the agency to build and create things the way you want to. Sometimes you want to stick with what you know, and other times you want to try and implement something new, this stack lets you do both. T3 automatically configures your package manager based on how you create the app so <code>npx create-t3-app@latest</code> uses npm, <code>yarn create t3-app</code> uses yarn, <code>pnpm dlx create-t3-app@latest</code> uses pnpm. Just as it should be 😉.</p><h2 id="what-are-the-t3-red-flags">What are the T3 red flags?</h2><p>There aren’t many T3 red flags, but consider if your project would have any issues with the following to see if they’re actually deal breakers. At the very least, come up with a plan on how you want to address them and know at what point they would become real pain points. </p><h3 id="its-a-monorepo">It's a Monorepo</h3><p>tRPC doesn’t do well outside of a monorepo setup. Its strength comes from actively building and modifying types on the server side and then having TypeScript inference do its magic on the client side to avoid any typing issues. </p><h3 id="scalability">Scalability</h3><p>Prisma and tRPC both have the possibility of performance issues at scale. Prisma begins to have performance issues when you need to run substantial, heavy queries or if you have complicated nested queries. With tRPC, you’ll start to see the TypeScript server begin to slow down if you’re doing a couple hundred queries and/or mutations per second. With tRPC v10, they’ve improved performance to a few thousand per second, but with a library that relies so heavily on inference, you're still likely to see things slow down as your project continues to grow. Fortunately, because tRPC is built using normal functions, it’s super easy to migrate away from when the scale of your project demands it.   </p><p>Theo, the author of T3, has a great video on <a href="https://www.youtube.com/watch?v=-R9uHm0G5m4&ref=dept-engineering-blog.ghost.io">why his stack “kinda sucks”</a> that I’d definitely encourage you to check out if you’re looking for any weaknesses that might come back to haunt you.  </p><h2 id="what-was-my-experience-like">What was my experience like?</h2><p>There is nothing better than a slick CLI. Create T3 delivers on this. It’s super easy to set up, implement, and there doesn’t feel like any overhead to keep you from diving into building your project. It sets up your package manager based on your installation method and you hit the ground running. </p><p>The promise of modularity was definitely delivered on and continues to shine as one of the best parts of the T3 stack. This meant I could easily pick up any of its individual packages, take a crash course on them, and build out a feature-rich experience with them. It doesn’t dictate how you should build things, but rather gives you the building blocks to implement your application however you see fit. As far as other packages I may want to use, I have no doubt I could take them and integrate them into the foundations that the T3 stack provides.</p><p>The one piece that did feel missing was state management. It’s easy to over-optimize and overcomplicate things out of the gate, but if you know your project is going to have a level of complexity that would benefit from a state management tool, T3 doesn’t accommodate that out of the box.  In my experience, having a tool like MobX to help manage state is an invaluable addition for more complex applications. I think it would be nice for T3 to take a stance on which state tool is best for the stack and provide some examples/configs for jump-starting a large project. They’re already doing this with an ORM in Prisma and for CSS in Tailwind, so there is precedent for this. Without state management, T3 relies heavily on React Context to manage shared data across the app (user session data for instance). tRPC wraps React Query to manage any of your route-specific data fetching needs. If the combination of these two meets your needs, then you’ll be set. Otherwise, if you need a more advanced state management tool, you’ll have to implement something on your own. Create T3 app recommends Zustand.</p><h3 id="my-takeaway">My takeaway</h3><p>T3 makes me excited about full-stack apps again. As a front-end developer, it’s easy to get into these front-end/back-end silos and T3 helps break those down by implementing a monorepo approach that doesn’t feel hacky or compromised. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/11/smile-arnold-schwarzenegger.gif" class="kg-image" alt="T3: Rise of the Monorepo" loading="lazy" width="300" height="228"><figcaption>My happy face</figcaption></img></figure><h2 id="how-i-would-start-if-i-was-you">How I would start if I was you</h2><p>T3 makes this particularly easy, but identify the pieces that you can break out and take crash courses on those individual parts. Start with as broad a crash course as you can. If you don’t know Next.js or Typescript I would start with these:</p><ul><li><a href="https://www.youtube.com/watch?v=OTuHnVvxTDs&ref=dept-engineering-blog.ghost.io">Learn Next.js With TypeScript in 30 Minutes</a></li><li><a href="https://www.youtube.com/watch?v=mTz0GXj8NN0&t=87s&ref=dept-engineering-blog.ghost.io">Next.js Crash Course</a></li><li><a href="https://www.youtube.com/watch?v=Sklc_fQBmcs&ref=dept-engineering-blog.ghost.io">Next.js in 100 Seconds</a></li><li><a href="https://www.totaltypescript.com/?ref=dept-engineering-blog.ghost.io">Total TypeScript</a></li><li><a href="https://www.youtube.com/watch?v=BCg4U1FzODs&ref=dept-engineering-blog.ghost.io">TypeScript Crash Course</a></li></ul><p>If you’ve never used heard of <strong>NextAuth</strong>, I’d start here:</p><ul><li><a href="https://www.youtube.com/watch?v=o_wZIVmWteQ&ref=dept-engineering-blog.ghost.io">NextJS Authentication Crash Course with NextAuth.js</a></li><li><a href="https://www.youtube.com/watch?v=h6wBYWWdyYQ&ref=dept-engineering-blog.ghost.io">Authentication: It’s Easier Than You Think</a></li></ul><p>If you’ve never used heard of <strong>Prisma</strong>, I’d start here:</p><ul><li><a href="https://www.youtube.com/watch?v=RebA5J-rlwg&ref=dept-engineering-blog.ghost.io">Learn Prisma In 60 Minutes</a></li></ul><p>If you’ve never heard of <strong>tRPC</strong>, I’d start here:</p><ul><li><a href="https://www.youtube.com/watch?v=Lam0cYOEst8&ref=dept-engineering-blog.ghost.io">tRPC: Smart and Easy APIs</a></li><li><a href="https://www.youtube.com/watch?v=aK5k8xoV3Ws&ref=dept-engineering-blog.ghost.io">Trying Out tRPC for the First Time | Learn with Me</a></li><li><a href="https://www.learnwithjason.dev/let-s-learn-trpc?ref=dept-engineering-blog.ghost.io">Let's Learn tRPC!</a></li></ul><p>If you’ve never heard of <strong>Tailwind</strong>, I’d start here:</p><ul><li><a href="https://www.youtube.com/watch?v=UBOj6rqRUME&ref=dept-engineering-blog.ghost.io">Tailwind CSS Crash Course</a></li></ul><p>Happy coding!</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/11/terminator-goodbye.gif" class="kg-image" alt="T3: Rise of the Monorepo" loading="lazy" width="220" height="91"/></figure>]]></content:encoded></item><item><title><![CDATA[Publishing to Maven Central, a retrospective]]></title><description><![CDATA[Package management can be something of a headache, but it doesn't have to be that way. Here's some tips to get you on your way with Maven Central.]]></description><link>https://engineering.deptagency.com/publishing-to-maven-central/</link><guid isPermaLink="false">634db358158ad0003d785f95</guid><category><![CDATA[java]]></category><category><![CDATA[Platforms]]></category><category><![CDATA[Maven]]></category><dc:creator><![CDATA[Mike Hadam]]></dc:creator><pubDate>Wed, 26 Oct 2022 16:11:22 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/taichi-nakamura-nS5ZIp3fcGc-unsplash-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/taichi-nakamura-nS5ZIp3fcGc-unsplash-1.jpg" alt="Publishing to Maven Central, a retrospective"/><p>As part of releasing <a href="https://github.com/deptagency/spring-sql-explain-logger/?ref=dept-engineering-blog.ghost.io">spring-sql-explain-logger</a>, a new logging library created at DEPT, I needed to upload the package to a repository so anyone could easily add it to their Java projects. Package management can be something of a headache, and my experience showed me that Maven Central is no exception.</p><p>Don't get me wrong - after all is said and done, it works well. For this project, we're dealing with Maven. So there should be no surprises, especially for a seasoned Java developer. The whole release is run through Maven plugins.</p><p>But beware! I found myself inadvertently stuck spinning my wheels a couple times. So I'm writing this post as a way to hopefully help people avoid some snags I faced on the journey of releasing a project to Maven Central for the first time.</p><p>First tip: always reference the <a href="https://central.sonatype.org/publish/publish-maven?ref=dept-engineering-blog.ghost.io">official Sonatype docs for Maven</a> (general Sonatype docs are found <a href="https://central.sonatype.org/?ref=dept-engineering-blog.ghost.io">here</a>). Anything related to the release is spelled out clearly, and correctly, there.</p><p>Let's talk about what might happen when you don't use the Sonatype docs. I copy-pasted a config from <a href="https://dzone.com/articles/publish-your-artifacts-to-maven-central?ref=dept-engineering-blog.ghost.io">an old tutorial</a> that had <code>&lt;nexusUrl&gt;...&lt;/nexusUrl&gt;</code> set as <code>https://oss.sonatype.org/</code> instead of <code>https://s01.oss.sonatype.org/</code>. So when I tried releasing with <code>mvn release:perform</code> I was faced with the unexplained response: <code>HTTP/1.1 403 Forbidden</code>. I had to correspond with Sonatype through their Jira, determine that the issue was on my end, and piece through the official docs comparing each section to my <code>pom.xml</code> until I found the offending <code>&lt;nexusUrl&gt;</code>. So please trust me when I say that you should treat the Sonatype docs as a single source of truth.</p><p>Another thing that confused me in the beginning deals with the two Nexus URLs: snapshot and release. Uploading artifacts to the snapshot URL creates a snapshot release. On the other hand, it's a somewhat subtle point, but the release URL is where releases are staged, rather than the final destination where the release is stored and publicly available. So uploading to the release URL creates a "staging repo" that can be approved, and only then does it sync to Maven Central where it's available to the public. Remember that once you do a release, there's no going back. So be very careful to review things ahead of time. Baeldung has <a href="https://www.baeldung.com/maven-snapshot-release-repository?ref=dept-engineering-blog.ghost.io">a great article</a> explaining snapshot and release repositories.</p><p>The whole journey of setting up the release process goes something like this:</p><ul><li>create an account for <a href="https://central.sonatype.org/publish/publish-guide/?ref=dept-engineering-blog.ghost.io#initial-setup">Sonatype's issue tracker</a> &amp; create a ticket requesting access to a groupId</li><li><a href="https://central.sonatype.org/publish/requirements/gpg/?ref=dept-engineering-blog.ghost.io#dealing-with-expired-keys">create a GPG key</a></li><li>add the <a href="https://central.sonatype.org/publish/publish-maven/?ref=dept-engineering-blog.ghost.io">various config changes</a> to your <code>pom.xml</code></li><li>cross your fingers</li><li>kick off the Maven goal for release</li></ul><p>This might not seem like a lot, but there are details I'm leaving out, enough that I decided to dedicate <a href="https://github.com/deptagency/spring-sql-explain-logger/blob/main/RELEASE.md?ref=dept-engineering-blog.ghost.io">an entire document to our release process</a>.</p><p>Keep in mind that things are a little more complicated if you're working in an organization. First, consider creating multiple accounts to access the Sonatype Jira so that multiple people can perform a release. Also, find a way to share the GPG key and passphrase. An organization-wide password manager that can store files should be adequate. And last, you'll need access to the DNS server in order to claim ownership over the domain. This involves <a href="https://central.sonatype.org/faq/how-to-set-txt-record/?ref=dept-engineering-blog.ghost.io">adding a TXT record to your DNS server</a>.</p><p>There's also a handy plugin we're using: <code>maven-release-plugin</code>.  It handles the rote steps of updating version values in <code>pom.xml</code>, creating release commits, and tagging the release. Afterward, it calls <code>nexus-staging-maven-plugin</code> to handle the release to Maven Central. We disabled <code>autoReleaseAfterClose</code>, so that the release needs to be manually approved from the Nexus web interface.</p><p>That wraps up all the ins and outs I learned from releasing a project onto Maven Central. If you stick to these tips you should be up and running in no time!</p>]]></content:encoded></item><item><title><![CDATA[Setting up Integration Tests for a Django backend & React frontend]]></title><description><![CDATA[With a React frontend, Django isn't responsible for the frontend and doesn't know how to start the frontend process. This causes the tests to fail due to there being no frontend to interact with. To fix the failing tests we need to have the frontend running when running the integration tests.]]></description><link>https://engineering.deptagency.com/setting-up-integration-tests-for-a-django-backend-react-frontend/</link><guid isPermaLink="false">635059bc158ad0003d786217</guid><category><![CDATA[python]]></category><category><![CDATA[testing]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Tim Durkee]]></dc:creator><pubDate>Thu, 20 Oct 2022 15:12:19 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/chau-cedric--6GinD-1NwE-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/chau-cedric--6GinD-1NwE-unsplash.jpg" alt="Setting up Integration Tests for a Django backend & React frontend"/><p>If you've worked on a <a href="https://www.djangoproject.com/?ref=dept-engineering-blog.ghost.io">Django</a> project with a React or similar frontend you'll know you can't take advantage of Django's <a href="https://docs.djangoproject.com/en/4.1/topics/testing/tools/?ref=dept-engineering-blog.ghost.io#django.test.LiveServerTestCase">LiveServerTestCase</a> for running integration tests. <code>LiveServerTestCase</code>  would allow you to use automated test clients such as <a href="https://www.selenium.dev/?ref=dept-engineering-blog.ghost.io">Selenium</a>. But in a setup with a React frontend, Django isn't responsible for the frontend and doesn't know how to start the frontend process. </p><p>This causes the tests to fail due to there being no frontend to interact with. </p><p>To fix the failing tests we need to have the frontend running when running the integration tests. There are a number of ways to solve this, from manually starting the frontend to building a management command to handle the process. We’re creating a custom TestRunner, meaning developers can continue using the <code>./manage.py test</code> command they already know. There won't be a need for a separate management command or manually starting a process before running tests. The custom TestRunner also gives us the ability to easily add some additional functionality to streamline the testing process.</p><p>The <code>CustomTestRunner</code> will inherit from  <a href="https://docs.djangoproject.com/en/4.1/topics/testing/advanced/?ref=dept-engineering-blog.ghost.io#django.test.runner.DiscoverRunner">DiscoverRunner</a>.  At minimum we'll need to define two methods: <code>setup_test_environment</code> and <code>teardown_test_environment</code>.  These methods will be responsible for starting and stopping the frontend process.</p><pre><code class="language-python">from django.conf import settings
from django.test.runner import DiscoverRunner

import os

class CustomTestRunner(DiscoverRunner):
	def setup_test_environment(self, **kwargs):
		os.system("kill $(ps aux | grep '[y]arn.js run dev -p %s' | awk '{print $2}')"	% settings.TEST_FRONTEND_PORT)
		
		os.system(f"API_BASE_URL=http://{settings.TEST_HOST}: settings.TEST_BACKEND_PORT} &amp;&amp; nohup yarn run dev -p {settings.TEST_FRONTEND_PORT} &amp;")
		os.system(f"FRONTEND_URL=http://{settings.TEST_HOST}:{settings.TEST_FRONTEND_PORT}")
		
		DiscoverRunner.setup_test_environment(self, **kwargs)

	def teardown_test_environment(self, **kwargs):
		os.system("kill $(ps aux | grep '[y]arn.js run dev -p %s' | awk '{print $2}')" % settings.TEST_FRONTEND_PORT)

		DiscoverRunner.teardown_test_environment(self, **kwargs)
</code></pre><h3 id="setup-test-environment">Setup Test Environment</h3><p>The <code>setup_test_environment</code> is responsible for bringing up the frontend environment. However, before starting the frontend process we need to make sure it isn't already running. For this, we’ll run a <code>kill</code> command.</p><p><code>os.system("kill $(ps aux | grep '[y]arn.js run dev -p %s' | awk '{print $2}')"% settings.TEST_FRONTEND_PORT</code></p><p>Depending on how you're running your frontend you may need to update the <code>grep</code> string. The <code>[]</code> in the <code>grep</code> string are used to get the actual process and not the  <code>grep</code> command itself. This command and the ones to follow also pass ports specific to testing. We'll touch on that for the next step.</p><p>Now that we've made sure the frontend isn't running we can to bring up the frontend.</p><p><code>os.system(f"API_BASE_URL=http://{settings.TEST_HOST}:{settings.TEST_BACKEND_PORT} &amp;&amp; nohup yarn run dev -p {settings.TEST_FRONTEND_PORT} &amp;")</code></p><p>To do this we're using <code>nohup</code> which will bring the process up in the background. Like with the <code>grep</code> command you may need to adjust this depending on your frontend. As part of bringing up the process we're also setting a couple of environment variables: <code>API_BASE_URL</code>  and <code>TEST_FRONTEND_PORT</code>.</p><p>The <code>API_BASE_URL</code> is set so the frontend knows the host and port of the backend. We're also setting what port the frontend will be running on using <code>TEST_FRONTEND_PORT</code>. This is being done so the test environment can come up without conflicting with anyone's development environment. We'll also need to set the frontend URL for the backend. This is accomplished by the following:</p><p><code>os.system(f"FRONTEND_URL=http://{settings.TEST_HOST}:{settings.TEST_FRONTEND_PORT}")</code></p><p>The last piece for <code>setup_test_environment</code>  is to call the base class <code>setup_test_environment</code>.</p><p><code>DiscoverRunner.setup_test_environment(self, **kwargs)</code></p><h3 id="teardown-test-environment">Teardown Test Environment</h3><p>The other method that needs to be defined in our TestRunner is <code>teardown_test_environment</code>, it will be responsible for bringing down the frontend. To accomplish this we'll call the <code>kill</code> command we call at the start of the setup.</p><p><code>os.system("kill $(ps aux | grep '[y]arn.js run dev -p %s' | awk '{print $2}')"% settings.TEST_FRONTEND_PORT</code></p><p>As with  <code>setup_test_environment</code> we'll call the base classes <code>teardown_test_environment</code></p><p><code>DiscoverRunner.teardown_test_environment(self, **kwargs)</code></p><p>Now that <code>CustomTestRunner</code> has been defined we can set it as the TestRunner to use. In your settings file add  <code>TEST_RUNNER = "&lt;path to test runner&gt;.TestRunner"</code>. Now when someone runs <code>./manage.py test</code>, the frontend environment will be torn down as part of the testing process.</p><h3 id="including-and-excluding-integration-tests">Including and excluding integration tests</h3><p>Now that we have a custom TestRunner there is additional functionality we can add to improve the testing process. Integration tests are an important part of a test suite but they can be slow. Because of this, there may be times when we don't want to run them. There are also times when all that needs to be run are the integration tests. We'll look at adding two keyword arguments to our new CustomTestRunner: <code>--only-integration-tests</code> and <code>--exclude-integration-tests</code> to handle these use cases.</p><p>To add new keyword arguments we'll need to define the class method <code>add_arguments</code>. We'll also need to define the <code>__init__</code> method for handling the keyword arguments when passed.</p><pre><code class="language-python">@classmethod
def add_arguments(cls, parser):
	parser.add_argument(
		"--exclude-integration-tests",
		action="store_true",
		help="Exclude integration tests.",
	)

	parser.add_argument(
		"--only-integration-tests",
		action="store_true",
		help="Runs only integration tests. Superseeds --exclude-integration-tests",
	)

	DiscoverRunner.add_arguments(parser)
</code></pre><p>Both keywords will have an action of <code>store_true</code> which will set their param to <code>True</code> if the keywords are passed.</p><pre><code class="language-python">def __init__(self, *args, **kwargs):
	self.kwargs = kwargs
	self.frontend_needed = True

	if self.kwargs["only_integration_tests"]:
		self.add_tag("integration")
		self.remove_exclude_tag("integration")
		self.frontend_needed = True
	elif self.kwargs["exclude_integration_tests"]:
		self.remove_tag("integration")
		self.add_exclude_tag("integration")
		self.frontend_needed = False
	elif self.has_tag("integration"):
		# enable frontend if tag passed manually
		self.frontend_needed = True

	self.tags = self.kwargs["tags"]
	super().__init__(*args, **self.kwargs)

def add_exclude_tag(self, tag):
	self.append_kwarg_list("exclude_tags", tag)

def remove_tag(self, tag):
	if self.kwargs["tags"] and tag in self.kwargs["tags"]:
		self.kwargs["tags"].remove(tag)

def add_tag(self, tag):
	self.append_kwarg_list("tags", tag)

def remove_exclude_tag(self, tag):
	if self.kwargs["exclude_tags"] and tag in self.kwargs["exclude_tags"]:
		self.kwargs["exclude_tags"].remove(tag)
</code></pre><p>In the <code>__init__</code> method we need to check if the new keyword arguments have been passed. As the keywords are mutually exclusive we'll give <code>only_integration_tests</code> priority over <code>exclude_integration_tests</code>.</p><p>For <code>only_integration_tests</code> we'll add <code>integration</code> to the tags list as well as remove it from the exclude tags. We need to remove <code>integration</code> from the exclude tags as it's possible for <code>integration</code> to be added manually. The last step is to set <code>self.frontend_needed</code> to <code>True</code>. This will allow us to know in <code>setup_test_environment</code> and <code>teardown_test_environment</code> if the frontend should be loaded.</p><p>For <code>exclude_integration_tests</code> we'll do the opposite, adding <code>integration</code> to the exclude tags list and removing it from the tags list. We'll then set <code>self.frontend_needed</code> to <code>False</code> as the frontend won't need to be run.</p><p>With these new keywords, we can new run <code>./manage.py test --exclude-integration-tests</code> and <code>./manage.py test --only-integration-tests</code>.</p><p>If in your use case you want to be excluding integration tests most of the time. You can look at excluding integration tests by default and adding a keyword argument to include them when needed. Another use case would be to add a <code>slow</code> tag and keyword arguments include and exclude those tests as needed. Ultimately a custom TestRunner will give you flexibility in managing and running your test moving forward.</p>]]></content:encoded></item><item><title><![CDATA[Finding Database Issues Fast with Java and Spring Boot]]></title><description><![CDATA[What if you could catch badly performing queries during your local development process? The Spring Boot SQL Explain library does this for you.]]></description><link>https://engineering.deptagency.com/finding-database-issues-fast-with-java-and-spring-boot/</link><guid isPermaLink="false">6346fc63fe9806003d0cf80f</guid><category><![CDATA[java]]></category><category><![CDATA[Platforms]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[Sam Kamau]]></dc:creator><pubDate>Thu, 13 Oct 2022 15:11:45 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/z-LHUP6c.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/z-LHUP6c.jpeg" alt="Finding Database Issues Fast with Java and Spring Boot"/><p>If you’re a web developer, I’m sure you’ve deployed code to production and found out afterward that your new “simple” database query is taking <em>forever</em> to return data and is causing major user issues.  Maybe even worse, your query takes up so many computing resources it affects other queries your application’s users depend on. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/jaws.gif" class="kg-image" alt="Finding Database Issues Fast with Java and Spring Boot" loading="lazy" width="500" height="500"/></figure><p>In the worst cases, you’re not even sure <em>which</em> queries are causing issues. Debugging these issues involves triaging, collecting, and analyzing a bunch of disparate logs and metrics to finally determine that your seemingly innocuous query is the culprit because it was doing a <a href="https://en.wikipedia.org/wiki/Full_table_scan?ref=dept-engineering-blog.ghost.io">table scan</a> (read: finding data takes a loooong time) on a table that kept increasing in size.  Once you add a simple index to a table, the issue quickly disappears.</p><p>But what if you had a way to catch the issue before it turns into a production issue?  Even better, what if you could catch it <em>during your local development process</em>? This was the motivation for creating our new <a href="https://github.com/deptagency/sql-explain?ref=dept-engineering-blog.ghost.io">Spring Boot SQL Explain library</a> for Java/Spring.</p><p>We created this library for Spring applications so you can automatically execute an explain plan for all your queries and log the results. The results can then be monitored for potential issues from queries that have suboptimal ways of accessing data.  This will allow you to fix bad queries right away in local development environments or test environments.  For the database inclined, using explain plans can give you a glimpse of the performance of a query as you scale your data up.  A query that has a tablescan now may turn into a performance nightmare as your data grows.</p><p>We don’t recommend using this in production, of course!</p><p>We put it into JSON so it’s parseable by logging tools like Splunk.  Once configured, this is what the logging output looks like …</p><figure class="kg-card kg-code-card"><pre><code class="language-JSON">{
  "Query": "select customer0_.id as id1_0_0_, customer0_.first_name as first_na2_0_0_, customer0_.last_name as last_nam3_0_0_ from customer customer0_ where customer0_.id=?",
  "Explain": [
    {
      "Plan": {
        "Node Type": "Seq Scan",
        "Parallel Aware": false,
        "Async Capable": false,
        "Relation Name": "customer",
        "Alias": "customer0_",
        "Startup Cost": 0,
        "Total Cost": 1.01,
        "Plan Rows": 1,
        "Plan Width": 240,
        "Filter": "((last_name)::text = 'test_save'::text)"
      }
    }
  ]
}</code></pre><figcaption>A PostGreSQL Explain Plan Example Result</figcaption></figure><p/><figure class="kg-card kg-code-card"><pre><code class="language-JSON">{
  "Query": "select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as last_nam3_0_ from customer customer0_ where customer0_.last_name=?",
  "Explain": {
    "query_block": {
      "select_id": 1,
      "cost_info": {
        "query_cost": "0.95"
      },
      "table": {
        "table_name": "customer0_",
        "access_type": "ALL",
        "rows_examined_per_scan": 7,
        "rows_produced_per_join": 0,
        "filtered": "14.29",
        "cost_info": {
          "read_cost": "0.85",
          "eval_cost": "0.10",
          "prefix_cost": "0.95",
          "data_read_per_join": "367"
        },
        "used_columns": [
          "id",
          "first_name",
          "last_name"
        ],
        "attached_condition": "(`sql_explain`.`customer0_`.`last_name` = 'test_save')"
      }
    }
  }
}</code></pre><figcaption>A MySQL Explain Plan Example Result</figcaption></figure><p/><h1 id="how-to-use-it">How To Use It</h1><p>You can install it from our Github maven repo by adding this repository to your Maven/Gradle configs:</p><p/><figure class="kg-card kg-code-card"><pre><code class="language-XML">&lt;dependency&gt;
    &lt;groupId&gt;com.deptagency&lt;/groupId&gt;
    &lt;artifactId&gt;spring-sql-explain-logger&lt;/artifactId&gt;
    &lt;version&gt;0.0.3&lt;/version&gt;
&lt;/dependency&gt;</code></pre><figcaption>Maven, using pom.xml</figcaption></figure><p/><figure class="kg-card kg-code-card"><pre><code>implementation 'com.deptagency:spring-sql-explain-logger:0.0.3'</code></pre><figcaption>Gradle, using gradle.build</figcaption></figure><p/><p>Then, add the config property below wherever you usually set your application properties (for example <code>src/main/resources/application.properties</code>) and set value to true to enable explain plan logging:</p><figure class="kg-card kg-code-card"><pre><code>com.deptagency.sqlexplain.enabled=true</code></pre><figcaption>application.properies (or the like)</figcaption></figure><p/><p>Of course, we would definitely <em>not </em>turn on this tool in production!  Running it locally before committing code, or in a pinch, turning it on in a dev/test environment to get an idea of your heavy queries though might be a great use case.</p><p>Finally, add a Spring <code>@ComponentScan</code> for the package somewhere in your project.</p><pre><code>@ComponentScan("com.deptagency.sqlexplain")
public class MyApplication {

    // amazing things happen here

}</code></pre><p>There are a lot more details in the project's <a href="https://github.com/deptagency/spring-sql-explain-logger?ref=dept-engineering-blog.ghost.io#readme">README</a>!</p><h1 id="limitations">Limitations</h1><p>Right now, the library only supports PostgreSQL and MySQL.  If there's demand, we can support others as well though.</p><p>Also, right now there’s no way to customize the logging levels for different types of scans, but we plan on doing that soon.</p><h1 id="how-it-was-made">How it was made</h1><p>If you’re like us, you love the details of how things are made, so we’d love to tell you how we cooked this up.  If you’re interested in that stuff read on, otherwise, just enjoy using the library!</p><p>When we first had this idea, we thought it would be simple to do this, especially for applications that use Hibernate. We could easily get the SQL query of the prepared statement that Hibernate was executing. Then we started to look for a way to get the prepared statement bind parameters (the data that’s sent into the parameters) so we could execute the explain plan query. </p><p>That turned into a much harder problem: Neither Hibernate nor JDBC API provided an easy way to get the data for the parameters. One of the solutions we explored was proxying the Datasource and intercepting JDBC calls but we wanted the proxy to be as least intrusive as possible and easy to enable and disable per environment. </p><p>What we finally found was that the Spring <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/BeanPostProcessor.html?ref=dept-engineering-blog.ghost.io">BeanPostProcessor</a> has a great way of accomplishing this. You can intercept any bean during creation and add a proxy and you can also make the proxy conditional based on a configuration property.  We were lead on to this solution when we stumbled across the general purpose <a href="https://github.com/gavlyukovskiy/spring-boot-data-source-decorator?ref=dept-engineering-blog.ghost.io">Spring Boot Data Source Decorator</a> on Github.  That project is excellent, but we wanted something purpose-built to help with database query profiling that was very easy to set up.</p><p>In the end, this is a bit of the magic that makes it work (<a href="https://github.com/deptagency/spring-sql-explain-logger/blob/main/src/main/java/com/deptagency/sqlexplain/ExplainPlanDatasourceProxyBean.java?ref=dept-engineering-blog.ghost.io#L48">full source here</a>):</p><pre><code class="language-java">@Component
@ConditionalOnProperty(value = "com.deptagency.sqlexplain.enabled", havingValue = "true")
public class ExplainPlanDatasourceProxyBean implements BeanPostProcessor {
    ...
    @Override
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
        if (bean instanceof DataSource) {
            Optional&lt;DatabaseDialect&gt; dbDialect = DatabaseDialect.getDatabaseDialectByURL(jdbcURL);

            if (dbDialect.isPresent() &amp;&amp; dbDialect.get().isSupported()) {
                ProxyFactory factory = new ProxyFactory(bean);
                factory.setProxyTargetClass(true);
                factory.addAdvice(new ProxyDataSourceInterceptor((DataSource) bean, dbDialect.get(), maxCacheSize, queryCacheExpiry));
                return factory.getProxy();
            } else {
                logger.warn("WARN database is not currently supported. Currently supported databases include {} ",
                        DatabaseDialect.getSupportedDatabases());
            }
        }
        return bean;
    } 
}</code></pre>]]></content:encoded></item><item><title><![CDATA[Integration Testing NestJS APIs with a Test Database]]></title><description><![CDATA[Unit tests for APIs can sometimes provide little to no value, so the next logical step is often to write integration tests. You don’t want to touch live data though, and want tests to make assertions against actual (un-mocked) service logic. The following is an approach to do just that.]]></description><link>https://engineering.deptagency.com/integration-testing-nestjs-apis-with-a-test-database/</link><guid isPermaLink="false">6345b7fbfe9806003d0cf4cf</guid><category><![CDATA[Platforms]]></category><category><![CDATA[nestjs]]></category><dc:creator><![CDATA[Ali Halim]]></dc:creator><pubDate>Wed, 12 Oct 2022 20:39:56 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/cristiano-firmani-tmTidmpILWw-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/cristiano-firmani-tmTidmpILWw-unsplash.jpg" alt="Integration Testing NestJS APIs with a Test Database"/><p>With all the mocking and stubbing that often comes with unit tests, writing them can feel like a waste of time. For example:</p><pre><code>describe('findAll', () =&gt; {
  it('should return an array of dogs', async () =&gt; {
    const result = ['Snoopy'];
    jest.spyOn(dogService, 'findAll').mockImplementation(() =&gt; result);

    expect(await dogController.findAll()).toBe(result);
  });
});</code></pre><p>Since the implementation of <code>findAll()</code> in <code>dogService</code> is mocked to just return <code>['Snoopy']</code>, all this test really does is assert that <code>dogController</code>'s <code>findAll()</code> function makes a call to and returns the result of <code>dogService</code>'s <code>findAll()</code> function. There isn’t too much value in such a test as far as ensuring sound API logic.</p><p>Unit testing is for small individual pieces of code (units), such as functions, with heavy use of mocks and stubs.  It's great for making sure isolated chunks of code work as expected on their own, but that's not quite what we're looking for here.</p><p>Integration testing covers interactions between components of an entire system (think APIs and databases) and thus makes a lot more sense here as it involves simulating calls to API endpoints and checking that table data is read/written as expected.  <a href="https://github.com/visionmedia/supertest?ref=dept-engineering-blog.ghost.io">Supertest</a> is a great tool for this as it allows you to simulate API calls and make assertions against responses.</p><p><a href="https://docs.nestjs.com/?ref=dept-engineering-blog.ghost.io">NestJS</a>, which supports Supertest out of the box (and as you can see has a lot of cool cat pics throughout its documentation), provides a useful way to spin up a Nest application context for tests to run against, including the ability to make overrides to the things you include in that mock application.</p><p><strong>This post lays out an approach that makes use of this feature to</strong></p><ol><li><strong>write integration tests for Nest APIs using Supertest and </strong></li><li><strong>ensure that test logic runs against a test database as opposed to a default/live database.</strong></li></ol><p>This will not be an in-depth project setup walkthrough, but rather a happy-path overview of key pieces needed to achieve our goal.  I'm mainly assuming familiarity with NestJS concepts, or better yet, that you have a working NestJS application that the approach can be applied to.  If you have neither, that's fine!  I hope it's still a valuable read.</p><h3 id="database-and-data-source-setup">Database and Data Source Setup</h3><p>I used <a href="https://typeorm.io/?ref=dept-engineering-blog.ghost.io">TypeORM</a> as my ORM and <a href="https://www.postgresql.org/?ref=dept-engineering-blog.ghost.io">PostgreSQL</a> for my database, but the general approach can be applied to any Nest testing regardless of the database technologies used.</p><p>The first step is to get the databases up and running. Here I use <a href="https://docs.docker.com/compose/?ref=dept-engineering-blog.ghost.io">Docker Compose</a> to launch Postgres instances for a default and test database:</p><figure class="kg-card kg-code-card"><pre><code>version: "3"
services:
  database:
    image: "postgres:latest"
    ports:
      - ${POSTGRES_DEFAULT_PORT}:${POSTGRES_DEFAULT_PORT}
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DEFAULT_DB}
    volumes:
      - ./pg-data/:/var/lib/postgresql/data/
  test_database:
    image: "postgres:latest"
    ports:
      - ${POSTGRES_TEST_PORT}:${POSTGRES_DEFAULT_PORT}
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_TEST_DB}</code></pre><figcaption><em>docker-compose.yml</em></figcaption></figure><p/><p>With a simple <code>docker-compose up</code> command, Docker uses this file to spin up two containers (one for each database), each using the latest postgres version.</p><p>Notice the differences in ports: <code>POSTGRES_DEFAULT_PORT</code> is 5432 while the <code>POSTGRES_TEST_PORT</code> is simply something else – I used 5433.</p><p>Now here is the data source provider where the database connection is made in my Nest code:</p><figure class="kg-card kg-code-card"><pre><code>import { DataSource, DataSourceOptions } from 'typeorm';
import { DATA_SOURCE } from './constants';
import { Dog } from './dog/dog.entity';

export const DataSourceProvider = {
  provide: DATA_SOURCE,
  useFactory: async () =&gt; getInitializedDataSource(),
};

export const getInitializedDataSource = (database?: string, port?: string): Promise&lt;DataSource&gt; =&gt; {
  const dataSource = new DataSource({
    type: 'postgres',
    host: process.env.POSTGRES_HOST,
    username: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    entities: [Dog],
    database: database || process.env.POSTGRES_DEFAULT_DB,
    port: parseInt(port || process.env.POSTGRES_DEFAULT_PORT),
  } as DataSourceOptions);

  return dataSource.initialize();
};</code></pre><figcaption><em>/modules/database/datasource.provider.ts</em></figcaption></figure><p/><p>The key piece here is how the <code>database</code> and <code>port</code> fields are set in the <code>DataSource</code> constructor – default values are used when no parameters are passed into the provider’s factory function.  This is how the live/production NestJS application connects to the default database.  I’ll make use of the parameters in a bit when I override this provider for tests.</p><p>For now, take a look at how the data source provider can be used to create repository providers via injection:</p><figure class="kg-card kg-code-card"><pre><code>import { DataSource } from 'typeorm';
import { DATA_SOURCE, DOG_REPOSITORY } from '../constants';
import { Dog } from './dog.entity';

export const DogRepositoryProvider = {
  provide: DOG_REPOSITORY,
  useFactory: (dataSource: DataSource) =&gt; dataSource.getRepository(Dog),
  inject: [DATA_SOURCE],
};</code></pre><figcaption><em>/modules/database/dog/dog.provider.ts</em></figcaption></figure><p/><p>Repository providers can then be used for database operations inside of API service logic, like so:</p><figure class="kg-card kg-code-card"><pre><code>import { Inject, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { DOG_REPOSITORY } from 'src/modules/database/constants';
import { Dog } from 'src/modules/database/dog/dog.entity';

@Injectable()
export class DogService {
  constructor(
    @Inject(DOG_REPOSITORY) private dogRepository: Repository&lt;Dog&gt;,
  ) {}

  getDogByName(name: string): Promise&lt;Dog&gt; {
    return this.dogRepository.findBy({ name });
  }
}</code></pre><figcaption><em>/modules/api/dog/dog.service.ts</em></figcaption></figure><p/><h3 id="populate-test-database">Populate Test Database</h3><p>The databases are up and running and we just saw how APIs can operate on the database via repository provider injection…by way of data source provider injection (injection <a href="https://en.wikipedia.org/wiki/Inception?ref=dept-engineering-blog.ghost.io"><em>Inception</em></a>?).</p><p>Now, the test database needs to be populated with actual tables since the Docker script from earlier only created a table-less database for us.</p><p>The approach I took on this was to write a simple script that connects to the test database and runs synchronize() on it, which updates the database with the entities (tables) passed into the data source initialization:</p><figure class="kg-card kg-code-card"><pre><code>import { getInitializedDataSource } from '../modules/database/datasource.provider';

async function initTestDatabase() {
  try {
    const dataSource = await getInitializedDataSource(
      process.env.POSTGRES_TEST_DB,
      process.env.POSTGRES_TEST_PORT,
    );

    await dataSource.synchronize();
    process.exit();
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
}

initTestDatabase();</code></pre><figcaption><em>/test/initialize.ts</em></figcaption></figure><p/><p>Notice the parameters being passed into the data source initialization function that we saw earlier. This is how we connect to the test database instead of the default one. As we’ll see in a bit, I make the exact same function call to override the data source provider on the mock Nest context.</p><p>Synchronizing creates (the very first time the script is run) or updates (subsequent script runs) the tables for entities according to how they are defined in their respective TypeORM entity classes. Let’s take a look back at the data source function from earlier:</p><figure class="kg-card kg-code-card"><pre><code>...
export const getInitializedDataSource = (database?: string, port?: string): Promise&lt;DataSource&gt; =&gt; {
    ...
    entities: [Dog],//pass in any entities that the data source should create/update tables for
    ...
}
...</code></pre><figcaption><em>/modules/database/database.provider.ts</em></figcaption></figure><p/><p>So in this example, the script creates the <code>dog</code> table when it’s run the first time and updates it any time there are changes to its entity class thereafter.</p><p>The script can be run via an npm command (e.g. <code>npm run test:init</code>).</p><figure class="kg-card kg-code-card"><pre><code>"scripts": {
  .
  .
  .
  "test": "jest",
  "test:init": "./node_modules/.bin/ts-node src/test/initialize.ts"
}</code></pre><figcaption><em>package.json</em></figcaption></figure><p/><p>The <code>dog</code> table is now created and ready for some test runs!</p><h3 id="run-tests">Run Tests</h3><p>Here is where the rubber meets the road.  For my service’s test file, I initialize the aforementioned Nest application context with the data source provider overridden as described in <a href="https://docs.nestjs.com/fundamentals/testing?ref=dept-engineering-blog.ghost.io">this documentation</a>:</p><figure class="kg-card kg-code-card"><pre><code>import * as request from 'supertest';
...
describe('DogService', function () {
  const mockDogName = 'Snoopy';
  let mocks: any;
  
  beforeAll(async function () {
    const mockModule: TestingModule = await Test.createTestingModule({
      imports: [DatabaseModule, DogModule],
      providers: [DogProvider, HelperService],
    })
    .overrideProvider(DATA_SOURCE)
    .useFactory({
      factory: async (): Promise&lt;DataSource&gt; =&gt; {
        return getInitializedDataSource(
          process.env.POSTGRES_TEST_DB,
          process.env.POSTGRES_TEST_PORT,
        );
      },
    })
    .compile();
    
    const helperService: HelperService = await mockModule.resolve(HelperService);
    await helperService.insertMockDog(mockDogName);

    const mockApp: INestApplication = mockModule.createNestApplication();
    await mockApp.init();
    const mockAppServer: any = mockApp.getHttpServer();

    mocks = {
      app: mockApp,
      helperService,
      appServer: mockAppServer,
    };
  });

  it('should successfully find dog given name', async function () {
    const response = await request(mocks.appServer).get(`/dogs/byName?name=${mockDogName}`);
    const results = response.body;
    
    expect(response.status).toBe(200);
    expect(results.length).toBe(1);
    expect(results[0].name).toBe(mockDogName);
  });

  afterAll(async function () {
    await mocks.app.close();
    await mocks.helperService.disconnectFromDatabase(); 
  });
});</code></pre><figcaption><em>/modules/api/dog/dog.service.spec.ts</em></figcaption></figure><p/><p>Note: Nest is agnostic to testing frameworks but supports Jest out of the box, which I’m using here.</p><p>Now let's break this down.</p><p>First, in the <code>beforeAll()</code> block, a <code>TestingModule</code> (which is akin to a Nest application’s root <code>AppModule</code>) is created.  In its initialization you can see where I override the specified provider, in this case <code>DATA_SOURCE</code>, with what’s returned from the <code>useFactory()</code> function: a new test-specific <code>DataSource</code>:</p><pre><code>...
  .overrideProvider(DATA_SOURCE)
    .useFactory({
      factory: async (): Promise&lt;DataSource&gt; =&gt; {
        return getInitializedDataSource(
          process.env.POSTGRES_TEST_DB,
          process.env.POSTGRES_TEST_PORT,
        );
      },
    })
  ...</code></pre><p>Now all API table operations during test runs are done against the test database. Hip-hip hooray!</p><p>This mocked module is then used to resolve a helper service  (<code>insertMockDog()</code> on the next line saves a puppy named ‘Snoopy’ to the test database’s <code>dog</code> table), create the application context, and set up a mock server for Supertest calls:</p><pre><code>...
const helperService: HelperService = await mockModule.resolve(HelperService);
await helperService.insertMockDog(mockDogName);

const mockApp: INestApplication = mockModule.createNestApplication();
await mockApp.init();
const mockAppServer: any = mockApp.getHttpServer();
...</code></pre><p>Then in the lone unit test, the dog service is hit via Supertest and assertions are made to see if it finds ‘Snoopy’ in the table:</p><pre><code>...
it('should successfully find dog given name', async function () {
  const response = await request(mocks.appServer).get(`/dogs/byName?name=${mockDogName}`);
  const results = response.body;
    
  expect(response.status).toBe(200);
  expect(results.length).toBe(1);
  expect(results[0].name).toBe(mockDogName);
});
...</code></pre><p>Finally in the <code>afterAll()</code> block, the mock app context is closed and the helper service is used to close the data source. <strong>This teardown is required to prevent Jest leaks</strong>:</p><pre><code>...
afterAll(async function () {
  await mocks.app.close();
  await mocks.helperService.disconnectFromDatabase(); 
});
...</code></pre><p>Here's the implementation of <code>disconnectFromDatabase()</code>:</p><figure class="kg-card kg-code-card"><pre><code>@Injectable()
export class HelperService {
  constructor(
    @Inject(DATA_SOURCE) private dataSource: DataSource,
  ) {}
  ...
  async disconnectFromDatabase(): Promise&lt;void&gt; {
    return this.dataSource.destroy();
  }
}</code></pre><figcaption><em>/modules/api/test/helper.service.ts</em></figcaption></figure><p/><p>Sweet! APIs can now be hit freely with their logic un-mocked and hitting the (test) database, just in like real life.</p><p>You see how the helper service can be used for all of your setup and teardown needs, as well as how the logic in <code>beforeAll()</code> could be functioned out for reuse in other <code>.spec</code> files, like so:</p><figure class="kg-card kg-code-card"><pre><code>import { initTestingModule } from '../test/helper.service';
...
describe('BirdService', function () {
  let mocks: any;

  beforeAll(async function () {
    mocks = await initTestingModule(BirdModule);
  });
  ...
}</code></pre><figcaption><em>/modules/api/bird/bird.service.spec.ts</em></figcaption></figure><p/><figure class="kg-card kg-code-card"><pre><code>...
export async function initTestingModule(moduleBeingTested: any): Promise&lt;any&gt; {
  const mockModule: TestingModule = await Test.createTestingModule({
    imports: [DatabaseModule, moduleBeingTested],//dynamically import the module of the service being tested
    providers: [DogProvider, BirdProvider],//pass the providers for any services that are tested
  })
    .overrideProvider(DATA_SOURCE)
  ...
  return {
    app: mockApp,
    helperService,
    appServer: mockAppServer,
  };
}
...
//data-mocking and teardown functions below!</code></pre><figcaption>/modules/api/test/helper.service.ts</figcaption></figure><p/><p>With this setup, any service test file simply passes its module into the function, adds its provider to the <code>providers</code> list, and gets back its own object with the mocked context it needs to run great integration tests.</p><h3 id="moving-up-out-of-local">Moving Up Out of Local</h3><p>Let’s now take a look at how this approach can apply to test runs in the CI/CD pipeline.</p><p>For this application, tests are automatically run when any GitHub pull requests are opened to its main branch. This is done via <a href="https://docs.github.com/en/actions/using-workflows/about-workflows?ref=dept-engineering-blog.ghost.io">workflows</a>. Read all about 'em!</p><figure class="kg-card kg-code-card"><pre><code>name: Test

on:
  pull_request:
    types:
      - opened
      - synchronize
    branches:
      - main

jobs:
  build:
    env:
      POSTGRES_TEST_DB: ${{ secrets.POSTGRES_TEST_DB }}
      POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
      POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          cache: "npm"
      - run: npm install
      - name: Start database instance for testing
        uses: harmon758/postgresql-action@v1
        with:
          postgresql db: ${{ env.POSTGRES_TEST_DB }}
          postgresql user: ${{ env.POSTGRES_USER }}
          postgresql password: ${{ env.POSTGRES_PASSWORD }}
      - name: Create/synchronize database tables before tests
        run: npm run test:init
      - run: npm run test</code></pre><figcaption><em>.github/workflows/test.yml</em></figcaption></figure><p/><p>As you can see in the workflow steps, a postgres database instance is spun up before tests are run, then our handy <code>test:init</code> script is run to populate the database with tables, and finally the actual tests run. Cool!</p><h3 id="conclusion"><strong>Conclusion</strong></h3><p>I hope this post helps others out there struggling with testing in NestJS like I was. The idea is to have API service logic actually tested (not mocked) using a dedicated test database and in a way that takes advantage of Nest’s great architecture. I believe this approach can help you accomplish that.</p><p>Thanks for reading!</p>]]></content:encoded></item><item><title><![CDATA[A risk management framework for software projects]]></title><description><![CDATA[The goal of risk management is to minimize risk in a technical project by minimizing the probability of failure and minimizing the impact of failure.]]></description><link>https://engineering.deptagency.com/risk-management-framework-software/</link><guid isPermaLink="false">633abbdf561217003d683117</guid><category><![CDATA[Risk Management]]></category><category><![CDATA[FMEA]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Ashwin Sundar]]></dc:creator><pubDate>Tue, 11 Oct 2022 13:06:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/scott-graham-5fNmWej4tAA-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h1 id="introduction">Introduction</h1>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/scott-graham-5fNmWej4tAA-unsplash.jpg" alt="A risk management framework for software projects"/><p>If you haven't worked in a regulated environment before, then you may not be familiar with risk management documents such as FMEAs, FTAs, or PRAs. Risk management is notoriously a snoozer of a topic, but behind this deceptive veneer lie some valuable tools that could benefit software projects at large, and not just in regulated environments.</p>
<p>One of these tools is the FMEA, or <strong>Failure Modes and Effects Analysis</strong>. Before we start diving into the FMEA though, let's set up a little background information.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/Screenshot-from-2022-10-03-17-38-45.png" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="343" height="339"><figcaption><em>"The Fatigues", Seinfeld</em> <em>(1996)</em></figcaption></img></figure><p/><!--kg-card-begin: markdown--><h1 id="background">Background</h1>
<p>In the broader discipline of engineering, a failure can be defined as an unintended consequence in a manufactured item. The manifestation of a failure in software development is commonly known as a bug.</p>
<p>To formalize the definition of a bug, I'll define it as:<br>
<em>Unexpected behavior in a deployed application as a result of code that does not adequately cover all possible field use conditions</em></br></p>
<p>That's a mouthful. Let's just say it's:<br>
<em>Code that doesn't behave the way it was designed</em></br></p>
<p>Some bugs are immediately obvious and can be fixed in the moment. Others are latent, and only manifest themselves in the field or in a production environment. The cost of fixing a bug grows <a href="https://deepsource.io/blog/exponential-cost-of-fixing-bugs/?ref=dept-engineering-blog.ghost.io">exponentially</a> from the time it is created all the way to field deployment. This means the cost to fix a bug in code as soon as you write the code is very minimal. This is compared to noticing it when the code base is more mature, core engineers have left the project, and eventually the product is deployed to the field and might require a recall. Depending on the software, you might be able to deploy an over-the-air patch, but in critical embedded systems this may not be an option.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/Silicon-Valley-Bugs.jpg" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="960" height="541" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/10/Silicon-Valley-Bugs.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/Silicon-Valley-Bugs.jpg 960w" sizes="(min-width: 720px) 720px"><figcaption><em>"RussFest", Silicon Valley (2019)</em></figcaption></img></figure><p/><!--kg-card-begin: markdown--><h2 id="just-eliminate-risk">&quot;Just eliminate risk!&quot;</h2>
<p>This is extremely concerning to most managers. If you hang around management long enough, you might start to hear phrases like:</p>
<ul>
<li>
<p><em>By attaining 100% test coverage, we will eliminate risk from this project.</em></p>
</li>
<li>
<p><em>When we can guarantee 99.9999% uptime, we can safely assume that failure will never happen.</em></p>
</li>
</ul>
<p>And my personal favorite:</p>
<ul>
<li><em>Just eliminate risk!</em></li>
</ul>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/office-image.png" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="1600" height="842" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/10/office-image.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/10/office-image.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/office-image.png 1600w" sizes="(min-width: 720px) 720px"><figcaption><em>"A.A.R.M.", The Office (2013)</em></figcaption></img></figure><p/><!--kg-card-begin: markdown--><p>With a bold statement like that, context matters. The gold standard in highly regulated industries is 6 nines - or 99.9999% reliability. This corresponds to approximately <a href="https://en.wikipedia.org/wiki/Six_Sigma?ref=dept-engineering-blog.ghost.io#Sigma_levels">3.4 defects per million opportunities of failure</a>.</p>
<p><strong>It is a logical fallacy to state that risk can be eliminated.</strong> Unless you are working in first principles where <strong>MAYBE</strong> you can get away with certain guarantees, then you cannot and should not guarantee that your product, whether that be software or hardware, will never fail. The likelihood of failure might range from common to astronomically rare, but it always exists. For this article, I've left the definition of failure rather broad, and we'll find out in another article that there are failures with a range of effects, from minimal to catastrophic.</p>
<p><strong>The goal of risk management is to minimize, not eliminate, risk in a technical project by minimizing the probability of failure and minimizing the impact of failure.</strong></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="how">How?</h2>
<p>How can we go about minimizing risk effectively? There have been books written about the subject, software tools developed, and an entire risk management consulting industry dedicated to aerospace, medical device, and pharmaceutical risk management. One of the core tools used by every company in these industries is called the FMEA.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="the-fmea">The FMEA</h1>
<p>The Failure Modes and Effects Analysis, or FMEA, tells you how failures at a component level propagate to system failures. This document can get massively complex for a large system with many subsystems and components, such as an airplane.</p>
<p>So what's the motivation? Why are we going to all this effort to analyze a manufactured product from a risk perspective? A good FMEA tells a good story, and it does so in two key ways:</p>
<ul>
<li>The FMEA tells a top-down story to regulatory bodies</li>
<li>The FMEA allows engineers to tell a bottom-up story</li>
</ul>
<p>When a regulated device fails, the manufacturer has to explain to the regulatory body WHY it failed, and how they will fix it in the future. This is the top-down story. The FDA and FAA are examples of regulated bodies. All they know is that your device failed at the user level, and it's up to the manufacturer to guide them into the details of your device to identify the root cause of the failure, and ultimately fix it.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/Screenshot-from-2022-10-03-18-12-21.png" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="448" height="560"/></figure><!--kg-card-begin: markdown--><p>If a manufacturer is unable to provide a resolution, the device can be pulled from the market, either voluntarily by the manufacturer or as mandated by a regulatory body. A recent high-profile instance of this was the Boeing 737 MAX, which experienced a software failure in a program called MCAS (Maneuvering Characteristics Augmentation System) and ultimately caused 2 <a href="https://en.wikipedia.org/wiki/Maneuvering_Characteristics_Augmentation_System?ref=dept-engineering-blog.ghost.io">fatal accidents</a>.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="an-example">An Example</h1>
<h2 id="setup">Setup</h2>
<p>Airplanes and medical devices are very complicated systems, so let's use an example everyone is familiar with - a bicycle. Let's pretend that DEPT® is feeling particularly entrepreneurial and wants to get into the road bike space. Our task as engineers is to build a bicycle that is fast and safe. After all, if we want to sell our bike, we need to both convince customers that it's fast enough to win races, and safe enough to meet the minimum requirements imposed by the FBA (Federal Bicycle Administration), an imaginary entity that allows manufacturers to sell bicycles in the United States.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/image.png" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="535" height="201"/></figure><!--kg-card-begin: markdown--><h2 id="components-and-functions">Components and Functions</h2>
<p>The first step is to define the system architecture. Let's call this grouping of top-level entities <strong>Components</strong>. On our bicycle I’ll keep things simple and only analyze the <em>Drivetrain</em>, <em>Brakes</em>, and <em>Frame</em> for this example. Of course there are many other subsystems we could define and analyze as well, and we could even slice-and-dice the bike architecture in a different way depending on the goals of our analysis.</p>
<p>Some important questions to answer include:</p>
<ul>
<li><em>What is the boundary of analysis for each component?</em> This means, what sub-components of the bicycle are members of the drivetrain, which are components of the brake, and which are components of the frame?</li>
<li><em>What are the functions of each component?</em> What are some primary and secondary purposes of the drivetrain, the brakes, and the frame?</li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Let's build an actual FMEA for our bicycle. The primary function of the drivetrain is to move the bicycle forwards. In engineering terms we can state this as <em>Converting rotational motion into horizontal motion</em>. The primary function of the brakes is to bring the bicycle to a stop, which we can state as <em>Converting kinetic energy into heat</em>. Finally, the frame has two functions: <em>Supporting the cyclist</em> and <em>Supporting the components of the bicycle</em>.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h3 id="span-style-color-greencomponent-drivetrainspan"><span style="color: green">Component: Drivetrain</span></h3>
<p><span style="color: green"><strong>Function:</strong> Convert rotational motion into horizontal motion</span></p>
<h3 id="span-style-color-redcomponent-brakesspanh2"><span style="color: red">Component: Brakes</span></h3>
<p><span style="color: red"><strong>Function:</strong> Convert kinetic energy into heat</span></p>
<h3 id="span-style-color-bluecomponent-framespan"><span style="color: blue">Component: Frame</span></h3>
<p><span style="color: blue"><strong>Function:</strong> Support the cyclist</span><br>
<span style="color: blue"><strong>Function:</strong> Support the components of the bicycle</span></br></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="failure-modes">Failure Modes</h2>
<p>Ok great! So now that we have defined some components and each of their functions, let’s take a look at how these components can fail. (As a little sneak peak, we will be able to recursively define each subsequent level of analysis. In a later example, we will consider the Drivetrain as its own system, identify its Components, and so on.)</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>A convenient and standard way to define how a component might fail is by writing the Failure Mode as an anti-function. Generally, this would be written something like:</p>
<p>The <em>Component</em> fails to <em>deliver the function</em>.</p>
<p>For our drivetrain, we would say that <em>the Drivetrain fails to convert rotational motion into horizontal motion</em>. This might seem pretty obvious and redundant, but simple verbiage like this keeps things consistent across our risk analysis. This allows us to focus on why things are failing, and how to mitigate those failures.</p>
<p>Let’s use this same template to come up with failure modes for the Brakes and Frame. A way that our brakes can fail is by <em>failing to convert kinetic energy into heat</em>. Since our frame has two functions, we can write two failure modes, one for each function - <em>Frame fails to support cyclist</em>, and <em>Frame fails to support components</em>.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h3 id="span-style-color-greencomponent-drivetrainspan"><span style="color: green">Component: Drivetrain</span></h3>
<p><strong>Function:</strong> Convert rotational motion into horizontal motion<br>
<span style="margin-left: 40px;"><span style="color: green"><strong>Failure Mode</strong>: Drivetrain fails to convert rotational motion into horizontal motion</span></span></br></p>
<h3 id="span-style-color-redcomponent-brakesspanh2"><span style="color: red">Component: Brakes</span></h3>
<p><strong>Function:</strong> Convert kinetic energy into heat<br>
<span style="margin-left: 40px;"><span style="color: red"><strong>Failure Mode:</strong> Brakes fail to convert kinetic energy into heat</span></span></br></p>
<h3 id="span-style-color-bluecomponent-framespan"><span style="color: blue">Component: Frame</span></h3>
<p><strong>Function:</strong> Support the cyclist<br>
<span style="margin-left: 40px;"><span style="color: blue"><strong>Failure Mode:</strong> Frame fails to support cyclist</span></span><br>
<strong>Function:</strong> Support the components of the bicycle<br>
<span style="margin-left: 40px;"><span style="color: blue"><strong>Failure Mode:</strong> Frame fails to support components</span></span></br></br></br></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Great! We’ve come up with a bunch of ways that our bike can fail. But are these the only ways these components can fail? Of course not! There are a wide variety of ways anything can fail, and not just catastrophically as we seem to have defined here.</p>
<p>As a reminder, a failure mode is defined as <em>the manner in which a component fails to meet or deliver its intended function</em>. According to risk management theory, there are <a href="https://www.elsevier.com/books/safety-risk-management-for-medical-devices/elahi/978-0-323-85755-0?ref=dept-engineering-blog.ghost.io">6 main ways</a> most components can fail. The component:</p>
<ul>
<li>Has no ability to perform the function</li>
<li>Delivers only part of the function</li>
<li>Delivers excessive function</li>
<li>Delivers intermittent function</li>
<li>Delivers a degraded function</li>
<li>Delivers function at the wrong time</li>
</ul>
<p>Armed with this new information, let’s head back to our risk analysis. I think total brake failure is an important consideration, but not the only way our bicycle can fail to stop. Partial brake failure is pretty common and can be just as dangerous, so let’s incorporate that into our analysis.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h3 id="span-style-colorgreencomponent-drivetrainspan"><span style="color:green">Component: Drivetrain</span></h3>
<p><strong>Function:</strong> Convert rotational motion into horizontal motion<br>
<span style="margin-left: 40px;"><strong>Failure Mode</strong>: Drivetrain fails to convert rotational motion into horizontal motion</span></br></p>
<h3 id="span-style-colorredcomponent-brakesspanh2"><span style="color:red">Component: Brakes</span></h3>
<p><strong>Function:</strong> Convert kinetic energy into heat<br>
<span style="margin-left: 40px;"><span style="color: red"><strong>Failure Mode:</strong> Brakes COMPLETELY fail to convert kinetic energy into heat</span></span><br>
<span style="margin-left: 40px;"><span style="color: red"><strong>Failure Mode:</strong> Brakes PARTIALLY fail to convert kinetic energy into heat</span></span></br></br></p>
<h3 id="span-style-colorbluecomponent-framespan"><span style="color:blue">Component: Frame</span></h3>
<p><strong>Function:</strong> Support the cyclist<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Frame fails to support cyclist</span><br>
<strong>Function:</strong> Support the components of the bicycle<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Frame fails to support components</span></br></br></br></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>I’ve updated our existing failure mode to reflect total brake failure, and I’ve created a new one called <em>Brakes PARTIALLY fail to convert kinetic energy into heat.</em> We’ll find that when it comes time to mitigate these risks, we can address each individually with some specific mitigation plans.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Whew! We’ve gone through a lot so far, so let’s take a breather and let some of this sink in.</p>
<p>The reason we’re doing this analysis is to understand the risk profile of a bicycle more thoroughly. We are pretending that we are the manufacturer of the bicycle, and we need to first and foremost demonstrate that our bicycle is safe to operate in its intended use conditions. As competent mechanical engineers in the field of bicycle manufacturing, we will always strive to design a safe bicycle. As part of our competency, we need to understand which components of the bicycle have the highest likelihood of failing, and what the impacts of their failure are. So we do this kind of risk analysis to create an accurate risk profile and potentially introduce mitigation plans if we discover that a component introduces unacceptable levels of failure.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="causality">Causality</h2>
<p>The next step is to determine causality. Why are these failures occurring? In the real world you might systematically determine causality by:</p>
<ul>
<li>Determining the design deficiency that results in the failure mode</li>
<li>Determining a manufacturing process deficiency</li>
<li>Determining a user’s failure to perform a task</li>
</ul>
<p>These 3 span a wide variety of potential root causes, and it’s common to separate out an FMEA into design FMEAs, process FMEAs, and use case FMEAs in order to organize the risk analysis better. For our example, we’ll only focus on design deficiencies.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/image-1.png" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="835" height="350" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/10/image-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/image-1.png 835w" sizes="(min-width: 720px) 720px"/></figure><!--kg-card-begin: markdown--><p>Let's identify some causes of failure. A reason the drivetrain might fail to convert rotational motion into horizontal motion is because the <em>pedals detach</em>. Brake failure might occur because the <em>brake cable snaps</em>, or the <em>brake disc overheats</em>. Finally, the frame may fail because the <em>seat tube or head tube detaches</em>.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h3 id="span-style-color-greencomponent-drivetrainspan"><span style="color: green">Component: Drivetrain</span></h3>
<p><strong>Function:</strong> Convert rotational motion into horizontal motion<br>
<span style="margin-left: 40px;"><strong>Failure Mode</strong>: Drivetrain fails to convert rotational motion into horizontal motion</span><br>
<span style="margin-left: 80px;"><span style="color: green"><strong>Cause</strong>: Pedals detach</span></span></br></br></p>
<h3 id="span-style-color-redcomponent-brakesspanh2"><span style="color: red">Component: Brakes</span></h3>
<p><strong>Function:</strong> Convert kinetic energy into heat<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Brakes COMPLETELY fail to convert kinetic energy into heat</span><br>
<span style="margin-left: 80px;"><span style="color: red"><strong>Cause</strong>: Brake cable snaps</span></span><br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Brakes PARTIALLY fail to convert kinetic energy into heat</span><br>
<span style="margin-left: 80px;"><span style="color: red"><strong>Cause</strong>: Brake disc overheats</span></span></br></br></br></br></p>
<h3 id="span-style-color-bluecomponent-framespan"><span style="color: blue">Component: Frame</span></h3>
<p><strong>Function:</strong> Support the cyclist<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Frame fails to support cyclist</span><br>
<span style="margin-left: 80px;"><span style="color: blue"><strong>Cause</strong>: Seat tube detaches</span></span><br>
<strong>Function:</strong> Support the components of the bicycle<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Frame fails to support components</span><br>
<span style="margin-left: 80px;"><span style="color: blue"><strong>Cause</strong>: Head tube detaches</span></span></br></br></br></br></br></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="effects-of-failure">Effects of Failure</h2>
<p>We're almost there! The last step is to identify the <strong>effect</strong> of each of our failure modes. These failure modes are once again written generically, because once we get deeper into this analysis, we will want to make it easy to connect multiple failure modes to the same end effect. This will allow us to make interesting insights at the end of the analysis about how many end effects are the results of certain types of failures.</p>
<p>For now though, for the drivetrain component, the effect of being unable to convert rotational motion into horizontal motion is that the <em>cyclist cannot accelerate the bicycle</em>.</p>
<p>For the brakes, the effect of being unable to completely convert kinetic energy into heat is that the <em>cyclist cannot decelerate the bicycle</em>. The effect of partially converting kinetic energy into heat is that the <em>cyclist's ability to decelerate the bicycle is reduced</em>.</p>
<p>Finally, for the frame, the effect of being unable to support the cyclist is that the <em>cyclist falls from the bicycle</em>. The effect of being unable to support the components is that the <em>components detach from the bicycle</em>.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h3 id="span-style-color-greencomponent-drivetrainspan"><span style="color: green">Component: Drivetrain</span></h3>
<p><strong>Function:</strong> Convert rotational motion into horizontal motion<br>
<span style="margin-left: 40px;"><strong>Failure Mode</strong>: Drivetrain fails to convert rotational motion into horizontal motion</span><br>
<span style="margin-left: 80px;"><strong>Cause</strong>: Pedals detach</span><br>
<span style="margin-left: 80px;"><span style="color: green"><strong>Effect</strong>: Cyclist cannot accelerate bicycle</span></span></br></br></br></p>
<h3 id="span-style-color-redcomponent-brakesspanh2"><span style="color: red">Component: Brakes</span></h3>
<p><strong>Function:</strong> Convert kinetic energy into heat<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Brakes COMPLETELY fail to convert kinetic energy into heat</span><br>
<span style="margin-left: 80px;"><strong>Cause</strong>: Brake cable snaps</span><br>
<span style="margin-left: 80px;"><span style="color: red"><strong>Effect</strong>: Cyclist cannot decelerate bicycle</span></span><br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Brakes PARTIALLY fail to convert kinetic energy into heat</span><br>
<span style="margin-left: 80px;"><strong>Cause</strong>: Brake disc overheats</span><br>
<span style="margin-left: 80px;"><span style="color: red"><strong>Effect</strong>: Cyclist's ability to decelerate bicycle is reduced</span></span></br></br></br></br></br></br></p>
<h3 id="span-style-color-bluecomponent-framespan"><span style="color: blue">Component: Frame</span></h3>
<p><strong>Function:</strong> Support the cyclist<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Frame fails to support cyclist</span><br>
<span style="margin-left: 80px;"><strong>Cause</strong>: Seat tube detaches</span><br>
<span style="margin-left: 80px;"><span style="color: blue"><strong>Effect</strong>: Cyclist falls from bicycle</span></span><br>
<strong>Function:</strong> Support the components of the bicycle<br>
<span style="margin-left: 40px;"><strong>Failure Mode:</strong> Frame fails to support components</span><br>
<span style="margin-left: 80px;"><strong>Cause</strong>: Head tube detaches</span><br>
<span style="margin-left: 80px;"><span style="color: blue"><strong>Effect</strong>: Components detach from bicycle</span></span></br></br></br></br></br></br></br></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="subsystem-dfmea-and-mitigations">Subsystem DFMEA and Mitigations</h2>
<p>We made it! Great job for sticking through that.  We slogged our way through a system DFMEA and made a pretty basic, but useful framework to run with and expand upon. Before we wrap up, let's take a look at a completed subsystem DFMEA so we can see how it has its own recursive definition of all these elements as well. We will focus on the <em>Pedals</em> sub-component here.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><span style="color: green"><strong>Drivetrain Component</strong></span><br>
<strong>Sub-Component:</strong> Pedals<br>
<span style="margin-left: 40px"><strong>Function</strong>: Translate force of legs to rotational motion of crankset</span><br>
<span style="margin-left: 80px"><strong>Failure Mode:</strong> Pedal detaches from mounting point</span><br>
<span style="margin-left: 120px"><strong>Cause:</strong> Precession causes pedals to loosen </span><br>
<span style="margin-left: 120px"><strong>Effect:</strong>   Drivetrain fails to convert rotational motion into horizontal motion</span><br>
<span style="margin-left: 120px"><strong>Mitigation:</strong> Reverse-threaded pedal mount</span><br>
<span style="color: lightgrey"><br>
<strong>Sub-Component:</strong> Chain<br>
<span style="margin-left: 40px"><em>Function</em>: Translate force from crankset to force in sprocket</span><br>
<span style="margin-left: 80px"><strong>Failure Mode:</strong> Chain snaps</span><br>
<span style="margin-left: 120px"><strong>Cause:</strong> Coarse particles wear down links</span><br>
<span style="margin-left: 120px"><strong>Effect:</strong> Drivetrain fails to convert rotational motion into horizontal motion</span><br>
<span style="margin-left: 120px"><strong>Mitigation:</strong> [none]</span><br>
<strong>Sub-Component:</strong> Crankset<br>
<span style="margin-left: 40px"><em>Function</em>: Translate rotational motion of pedals to linear motion of chain</span><br>
<span style="margin-left: 80px"><strong>Failure Mode:</strong> Crank arm detaches from mounting point</span><br>
<span style="margin-left: 120px"><strong>Cause:</strong> Precession causes crankset to loosen</span><br>
<span style="margin-left: 120px"><strong>Effect:</strong> Drivetrain fails to convert rotational motion into horizontal motion</span><br>
<span style="margin-left: 120px"><strong>Mitigation:</strong> Reverse-threaded crank mount</span><br>
<strong>Sub-Component:</strong> Rear sprocket<br>
<span style="margin-left: 40px"><em>Function</em>: Translate linear motion of chain to rotational motion of wheel</span><br>
<span style="margin-left: 80px"><strong>Failure Mode:</strong> Sprocket teeth wear down</span><br>
<span style="margin-left: 120px"><strong>Cause:</strong> Coarse particles wear down teeth</span><br>
<span style="margin-left: 120px"><strong>Effect:</strong> Drivetrain fails to convert rotational motion into horizontal motion</span><br>
<span style="margin-left: 120px"><strong>Mitigation:</strong> Add holes to sprocket to better retain grease</span><br>
</br></br></br></br></br></br></br></br></br></br></br></br></br></br></br></br></br></br></br></span></br></br></br></br></br></br></br></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Recall that a good FMEA should tell a good story. In this case, the story being told is that we have a sub-component called the pedals, whose primary function is to translate the force of the legs pushing down on them into rotational motion in the crankset. One way this function can fail to occur is if the pedal detaches from its mounting point on the crank arm. A reason this might occur is because of precession, which is a phenomenon by which rotational motion causes a screwed object to unscrew itself from its mounting point. Fortunately, a very simple way to prevent this from occuring is to reverse-thread one of the pedal mounts. That way, when the cyclist pedals, they tighten the pedal against the crank arm, as opposed to loosening it.</p>
<p>Of course this is just an example, and any competent bike manufacturer will include this feature from the start. But here at DEPT®, we're new to bikes and learning along the way! Good thing we did some risk analysis to figure this out ahead of time.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="summary">Summary</h1>
<p>With this exercise, we have developed a rudimentary FMEA, but more importantly we have developed <em>a framework to think about any engineering project in terms of risk</em>. This can be a very powerful tool especially in software.</p>
<p>Remember that your code base often contains a LOT of code that no one on your team wrote. This can be in the form of NPM modules or other ecosystem dependencies. In the biz, this is sometimes called Software of Unknown Provenance, or <strong>SOUP</strong>. It's just as important to understand what's in the SOUP as it is to understand what's in your own code.</p>
<p>A great interface document for understanding what's in the SOUP that might make you sick is an FMEA made by the provisioner of the SOUP. When that's not available, or another equivalent that describes potential failure points in the code, then you really ought to be checking out what's in those dependencies to make sure you're not introducing unknown amounts of risk into your project. It is common for dependency developers to provide an automated test suite, which is helpful to study in order to identify any blindspots in their testing that might impact the behavior of your program.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="whats-next">What's Next?</h1>
<p>Now that we have created a rudimentary FMEA, what's next?</p>
<ul>
<li>We should introduce a mathematical framework that allows us to compute probabilities of failure and severities of effects.</li>
<li>We should also organize our efforts to burn down risk by focusing on components with the highest probabilities of failure and/or severities of effects</li>
</ul>
<p>By introducing some math, we can start to visualize our risk burndown in much more interesting ways.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/SankeyMatic-Risk-Management-1.png" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="1600" height="1600" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/10/SankeyMatic-Risk-Management-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/10/SankeyMatic-Risk-Management-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/SankeyMatic-Risk-Management-1.png 1600w" sizes="(min-width: 720px) 720px"/></figure><!--kg-card-begin: markdown--><p>For example, here is a <a href="https://en.wikipedia.org/wiki/Sankey_diagram?ref=dept-engineering-blog.ghost.io">Sankey diagram</a> I created. This type of chart is called a flow diagram, where the width of each connection represents a vaguely-defined &quot;riskiness&quot; measure that I've just made up. You could think of this as some sort of product of <code>probability of failure</code> x <code>severity of failure</code>, broken down by component. In this naive example, I'm assuming that every component contributes equally to this made-up &quot;riskiness&quot; measure.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/SankeyMatic-Risk-Management-2.png" class="kg-image" alt="A risk management framework for software projects" loading="lazy" width="1600" height="1600" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/10/SankeyMatic-Risk-Management-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/10/SankeyMatic-Risk-Management-2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/10/SankeyMatic-Risk-Management-2.png 1600w" sizes="(min-width: 720px) 720px"/></figure><!--kg-card-begin: markdown--><p>But by creating a mathematical framework for our components to interact with, we can start to come up with a more interesting diagram. This diagram can now tell us which particular components and sub-components contribute the most to our overall risk profile. With this information, we can decide where our efforts are best directed in order to reduce the overall risk profile of the project.</p>
<p>For example, here at DEPT® Bicycle Industries (a made up company), we <em>know</em> what we're doing when it comes to drivetrains and brakes. That's old hat to us. But bicycle <em>frames</em>? That's new, and difficult. There's a lot of risk involved in our current techniques. So we have some extra work to do to burn down the risk associated with our brittle bicycle frames.</p>
<p>And we can clearly see that in the above diagram! The bicycle frame component has an overall risk value of 150, representing nearly half of the risk associated with our bike in total. Program managers can use this information to better allocate some extra resources to the parts of the project with the most risk.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="conclusion">Conclusion</h1>
<p>Understanding the full risk profile of your project can be extremely lengthy. In regulated industries there are entire groups dedicated to this single task. Depending on the type of development you're doing, it's probably not necessary to construct an FMEA for <em>every</em> single component of your project.</p>
<p>Perhaps on the next project you start, you could take some time up front to identify a few critical components of your application that might benefit from risk analysis. You will find that:</p>
<ul>
<li>You understand your code base in more detail than before</li>
<li>You're able to communicate how long a bug fix might take with more accuracy</li>
<li>You're able to identify future issues before they manifest themselves in a production environment</li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Union and Intersection Typeguards in TS]]></title><description><![CDATA[If you’ve ever been interested in complex typings in TypeScript, particularly when it comes to typeguards, this is the article for you!]]></description><link>https://engineering.deptagency.com/union-and-intersection-typeguards-in-ts/</link><guid isPermaLink="false">62a73c6862612a003d67220c</guid><category><![CDATA[Platforms]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Richard Coates]]></dc:creator><pubDate>Fri, 07 Oct 2022 11:50:42 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Header.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Header.jpeg" alt="Union and Intersection Typeguards in TS"/><p>If you’ve ever been interested in complex typings in TypeScript, particularly when it comes to typeguards, this is the article for you! At BYTE/DEPT®, we’ve been developing our open-source in-house typeguards library for TypeScript (found <a href="https://github.com/ByteLondon/byteguards?ref=dept-engineering-blog.ghost.io">here</a>) for a few years, and we’ve recently dedicated some time to cleaning up some of the more complex typeguards that we use (for intersections and unions of types). This article explains some of the techniques that we’ve used, in the hope that understanding these TypeScript patterns in practice might assist with your TypeScript requirements!</p><p>Having started my development journey with PHP, Ken Kantzer’s recent <a href="https://kenkantzer.com/learnings-from-5-years-of-tech-startup-code-audits/?ref=dept-engineering-blog.ghost.io">article</a> on security struck a chord with me, particularly where he talks about trusted and un-trusted data at runtime boundaries. Since moving to the TypeScript ecosystem, I’ve loved the approach adopted at BYTE/DEPT® to have type validations at all runtime boundaries, much like those described in <a href="https://web.archive.org/web/20190204094636/https://lorefnon.tech/2018/03/25/typescript-and-validations-at-runtime-boundaries/">this article from 2018</a>. Not only does this make everything much more secure, it also ensures that you can leverage all of the best parts of TypeScript’s type-checking inside your application!</p><h3 id="typeguards">Typeguards</h3><p>Because TypeScript doesn’t have runtime type-checking, only compile time, there is no way to inherently ensure that data that comes from a ‘dirty’ source (API, Database etc) is structured as expected just by writing typed functions. This can lead to dreaded <code>TypeError</code>s at runtime. As a partial solution, TypeScript allows us to write so-called "type predicates": functions that have the signature <code>x is y</code>, which allow us to indicate to the compiler that "if an input <code>x</code> returns true in this function, it is of type <code>y</code>". This is very powerful, because it allows us to write tightly-typed code, and be confident that at runtime, there will be no <code>TypeError</code>s, even when we are dealing with data from a ‘dirty’ source.</p><h3 id="guards-in-action-at-bytedept%C2%AE">Guards in Action at BYTE/DEPT®</h3><p>In our library, there is a generic <code>Is</code> function type that takes the following form:</p><pre><code>type Is&lt;A&gt; = (a: unknown) =&gt; a is A
</code></pre><p>This allows me to write composable typeguards of the form</p><pre><code>const isString: Is&lt;string&gt; = (u: unknown): u is string =&gt; typeof u === 'string'
const isNumber: Is&lt;number&gt; = (u: unknown): u is number =&gt; typeof u === 'number'
</code></pre><p>There are also similar typeguards for records, structs, arrays and so on. For example, the array one curries a typeguard for the elements, and returns a type predicate.</p><pre><code>const isArray = &lt;A&gt;(isa: Is&lt;A&gt;) =&gt; (u: unknown): u is A[] =&gt; Array.isArray(u) &amp;&amp; u.every(isa)
</code></pre><p>For example:</p><pre><code>const isArrayOfStrings: Is&lt;Array&lt;string&gt;&gt; = isArray(isString)</code></pre><p>The guard used for objects similarly curries a definition, though the underlying JavaScript means that the function is a lot less clean!</p><pre><code>export const isStruct = &lt;O extends { [key: string]: unknown }&gt;(isas: { [K in keyof O]: Is&lt;O[K]&gt; }): Is&lt;O&gt; =&gt; (
  o
): o is O =&gt; {
  if (o === null || typeof o !== 'object') return false
  const a = o as any
  for (const k of Object.getOwnPropertyNames(isas)) {
    if (!isas[k](a[k])){
      return false
    }
  }
  return true
}
</code></pre><p>For example:</p><pre><code>const isFoo: Is&lt;{foo: string}&gt; = isStruct({foo: isString})</code></pre><p>This works well for basic types, when you know the structure that an object will take, but sometimes there is ambiguity: you might want to create a typeguard for an array that takes strings, numbers or booleans, or even a typeguard for a type that is made up of an overlap of several complex objects. How do we write a typeguard for that? What form does the type predicate take?</p><h3 id="union-typeguards-a-or-b">Union Typeguards: A OR B</h3><p>Until recently, we had a very basic overloaded isUnion function:</p><pre><code>export function isUnion&lt;A, B&gt;(isA: Is&lt;A&gt;, isB: Is&lt;B&gt;): (u: unknown) =&gt; u is A | B
export function isUnion&lt;A, B, C&gt;(isA: Is&lt;A&gt;, isB: Is&lt;B&gt;, isC: Is&lt;C&gt;): (u: unknown) =&gt; u is A | B | C
export function isUnion&lt;A, B, C, D&gt;(isA: Is&lt;A&gt;, isB: Is&lt;B&gt;, isC: Is&lt;C&gt;, isD: Is&lt;D&gt;): (u: unknown) =&gt; u is A | B | C | D
export function isUnion&lt;A, B, C, D, E&gt;(
  isA: Is&lt;A&gt;,
  isB: Is&lt;B&gt;,
  isC: Is&lt;C&gt;,
  isD: Is&lt;D&gt;,
  isE: Is&lt;E&gt;
): (u: unknown) =&gt; u is A | B | C | D | E
export function isUnion&lt;A, B, C, D, E, F&gt;(
  isA: Is&lt;A&gt;,
  isB: Is&lt;B&gt;,
  isC: Is&lt;C&gt;,
  isD: Is&lt;D&gt;,
  isE: Is&lt;E&gt;,
  isF: Is&lt;F&gt;
): (u: unknown) =&gt; u is A | B | C | D | E | F
export function isUnion&lt;A, B, C, D, E, F&gt;(isA: Is&lt;A&gt;, isB: Is&lt;B&gt;, isC?: Is&lt;C&gt;, isD?: Is&lt;D&gt;, isE?: Is&lt;E&gt;, isF?: Is&lt;F&gt;) {
  return (u: unknown): u is A | B | C | D | E | F =&gt;
    isA(u) || isB(u) || (isC &amp;&amp; isC(u)) || (isD &amp;&amp; isD(u)) || (isE &amp;&amp; isE(u)) || (isF &amp;&amp; isF(u)) || false
}
</code></pre><p>This allowed us to write something like:</p><pre><code>const isNumberOrString: Is&lt;number | string&gt; = isUnion(isNumber, isString)</code></pre><p>From the function signature it quickly becomes apparent that this falls down eventually: if you wanted to have more than six typeguards added to this, you needed to nest isUnion typeguards, which wasn’t great from a code smell point of view, nor for debugging stack traces. But how could you write a type predicate without knowing how many types you were predicating?!</p><p>The answer came to one of our engineers while fixing another problem, and we are indebted to the legendary <em><strong>jcalz</strong></em> on StackOverflow, for his exceptionally slick answers to a myriad of TypeScript questions that have been asked over the years. The Typescript community really is great! The answer we came up with was this:</p><pre><code class="language-typescript">type UnionTupleElements&lt;T extends any[]&gt; = T extends Array&lt;Is&lt;infer U&gt;&gt; ? U : never

export function isUnion&lt;T extends any[]&gt;(
  ...allTypeGuards: T) {
  return (u: unknown): u is UnionTupleElements&lt;T&gt; =&gt; allTypeGuards.some((isT) =&gt; isT(u))
}
</code></pre><p>Let’s break this down. What we are doing here is making TypeScript, at compile time, create a type that is the union of the provided types, which we can then use in the predicate. How do we do this?<br><code>UnionTupleElements</code>, and in particular the <code>infer U</code> part, is basically saying: <em>assume that there was a type for which <strong>any</strong> of the provided typeguards would be typeguards</em>, what would the definition of that type be? If there is an answer, then return it. If there is not, return <code>never</code>. Now that we have that type, we can use it in a type predicate. Note that this is a slightly more secure version of <code>u is T[number]</code>, since you are using TypeScript to extract the types themselves from the array of typeguards, and so if there were a scenario where there could by definition be no overlap, an <code>never</code> will be thrown at compile time.</br></p><h3 id="intersection-types-a-and-b">Intersection Types: A AND B</h3><p>It gets much harder when we’re thinking about an intersection. We started with a similarly blunt tool that would allow us to create a typeguard where an input would have to match <u>all</u> of the guards in order to satisfy the type predicate:</p><pre><code class="language-typescript">export function isIntersection&lt;A, B&gt;(isA: Is&lt;A&gt;, isB: Is&lt;B&gt;): (u: unknown) =&gt; u is A &amp; B
export function isIntersection&lt;A, B, C&gt;(isA: Is&lt;A&gt;, isB: Is&lt;B&gt;, isC: Is&lt;C&gt;): (u: unknown) =&gt; u is A &amp; B &amp; C
export function isIntersection&lt;A, B, C&gt;(isA: Is&lt;A&gt;, isB: Is&lt;B&gt;, isC?: Is&lt;C&gt;) {
  return (u: unknown): u is A &amp; B &amp; C =&gt; isA(u) &amp;&amp; isB(u) &amp;&amp; (!isC || isC(u))
}
</code></pre><p>For example:</p><pre><code class="language-typescript">const isFooAndBar: Is&lt;{foo: string} &amp; {bar: string}&gt; = isIntersection(isStruct({foo: isString}), isStruct({bar: isString}))</code></pre><p>But again, this maxed out at a set of three typeguards. After a long time of fiddling with tuple manipulation, we came up with this:</p><pre><code class="language-typescript">type IntersectTupleElements&lt;T extends any[]&gt; = { [I in keyof T]: (x: T[I]) =&gt; void }[number] extends (x: infer I) =&gt; void ? I : never;

export function isIntersection&lt;T extends any[]&gt;(...args: { [I in keyof T]: Is&lt;T[I]&gt; }): Is&lt;IntersectTupleElements&lt;T&gt;&gt; {
  return (u: unknown): u is IntersectTupleElements&lt;T&gt; =&gt; args.every(isX =&gt; isX(u))
}</code></pre><p>Let’s break this one down: we are basically asking Typescript to take the array of Typeguards, derive the type for which they are guarding (<code>&lt;T extends any[]&gt;(...args: { [I in keyof T]: Is&lt;T[I]&gt; })</code>) and treat that as a tuple, T. From there, we can use the combination of distributive conditional types and inference from conditional types to reverse engineer the type for the predicate from that Tuple. I like to think of it as asking TypeScript to do a thought experiment: <em>You have this collection of types, imagine that there was a hypothetical type that was all of these types? What would that type look like?</em><br>To do this we make a function <code>(x: I) =&gt; void</code>, and infer the type I as the type with which <em>all</em> of the types in the tuple T intersect (and hence will pass the typeguards). The TS documentation puts it as:</br></p><blockquote>“multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred”.</blockquote><p>Without going into too much detail about what covariance and contravariance are (though I love <a href="https://news.ycombinator.com/item?id=14649302&ref=dept-engineering-blog.ghost.io">this explanation</a>!), it’s basically saying that by reversing engineering “what would the type be that would satisfy all of these guards?” from “does this variable satisfy all of these guards?”, we can get the intersection type, and then use that in our definition. If, as with the union type, there is no type that could satisfy such a definition (for example <code>string &amp; number</code>), then it will return <code>never</code>, which is helpful.</p><h3 id="conclusion">Conclusion</h3><p>It’s fair to say that if you’re not familiar with TypeScript, this might take some wrapping your head around. In fact, even if you’re intimately familiar with TypeScript, this might take some wrapping your head around! Even so, understanding why these signatures work is a great exercise in both CS and TypeScript, and has been a great levelling-up journey for me!</p>]]></content:encoded></item><item><title><![CDATA[Why I built a new state management library]]></title><description><![CDATA[I know what you're thinking because that's my reaction when I hear something like this, so I get it. But it's not like that, I swear.]]></description><link>https://engineering.deptagency.com/why-i-built-a-new-state-management-library/</link><guid isPermaLink="false">633662ae65c108003db9608e</guid><category><![CDATA[Platforms]]></category><category><![CDATA[state management]]></category><dc:creator><![CDATA[Ethan Standel]]></dc:creator><pubDate>Mon, 03 Oct 2022 14:00:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/09/yifu-wu-L3gq2u2H6Us-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h2 id="okay-i-know-what-youre-thinking">Okay, I know what you're thinking...</h2>
<blockquote>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/09/yifu-wu-L3gq2u2H6Us-unsplash.jpg" alt="Why I built a new state management library"/><p>Why another state management library? Haven't we all suffered enough having to learn Redux &amp; MobX &amp; Zustand &amp; Jotai &amp; Recoil &amp; Valtio &amp; XState? What possible use case or object pattern or mental model hasn't been covered yet? How could you be so arrogant as to think you could create something that's better than all of those?<br><br>
- You, probably</br></br></p>
</blockquote>
<p>I know because that's my reaction when I hear something like this, so I get it. But it's not like that, I swear. I was filling a gap in the market.</p>
<h2 id="the-introduction-of-the-preact-signal-primitive">The introduction of the Preact Signal primitive</h2>
<p>In early September, the Preact team made the <a href="https://preactjs.com/blog/introducing-signals/?ref=dept-engineering-blog.ghost.io">announcement</a> that they were officially releasing a new state primitive.</p>
<details style="margin-bottom: 1em">
<summary>
    <h3 id="wait-whats-preact-again" style="display:inline; cursor: pointer;">Wait, what's Preact again?</h3>
</summary>
    <p>Preact is a library that is designed to be a faster <em>implementation</em> of React. It fulfills all the same core APIs as are exposed by React but in a more lightweight fashion.</p>
<p>Because this is the web, one of the first things you should do to get faster is get smaller. The <code>preact</code> package currently claims to bundle down to only 3kb of JS to function, whereas <code>react</code> + <code>react-dom</code> requires upwards of 100kb. Where React is built to be deployed with any of several rendering engines, most notably <code>react-dom</code> & <code>react-native</code> (and more recently <code>@react-three/fiber</code>), Preact is just built for the web and that allows it to optimize strictly for that task.</p>
<p>It's worth noting however that Preact has always had several projects associated with it that are outside of the bounds of just being "P(erformant) React." However, its decision to closely support the React ecosystem has allowed it to grow to be one of the most popular modern frameworks at about 1.5 million installs weekly, at the time of this writing.</p>
</details>
<p>The new state primitive that they were supporting for both Preact &amp; React is the &quot;signal&quot; model. The signal model is designed to be a reactive observable for atomic state. So that is to say it's a container for state which has no substate, that can be listened to and written to. If you're familiar with React, then you might just say &quot;<em>that just sounds like <code>useState</code></em>,&quot; and if you say that then you're kind of right.</p>
<p>The <code>useState</code> hook accepts an initial atomic state, and returns a way to get the current state and update the state. However, when the state declared from a <code>useState</code> is updated, the component that the state was initialized in, as well as anything in its subtree that's not memoized, must be rerun. That's a big part of how React works. All state changes force the section of the application tree where the state was declared rerun and build up the virtual DOM (or VDOM). At that point React identifies the differences between the currently rendered real DOM and the latest VDOM, in a process called <em>diffing</em>. It then cherry picks the things it needs to update in a process called <em>reconciliation</em>.</p>
<p>Preact also works this way, going through all of these steps for every new state update, but the new signal primitive provides an escape hatch from this behavior. Instead of binding direct values into the DOM that must be recalculated &amp; reconciled, a signal will allow you to bind the signal itself (as opposed to the value) to the DOM which acts as a self-updating state container. So that gives you code that looks like this.</p>
<pre><code class="language-tsx">import { useSignal } from &quot;@preact/signals&quot;;

const Counter = () =&gt; {
  const count = useSignal(0);
  console.log(&quot;Counter rendered&quot;);

  return (
    &lt;&gt;
      &lt;div&gt;{count}&lt;/div&gt;
      &lt;button onClick={() =&gt; count.value++}&gt;
        Increment
      &lt;/button&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>In this example, you can see that the underlying value property can be written to directly by just reassigning it like a normal variable. But what's bound as the children of the <code>div</code> is not the value, but the signal itself. The advantage this gives you against traditional React &amp; Preact code is that no matter how many times you click the button to update that state, <code>Counter rendered</code> will only log once because the signal is bound to the DOM and the component isn't listening to it.</p>
<p>This makes state updates far more performant and scale much better because they get to skip VDOM construction, diffing, and reconciliation. And if you are in a situation where you need the signal to act more like a VDOM, then if you bind the <code>value</code> of a signal into the VDOM then that component will subscribe to the signal just like <code>useState</code>! So it also has fallback behavior to continue to support the React ecosystem in full.</p>
<pre><code class="language-tsx">import { Input, Button } from &quot;@mui/material&quot;;
import { signal } from &quot;@preact/signals&quot;;

const field = signal(&quot;&quot;);

const Form = () =&gt; {
  const onSubmit = (e: Event) =&gt; {
    e.preventDefault();
    alert(`Submitted value: ${field.peek()}`);
  }

  return (
    &lt;form onSubmit={onSubmit}&gt;
      &lt;Input value={field.value} onInput={e =&gt; field.value = e.target.value} /&gt;
      &lt;Button type=&quot;submit&quot;&gt;Submit&lt;/Button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>In the code above, for example, the <code>@mui/material</code> package is built for React but is fully compatible with Preact applications. Because this is a React package, it expects that the way data is managed around it follows a VDOM model. By binding <code>input.value</code> to the <code>value</code> prop in the <code>&lt;Input /&gt;</code> component, the whole <code>Form</code> component is now subscribed to changes to the <code>input</code> signal, as if it were a regular <code>useState</code>. So when the <code>onInput</code> event (which is equivalent to React's <code>onChange</code> event) fires, <code>Form</code> will rerun like a traditional VDOM driven component.</p>
<p>So Preact made a faster state container which is cool, but it actually gets better than that. The signal primitive isn't inherently stuck in components like <code>useState</code>. You can initialize it globally and it acts exactly the same!</p>
<pre><code class="language-tsx">import { signal } from &quot;@preact/signals&quot;;

const count = signal();

const Counter = () =&gt; (
  &lt;&gt;
    &lt;div&gt;{count}&lt;/div&gt;
    &lt;button onClick={() =&gt; count.value++}&gt;
      Increment
    &lt;/button&gt;
  &lt;/&gt;
);
</code></pre>
<p>So suddenly, the new primitive fulfills a faster version of an old API and it allows for global atomic state. There's a lot of people who are pulling in a library like Jotai or Recoil to do this in React, but Preact now offers it as an officially supported primitive. Oh and there's also a <a href="https://www.npmjs.com/package/@preact/signals-react?ref=dept-engineering-blog.ghost.io">React package</a> too, but it does <a href="https://github.com/preactjs/signals/blob/2c6e30d1ba74e64d1ffa4cb52e0321fe45450dfb/packages/react/src/index.ts?ref=dept-engineering-blog.ghost.io#L27">some things</a> that a lot of people might consider worrisome for long term stability.</p>
<h2 id="the-gap-in-the-market">The gap in the market</h2>
<p>I was initially really excited about signals, but the thing I couldn't get over was that it was for atomic state only, so no substates. This design model felt like a golden opportunity for a highly performant full state management system, because it means that large state updates only have to update <strong>exactly</strong> what is required of them. This kind of fine-grained updating is exactly what led Redux to offer a <code>useSelector</code> hook, and for Zustand to offer a selector function as the argument when calling <code>useStore</code>. They were trying to encourage developers to only have to rely on the data that they need when pulling data out of state, but the fine-grained reactivity of being able to avoid the VDOM entirely can't really be matched for performance.</p>
<h2 id="initial-implementation">Initial implementation</h2>
<p>It seemed like nobody had really jumped on this yet, so I took the opportunity! My initial mental model was very simplistic. There would be two functions <code>store</code> and <code>destore</code>. The <code>store</code> function would take in an object and convert all of its deeply nested atomic values into signals. So very simply this code...</p>
<pre><code class="language-tsx">import { store } from &quot;preact-signal-store&quot;;

const userStore = store({
  name: {
    first: &quot;Thor&quot;,
    last: &quot;Odinson&quot;
  },
  email: &quot;thor@avengers.org&quot;
});
</code></pre>
<p>was equivalent to this code...</p>
<pre><code class="language-tsx">import { signal } from &quot;@preact/signals&quot;;

const userStore = {
  name: {
    first: signal(&quot;Thor&quot;),
    last: signal(&quot;Odinson&quot;)
  },
  email: signal(&quot;thor@avengers.org&quot;)
};
</code></pre>
<p>and this code...</p>
<pre><code class="language-tsx">import { destore, store } from &quot;preact-signal-store&quot;;

const userStore = destore(
  store({
    name: {
      first: &quot;Thor&quot;,
      last: &quot;Odinson&quot;
    },
    email: &quot;thor@avengers.org&quot;
  })
);
</code></pre>
<p>is equivalent to this code...</p>
<pre><code class="language-tsx">const userStore = {
  name: {
    first: &quot;Thor&quot;,
    last: &quot;Odinson&quot;
  },
  email: &quot;thor@avengers.org&quot;
};
</code></pre>
<p>So what I had created was a method of going into an object and finding every atomic property and turning it into a signal, as well as the ability to turn all of those signals back into their underlying values. But this, to me, still felt more like a valuable utility than a full state management system.</p>
<h2 id="a-new-primitive-for-substate">A new primitive <em>for substate</em></h2>
<p>After some criticism of language &amp; titles in the library from another developer, the idea came to me that I could essentially give these &quot;stores&quot; the same API as signals already offer. I had all the tools available, but I just had to place them into a sensible object model. So if a <code>Signal</code> is a holder of an atomic state, then it seemed natural to call a holder of substates a <code>DeepSignal</code>. So for version 2.0 of the library, there is just one main export, the <code>deepSignal</code> function. So now this code...</p>
<pre><code class="language-tsx">import { deepSignal } from &quot;preact-signal-store&quot;;

const userStore = deepSignal({
  name: {
    first: &quot;Thor&quot;,
    last: &quot;Odinson&quot;
  },
  email: &quot;thor@avengers.org&quot;
});
</code></pre>
<p>is equivalent to this code...</p>
<pre><code class="language-tsx">import { signal, batch } from &quot;@preact/signals&quot;;

const userStore = {
  name: {
    first: signal(&quot;Thor&quot;),
    last: signal(&quot;Odinson&quot;),
    get value(): { first: string, last: string } {
      return {
        first: this.first.value,
        last: this.last.value
      }
    },
    set value(payload: { first: string, last: string }) {
      batch(() =&gt; {
        this.first.value = payload.first;
        this.last.value = payload.last;
      });
    },
    peek(): { first: string, last: string } {
      return {
        first: this.first.peek(),
        last: this.last.peek()
      }
    },
  },
  email: signal(&quot;thor@avengers.org&quot;),
  get value(): { name: { first: string, last: string }, email: string } {
    return {
      name: {
        first: this.name.first.value,
        last: this.name.last.value
      },
      email: this.email.value
    }
  },
  set value(payload: { name: { first: string, last: string }, email: string }) {
    batch(() =&gt; {
      this.name.first.value = payload.name.first;
      this.name.last.value = payload.name.last;
      this.email.value = payload.email;
    });
  },
  peek(): { name: { first: string, last: string }, email: string } {
    return {
      name: {
        first: this.name.first.peek(),
        last: this.name.last.peek()
      },
      email: this.email.peek()
    }
  },
};
</code></pre>
<p>So this now builds the original concept of <code>destore</code> and <code>store</code> into one recursive model. The advantage that this model provides to developers is that to the greatest extent possible, you no longer have to worry about things like where to put commonly updated state subscriptions or how to construct the most optimal selector function. Now you just take data straight off a static <em>looking</em> object and you place it where you feel it should be placed.</p>
<p>And just as <code>@preact/signals</code> exports both <code>signal</code> and <code>useSignal</code>, <code>preact-signal-store</code> now exports <code>deepSignal</code> and <code>useDeepSignal</code>. This makes for a version of state that's far more like class components with only a single object for all states in a component. However, it continues to maintain the performance advantages of signals.</p>
<pre><code class="language-tsx">import { useDeepSignal } from &quot;preact-signal-store&quot;;

const UserRegistrationForm = () =&gt; {
  const { form, submitting } = useDeepSignal(() =&gt; ({
    form: {
      name: {
        first: &quot;&quot;,
        last: &quot;&quot;
      },
      email: &quot;&quot;
    },
    submitting: false
  }));

  const submitRegistration = (event) =&gt; {
    event.preventDefault();
    submitting.value = true;
    fetch(
      &quot;/register&quot;,
      { method: &quot;POST&quot;, body: JSON.stringify(form.peek()) }
    ).finally(() =&gt; submitting.value = false);
  }

  return (
    &lt;form onSubmit={submitRegistration}&gt;
      &lt;label&gt;
        First name
        &lt;input value={form.name.first}
          onInput={e =&gt; form.name.first.value = e.currentTarget.value} /&gt;
      &lt;/label&gt;
      &lt;label&gt;
        Last name
        &lt;input value={form.name.last}
          onInput={e =&gt; form.name.last.value = e.currentTarget.value} /&gt;
      &lt;/label&gt;
      &lt;label&gt;
        Email
        &lt;input value={form.email}
          onInput={e =&gt; form.email.value = e.currentTarget.value} /&gt;
      &lt;/label&gt;
      &lt;button disabled={submitting}&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>I would argue that this code aesthetic reads far more meaningfully than having to call <code>useState</code> four different times and having to declare four different getters and four different setters, like you would have to in the example above. Although people are accustomed to hooks and like them for many valid reasons, I think even simple examples of state management like this scale poorly with <code>useState</code>, and push people towards libraries like <code>formik</code>.</p>
<h2 id="conclusions">Conclusions</h2>
<p>For anyone who has the initial reaction described in the beginning of this post, I hope that the reasons I've provided give some form of clarity as to why I decided to make a state management library. I hope the performance &amp; developer experience gains justify its existence even if it is for a niche set of developers who are using Preact for Preact and not just as better React.</p>
<p>For more information on the <code>preact-signal-store</code> library, go checkout <a href="https://www.npmjs.com/package/preact-signal-store?ref=dept-engineering-blog.ghost.io">the docs</a> and please give it a shot and see what you think! And if you feel that there's a use-case that's missing, I'd love to consider expanding to meet more needs, so please <a href="https://github.com/EthanStandel/preact-signal-store/issues?ref=dept-engineering-blog.ghost.io">file an issue</a>! And if you end up using the package and liking it, I'd be so appreciative if you would <a href="https://github.com/EthanStandel/preact-signal-store/stargazers?ref=dept-engineering-blog.ghost.io">slap a star on the repo</a>! And even if you do none of that, thank you so much for reading!</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[How to reduce costs in AWS]]></title><description><![CDATA[Templates may use configurations adding up unnecessary expenses. Here's my journey to move away from this config to reduce this project's AWS cost.]]></description><link>https://engineering.deptagency.com/reducing-aws-cost/</link><guid isPermaLink="false">629fd69d62612a003d671fb1</guid><category><![CDATA[Process]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[David Wang]]></dc:creator><pubDate>Wed, 20 Jul 2022 20:04:46 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/taylor-vick-M5tzZtFCOfs-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/taylor-vick-M5tzZtFCOfs-unsplash.jpg" alt="How to reduce costs in AWS"/><p>It was easy to set up a production-ready database in <em>Amazon Web Services</em> (AWS). <strong>Select</strong> the production template, <strong>hit</strong> the <em>Create database</em> button, and <strong>ship it</strong>! Easy.<br><br>However, templates may use configurations adding up unnecessary expenses. Here's my journey to move away from this config to reduce this project's AWS cost.</br></br></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Screenshot-from-2022-06-08-17-35-39.png" class="kg-image" alt="How to reduce costs in AWS" loading="lazy" width="856" height="284" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/06/Screenshot-from-2022-06-08-17-35-39.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Screenshot-from-2022-06-08-17-35-39.png 856w" sizes="(min-width: 720px) 720px"/></figure><h3 id="%F0%9F%92%B8-the-price-is-right">💸 The price is right: </h3><p>AWS serves infrastructure options easily to its users à la carte in the form of simple UI. Just click on a drop-down, select a radio button, or move the slider to get scale resources up or down. However, due to the number of options, I often encounter choice paralysis. It takes time to research each option, and within an option, it opens up more infrastructure decisions. </p><p>To help in some instances AWS offers templates for an easy setup. The drawback is that the out-of-box configurations may be too expensive and overkill for a project. For this project's production environment I chose the production template for <em>AWS Relational Databases Service</em> (RDS). However, I notice the billing is higher than other projects that required more back-end resources than this one. It's time to take a step back and reevaluate! Maybe the production template does not fit my project's needs. Where do I begin? Do I just guess?</p><p>Thankfully AWS provides a pricing calculator: <a href="https://calculator.aws/?ref=dept-engineering-blog.ghost.io">https://calculator.aws/</a>, I will use this extensively to figure out ways to reduce the bill.</p><h3 id="%F0%9F%93%9D-configure-the-price-calculator">📝 Configure the price calculator:</h3><p>The first step is to replicate and configure the service in the calculator.</p><p>Current setup:</p><ul><li><strong>AWS service: </strong>Amazon RDS for PostgreSQL </li><li><strong>region</strong>: us-east-1</li><li><strong>instance</strong>: db.m5.xlarge (vCPU: 4, Memory: 16 GiB)</li><li><strong>storage</strong>: 100 GiB</li><li><strong>provisioned</strong> IOPS: 3000</li><li><strong>Multi-AZ</strong>: Yes, secondary zone us-east-1b</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-09-33-53.png" class="kg-image" alt="How to reduce costs in AWS" loading="lazy" width="1173" height="1148" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/07/Screenshot-from-2022-07-20-09-33-53.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/07/Screenshot-from-2022-07-20-09-33-53.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-09-33-53.png 1173w" sizes="(min-width: 720px) 720px"/></figure><p>The calculator is useful because I can see the breakdown of the price, I can interact with different configurations to see the price difference, and it provides links to documentation to learn more about the terms and prices. After putting in my config I see that it adds up close to the actual bill. Now I can start my cost-saving journey.</p><h3 id="%F0%9F%94%8E-examine-the-bill">🔎 Examine the bill:</h3><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Screenshot-from-2022-06-08-18-21-20.png" class="kg-image" alt="How to reduce costs in AWS" loading="lazy" width="1513" height="348" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/06/Screenshot-from-2022-06-08-18-21-20.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/06/Screenshot-from-2022-06-08-18-21-20.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Screenshot-from-2022-06-08-18-21-20.png 1513w" sizes="(min-width: 720px) 720px"/></figure><p>First, take a look at the bill in the AWS console. It provides an itemized receipt to help pinpoint the different options I can look to scale down or remove.</p><!--kg-card-begin: markdown--><ul>
<li>
<p><strong>RDS Proxy</strong> - This project stack uses <em>AWS Serverless Lambdas</em>. That means each lambda function call makes a database connection, and due to the nature of lambdas they can scale to tens of thousands of concurrent connections using database resources. Each database connection consumes memory and CPU resources. An RDS Proxy automatically handles the db connection pools. (<a href="https://aws.amazon.com/blogs/compute/using-amazon-rds-proxy-with-aws-lambda?ref=dept-engineering-blog.ghost.io">https://aws.amazon.com/blogs/compute/using-amazon-rds-proxy-with-aws-lambda</a>)</p>
<ul>
<li>Since our stack uses Serverless RDS Proxy is required.</li>
</ul>
</li>
<li>
<p><strong>Multi-AZ instance</strong> - A redundancy option to synchronously replicate the database  into a different availability zone. For example, if <em>us-east-1a</em> fails, it'll fall back to <em>us-east-1b</em>.</p>
<ul>
<li>Note that region failover provides an extra instance so 2X the cost. However, for a production environment reliability is necessary.</li>
</ul>
</li>
<li>
<p><strong>Hardware</strong> - AWS has different instance tiers of computation power, memory, and bandwidth. There are different focuses such as <em>general purpose</em> for diverse workloads, <em>memory optimized</em> for processing large data sets, and <em>burstable</em> to burst above a baseline hardware spec for increased loads.</p>
<ul>
<li>Based on monitoring, the database never hits the CPU limit so I can try lowering the tier from <em>db.m5.xlarge</em> to <em>db.m5.large</em>.</li>
</ul>
</li>
<li>
<p><strong>Storage</strong> - AWS has different storage types: general purpose is charged for storage provision, IOPS focuses on input and output capacity, and magnetic previous generation storage for backward compatibility.</p>
<ul>
<li>The default was set to IOPS which is really expensive. IOPS is meant for intensive read/write per second. Currently, the app only hits a max of 3 IOPS. I  can try switching the storage type to General SSD.</li>
<li>General SSD does provide IOPS at the rate of 3x the size, so 100 GB/month will provide 300 IOPS included in the price. (<a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html?ref=dept-engineering-blog.ghost.io">https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html</a>)</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><h3 id="%F0%9F%9B%A0%EF%B8%8F-tweaking-the-settings">🛠️ Tweaking the settings:</h3><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-10-21-55.png" class="kg-image" alt="How to reduce costs in AWS" loading="lazy" width="754" height="1162" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/07/Screenshot-from-2022-07-20-10-21-55.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-10-21-55.png 754w" sizes="(min-width: 720px) 720px"/></figure><p>After examining the bill I've decided to make two changes:</p><ul><li>Lower the instance tier <em><strong>db.m5.xlarge</strong></em> @ 4 cpus, 16 gig, 4750 mbps to <strong><em>db.m5.large</em> </strong>@ 2 cpus, 8 gig, 4750 mbps</li><li>Change storage type from <em><strong>IOPS</strong></em> @ 3000 IOPS to <em><strong>General SSD</strong></em> @ 100 GB per month</li></ul><p>The calculator dropped from $1,144.76 to $259.88, saving $884.88 (77%)!</p><h3 id="%F0%9F%92%B5-conclusion">💵 Conclusion:</h3><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-10-32-08.png" class="kg-image" alt="How to reduce costs in AWS" loading="lazy" width="2000" height="452" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/07/Screenshot-from-2022-07-20-10-32-08.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/07/Screenshot-from-2022-07-20-10-32-08.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2022/07/Screenshot-from-2022-07-20-10-32-08.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-10-32-08.png 2114w" sizes="(min-width: 720px) 720px"/></figure><p>After making the two changes, I can see that the bill was reduced from $1275.39 to $239.93 (81%) after a month with the new configuration! I've also monitored and looked at the metrics never reaching the hardware limits.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-10-49-39.png" class="kg-image" alt="How to reduce costs in AWS" loading="lazy" width="1908" height="916" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/07/Screenshot-from-2022-07-20-10-49-39.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/07/Screenshot-from-2022-07-20-10-49-39.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2022/07/Screenshot-from-2022-07-20-10-49-39.png 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/Screenshot-from-2022-07-20-10-49-39.png 1908w" sizes="(min-width: 720px) 720px"/></figure><p>It's totally worth utilizing the calculator and exploring other configurations to reduce costs. Even if the project cost is right, it's still an excellent exercise to take a second to evaluate the costs.</p>]]></content:encoded></item><item><title><![CDATA[CI/CD with Airflow and dbt]]></title><description><![CDATA[How do you integrate data models in a source control repository into a database, using a CI/CD process?]]></description><link>https://engineering.deptagency.com/ci-cd-with-airflow-and-dbt/</link><guid isPermaLink="false">642b12ec9527e0003dca4038</guid><category><![CDATA[CI/CD]]></category><category><![CDATA[dbt]]></category><dc:creator><![CDATA[Hector Budiel]]></dc:creator><pubDate>Wed, 13 Jul 2022 17:55:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/ci-cd-concept.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/ci-cd-concept.jpeg" alt="CI/CD with Airflow and dbt"/><p><em>Author: Alexander Pickering</em></p><h2 id="the-problem"><strong>The Problem</strong></h2><p>We want a central location to store and manage data models, and then permit multiple systems to derive their “source of truth” for a model from that repository. A single source of truth allows us to use version control, perform code review, and permits CI/CD for our data models. The benefits include a structured way to change models, and mitigation of error-prone manual schema changes.</p><p>But how do you integrate data models in a source control repository into a database, using a CI/CD process?</p><p>Our approach integrates DBT, Airflow, and our data model repository.  More specifically, our integration requirement is that DBT takes a folder of folders with one schema.yml file in each, and parses that file for configuration detail.  We want to derive these schema.yml files from our source of truth when we can.</p><h2 id="implementation"><br><strong>Implementation</strong></br></h2><p>The particular input to DBT we want to generate is the schema.yml file, which includes the table schema, tests against those tables, and some metadata.</p><p>Our source of truth files are simple python files that create objects that represent our tables, e.g.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-1.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1248" height="2232" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-1.png 1248w" sizes="(min-width: 720px) 720px"/></figure><p>Here’s each table/column above, and the section of the schema.yml it generates:</p><p>Python: plugins/models/analytical/orders.py</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-2.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1282" height="596" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-2.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-2.png 1282w" sizes="(min-width: 720px) 720px"/></figure><p>DBT: plugins/dbt/models/analytical/schema.yml</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-3.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1304" height="572" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-3.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-3.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-3.png 1304w" sizes="(min-width: 720px) 720px"/></figure><p>In the above, we see that the <strong>DbtTagAnnotation("daily")</strong> object causes the string “daily” to be appended in the schema.yml file under the table’s <strong>config:{tags:[]}</strong> array. A similar thing happens for columns:</p><p>Python: plugins/models/analytical/orders.py</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-4.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1292" height="646" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-4.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-4.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-4.png 1292w" sizes="(min-width: 720px) 720px"/></figure><p>DBT: plugins/dbt/models/analytical/schema.yml</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-5.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1276" height="386" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-5.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-5.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-5.png 1276w" sizes="(min-width: 720px) 720px"/></figure><p>Here, we see the <strong>ColumnAnnotation("unique")</strong> and <strong>ColumnAnnotation("not null")</strong> get added to the column’s <strong>tests:[] </strong>array.</p><p>The two tests shown so far are comparatively simple tests. Let’s consider a more complex test:</p><p>Python: plugins/models/analytical/orders.py</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-6.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1300" height="838" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-6.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-6.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-6.png 1300w" sizes="(min-width: 720px) 720px"/></figure><p>DBT: plugins/dbt/models/analytical/schema.yml</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-7.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1280" height="510" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-7.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-7.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-7.png 1280w" sizes="(min-width: 720px) 720px"/></figure><p>Here, we have a test with multiple parts, constructed from a different class, <strong>TestAnnotation</strong>, which receives as arguments a name of a test, and then all the keys and values to the test.</p><p>The trick that unites all these annotations is they’re all represented as string-like objects under the hood:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-8.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1284" height="506" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-8.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-8.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-8.png 1284w" sizes="(min-width: 720px) 720px"/></figure><p>Even the TestAnnotation is just a clever way to map arguments into a string like object:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-9.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1286" height="244" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-9.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-9.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-9.png 1286w" sizes="(min-width: 720px) 720px"/></figure><p>The benefit of this approach is it simplifies the process from annotations to something else. When we want to render a table’s annotations into things we can put into the schema file, we only need to do a straightforward mapping of strings to collections, and then merge the collections.</p><h2 id="all-my-best-ideas-are-stolen"><strong>All My Best Ideas Are Stolen</strong></h2><p>For a recent client project, we chose to implement this mapping as a set of regular expressions, that map to functions, that implement the logic that generates the collection. We borrowed an idea from another place that frequently has to map strings that match a pattern to functions, web servers, which usually need to map urls to a particular function to handle the http request.</p><p>Web frameworks usually encapsulate this logic in a “Router” class, so we implemented a simple regular expression router to do something similar for our annotation strings. We call the interface “Handlers” but so far they’re all implemented as Routers too. We write handlers like the following:</p><p>Python: plugins/modeling/handlers.py</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-10.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1300" height="1138" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-10.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-10.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-10.png 1300w" sizes="(min-width: 720px) 720px"/></figure><p>The <strong>AppendValueChange</strong> class is just a lazy evaluation way to call <strong>list.append</strong> with the given values.</p><p>The router allows us to write regular expressions to handle certain annotation cases, and generally prepending “magic words” like <strong>test: </strong>or <strong>dbt_tag: </strong>when constructing the annotation helps us avoid conflicts.</p><p>Several open-source projects implement routers for many different reasons. A list is available on the <a href="https://wiki.python.org/moin/Routing?ref=dept-engineering-blog.ghost.io">Python Wiki</a>.</p><h2 id="things-this-design-did-well"><strong>Things This Design Did Well:</strong></h2><p><strong>Extendability</strong></p><p>It allowed us to extend this system past just DBT, we use the same system to generate sql statements to send to our database by just implementing a new Handler/Router. The <strong>SqlHandler </strong>ignores DBT-specific annotations, and any new snowflake-specific annotations that we added didn’t require changes to the <strong>DbtHandler.</strong></p><p>In addition, the system is flexible enough that the core (Table, Column, Annotation, Router classes) can be lifted from the project and dropped into another project with minimal rewrites, only requiring reimplementation of the Handlers and Change classes to interact with the new project’s external systems. If the new project also uses DBT, the DbtHandler can be used, in effect, we only need to write Handler and Change classes for a system once, and then those classes can be reused in other projects.</p><p><strong>Testability</strong></p><p>Since tables are just python code, we can write pytest unit tests that get executed by CircleCI that check and report errors about incorrect models.</p><h2 id="things-this-design-did-poorly"><br><strong>Things This Design Did Poorly:</strong></br></h2><p><strong>It’s not immediately clear whether to use one or multiple annotations</strong></p><p>For example, it’s not immediately clear from the syntax if a data modeler needs to use is</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-11.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1282" height="110" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-11.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-11.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-11.png 1282w" sizes="(min-width: 720px) 720px"/></figure><p>or if one annotation with multiple arguments should be used, ex:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-12.png" class="kg-image" alt="CI/CD with Airflow and dbt" loading="lazy" width="1298" height="126" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/ci-cd-12.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/ci-cd-12.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/ci-cd-12.png 1298w" sizes="(min-width: 720px) 720px"/></figure><p>What we have so far decides one-or-multiple based on whether the target system considers the metadata to be one-per-object, or multiple-per-object.</p><p><strong>Limitations on annotations are not enforced by the syntax</strong></p><p>For some annotations it makes no sense to have multiple copies. For example, there’s no meaningful way to have multiple “not null” annotations, either in DBT or SQL.</p><p>This system doesn’t force models to have “correct and minimal” Table classes, by syntax. Although they could be caught by a unit test, if knowledge about the annotation exists somewhere in Python. In some cases, for example with <strong>TestAnnotation</strong>, the json chunk is written out into the <strong>schema.yml</strong> file and it’s not until DBT is run that errors with the arguments can be detected.</p><p><strong>Extension requires knowledge of regular expressions</strong></p><p>Regular expressions are hard. Not everyone is familiar and even when you are, they’re sometimes cryptic and hard to parse. Extending handlers requires the developer to be familiar with regular expressions to match their newly added annotation. If we were to rewrite this system, we would consider making annotations into dictionaries instead of strings, and using some sort of dictionary-match router (perhaps taking inspiration from <a href="https://graphql.org/learn/queries/?ref=dept-engineering-blog.ghost.io">GraphQL</a>).</p><h2 id="conclusion"><br><strong>Conclusion</strong></br></h2><p>In this document we outlined a challenge we faced integrating DBT and making it easy to use. We described some of the barriers we faced, and how we solved them in an extensible way that could be reused in other projects.  We hope you recall our solution and let it inspire you if you find yourself in a similar situation.</p>]]></content:encoded></item><item><title><![CDATA[Dependency Injection for Type Script AWS Lambdas]]></title><description><![CDATA[We talk about Microsoft's dependency injection package TSyringe and its different dependency scopes.  We explain how to use resolution scoped dependencies over transient, singleton, and container scoped dependencies to limit the scope of a dependency to a single lambda invocation.]]></description><link>https://engineering.deptagency.com/type-script-di-in-aws-lambda/</link><guid isPermaLink="false">62c73e7a7394ce003dbf506f</guid><category><![CDATA[Platforms]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Brandon Pfeiffer]]></dc:creator><pubDate>Fri, 08 Jul 2022 16:18:40 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/cdc-jgvTYz6UT_g-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/07/cdc-jgvTYz6UT_g-unsplash.jpg" alt="Dependency Injection for Type Script AWS Lambdas"/><p>In this article, we talk about Microsoft's dependency injection package called TSyringe and its different dependency scopes. We explain how to use resolution scoped dependencies to limit the scope of a dependency to a single lambda invocation.  Along the way, we talk about how this task would be hard to accomplish with transient, singleton, and container scoped dependencies.</p><h2 id="tsyringe">TSyringe</h2><p>TSyringe is an open-source TypeScript package maintained by Microsoft that allows us to use dependency injection within our TypeScript code. The code and documentation can be found on the <a href="https://github.com/microsoft/tsyringe?ref=dept-engineering-blog.ghost.io">TSyringe github page</a>.</p><p>Dependency injection allows us to resolve all of the dependencies without having to instantiate each dependency and pass it to the dependent's constructor.</p><p>So, we can simplify this:</p><pre><code class="language-javascript">const loggerService = new LoggerService();
const emailService = new EmailService(loggerService);
const personRepository = new PersonRepository(loggerService);

const personService = new PersonService(loggerService, emailService, personRepository);</code></pre><p>To this:</p><pre><code class="language-typescript">const personService = diContainer.resolve(PersonService);</code></pre><p>And all of the dependencies of the <code>PersonService</code> get resolved automatically, as well as any of the chained dependencies, such as the <code>LoggerService</code> that gets injected into the <code>EmailService</code> and <code>PersonRepository</code>.</p><h2 id="scenario">Scenario</h2><p>Let's take the following scenario: We have a lambda that can get a person with a given id.  The handler will use a <code>PersonService</code>, which takes care of getting the person, and the handler and <code>PersonService</code> will use a <code>LoggerService</code> to format our logs and log them to the console.</p><p><strong>LoggerService</strong></p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">import { injectable } from "tsyringe";

@injectable()
export class LoggerService {
  private lambdaName: string;
  private personId: string;
  private logsLogged: number = 0;

  public setContext(lambdaName: string, personId: string) {
    this.lambdaName = lambdaName;
    this.personId = personId;
  }

  public log(message: string) {
    this.logsLogged++;
    
    console.log(JSON.stringify({
      lambdaName: this.lambdaName,
      personId: this.personId,
      logsLogged: this.logsLogged,
      message: message
    }));
  }
}</code></pre><figcaption>logger.service.ts</figcaption></figure><p><strong>PersonService</strong></p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">import { injectable } from "tsyringe";
import { LoggerService } from "./logger.service";

interface Person {
  personId: string;
}

@injectable()
export class PersonService {
  constructor(private logger: LoggerService) {}

  public async getPerson(personId: string): Promise&lt;Person&gt; {
    this.logger.log("PersonService.getPerson: Getting person.");

    await new Promise(resolve =&gt; setTimeout(resolve, 1000));

    this.logger.log("PersonService.getPerson: Got person.");

    return {
      personId: personId
    }
  }
}</code></pre><figcaption>person.service.ts</figcaption></figure><p>We use the <code>@injectable</code> decorator to denote which classes the DI container can resolve as dependencies.  Both the <code>LoggerService</code> and <code>PersonService</code> will be resolved through the DI container.</p><p>The <code>PersonService</code>'s constructor has a typed <code>logger</code> parameter, which tells the DI container to resolve a <code>LoggerService</code> for that dependency.</p><p>You can see us resolving the <code>PersonService</code> from the DI container in our lambda handler below.</p><p><strong>Lambda Handler</strong></p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">import "reflect-metadata";
import { APIGatewayProxyEventV2, APIGatewayProxyResult } from "aws-lambda";
import { container } from "tsyringe";
import { PersonService } from "./person.service";

export const getPerson = async (event: APIGatewayProxyEventV2): Promise&lt;APIGatewayProxyResult&gt; =&gt; {
  const service = container.resolve(PersonService);
  const personId = event.queryStringParameters.id;

  const result = await service.getPerson(personId);
  
  return {
    statusCode: 200,
    body: JSON.stringify(result)
  };
};</code></pre><figcaption>person-controller.ts</figcaption></figure><p>When calling <code>container.resolve</code>, the DI container sees that the <code>PersonService</code> has a dependency on the <code>LoggerService</code> and will inject an instance of the <code>LoggerService</code> into the constructor of the <code>PersonService</code> while instantiating it.</p><h2 id="loggerserivce-state">LoggerSerivce State</h2><p>The logger service has a couple properties that should be part of the output of each log:</p><ul><li><strong>lambdaName</strong>: The name of the lambda handler that is being executed.</li><li><strong>personId</strong>: The personId that is being requested.</li><li><strong>logsLogged</strong>: The number of logs that have been output during the lambda execution.</li></ul><p>We will want to add this context to the <code>LoggerService</code> via its <code>setContext</code> function before any logs are logged, and we want to do that in the controller so that we can pass it the correct <code>lambdaName</code>.  Our handler should now look like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">export const getPerson = async (event: APIGatewayProxyEventV2): Promise&lt;APIGatewayProxyResult&gt; =&gt; {
  const loggerService = container.resolve(LoggerService);
  const service = container.resolve(PersonService);
  const personId = event.queryStringParameters.id;

  loggerService.setContext('getPerson', personId);

  loggerService.log("Controller.getPerson: Handler invoked.")

  const result = await service.getPerson(personId);
  
  return {
    statusCode: 200,
    body: JSON.stringify(result)
  };
};</code></pre><figcaption>person-controller.ts</figcaption></figure><h2 id="loggerservice-scopes">LoggerService Scopes</h2><p>TSyringe has several scopes available to use, with transient scope being its default. To have a transient scoped dependency means that you will get a new instance from the DI container every time that dependency is resolved.  Here are the scopes that TSyringe provides, directly from their <a href="https://github.com/microsoft/tsyringe?ref=dept-engineering-blog.ghost.io#scoped">documentation</a>, which is similar to most other DI containers.</p><blockquote><strong>Transient</strong>: The default registration scope, a new instance will be created with each resolve</blockquote><blockquote><strong>Singleton</strong>: Each resolve will return the same instance (including resolves from child containers)</blockquote><blockquote><strong>ResolutionScoped</strong>: The same instance will be resolved for each resolution of this dependency during a single resolution chain</blockquote><blockquote><strong>ContainerScoped</strong>: The dependency container will return the same instance each time a resolution for this dependency is requested. This is similar to being a singleton, however if a child container is made, that child container will resolve an instance unique to it.</blockquote><p>Let's see how applying different scopes to the <code>LoggerService</code> will affect the logs.</p><h3 id="loggerservice-as-transient">LoggerService as Transient</h3><p>From the class definition above, you can see that the <code>LoggerService</code> has the <code>@injectable</code> decorator.  This means that it will be a transient dependency.  When we run the lambda, this is the output from CloudWatch:</p><figure class="kg-card kg-code-card"><pre><code>{"lambdaName":"getPerson","personId":"AF1234","logsLogged":1,"message":"Controller.getPerson: Handler invoked."}
{"logsLogged":1,"message":"PersonService.getPerson: Getting person."}
{"logsLogged":2,"message":"PersonService.getPerson: Got person."}</code></pre><figcaption>transient-logs.txt</figcaption></figure><p>We are not quite getting what we expect.  For the duration of the handler invocation, we got three logs.  The first log, from the handler, has the proper <code>lambdaName</code> and <code>personId</code>, but the next two logs from the <code>PersonService</code>, do not.  Likewise, you can see that the <code>logsLogged</code> starts over when we start logging from the <code>PersonService</code>.</p><p>Why aren't we getting what we expect? Because with a transient dependency, we get a new instance each time it is resolved.  Therefore, the <code>LoggerService</code> instance that we resolved in the handler, is not the same instance that is resolved for the <code>PersonService</code>.</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">const loggerService = container.resolve(LoggerService); // LoggerService is resolved directly from the container
const service = container.resolve(PersonService); // LoggerService is resolved again during the PersonService resolution.</code></pre><figcaption>person-controller.ts</figcaption></figure><h3 id="loggerservice-as-singleton">LoggerService as Singleton</h3><p>Let's change the LoggerService to a singleton dependency and take a look at the logs.</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">@singleton()
export class LoggerService {</code></pre><figcaption>logger.service.ts</figcaption></figure><figure class="kg-card kg-code-card"><pre><code>{"lambdaName":"getPerson","personId":"AF1234","logsLogged":1,"message":"Controller.getPerson: Handler invoked."}
{"lambdaName":"getPerson","personId":"AF1234","logsLogged":2,"message":"PersonService.getPerson: Getting person."}
{"lambdaName":"getPerson","personId":"AF1234","logsLogged":3,"message":"PersonService.getPerson: Got person."}</code></pre><figcaption>singleton-logs-1.txt</figcaption></figure><p>That looks like what we expect, each log has the <code>lambdaName</code> and <code>personId</code>, and the <code>logsLogged</code> reflects the number of logs that were logged during the lambda execution.</p><p>Why does this work? Because with a singleton dependency, the same instance of the dependency is returned from the container with each resolution.</p><pre><code class="language-javascript">const loggerService = container.resolve(LoggerService); // LoggerService is resolved for the first time
const service = container.resolve(PersonService); // Same instance of the LoggerService from the line above is resolved for the PersonService</code></pre><p><strong>BUT!</strong> Let's see what happens when executing the lambda twice, for two different <code>personId</code>, <code>DI1234</code> and <code>DI4321</code>.</p><figure class="kg-card kg-code-card"><pre><code>{"lambdaName":"getPerson","personId":"DI1234","logsLogged":1,"message":"Controller.getPerson: Handler invoked."}
{"lambdaName":"getPerson","personId":"DI1234","logsLogged":2,"message":"PersonService.getPerson: Getting person."}
{"lambdaName":"getPerson","personId":"DI1234","logsLogged":3,"message":"PersonService.getPerson: Got person."}

{"lambdaName":"getPerson","personId":"DI4321","logsLogged":4,"message":"Controller.getPerson: Handler invoked."}
{"lambdaName":"getPerson","personId":"DI4321","logsLogged":5,"message":"PersonService.getPerson: Getting person."}
{"lambdaName":"getPerson","personId":"DI4321","logsLogged":6,"message":"PersonService.getPerson: Got person."}</code></pre><figcaption>singleton-logs-2.txt</figcaption></figure><p>It looks like we got the correct <code>lambdaName</code> and <code>personId</code> through each log as expected, and the <code>personId</code> changed between requests to reflect the new <code>personId</code>.  Good, but what happened to <code>logsLogged</code>? It is not only reflecting the number of logs that were logged during a single lambda invocation but both lambda invocations.</p><p>Why is the <code>logsLogged</code> state being maintained across lambda invocations? Because the DI container is loaded outside of the handler as part of the lambda instance's code.  AWS will keep your lambda instance alive as long as it keeps getting invoked.  It will eventually terminate the instance when lambda code is updated or the lambda remains idle for around 45-60 mins.  So, for as long as AWS keeps that lambda instance running, the same instance of the <code>LoggerService</code> will be returned for each resolution.</p><p>So, let's take a look at the two other scopes.</p><h3 id="loggerservice-as-containerscoped">LoggerService as <strong>ContainerScoped</strong></h3><p>Container scoped classes will return the same instance of the dependency every time you resolve the dependency from a container.  If you are using multiple containers, there will be a single instance per container.</p><p>For the way our code is written, a container scoped <code>LoggerService</code> will function much like the singleton.  This is because we are using the default container that is loaded by TSyringe.  This single container will last through many lambda invocations and return the same instance of <code>LoggerService</code> for each invocation.</p><p>Let's try it anyway:</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">@scoped(Lifecycle.ContainerScoped)
export class LoggerService {</code></pre><figcaption>logger.service.ts</figcaption></figure><figure class="kg-card kg-code-card"><pre><code>{"lambdaName":"getPerson","personId":"DI6789","logsLogged":1,"message":"Controller.getPerson: Handler invoked."}
{"lambdaName":"getPerson","personId":"DI6789","logsLogged":2,"message":"PersonService.getPerson: Getting person."}
{"lambdaName":"getPerson","personId":"DI6789","logsLogged":3,"message":"PersonService.getPerson: Got person."}

{"lambdaName":"getPerson","personId":"DI9876","logsLogged":4,"message":"Controller.getPerson: Handler invoked."}
{"lambdaName":"getPerson","personId":"DI9876","logsLogged":5,"message":"PersonService.getPerson: Getting person."}
{"lambdaName":"getPerson","personId":"DI9876","logsLogged":6,"message":"PersonService.getPerson: Got person."}</code></pre><figcaption>container-scoped-logs.txt</figcaption></figure><p>Yep... same result. The state of <code>logsLogged</code> is being persisted through each lambda invocation.</p><h3 id="loggerservice-as-resolutionscoped">LoggerService as <strong>ResolutionScoped</strong></h3><p>Resolution scoped classes will have the same instance of the class resolved for each resolution in a single resolution chain.  You can think of a resolution chain as anytime the container is called to resolve a dependency.  Take the code in the controller for example:</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">const loggerService = container.resolve(LoggerService);
const service = container.resolve(PersonService);</code></pre><figcaption>person-controller.ts</figcaption></figure><p>There are two resolution chains above.  So if <code>LoggerService</code> was a resolution scoped dependency, one instance would be returned for the controller, and another instance would be returned as the <code>PersonService</code> dependency.  These two instances would result with the same logs as when <code>LoggerService</code> was a transient dependency, so the logs from the <code>PersonService</code> would not contain the needed <code>lambdaName</code> and <code>personId</code>.</p><p>So what can we do?  We can split the handler code into a handler function and a controller class and resolve the <code>PersonService</code> and <code>LoggerService</code> in a single resolution chain.</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">@scoped(Lifecycle.ResolutionScoped)
export class LoggerService {</code></pre><figcaption>logger.service.ts</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-javascript">@injectable()
class PersonController {
  constructor(private personService: PersonService, private loggerService: LoggerService) {}

  public async getPerson(event: APIGatewayProxyEventV2): Promise&lt;APIGatewayProxyResult&gt; {
    const personId = event.queryStringParameters.id;

    this.loggerService.setContext('getPerson', personId);

    this.loggerService.log("Controller.getPerson: Handler invoked.")

    const result = await this.personService.getPerson(personId);
    
    return {
      statusCode: 200,
      body: JSON.stringify(result)
    };
  }
}

export const getPerson = async (event: APIGatewayProxyEventV2): Promise&lt;APIGatewayProxyResult&gt; =&gt; {
  const controller = container.resolve(PersonController);
  return controller.getPerson(event);
};</code></pre><figcaption>person-controller.ts</figcaption></figure><p>We moved the actual business logic to an injectable class called <code>PersonController</code>, which has two dependencies that will be injected into its constructor, <code>PersonService</code> and <code>LoggerService</code>.  Now the logic in the handler is very simple.  There is a single resolution chain to resolve the <code>PersonController</code> dependency, which means the same instance of the <code>LoggerService</code> will be resolved for the <code>PersonController</code> and the <code>PersonService</code>.  This resolution chain will be different for each lambda invocation because the handler code gets executed each invocation.</p><pre><code>{"lambdaName":"getPerson","personId":"DI4567","logsLogged":1,"message":"Controller.getPerson: Handler invoked."}
{"lambdaName":"getPerson","personId":"DI4567","logsLogged":2,"message":"PersonService.getPerson: Getting person."}
{"lambdaName":"getPerson","personId":"DI4567","logsLogged":3,"message":"PersonService.getPerson: Got person."}

{"lambdaName":"getPerson","personId":"DI7654","logsLogged":1,"message":"Controller.getPerson: Handler invoked."}
{"lambdaName":"getPerson","personId":"DI7654","logsLogged":2,"message":"PersonService.getPerson: Getting person."}
{"lambdaName":"getPerson","personId":"DI7654","logsLogged":3,"message":"PersonService.getPerson: Got person."}</code></pre><p>There we go! This is what we expect.  The <code>lambdaName</code> and <code>personId</code> are showing up in each log, and the <code>logsLogged</code> is recording the number of logs that have been logged throughout a single lambda invocation and not carrying over into other invocations.</p><h2 id="conclusion">Conclusion</h2><p>Dependency injection makes it easy for us to separate out our code into controllers, services, repos, and APIs.  This creates clean code with clear responsibilities for each class.</p><p>We can create a single resolution chain per lambda invocation if we house our handler logic in a controller class, and resolve that controller in the simple handler.  This will create lambda invocation scoped dependencies that we can use to hold state for a single lambda invocation without the scope leaking into other lambda invocations.</p>]]></content:encoded></item><item><title><![CDATA[Benchmarking Rust code using Criterion.rs]]></title><description><![CDATA[Learn how to be a good custodian of your Rust code by using the Criterion crate effectively.]]></description><link>https://engineering.deptagency.com/benchmarking-rust-code-using-criterion-rs/</link><guid isPermaLink="false">629f6ffb62612a003d671eea</guid><category><![CDATA[rust]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Ashwin Sundar]]></dc:creator><pubDate>Tue, 14 Jun 2022 22:42:54 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/IMG_20151015_170922-1.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h1 id="benchmarking-rust-code-using-criterionrs">Benchmarking Rust code using Criterion.rs</h1>
<h1 id="introduction">Introduction</h1>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/IMG_20151015_170922-1.jpg" alt="Benchmarking Rust code using Criterion.rs"/><p>Benchmarking is a method of systematically assessing a program for performance. This process is a valuable component of regression testing because it helps you compare changes and improvements to your code. The systems programming language Rust offers many statistically rigorous analysis techniques, such as the <a href="https://docs.rs/criterion/latest/criterion/?ref=dept-engineering-blog.ghost.io"><em>Criterion</em></a> crate, which is a popular tool used for benchmarking in Rust. In this article, we'll go into more detail on how to use <em>Criterion</em> to compare various Rust functions to solve a problem from <a href="https://projecteuler.net/?ref=dept-engineering-blog.ghost.io">Project Euler</a>.</p>
<h2 id="about-criterion">About Criterion</h2>
<p><em>Criterion</em> is a benchmarking crate that specializes in statistically rigorous analysis techniques, as well as generating useful and attractive charts using <a href="http://www.gnuplot.info/?ref=dept-engineering-blog.ghost.io">gnuplot</a>. The <a href="https://github.com/bheisler/criterion.rs?ref=dept-engineering-blog.ghost.io#goals">primary goals</a> of <em>Criterion</em> are to measure the performance of code, prevent performance regressions, and accurately measure optimizations.</p>
<h1 id="example">Example</h1>
<p>To understand how to use <em>Criterion</em> for benchmarking, let's re-use an example from a previous article where we learned how to implement <a href="https://engineering.deptagency.com/parallel-processing-in-rust?ref=dept-engineering-blog.ghost.io">parallel processing in Rust</a>. In that article we explored how to parallelize a commutative loop. This time, let's compare the parallelized function to a function that solves the problem with an arithmetic series. The problem:</p>
<p><em>If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000000.</em></p>
<h2 id="setup-steps">Setup steps</h2>
<p>The code used in this article can be found at <a href="https://github.com/AshwinSundar/Criterion-Benchmarking?ref=dept-engineering-blog.ghost.io">https://github.com/AshwinSundar/Criterion-Benchmarking</a>. Let's start by creating a new Rust project in an empty folder called <code>Criterion_Benchmarking</code>. <code>cd</code> into the folder and type <code>cargo init</code>.</p>
<p><strong>Terminal</strong></p>
<pre><code>❯ cargo init
     Created binary (application) package
</code></pre>
<h2 id="1-add-criterion-to-dev-dependencies">1. Add Criterion to <code>dev-dependencies</code></h2>
<p><strong>$PROJECT/Cargo.toml</strong></p>
<pre><code class="language-json">[dev-dependencies]
criterion = {version = &quot;0.3&quot;, features = [&quot;html_reports&quot;]}

[[bench]]
name = &quot;euler1_benchmark&quot;
harness = false
</code></pre>
<p>First, navigate to the Rust manifest file <code>Cargo.toml</code> and create a <code>dev-dependencies</code> section. Adding <em>Criterion</em> to this section ensures that the benchmarking crate is only included during testing, and not in production builds. Let's also disable the default benchmarking harness <a href="https://github.com/rust-lang/libtest?ref=dept-engineering-blog.ghost.io"><code>libtest</code></a> by setting <code>harness=false</code>, that way the compiler invokes the <em>Criterion</em> benchmarking harness instead.</p>
<h2 id="2-create-benchmark-file">2. Create benchmark file</h2>
<p><strong>$PROJECT/benches/euler1_benchmark.rs</strong></p>
<pre><code class="language-rust">use criterion::{black_box, criterion_group, criterion_main, Criterion};
// use lib::euler1; // function to profile

pub fn criterion_benchmark(c: &amp;mut Criterion) {
// c.bench_function(&quot;euler1&quot;, |b| b.iter(|| euler1(black_box(input))));
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
</code></pre>
<p>Next, create a benchmark file at <code>$PROJECT/benches/euler1_benchmark.rs</code>. This file will import our function, run a benchmark, and then output the results to the console using a macro. We'll leave a couple placeholders for now and revisit this file once we've built the functions to benchmark.</p>
<h2 id="3-create-library-file">3. Create library file</h2>
<p><strong>$PROJECT/src/lib.rs</strong></p>
<pre><code class="language-rust">#[inline]
pub fn euler1_simple() {
    // For-loop solution
}

#[inline]
pub fn euler1_par() {
    // Parallelized solution
}

#[inline]
pub fn euler1_series() {
    // Arithmetic series solution
}
</code></pre>
<p>Finally, due to <a href="https://bheisler.github.io/criterion.rs/book/user_guide/known_limitations.html?ref=dept-engineering-blog.ghost.io">implementation constraints</a> within <em>Criterion</em>, we need to create a library file to host our functions at <code>src/lib.rs</code> and declare our functions as <code>public</code>, so that we can import the functions into <code>euler1_benchmark.rs</code>. The <a href="https://nnethercote.github.io/perf-book/inlining.html?ref=dept-engineering-blog.ghost.io">#[inline]</a> attribute above the function helps reduce execution time slightly and improves the accuracy of the benchmarking process.</p>
<p>Now that we've written some boilerplate code for the benchmark, let's return to the actual problem and compare a few possible solutions.</p>
<h1 id="solutions">Solutions</h1>
<p>If you're interested in the details of each solution, read this section. Otherwise, you can skip to the <a href="#creating-benchmarks">Creating Benchmarks</a> section. Just know that the three solutions are, in order of least to most efficient, a simple for-loop, a parallelized for-loop, and an arithmetic series.</p>
<p>To reiterate the problem:</p>
<p><em>If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000000.</em></p>
<h2 id="solution-1-for-loop">Solution 1: For-loop</h2>
<p><strong>$PROJECT/src/lib.rs</strong></p>
<pre><code class="language-rust">#[inline]
pub fn euler1_simple(input: i64) -&gt; i64 {
    let mut sum: i64 = 0;
    for i in 1..input {
        if i % 3 == 0 || i % 5 == 0 {
            sum += i as i64;
        }
    }
    sum

}
</code></pre>
<p>A simple brute-force solution is to iterate on every number in the range and determine if it's divisible by either 3 or 5. This solution will give us a good baseline to compare the other solutions to.</p>
<h2 id="solution-2-parallelized-for-loop">Solution 2: Parallelized for-loop</h2>
<p><strong>$PROJECT/src/lib.rs</strong></p>
<pre><code class="language-rust">#[inline]
pub fn euler1_par(input: i64) -&gt; i64 {
    use std::thread;

    let threads = thread::available_parallelism().unwrap().get() as f64;
    let input = input as f64;
    let mut handles: Vec&lt;JoinHandle&lt;i64&gt;&gt; = vec![];

    for t in 1..=(threads as i32) {
        let t = t as f64;
        let upper_bound = (input * (t / threads)) as i32;
        let lower_bound = (input * (t - 1f64) / threads) as i32;

        handles.push(thread::spawn(move || {
            let mut sum: i64 = 0;
            for i in lower_bound..upper_bound {
                if i % 3 == 0 || i % 5 == 0 {
                    sum += i as i64;
                }
            }
            sum
        }));
    }

    let mut sum = 0;

    for h in handles {
        sum += h.join().unwrap();
    }
    sum

}
</code></pre>
<p>In a <a href="https://engineering.deptagency.com/parallel-processing-in-rust?ref=dept-engineering-blog.ghost.io">previous article</a>, we started to explore parallelization using <code>std::thread</code>, a library for distributing program processing to OS threads. We stopped at two threads in that article, so in this example I've expanded the function to accomodate all available system threads.</p>
<p>Let's step through this code piece by piece.</p>
<pre><code class="language-rust">    use std::thread;

    let threads = thread::available_parallelism().unwrap().get() as f64;
    let input = input as f64;
    let mut handles: Vec&lt;JoinHandle&lt;i64&gt;&gt; = vec![];
</code></pre>
<p>First, we need to import the native parallelism module <a href="https://doc.rust-lang.org/std/thread/?ref=dept-engineering-blog.ghost.io"><code>std::thread</code></a> and determine how many OS threads are available on the system using <a href="https://doc.rust-lang.org/std/thread/fn.available_parallelism.html?ref=dept-engineering-blog.ghost.io"><code>std::thread::available_parallelism()</code></a>.</p>
<pre><code class="language-rust">    for t in 1..=(threads as i32) {
        let t = t as f64;
        let upper_bound = (input * (t / threads)) as i32;
        let lower_bound = (input * (t - 1f64) / threads) as i32;

        handles.push(thread::spawn(move || {
            let mut sum: i64 = 0;
            for i in lower_bound..upper_bound {
                if i % 3 == 0 || i % 5 == 0 {
                    sum += i as i64;
                }
            }
            sum
        }));
    }
</code></pre>
<p>Next, we set up all our threads using <code>thread::spawn</code>, and pass in a <a href="https://doc.rust-lang.org/rust-by-example/fn/closures.html?ref=dept-engineering-blog.ghost.io">closure</a> containing the same calculation as <code>euler1_simple</code>. The trick to this code is that the range of elements being iterated upon must be split up and processed. The range for each handle to process is computed in <code>upper_bound</code> and <code>lower_bound</code>. As each handle completes, its output is pushed into a vector of <code>handles</code>.</p>
<pre><code class="language-rust">    let mut sum = 0;

    for h in handles {
        sum += h.join().unwrap();
    }
    sum
}
</code></pre>
<p>Since the calculations in each subset of the range are independent of each other, the final outputs are commutative and can be summed up with <code>sum += h.join().unwrap();</code></p>
<h2 id="solution-3-arithmetic-series">Solution 3: Arithmetic Series</h2>
<p>The sum of the multiples of 3 or 5 is a composite arithmetic series. An <a href="https://mathworld.wolfram.com/ArithmeticSeries.html?ref=dept-engineering-blog.ghost.io"><em>arithmetic series</em></a> is the sum of a sequence of numbers where each subsequent term can be calculated by adding a constant <em>d</em> to the last term. For example, for the sequence:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/inline-1-1.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>This can be represented as:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/equation-2-1.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>Naturally, it follows that the sum of this sequence is:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/equation-3-1.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>Cleverly, the series formula is derived by rewriting this sum in another way, with respect to the last value in the sequence:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/equation-5.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>Adding these two formulas together causes all terms involving <em>d</em> to cancel out, and we're left with:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/inline-6.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>Which simplifies to:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/inline-7.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>In the special case where each number in the sequence is a multiple of the first (e.g. 3, 6, 9, 12), then:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/inline-8.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>And the formula can be written as:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/inline-9.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>Which simplifies to:</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/equation-10.svg" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>This is the formula we'll implement in the code below.</p>
<p><strong>$PROJECT/src/lib.rs</strong></p>
<pre><code class="language-rust">#[inline]
pub fn euler1_series(input: i64) -&gt; i64 {
    let val = input - 1;
    let n_3 = val / 3;
    let n_5 = val / 5;
    let n_15 = val / 15;

    let sum_three = 3 * n_3 * (1 + n_3) / 2;
    let sum_five = 5 * n_5 * (1 + n_5) / 2;
    let sum_fifteen = 15 * n_15 * (1 + n_15) / 2;

    sum_three + sum_five - sum_fifteen
}
</code></pre>
<p>Recall that the original problem asks to calculate the sum of the multiples <em>below</em> <code>input</code>, which is why we first declare <code>let val = input - 1;</code>. This was implicitly handled in the <code>for</code> loops in the previous two solutions, because the upper bound of the range iterator is <a href="https://doc.rust-lang.org/std/ops/struct.Range.html?ref=dept-engineering-blog.ghost.io#fields">exclusive</a>. Next, we find the number of terms <code>n</code> by using the fact that the integer type in Rust implements division in such a way that the result is floored, so that the answer remains an integer. Finally, we implement the series formula for multiples of 3 and multiples of 5, add the results, and subtract the sum of the multiples of 15, because it is the least common multiple of 3 and 5.</p>
<h1 id="creating-benchmarks">Creating Benchmarks</h1>
<p>Replace the code in <code>benches/euler1_benchmark.rs</code> with:</p>
<p><strong>$PROJECT/benches/euler1_benchmark.rs</strong></p>
<pre><code class="language-rust">use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion_benchmarking::{euler1_par, euler1_series, euler1_simple};

fn criterion_benchmark(c: &amp;mut Criterion) {
    let input = 1000000;
    c.bench_function(&quot;simple&quot;, |b| b.iter(|| euler1_simple(black_box(input))));
    c.bench_function(&quot;parallel&quot;, |b| b.iter(|| euler1_par(black_box(input))));
    c.bench_function(&quot;series&quot;, |b| b.iter(|| euler1_series(black_box(input))));
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
</code></pre>
<p>In the above code, <code>.bench_function</code> defines a benchmark with a <em>name</em> and a <em>closure</em>. The name must be unique among the benchmarks in the project. The closure accepts one argument, <a href="http://bheisler.github.io/criterion.rs/criterion/struct.Bencher.html?ref=dept-engineering-blog.ghost.io"><code>Bencher</code></a>, which is a <code>Timer</code> struct used to iterate a benchmarked function and measure the runtime. <a href="https://docs.rs/criterion/0.2.6/criterion/fn.black_box.html?ref=dept-engineering-blog.ghost.io"><code>black_box</code></a> is a function that prevents the Rust compiler from pre-optimizing the function prior to runtime, which can result in a quicker benchmark time than real-world use cases. <a href="https://docs.rs/criterion/latest/criterion/macro.criterion_group.html?ref=dept-engineering-blog.ghost.io"><code>criterion_group!(...)</code></a> and <a href="https://docs.rs/criterion/latest/criterion/macro.criterion_main.html?ref=dept-engineering-blog.ghost.io"><code>criterion_main!(...)</code></a> are <a href="https://doc.rust-lang.org/book/ch19-06-macros.html?ref=dept-engineering-blog.ghost.io">macros</a>, which together generate a benchmark group and a <code>main</code> function that executes the benchmarks. This is also why the benchmark is conducted in a separate file from <code>main.rs</code>, which already defines its own <code>main()</code> function.</p>
<p>To run the benchmark, type <code>cargo bench</code> in the terminal. After several seconds, you should see an output like this:</p>
<pre><code>Benchmarking simple: Warming up for 3.0000 s
simple                  time:   [1.1540 ms 1.1564 ms 1.1589 ms]
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe
Benchmarking parallel: Warming up for 3.0000 s
parallel                time:   [224.43 us 227.90 us 231.96 us]
Found 10 outliers among 100 measurements (10.00%)
  5 (5.00%) high mild
  5 (5.00%) high severe
Benchmarking series: Warming up for 3.0000 s
series                  time:   [2.4586 ns 2.4645 ns 2.4708 ns]
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild
</code></pre>
<p>The <code>time</code> array represents a 95% confidence interval, where the mean execution time is the second value in the array. Outliers are determined using a modified version of <a href="https://bheisler.github.io/criterion.rs/book/analysis.html?ref=dept-engineering-blog.ghost.io#outlier-classification">Tukey's method</a>.</p>
<p>A more consumable version of this information is automatically generated and is available in <code>$PROJECT/target/criterion/{benchmark-name}/report/index.html</code>.</p>
<h1 id="under-the-hood">Under the Hood</h1>
<p>Before we compare the charts for each function, let's briefly explore what <em>Criterion</em> is doing behind the scenes. The first step of the process is warm-up, which involves running the function repeatedly for a short period of time in order to allow the CPU and OS caches to adapt to the new load.</p>
<p>Second, after the warm-up process is complete, measurements are taken by repeatedly running the function under examination.</p>
<p>Third, the samples are analyzed and the results are compiled into useful statistics for the user. This involves custom outlier analysis and linear regression analysis.</p>
<p>Finally, the current run is compared to the previous run to determine if any statistically significant change has occurred, which is reported to the user.</p>
<p>More details about the analysis process can be found at <a href="https://bheisler.github.io/criterion.rs/book/analysis.html?ref=dept-engineering-blog.ghost.io">https://bheisler.github.io/criterion.rs/book/analysis.html</a>.</p>
<h2 id="comparing-functions">Comparing Functions</h2>
<p>Let's look at the reports for each function.</p>
<p><em>euler1_simple</em><br>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Criterion_Screenshot3.png" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></br></p>
<p><em>euler1_par</em><br>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Criterion_Screenshot4.png" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></br></p>
<p><em>euler1_series</em><br>
<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Criterion_Screenshot5.png" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></br></p>
<p>The top left plot in each report is a <a href="https://en.wikipedia.org/wiki/Probability_density_function?ref=dept-engineering-blog.ghost.io">probability density function</a>, which depicts the average time per iteration of the function. The blue bar represents the overall mean. The shaded region represents the probability that execution of this function will take a particular amount of time, while integrating the area between two times will provide the probability that the function will execute within that range of time. The top right plot is the linear regression for the function. Each iteration is plotted in order and the y-axis represents cumulative time to execute all iterations up to that point. The slope of this line is given under <strong>Additional Statistics</strong>.</p>
<p><em>R</em><sup><em>2</em></sup> and <em>standard deviation</em> are also useful statistics to look at. In the context of the benchmarking process, a low <em>R</em><sup><em>2</em></sup> means in statistical terms that a significant amount of the variability between each iteration isn't attributable to differences in the function itself. However, we know that every time the function executes, the exact same code is executed, so what does that sentence actually mean? In this context, it means that the benchmarking process we've written is not behaving the same way in each iteration.</p>
<p>For <em>euler1_par</em>, this is in fact the case. I think the reason that this function in particular has a lot of variability is because it is implementing a parallelization routine, which relies on the processor to queue up tasks. How each <code>handle</code> is queued may vary based on the instantaneous load when each iteration is executed.</p>
<p>The <em>standard deviation</em> tells you (for a roughly normally distributed curve) that 95% of the samples landed between µ ± 2σ (mean time ± 2 standard deviations).</p>
<h2 id="advanced-features">Advanced Features</h2>
<p><em>Criterion</em> has a lot of advanced features for the statistically inclined, two of which we'll explore in more detail: <code>benchmark_group</code> and <code>bench_with_input</code>.</p>
<h3 id="criterioncriterionbenchmarkgroup">criterion::Criterion::benchmark_group</h3>
<p>It's apparent that the parallelized function(µ=227.90us) is nearly 1 order of magnitude faster than the simple(µ=1.1564ms) function, while the series function(µ=2.4645ns) is nearly 3 orders of magnitude faster. We can generate a combined report that summarizes the performance of all three functions by associating them with each other using <code>benchmark_group</code>.</p>
<pre><code class="language-rust">use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion_benchmarking::{euler1_par, euler1_series, euler1_simple};

fn criterion_benchmark(c: &amp;mut Criterion) {
    let input = 1000000;
    let mut group = c.benchmark_group(&quot;Euler 1&quot;);
    group.bench_function( &quot;simple - test 1&quot;, |b| b.iter(|| euler1_simple(black_box(input))) );
    group.bench_function( &quot;parallel - test 1&quot; , |b| b.iter(|| euler1_par(black_box(input))));
    group.bench_function( &quot;series - test 1&quot;, |b| b.iter(|| euler1_series(black_box(input))));
    group.finish();
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
</code></pre>
<p>In the above code, we follow nearly the same process as individual benchmarking, except we first create and name a <code>BenchmarkGroup</code> using <code>c.benchmark_group(name)</code>, which is merely an entity used to group related benchmarks for analysis and reporting. The compiled output is available at <code>$PROJECT/target/Euler 1/report/index.html</code>, while individual reports for each function are available at <code>$PROJECT/target/Euler1/{benchmark-name}/report/index.html</code>.</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Criterion_Screenshot6.png" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<h3 id="criterioncriterionbenchwithinput">criterion::Criterion::bench_with_input</h3>
<p>To thoroughly benchmark a function, it's important to test it across a range of acceptable values. <code>bench_with_input</code> helps test a wide range of cases more effectively.</p>
<pre><code class="language-rust">use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use criterion_benchmarking::{euler1_par, euler1_series, euler1_simple};

fn criterion_benchmark(c: &amp;mut Criterion) {
    let inputs = [100, 1000, 10000, 100000, 1000000];
    let mut group = c.benchmark_group(&quot;Multiple inputs&quot;);
    for i in inputs {
        group.bench_with_input(BenchmarkId::new(&quot;euler1_simple&quot;, i), &amp;i, |b, &amp;i| {
            b.iter(|| euler1_simple(black_box(i)))
        });
        group.bench_with_input(BenchmarkId::new(&quot;euler1_par&quot;, i), &amp;i, |b, &amp;i| {
            b.iter(|| euler1_par(black_box(i)))
        });
        group.bench_with_input(BenchmarkId::new(&quot;euler1_series&quot;, i), &amp;i, |b, &amp;i| {
            b.iter(|| euler1_series(black_box(i)))
        });
    }
    group.finish();

}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

</code></pre>
<p>Note that we added <code>BenchmarkId</code> to the <code>use</code> declaration for <em>Criterion</em>. Next, we create an array of input values to assess, as well as a new <code>BenchmarkGroup</code>. We then iterate across the array of inputs, and call <code>bench_with_input</code> with a unique <code>BenchmarkId</code> and a closure that passes input <code>i</code> to the function being tested.</p>
<p><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Criterion_Screenshot2.png" alt="Benchmarking Rust code using Criterion.rs" loading="lazy"/></p>
<p>A summary of all of our reports so far is available at <code>$PROJECT/criterion/report/index.html</code>. The tabular format for the <em>Multiple inputs</em> tests is a great way to compare multiple functions across a range of inputs, as well as slice the data by two explanatory variables - the function and the input. This level of detail allows us to explore relationships between multiple explanatory variables using a concept called <a href="https://en.wikipedia.org/wiki/Response_surface_methodology?ref=dept-engineering-blog.ghost.io">Response Surface Methodology</a>. Unfortunately, Criterion is currently lacking in the abilty to generate 3-dimensional graphs needed to explore this concept in more detail.</p>
<p>Benchmarking across a wide range of inputs helps you find use cases that suffer from sub-optimal performance. For example, I found that <code>euler1_simple</code> and <code>euler1_par</code> take a significant amount of time to process the maximum value available in <code>i64</code>, ~9.22e18, indicating that these functions should be modified to accept an integer type with a smaller number space as an input parameter.</p>
<h1 id="final-thoughts">Final Thoughts</h1>
<p>If you write code for a living, chances are that someone else will have to reference, re-use, or adapt your code for another purpose in the future. Therefore, it's important to be a good custodian of the code you write by considering performance and ensuring it can function as expected through the range of its intended use conditions and beyond. Benchmarking is a key tool for understanding how your code functions under the hood and making improvements in a scientific manner.</p>
<p>The benchmarking process is highly sensitive to the testing environment, so great care should be taken to ensure that tests are conducted in as similar environment to each other as possible, for example on the same machine with similar background loads between tests.</p>
<p>I have found Rust to be incredibly well-documented, among the best I've encountered in my career. The Criterion library is no exception, and as a result a lot of the material for this article was derived from the <a href="https://bheisler.github.io/criterion.rs/book/getting_started.html?ref=dept-engineering-blog.ghost.io">original documentation</a> for <em>Criterion</em>.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Crypto Through Immersion - [ShipIt.io Podcast]]]></title><description><![CDATA[What we're trying to do is break down our part one and two, how you can immerse yourself in this world to learn it the fastest and come to appreciate it. Let's talk about “do you have to care about the project or not?”]]></description><link>https://engineering.deptagency.com/crypto-through-immersion-shipit-io-podcast/</link><guid isPermaLink="false">625d89911d0634003dc619ad</guid><dc:creator><![CDATA[Brandon Aaskov]]></dc:creator><pubDate>Thu, 26 May 2022 18:00:59 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/machine-linkedin--2-.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html--><iframe title="Episode 15: Crypto Through Immersion - Part 1" allowtransparency="true" height="300" width="100%" style="border: none; min-width: min(100%, 430px);" scrolling="no" data-name="pb-iframe-player" src="https://www.podbean.com/player-v2/?from=embed&i=83f3u-11eebab-pb&square=1&share=1&download=1&fonts=Arial&skin=1b1b1b&font-color=&rtl=0&logo_link=&btn-skin=1b1b1b&size=300" allowfullscreen=""/><!--kg-card-end: html--><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/machine-linkedin--2-.png" alt="Crypto Through Immersion - [ShipIt.io Podcast]"/><p><strong>Brandon Aaskov:</strong> Hi, I'm Brandon Aaskov. And this is another episode of Ship It with me. Today our guest is Dave Merwin, who does exactly what I do at DEPT®. We've been running a bunch of crypto projects and trying to get people to understand this world and to understand what this blockchain space and technologies have to offer.</p><p><strong>Dave Merwin:</strong> I'm David Merwin, as Brandon said, and I've been an artist/creator/developer for the past 20 years. I'm super stoked about the stuff that we're working on because I feel like it's providing some unique opportunities and some new stuff to explore.</p><p>These days the internet feels like how it did in 1992 like anything was possible.</p><p><strong>Brandon Aaskov:</strong> Yeah. I definitely think it's a little alienating to some people too, because it's a new space, and looking at it from the outside in can seem crazy and hectic, and I think it could also seem overwhelming and confusing.</p><p>There's a lot to crack into here, but I think what we're going to try to do today is break down our parts one and two, how you can immerse yourself in this world to learn it the fastest and come to appreciate it. So yeah, let's dig into this right away. Let's talk about “do you have to care about the project or not?”</p><p>“Should I just go buy a crypto punk?” I can't afford it. “How do I define what's going to be a good NFT or am I just riding the hype train? What am I doing here?”</p><p><strong>Dave Merwin:</strong> The thing that I love the most about this is being curious about these things. “Do you have to actually care about the project?</p><p>Do you have to care about what you're getting involved in? Do you have to understand it?” I think if you are curious that's enough to get started. That curiosity, the ability to explore and dream about what could be possible is the gateway drug to all of this, right? Like how can I figure that?</p><p>What this is supposed to do and how do I want to be involved.</p><p><strong>Brandon Aaskov:</strong> For me, I would say that it's really easy to take cracks at it until it's important to you. I did that myself, especially early on when I was like, I don't get this. And then I realized one day that I had been buying digital packs of magic, the gathering cards for playing that game digitally.</p><p>And I was like, man, I'd love to have an actual representation of this ownership instead of this black box that I'm buying right now. And then it hit me. And so that was my early days of realizing how I could have value for something that I already enjoy. And the same thing is true for NBA top shot, NFL, all these sports, there's a lot of things behind it.</p><p>And I think that, as soon as it taps into your fandom, it really shows. I was recently telling Dave this story. There's someone we work with who said  “I don't really see what the big deal is with NFTs”. then Flo came out with their new, “NFL all day”.</p><p>And he missed the first drop and he was really upset about it. Like he was really bummed that he couldn't get that NFT and he had to wait till the next drop was coming around. So it's very easy to hate on it until all of a sudden it makes sense.</p><p><strong>Dave Merwin:</strong> I think what was exciting for me about this whole space, from the beginning as an artist and where I fell out of love with the internet, was probably the [00:03:00] moment when digital cameras came out and everyone started becoming a photographer.</p><p>The ability now to actually, even though there are so many questions out there, there's so much that's not figured out, but the idea that unique ownership can become a thing again and that as an artist and a creator. Exert some sort of rights over your IP in a way that's meaningful. If you have less than a hundred thousand followers, it definitely feels like this is the opportunity.</p><p>To do some really interesting things.</p><p><strong>Brandon Aaskov:</strong> Again that's a good point and a decent segue to mention. I like classifying it this way for people, especially if they're new to it. There's really two categories of NFTs that you'll see. One is on the art and collectible side. We had people who made a  $69 million piece of art.</p><p>You can think of them as modern-day Banksy in the NFC realm. And then you have the collectible side. The collectibles would be board API club or magic, gathering cards like there's the utility side. Now board apps also offer this VIP membership that gives you exclusive access to certain things, a chance to stand in line early for certain things.</p><p>So that's one example of that. It could be discounts, whatever there's always a utility type. Today, we're seeing those two things overlap in a big way. I think we'll still see utility NFTs have some kind of arch individual tied to them, but “will it have to be unique every time?” Probably not on the art side.</p><p>On the other hand, we'll definitely see maybe one NFT represent this particular digital piece of art. Now it's interesting. In the regular physical art world. I don't know if you remember that banana duct tape to the wall that's called the “Comedian”.  That I think has four separate licenses that people or any museum has the right to display that banana and replace the banana as needed for the art display.</p><p>So right there, you can think of that as like four separate certificates that show that as the real installation. Because I could duct tape a banana to my wall, but it doesn't mean that I have the right to “show off that art installation”. I think that we'll see the art side differentiate itself from the utility side of things.</p><p>But right now the collectible side of things is huge. It's a new version of collectibles with new baseball cards. I don't want to call them beanie babies, but some of them have that. And I think it's a good segue to saying, I think that there are a lot of signs that point to most of these NFTs going to zero Massari research. It is similar to the Forrester research. Crypto land recently put out a report for their crypto thesis for 2022.</p><p>And they have a pretty clear sentence in their saying that most entities are going to go to zero today, but the technology there is very sound and it'll become an asset class, on its own that will be here for a long time to come.</p><p><strong>Dave Merwin:</strong> Yeah, I think to pick up on what you're talking about with the collectibles, one of my big pushbacks, at least at NFTs in the beginning, was  “I don't care about PFPs, I don't want to coordinate.”</p><p><strong>Brandon Aaskov:</strong> I just answered a profile picture of the F is the F and profile, I guess that's like the Twitter avatar thing.</p><p><strong>Dave Merwin:</strong> Exactly, But the point “I don't care about that stuff and that's okay” does not represent the entire body trying to equate the idea that NFTs as art. It is to what you see when you see a board.</p><p>It is as if Ape is saying a 1955 Plymouth is representative of all transportation. It just doesn't make any sense. And that's not how things work and that's not what things actually are. If you look at it and go, most of this is crap to you. It might be, and to massage the point, it may go to zero, but that's not the point.</p><p>The point is that there are things in there that are interesting to various different people and with four and a half billion people on there, there's going to be a lot of variety.</p><p><strong>Brandon Aaskov:</strong> I've been trying to liken it to people. When cars were first invented, we went from horses to cars.</p><p>That was a big change. It wasn't that it was more similar to the industrial revolution where we had factory lines, all of a sudden now we have that stuff available to everybody. It expands the scope of what's possible in a massive way that I don't think people see like I'm a person that is freaked out about 3D printers.</p><p>Everything will be 3D printed in the future. You'll be printing out kidneys on your desktop. Maybe in time, this tech here changed a lot of things in a short period of time. Let's say you can't find a product that you like. Maybe I don't care about the NBA or the NFL or “sports ball” as people like to call it.</p><p>How do I get involved with something I can look forward to?</p><p><strong>Dave Merwin:</strong> I would argue again, it goes back to your own curiosities. So first of all, no one is going to hold your hand as your friends will, your buddies will, but the internet and the internet community are not going to hold your hand in this.</p><p>So if you're curious about photography, let's say black and white photography, or let's get real specific, like black and white seascape photography. If you Googled black and white seascape photography and FTS, you're going to find a collection. You may not like it. And maybe you want to contribute to it.</p><p>Maybe you want to participate as an artist, but the idea of going to an NFT marketplace and realizing your curiosities being fulfilled is not necessarily going to happen. The marketplace and your curiosity are diametrically opposed. When I'm looking for projects to get involved in, in order to find out more about, I really start with Google and [00:08:00] the topic I'm interested in. For instance, for me, I really love generative art, and I love the idea.</p><p><strong>Dave Merwin:</strong> Yeah. So, you read a piece of code. It takes in some variables and it generates R and it's both ends kind of thing. You're cooperating. The machines cooperate with you. We could go into this for hours, but the idea of, I want to go see that stuff.</p><p>It's going to be an art block. That IO is devoted to generative art, and they actually are doing a really cool project the way they're doing that. So anyway, I would suggest. Looking in places that you don't like and using that as the basis to make your decision on instead, go follow your curiosities and go look for where those projects are being represented.</p><p><strong>Brandon Aaskov:</strong> Yeah, that's well said. And I should also point out that not all NFTs do have art tied to them. Without going down this other rabbit hole and we'll avoid this today, I'll mention something, a recent product that was announced. It was a big deal. If you pay attention to this space, it was called “LinksDao”, where essentially you're, pre-buying a membership to a golf course that doesn't exist yet.</p><p>By all these memberships, the community has contributed to this coffer of money that will be used to go by the golf course where they'll all have this exclusive membership to it. So that is a version of an NFT that is very different from what we see today. So maybe that speaks to you if you're a golfer, maybe that was just like, "Hey, I get it now." Maybe that was the moment. It doesn't always have to be.</p><p><strong>Dave Merwin:</strong> Yeah, I feel like you just opened the door in the hallway, in the matrix. And then all the green light just like blasts out behind it. </p><p><strong>Brandon Aaskov:</strong> We're trying our best to dear listeners to knock it down as many rabbit holes as we typically go down.</p><h2 id="lets-talk-about-wallets">Let's talk about wallets </h2><p><strong>Brandon Aaskov:</strong> I don't know if you've ever been in a room with crypto people. I know it can be very alienating, but it's because they go so deep down rabbit holes and say a lot. We'll try our best, not to get too deep into the lingo. So let's go right into lingo. Let's talk about wallets. I guess I'll keep a high level, is that a wallet is effectively served up typically through some kind of mobile app or browser extension, but what it is, you can think of it like an email address.</p><p>It's a way to reach you and you can send and receive things from, and people can't log into it, but it's just a way for you to log into. And this new web three landscape, that's a really high-level overview. I don't know how you feel about that.</p><p><strong>Dave Merwin:</strong> And I think that's fantastic. And it leads to the description that wallets, hopefully, this is the only time I say web three in this entire, but drops.</p><p>So typically if you could have Facebook or Instagram, these different services, you have to log into their system and you have to authenticate yourself against their database with a wallet, you log into your wallet, and then you can take that wallet and go to other places. And your credentials go with you.</p><p>The other service is not supposed to know anything about who you are as an individual outside of your address. So the idea is I can take this information about myself and go to all these different places like open C and R blocks.io or whatever. Like I can move my information around with me interacting with them, but they don't get to know who I am.</p><p>So I get to keep my information private</p><p><strong>Brandon Aaskov:</strong>  That's the goal. It's funny. I think that people tend to focus on privacy, but I think that there's actually a much more community-involved with this new space, especially now, again like early web days, everyone's excited. There's a lot of this swirl around the community that I think we'll actually end up seeing.</p><p>I'll use a good example of Steve Aoki got in early with NFTs. He's got three board apes. He's got three mutant apes. We've already mentioned some mute names. A few times they are popular NFTs. I'll leave it at that. They were expensive. Celebrities love them. Steve Aoki has a vault open. </p><p>And I thought that just goes to show that people are willing to tie their wallet address to their showcase. That just shows what they've done to show what they've held. And while that does expose you to all of the transactions that have ever come in and, or out of that wallet, I think that people are generally okay with that.</p><p>We've gone from where my parents and my grandparents would default to everything, to what I do online is private. Unless I specify that. Our generation was everything I do online is public, unless I specify otherwise. And this new generation is more everything's public. Anyway, I might as well just shout it from the rooftops and be really careful about what I do online, because it's gonna be.</p><p><strong>Dave Merwin:</strong> And for those of you who might be freaking out when you hear that you have an option. So it, and we can go into the details of this later, but you can have a public and a private wallet. </p><p><strong>Brandon Aaskov:</strong> Yeah, it's true. We've simplified what a wallet is on the surface. It does get very deep and what you can do with them at a high level. It's “Hey, now everyone can be their own”.</p><p>But, with being your own bank comes some responsibility too, in terms of how you handle the thing. It comes with a lot of risks. So it's a good segue into talking about a little bit of lingo. Let's stick with us here. Versus non-custodial wallets, custodial wallet, being something where someone holds that wallet on behalf of you.</p><p>It's very similar to what happens today. When you log into something they're holding that profile on behalf of you in this case, your money, your assets, your NFTs could be tied to that wallet. So you have to trust the institution. That's holding it on behalf of you. There's a saying in crypto that says not your keys, not your wallet.</p><p>And that is definitely true here where you might want that responsibility. It comes with some risk too, right? Own your own wallet and move those things into your own wallet. You have to be [00:13:00] responsible enough to do it properly, and you do run the risk of doing it improperly. And if you think that's scary, then hopefully the institution that is holding it on behalf of you is trustworthy and that you trust them.</p><p>And that's okay because custodial wallets are okay. Hate on them or anything like that. But those are really the two differences, that this while we've been describing so far, would be considered a noncustodial wallet. You have to own the keys. And actually let's talk about that. What are keys? </p><h3 id="what-are-keys">What are keys? </h3><p>So when we say, I guess we should call it by what the lingo is in the world right now. I've heard two F two terms at seed phrase or mnemonic being that word that starts like Johnny mnemonic, starring Kianu Reeves from the early nineties. M N E M O N I C a. Yeah, a seed phrase essentially, instead of a password it's 24, 25, 26 different words that are in a specific order and you don't pick them.</p><p>They are generated for you. And then you'll see when you create a wall, it's write these down, put them somewhere safe. Don't take a picture of it. Don't write it on a post. If you lose this, you lose [00:14:00] access to this thing forever. And that is true. I can't stress that enough. We're going to scare some people. So what's a good solution there for people who. I don't know. I just remember I use one password for everything I'm supposed to remember. 25 words. What do people do about that?</p><p><strong>Dave Merwin:</strong>  I think it's a great question. I think one of the things that you should not do is take a picture of it with your phone. Don't put that it's backing up and sinking to Google drive or Google photos or Apple or whatever.</p><p>So what I do personally, I'm going to share my practice. I went to my local hardware store. I put the safe in a closet. It's a fireproof safe. And when I create a phrase, I write it down on a piece of paper, and I label what it is. And then I put that thing in a safe, don't take pictures of it. I don't do any of those things.</p><p>And I know it sounds like a lot of work, but it's definitely like the first time. And this happens all to me as an adult with ADHD. I know that this is Evan's story. So I have no problem writing things down because the first time you forget it and notice you have $150 in that wallet.</p><p>And you forget, like it's gone. [00:15:00] I'm pretty diligent about writing it down, putting it in the safe and closing the safe. And then if I forget the combination to the safe, at least then I can bust it open with a sledgehammer or something.</p><p><strong>Brandon Aaskov:</strong> You'll figure out how to get into the safe somehow again, like going back to you had that responsibility.</p><p>You have to make sure that you can protect yourself from bad actors. And that means in a lot of cases, using some sort of physical non-connected non-digital means of storing your password, a company called ledger, which makes hardware, wallets, called storage wallets, which we can get into, but they also make other products which are called “crypto steel” and “bill photo”.</p><p>And basically they're versions where you can store that 25 seed phrase, whatever it is and keep it in something that is fireproof itself, is crushproof, and withstand an amazing amount of force. So there are basically different ways to do this, but the proper way to do it is offline.</p><p>Whether it be physical, whatever. And as Dave mentioned, he put it in a fireproof safe. You want to protect it from the elements and God forbid, there should be some accident at your home. We have insurance [00:16:00] for that kind of thing, but they won't cover your crypto wallet again. If all you have is a password manager and you trust it, that's an option.</p><p>It's not my favorite option, but it's an option. Just don't write it down on a post-it or take a picture of a police officer.</p><p><strong>Dave Merwin:</strong> Yeah, the picture is the worst. Probably the worst thing you could do, unless it's like a Polaroid.</p><p><strong>Brandon Aaskov:</strong>  Maybe I can see that being okay. That's probably like the one, good use of Polaroids.</p><p>Now it is. </p><p><strong>Dave Merwin:</strong> So let's talk about, f you pull the right, if you're listening, we can help you design your app and get that started for you. </p><p><strong>Brandon Aaskov:</strong> Exactly. So MetaMask. Popular term around wallet, terminology, and people think that's the blockchain thing I do now MetaMask and I guess it's worth pointing out that MetaMask is built on top of the Ethereum blockchain.</p><p>It does work-based blockchains, which I won't go down that rabbit hole either. There are other things that work with, besides the Ethereum, which has polygon, you can also connect to test networks and do a bunch of advanced things with it. In fact, it's a little overwhelming because of that for first timers.</p><p>And on top of that, Ethereum, I don't know if you know this or not, but Ethereum is known for having very high transaction fees right [00:17:00] now. So I think that for the sake of this discussion, we'll just not talk about MetaMask. Let's talk about maybe what's a great user-friendly wallet.</p><h3 id="whats-a-great-user-friendly-wallet">What's a great user-friendly wallet </h3><p>User-friendly blockchain, where the transaction fees are low there. People can get involved.</p><p><strong>Dave Merwin:</strong> I actually have a bunch of wallets that I don't even use. And the reason is I went through and explore. What the user experience was like for the wallets. Does it make sense to me? Do, does it give me the control that I want?</p><p>That kind of stuff. So for me, it's, the rules of thumb are one. So I get pissed when I open it. Like when I open it, is it confusing? Do I have to hunt and pack to try to find the information like it needs to just make sense for what I'm trying to do? So the ones that I found on Solano Phantom is a really great wallet to me.</p><p>It's super easy on algo. It's my algo. So then, my algo browser extension, while if you're a developer, that's really fun. </p><p><strong>Brandon Aaskov:</strong> Yeah.</p><p><strong>Dave Merwin:</strong>  There's a lot you can do, but in both of those instances though, Mel, my algo, the UI is a little rough, but the [00:18:00] functionality is all there. So I get real message confirmation.</p><p>So if I do a transaction, I get confirmation that something happened or it failed or it's timing out, or something is right. </p><p><strong>Brandon Aaskov:</strong> You get some sort of information back in a meaningful way and assets governance.</p><p><strong>Dave Merwin:</strong> My assets, what I own, are front and center. And so like with Phantom, eh, I can see what I own and what I don't own and what's happening and where I've staked stuff.</p><p>And even the liquidity tokens that I have, the information is there available to me so that I can track what's going on. So to me, it's about, does the user experience make sense to me? Am I getting. meaningful information when it comes to the confirmations? If you think about it, like with your bank, you do a transaction with your bank and then you get this like weird ledger three weeks later that hasll of this. Like how, I don't even know what this means. Like, why are you sending this to me? Whereas I, feel like a wallet that just says, this is how much went out. This is how much came [00:19:00] in. This is what happened and here's the address. And then links out to the actual blockchain to see the transaction in real-time.</p><p>That's also a super.</p><p><strong>Brandon Aaskov:</strong> Yeah, I know. And when people do that, they go though, that's called a block Explorer. When you go to one of those things to explore what the transaction was and where it came from, where it went to and what happened, what block it was in what time, all those details. It's an overwhelming amount of information to look at, and it's boring.</p><p>I think that just goes to speak to the user experience stage that we're at, we're still early and just like the early web and like the early nineties it's awkward and clunky. So that Phantom wallet that Dave mentioned is a Solano based wallet for the salon of blockchain. And the user experience is very good.</p><p>It's very friendly. I think it's a fantastic first wallet choice. Just exploring the space. So let's put ourselves in the seat of the person who's getting involved. They have now gotten a salon, a wallet. Now I should point out in part one. We're not gonna talk. Buying cryptocurrency and moving it into your wallet or even acquiring NFTs.</p><p>We're in browse mode right now. We're just going to be checking some stuff out and understanding this wallet thing and how do I [00:20:00] connect with it and all that stuff. So we've got a Phantom wallet. It's a browser extension. And am I using Chrome here? Probably. Yeah. In fact, if you're not using, you're using safari, please stop.</p><p>They're just way behind other browsers and there's bugs in it and security issues. And just try to avoid that, but any Chrome based browser. So I'll say Chrome. Actually I know for facts, this works in Firefox edge, which is the Microsoft internet Explorer. Nowadays you've got “Brave”, which is the auto based browser.</p><p>It's kind of privacy-focused. Now there's a bunch of browsers out there that support these connections, these browser extensions for a second. </p><p><strong>Dave Merwin:</strong>  When we were talking earlier about NFTs doing things. So you know how like everyone is used to having browsers serve ads to you and track you and all that stuff, brave will pay you in a native token.</p><p>Brave for letting advertising come to you and you can get paid for participating in those ads or looking at those ads full disclosure.</p><p><strong>Brandon Aaskov:</strong>  I've been using “Brain” for a little while. So I think at this point I've earned close to [00:21:00] 30 VAT and I actually, I kicked back some of that. So you can actually specify which sites get some of your share and how much that share is of what you've earned.</p><p>And so I kicked some of that back to my favorite creators on YouTube and my favorite sites that I go to, they get different percentage breakouts that I get to choose. And so it's nice to put that in my hands. I could say they get nothing, but honestly it felt slimy. And I'd love to kick back to those people and encourage them to move away from traditional models into these new models.</p><p>But yeah, I'm 30, but it's only what, like a plus than a dollar now because we're hitting crypto winter. But at one point that would have been like 60 bucks. Just for browsing the web the same way I always do. Yeah. So that was a good little tangent on brave. If you want to check out a new browser and you want to dip your toe into the crypto game, that's another one you can check out.</p><p>We've got our Phantom wall extension in the browser. Let's say we're past the seed phrase. You've written it down. You started somewhere safely, you took a Polaroid of it, and now it's in a safe place. It's not going to get destroyed. And it's certainly not online and not in the cloud. Great. Good for you.</p><p>Now you're going to have to make a password. It didn't. I just have a password. What was the seed phrase? Why is that different than a password? The seed phrase basically allows you to [00:22:00] reestablish your bank vault at any time. So let's say you lost your computer. It fell into the ocean.</p><p>What am I gonna do? Oh yeah. That seed phrase. Now you can regenerate all that stuff. Based on that phrase you had to write down before. Whereas the password is really more of a, I want to keep my snotty kids out of this thing. So they don't go by the teas that I don't want to defy. It's almost like the action code on your TV for renting movies or something like that.</p><p>You basically confirm your password. Maybe this is the same password you use for everything. I try not to do that, but you may trust everyone else. You share that password with, I dunno. So you have that password assigned to this wallet now. You're you've opened it. You've got nothing in there.</p><p>There's no assets. What does that mean? What's a Sol?</p><p><strong>Dave Merwin:</strong> Oh my gosh. I thought that was awesome. So the Sol is the actual token on that blockchain. </p><p><strong>Brandon Aaskov:</strong> So it's seeing like for Salonos.</p><p><strong>Dave Merwin:</strong> Yeah. So if you want to conduct transactions on the salon of blockchain, you need some variation of a Solano, a coin.</p><p>And if you're going to buy an NFT on the salon of blockchain, like from salon art or something [00:23:00] like that, you'll need to have sold it in order to do it. And in order to get that Sol, it has a USD value right now, as I'm looking at it, Sols price is a hundred dollars per soul. So if I put a hundred dollars in, I'll get one Sol in return that I can then use to go buy an NFT or whatever.</p><p><strong>Brandon Aaskov:</strong> Yeah, exactly. It's just, that you're swapping your regular dollars for this digital currency called Sol. And since the price fluctuates right now and you're in a, what was it maybe a couple of months ago? I think one Sol was around 200. So it slides up and down and the market responds as well.</p><p>So the pricing of things will reflect the current value of Sol. Okay. We're in Solano, we've got our Phantom wallet. That's for Solana. I should point out to you and I won't go deep down this, but because of Ethereum is a different blockchain, my Phantom wallet, won't just work with OpensSea, which is based on top of it.</p><p>You would need the theory-based wallet for that. This is again, we're in the Salono world. Now it's a different blockchain with similar features and a similar ecosystem, but it's much more targeted [00:24:00] toward NFTs. And it's very user-friendly and the transaction fees are cheap. I believe it's what is it like?</p><p>Point zero, zero, zero one percent or something like that? Every transaction is very affordable in that regard.</p><p><strong>Dave Merwin:</strong> So for instance, if you are going to buy a hundred Sol right now, which is, if you're going to spend a hundred dollars to buy Salano, you would get 0.98 Sol and you would pay a 78 cents as a network fee.</p><p>Whereas on the Ethereum, if you wanted a hundred dollars worth of Ethereum, you'd probably pay $25 in fees. </p><p><strong>Brandon Aaskov:</strong> Somewhere between 20 and 30 bucks. I just did it the other day. It's something every time I hate, it's actually, it's definitely off-putting for newbies. So that's one of the reasons why a Salono is a great choice to start.</p><p>Yeah. Great user experience across the board. In that regard, I should point out too, for any developers that might be listening or interested in this, there is a standard that drives a lot of these wallets called wallet connect. So you can go check out the docs to that, to see how they interact with wallet connect and basically.</p><p>I think there's at least one [00:25:00] wallet on every major blockchain that has a wallet connect, driven applications on it. So you can allow people to connect to your website using wallet connect. All right. So they've got this wallet. They have not stolen it. We'll talk about acquiring that cryptocurrency differently.</p><p>And there are no entities in and yet, because I don't have anything to buy it with, but I want to go start poking around at some places. What should I do? Where should I go?</p><p><strong>Dave Merwin:</strong> Yeah, that's a good question. I would go again. If you're on Solano, you might like, for instance, if I Google something like black and white photo.</p><p>Solana, you'll start to come across, like just, this has been a couple of seconds and I'm already coming across salon, NFT photographers, and where you can go look at their work and you can explore some of the creativity of what's going on in the different networks that they're on. So I personally would not start with going to.</p><p>Service like Solano art or one of those I would instead go look at what I'm curious in and what I can, where I can go [00:26:00] find that stuff. </p><p><strong>Brandon Aaskov:</strong> I see. Yeah. It's worth pointing out that you think that there's a misconception, thanks to open. See that you have to go to a marketplace to get these NFTs. hat's actually very much not true and it's especially not true in the salon.</p><h2 id="cool-projects">Cool projects </h2><p>There is a piece of software that they have and the open-source ecosystem called Mediplex and a little piece of software inside of that called candy machine that allows people to set up these sites and easily allow other people to come connect their wallet. Like we mentioned that Phantom wild, so you can connect and log in.</p><p>And then I click a button that allows the user themselves to actually mint and create one of those NFTs. That's still remaining, essentially taking a gumball from the candy machine and it is a pretty easy way to get people up and running. So you'll find a lot of standalone, running that software. So you might not find this art you're looking for by going to a magic Eden or a salon art, or maybe even an open, see if you're again in an Ethereum world, but you can find individuals.</p><p>The homepage is “insights” that will drive the NFT sales. So it's Google. [00:27:00]</p><p><strong>Dave Merwin:</strong> Yeah. So for instance even while we're talking, I just found this site that has a really cool mix of both PFPs and photography and art. And it's got a whole bunch of stuff it's called the exchange.art.</p><p><strong>Brandon Aaskov:</strong> She also pointed out that non-key top-level domains like art.</p><p>That XYZ dot labs are actually, that's not even one, but there's a bunch of, you'll see a bunch of those weird top-level domains.gallery and the NFT world. So don't be put off by that. So that's it.</p><p><strong>Dave Merwin:</strong> A lot of artists won't necessarily have a homepage. Like one of my favorite painters, Trevor Jones did a project with Ice Cube and NFT project, and it's a fabulous project.</p><p>It's so cool. He did a bunch of paintings to lyrics that ice cube wrote and then did some augmented realities that when you looked at the painting, it brought up ice cube. A custom music video for that piece. It's such a neat project, but Trevor Jones has his own homepage and his home personality, and he's a really well-known painter and that kind of stuff.[00:28:00]</p><p>But a lot of artists are just trying to launch stuff and they won't have a homepage. So they might be discord or on Twitter or just on Instagram. So if you don't find a homepage that promotes an artist, don't panic, it just means they're busy producing art. And they're just trying to. Easiest way to talk about their stuff without having to do a whole bunch more work on creating a website.</p><p><strong>Brandon Aaskov:</strong> Yeah. That's true. Not everyone's oh, I'll just whip up some metroplex and deploy my candy machine. Like they don't know what that means in three artists, they just want to have a community and get going. Yeah, that's a good point. Twitter being almost like the landing page, the homepage for a lot of projects is also not uncommon and finding.</p><p>I have a large community of people around a project. You'll usually find them in either discord or telegram. Those seem to be the two popular ecosystems that drive a lot of these communities and these projects. Yes, that's right. If you don't find a homepage, don't feel discouraged. There's probably more digging to do.</p><p>Unfortunately, just like that early web, it's not very well indexed. So there are a lot of new projects that come up all the time and you have to do. That product was a really great example. We talked about some [00:29:00] marketplaces to check checkout as well. I think we mentioned it like magic Eden salon art.</p><p>There are a few others. This one here is exchanged art. That's good to poke around, but think about cards that speak to you. </p><p><strong>Dave Merwin:</strong> So I will say one of the things I've started to see come up, and this is totally just no, no opinions, no judgements, but there is a lot more NSFW stuff coming out.</p><p>And as people are getting close to the idea of oh, I don't know how to create good art, but I sure do know how to drop porn. So that's definitely becoming more popular. Buyer beware. If you click on the NSFW link on a marketplace, you're going to get what you expect.</p><p><strong>Brandon Aaskov:</strong> So just don't have your kids sit next to you while you're poking around here.</p><p>Exactly. Childspace, it's still the wild west. Yeah. So let's wrap this one up here, but we can move into what to expect, how the whole process goes for buying an NFT, reselling that if people do that next time and getting acquired  to your crypto. So we've mentioned some marketplaces to check out, put yourself in the mindset of someone who's doing that.</p><p>Again. I mentioned the Dow thing, actually not I'll make that my pick because I think that's right. We do picks at the end of these. So I'll make my pick links Dow which is if you're a [00:30:00] golfer, go check that out. And there's a similar one, which is the Australian open. They did something like that.</p><p>I think the links down one is much more avant-garde and that it's, “Hey, let's raise a bunch of money”. Let's forget all the members of this thing already, that doesn't exist. Now let's go buy a golf course. And that has not, first of all, it hasn't come to pass yet. So let's see what happens there, but also it's a great idea and it might, it could spawn an entirely new generation of these clubs. </p><p>So that might, it's a little bit different from the art stuff. So I like pointing that out and that's why I recommend that one. If you want to go poke into that project and see what they did there.</p><p><strong>Dave Merwin:</strong> So I cheated, cause I said Trevor Jones earlier, so I'm going to switch on you and come up with a different one.</p><p>So I'm a big fan of the helium project. And what they're trying to do is create essentially a people's network. So like a wireless economy where if you stake your coins, this...</p><p><strong>Brandon Aaskov:</strong> is not a salon of project, correct? </p><p><strong>Dave Merwin:</strong> Yeah. Sorry. No, that's its own thing. Oh, do you want a salon for a  project?</p><p><strong>Brandon Aaskov:</strong> Yeah. I just want to point out that Helium's just unblocked yet and I was just informing them of something.</p><p><strong>Dave Merwin:</strong> Essentially the helium [00:31:00] piece is designed to take wireless networking away from the big corporations and make it available to the people. Now I realize that's a bit naive because where are they getting their access from? But that's a whole different question. But anyway, I think it's a really interesting project and I actually own a little bit of it and it seems to be doing okay. </p><p><strong>Brandon Aaskov:</strong> Yeah, actually maybe we'll wrap up on this note since it comes up a lot and it came up a lot recently in a talk we did with DEPT®. There were a bunch of questions in the chat and I ended up answering those afterward and it was a lot of them centered around aren't these things supposed to be decentralized.</p><p>Why are they all so centralized? And there are two categories there. One is how the capital is centralized. I don't want to talk about that. That's a different story, but the tech itself, there's a lot of these hosting services where we have cloud providers today that are owned by big tech. And it is the best way to deploy things quickly and easily across the world.</p><p>So a lot of these products are hosted on those big tech platforms. We're still so early on, we're still making building blocks every day in this space. What the helium project, he just referred to as, is a great example of is another building block toward a brighter future where we don't [00:32:00] have all the centralization.</p><p>And there's another project that's so complicated and convoluted. I dare you to go look at it, but it's called an internet computer, and it's supposed to be replacing some cloud providers we have today. And you know what, like I said, it's complicated. There's a lot of lingo. We're still early days. We have a long way to go.</p><p>We're making baby steps. And if you feel confused and a little lost, that's normal because it's a crazy wild space. There's information blasting at you from all sides and people telling you what's cool and what's hot and what's coming up and what's gonna, Just trust your instincts, try to ignore the noise and just look at a project, turn off the rest of it, read the stuff.</p><p>And if you get any of that spidey sense of this feels not right to me, there's too much lingo, too much buzzwords then. Great. It's not a project for you. It's probably not a good product for anybody. Move on to the next one. There's plenty of things to look at. Don't feel like you have to understand everything.</p><p>If they're not doing a good job of explaining it to you, that's on them.</p><p><strong>Dave Merwin:</strong> That's a great point. We can be done on that. I don't think that's. <strong>Brandon Aaskov:</strong> That's beautiful. Yeah. All right. That's another episode of shipping. Thank you, Dave. And then join us next time. We'll go over all the buying stuff [00:33:00] and tell you what to expect so that you can actually fill your wallet with something meaningful.</p><p>Awesome. All right, thanks. Have a good day.<br/></p>]]></content:encoded></item><item><title><![CDATA[Why You Need KYC]]></title><description><![CDATA[Know Your Customer (KYC) is an integral part of any Anti-Money Laundering program. In this article I'll go through what KYC is, why you may need it, and how to choose a SaaS provider that fits your needs.]]></description><link>https://engineering.deptagency.com/why-you-need-kyc/</link><guid isPermaLink="false">626fff42dae0a9003d0d2067</guid><dc:creator><![CDATA[Dallas Huggins]]></dc:creator><pubDate>Mon, 09 May 2022 13:59:05 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/05/pexels-anna-shvets-4482900--1-.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/05/pexels-anna-shvets-4482900--1-.jpg" alt="Why You Need KYC"/><p>Unless you work in the finance industry, it’s likely you’ve never heard of anti-money laundering (AML) or know your customer (KYC). Up until somewhat recently I had not heard of either of these acronyms, let alone understood the complexities around them. At DEPT® we’ve been working with clients to build NFT marketplaces, often using the open source <a href="https://www.deptagency.com/en-us/insight/dept-launches-first-ever-open-source-platform-for-creating-an-nft-marketplace/?ref=dept-engineering-blog.ghost.io">white-label NFT marketplace solution</a> we developed for the Algorand blockchain as the foundation. I’ve needed to better understand these concepts since we’ll be adding support for KYC in the marketplace implementations for our clients, as well as integrating into the white-label solution, though the specific KYC needs will vary based on the business. </p><p>We’ll go through AML/KYC at a high level and discuss options available to you and what to look for when choosing a SaaS provider. Heads up, there’s going to be a whole lot of acronyms in this article, and in the AML/KYC world there’s an acronym around every corner. Below is a cheat sheet of relevant acronyms to refer to.</p><p><strong>Acronyms</strong></p><ul><li>KYC = Know Your Customer</li><li>AML = Anti-Money Laundering</li><li>CIP = Customer Identification Program</li><li>CDD = Customer Due Diligence</li><li>EDD = Enhanced Due Diligence</li><li>SSN = Social Security Number</li><li>PII = Personal Identifiable Information</li><li>PEP = Politically Exposed People</li><li>BSA = Bank Secrecy Act</li><li>MSB = Money Services Business</li><li>FinCEN = United States Financial Crimes Enforcement Network</li><li>BaFIN = German Federal Financial Supervisory Authority</li></ul><h2 id="what-is-kyc-and-who-needs-it">What is KYC and who needs it?</h2><p>AML legislation is enacted with the intention of preventing financial crimes, and the relevant laws and regulations vary by country, though there are accepted international standards. Financial institutions have to abide by local laws and regulations, and a specific AML program is put in place according to the risk profile of the business and where it’s operating. KYC is an integral part of any AML program, where a customer’s identity is verified, and they are screened and assessed for their risk.</p><p>In the United States, the Financial Crimes Enforcement Network (FinCEN), <a href="https://www.fincen.gov/index.php/resources/statutes-regulations?ref=dept-engineering-blog.ghost.io">a bureau within the Treasury Department</a>, is responsible for enforcing the Bank Secrecy Act (BSA). The BSA is a set of laws and regulations for financial institutions that require such institutions to assist government agencies in detecting and preventing money laundering. These laws apply to money services businesses (MSBs), which <a href="https://www.fincen.gov/money-services-business-definition?ref=dept-engineering-blog.ghost.io">are defined by FinCEN</a> to include any person doing business as a currency dealer or exchanger, a check cashier, an issuer or seller/redeemer of traveler's checks, a money transmitter, or the US Postal Service. MSBs are required to implement an anti-money laundering (AML) program, as well as keep records and file reports to FinCEN. </p><p><a href="https://www.circle.com/?ref=dept-engineering-blog.ghost.io">Circle</a>, for example, is <a href="https://support.circle.com/hc/en-us/articles/212059046--Is-Circle-regulated-?ref=dept-engineering-blog.ghost.io">registered as a money transmitter</a> with the U.S. Treasury Department, requiring them to comply with BSA laws and have an AML program in place. When creating a Circle account there are KYC verification steps needed, and when using Circle’s services, businesses can enter a KYC reliance agreement which requires them to establish their own AML program according to Circle’s guidelines. In higher risk situations KYC is required to exchange funds using Circle. </p><p>Outside of the United States, there are other regulating bodies responsible for enforcing their country’s laws and regulations to prevent money laundering. An AML program should be developed according to the nature of the business as well as the relevant local legislation. KYC protocols will need to be put in place to understand the customers you’re doing business with.</p><h2 id="what-does-identity-verification-look-like">What does identity verification look like?</h2><p>A KYC plan can have multiple components, and starts with a customer identification program (CIP) where information is gathered from a customer and verified. Identity verification can include gathering identifying information for the customer (i.e., name, address, date of birth, SSN, etc.), biometric measures such as photos and videos, checking documents (i.e., passport, proof of residency, etc.) or even person-to-person verification where a trained representative verifies the individual’s identity. The type of verification done will depend on the AML program and the risk assessment for the business.</p><p>Let’s go through an example verification flow for a customer on a platform where KYC is required. The CIP includes information gathering, document verification, and biometric verification. First a customer creates an account and provides information about themselves during onboarding, including their full name and date of birth. After onboarding, the customer would like to make purchases on the platform, and needs to verify their identity. They are prompted to confirm the details they provided during onboarding, and provide additional details such as an identification number. The customer is in the US and provides their SSN. A verification check is run, and if there’s a confirmed match that’s cleared verification, the customer can continue with the purchase. However, in this case there’s an issue with the verification (i.e., suspicion or no clear match), so the customer is asked to provide document(s), of which they choose to use their passport. After uploading the passport successfully, the customer is prompted to take a selfie, and facial recognition is used to assess the likeness of the photo from the document. If there’s a cleared match, the user is considered verified on the platform and can continue with the purchase.</p><p>The next phase of the KYC plan is customer due diligence (CDD) or enhanced due diligence (EDD), where a customer is screened and background checks are performed to assess their risk. EDD is carried out for higher risk customers, which includes a more rigorous assessment. In the example, the customer’s details would be checked against government watchlists, PEP and adverse media lists, as well as sanctions lists. If there’s a confirmed positive hit, there needs to be a plan in place on how you’ll freeze funds and report the match to the relevant government bodies.</p><p>The final step of a KYC plan is continuous monitoring. Continuous monitoring is the process to re-screen customers against watchlists, sanctions and PEP lists, and is important to identify increased risk after the initial verification. Another form of ongoing monitoring that may or may not be important for you to do regularly, is monitoring to identify changes in a customer’s details (i.e., address or name change).</p><p>There are numerous providers who have products which can be leveraged to implement KYC for your business, from identity verification to screening and ongoing monitoring. Many providers have similar features, and it can be difficult to know what to look for. Next we’ll go through what you may want to look out for when deciding on the provider. </p><h2 id="how-do-you-choose-a-provider">How do you choose a provider?</h2><p>When you’re looking for a KYC provider, there’s multiple things to consider. We’ll discuss each in more detail. </p><ol><li>What detection techniques are supported?</li><li>Is there customer screening and ongoing monitoring?</li><li>What options are available for relevant locales?</li><li>How does it work?</li></ol><h3 id="what-detection-techniques-are-supported">What detection techniques are supported?</h3><p>Services may use different terminology for features provided, but often include verification of personal identifiable information (PII) and proof in the form of documents such as government identification. Another common verification method is biometric verification such as facial recognition using photos/videos, and some services offer person-to-person verification where customers are meeting with a live representative. </p><p>Depending on the provider, different detection techniques may be offered. The type of verification needed should be determined based on the unique needs of the business. Be aware of local regulations which could dictate which techniques are required. For example, in Germany video detection is mandatory under AML law <a href="https://www.bafin.de/SharedDocs/Veroeffentlichungen/EN/Rundschreiben/2017/rs_1703_gw_videoident_en.html;jsessionid=A08CF19723EC8FB455D94CAEBA24BDF3.2_cid502?nn=9451706&ref=dept-engineering-blog.ghost.io#doc9318762bodyText1">according to the Federal Financial Supervisory Authority (BaFIN)</a>. </p><h3 id="is-there-customer-screening-and-ongoing-monitoring">Is there customer screening and ongoing monitoring?</h3><p>As discussed, CDD/EDD and ongoing monitoring are important components of a KYC plan, and you’ll want to confirm there’s support for all stages of your business’ plan. CDD is very important in preventing money laundering, where customers are screened against watchlists, sanctions and PEP lists. Ongoing monitoring is often done in addition, screening against these lists on a regular basis. Check if there are webhooks available to get updates from the provider when risks are identified. </p><p>It also may be important to identify changes in a customer’s information, such as a name or address changes. Determine if you need this support and at what interval you need to get updates, and see what providers offer in this way. </p><h3 id="what-options-are-available-for-relevant-locales">What options are available for relevant locales?</h3><p>Find a KYC provider that offers document support for any countries where your business is operating. If you plan to use an all-in-one type solution, confirm localization is offered for languages you plan to support. </p><h3 id="how-does-it-work">How does it work?</h3><p>There’s a lot of variability in the different providers, so you’ll likely want to try out any solution and the tools provided before making a final decision. Are there relevant SDKs available? What’s the user flow going to look like with this tool? How’s the documentation? If it still seems like a good solution after an initial scope, request a sandbox account for future testing. </p><p>In addition, make sure you’re comfortable with how to find customers in your system, and know what information you need to store in order to match your customers to the records kept by your KYC SaaS provider. Depending on the AML program, you will be required to hold onto customer records for a certain length of time. For instance, FinCEN requires records be kept for at least 5 years in some cases, and you’ll be expected to provide that information if requested. </p><h2 id="final-thoughts">Final thoughts</h2><p>An anti-money laundering (AML) program and it’s know your customer (KYC) requirements will vary based on the business and the risk associated. The components of a KYC program can include: a customer identification program (CIP), customer due diligence (CDD) and enhanced due diligence (EDD), and ongoing monitoring. Identify KYC SaaS providers that have relevant detection techniques and screening capabilities, and work with them to determine the best plan based on your business’ needs.</p><p>You’ll want to be careful about choosing a solution up front, especially if there’s reporting requirements that make you responsible for keeping customer records for a length of time, since you may need to persist your subscription with that provider for the amount of time you’re required to hold onto records. </p><p>I hope this information was a helpful jumping off point in the world of AML and KYC verification, and welcome any feedback. If this is a lot to absorb or you’re still feeling lost, I know a great agency you can reach out to :) </p>]]></content:encoded></item><item><title><![CDATA[Best Practices for Terraform AWS Tags: Part 2]]></title><description><![CDATA[AWS tags are key-value labels you can assign to AWS resources that give extra information about it.]]></description><link>https://engineering.deptagency.com/best-practices-for-terraform-aws-tags-part-2/</link><guid isPermaLink="false">6268a5aedae0a9003d0d1fca</guid><category><![CDATA[AWS]]></category><category><![CDATA[Platforms]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[DevOps]]></category><dc:creator><![CDATA[Jirawat Uttayaya]]></dc:creator><pubDate>Wed, 27 Apr 2022 03:37:43 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/AdobeStock_111395208_small-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/AdobeStock_111395208_small-1.jpg" alt="Best Practices for Terraform AWS Tags: Part 2"/><p><em>Thank you for making <a href="https://engineering.deptagency.com/best-practices-for-terraform-aws-tags?ref=dept-engineering-blog.ghost.io">the original Terraform AWS Tags blog post</a> so popular. As of December 2021, it is a first page Google and Bing search result for <code><a href="https://www.google.com/search?q=aws+terraform+tags&ref=dept-engineering-blog.ghost.io">aws terraform tags</a></code>.</em></p><p><em>Update Feb 17, 2022: I gave a <a href="https://www.hashicorp.com/resources/best-practices-for-terraform-aws-tags?ref=dept-engineering-blog.ghost.io">talk at HashiCorp 2022</a> about this blog post.</em></p><p>I gave a presentation about Terraform AWS tags at the <a href="https://www.meetup.com/Boston-Devops/events/281678840/?ref=dept-engineering-blog.ghost.io">Boston DevOps Meetup</a>. The attendees and I had a lively discussion afterwards. The feedback to that talk inspired me to write this follow-up article.</p><p>The main topics of conversation of my presentation centered around using Terraform AWS default_tags in production. The following is the list of the specific topics, bugs, and best practices that I felt would be helpful to make available in this blog post.</p><h2 id="workarounds-for-defaulttags-bugs">Workarounds for default_tags bugs</h2><p>Especially when refactoring existing Terraform code to use <a href="https://learn.hashicorp.com/tutorials/terraform/aws-default-tags?in=terraform/aws&ref=dept-engineering-blog.ghost.io">default_tags</a>, you will run into two common bugs:</p><ul><li><a href="https://github.com/hashicorp/terraform-provider-aws/issues/19204?ref=dept-engineering-blog.ghost.io">Exactly identical tags bug</a></li><li><a href="https://github.com/hashicorp/terraform-provider-aws/issues/19204?ref=dept-engineering-blog.ghost.io#issuecomment-840753622">Partially identical tags bug</a></li></ul><h3 id="exactly-identical-tags-bug">Exactly identical tags bug</h3><p>Terraform example of bug and workaround here on <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/bugs-workarounds/exact-identical?ref=dept-engineering-blog.ghost.io">Rocket Insights GitHub repo</a>.</p><blockquote><em>Error: "tags" are identical to those in the "default_tags" configuration block of the provider: please de-duplicate and try again</em></blockquote><p>When the AWS provider default_tags and the resource tags are exactly the same, you will get the above error.</p><pre><code class="language-hcl">provider "aws" {
  default_tags {
    tags = {
      owner   = "Rocket Insights"
      project = "Project A"
    }
  }
}

resource "aws_dynamodb_table" "exact-identical-bug" {
  name         = "exact-identical-bug"

  # The resource tags are exactly identical to the 
  # AWS provider default_tags.
  # This causes the exception
  #
  # Error: "tags" are identical to those in the 
  #   "default_tags" configuration block of the provider:
  #   please de-duplicate and try again
  #
  # To fix, just delete the resource tags.
  tags = {
    owner   = "Rocket Insights"
    project = "Project A"
  }
}  </code></pre><p>To work around the "exactly identical tags bug", just delete the duplicated resource tags.</p><pre><code class="language-hcl">provider "aws" {
  default_tags {
    tags = {
      owner   = "Rocket Insights"
      project = "Project A"
    }
  }
}

resource "aws_dynamodb_table" "exact-identical-workaround" {
  name         = "exact-identical-workaround"

  # To fix the exactly identical tags bug, 
  # just delete the duplicate resource tags
}</code></pre><h3 id="partially-identical-tags-bug">Partially identical tags bug</h3><p>Terraform example of bug and workaround here on <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/bugs-workarounds/partial-identical?ref=dept-engineering-blog.ghost.io">Rocket Insights GitHub repo</a>.</p><p>When the AWS provider default_tags and the resource tags have some identical entries, <code>terraform plan</code> will always show an update on the tags even though there are no changes.</p><pre><code class="language-hcl">provider "aws" {
  default_tags {
    tags = {
      owner   = "Rocket Insights"
      project = "Project A"
    }
  }
}

resource "aws_dynamodb_table" "partial-identical-bug" {
  name         = "partial-identical-bug"

  # The resource tags have some entries that are identical
  # to the AWS provider default_tags. 
  # This causes `terraform plan` to always show 
  # an update on the tags even though there are no changes.
  # To fix, just delete the duplicated resource tags.
  tags = {
    owner   = "Rocket Insights"
    project = "Project A"
    cost-center = "Rocket Insights Billing"
  }
}</code></pre><p>The above Terraform code will always show the following <code>terraform plan</code></p><pre><code class="language-hcl">Terraform will perform the following actions:

  # aws_dynamodb_table.partial-identical-bug will be updated in-place
  ~ resource "aws_dynamodb_table" "partial-identical-bug" {
        id             = "partial-identical-bug"
        name           = "partial-identical-bug"
      ~ tags           = {
          + "owner"       = "Rocket Insights"
          + "project"     = "Project A"
            # (1 unchanged element hidden)
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.</code></pre><p>To work around this bug, just delete the duplicate resource tags</p><pre><code class="language-hcl">provider "aws" {
  default_tags {
    tags = {
      owner   = "Rocket Insights"
      project = "Project A"
    }
  }
}

resource "aws_dynamodb_table" "partial-identical-workaround" {
  name         = "partial-identical-workaround"

  # To fix, just delete the duplicated resource tags.
  tags = {
    cost-center = "Rocket Insights Billing"
  }
}
</code></pre><h2 id="access-individual-default-tags">Access individual default tags</h2><p>Terraform example of the default_tags data source here on <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/aws-default-tags-ds-examples/asg?ref=dept-engineering-blog.ghost.io">Rocket Insights GitHub repo</a>.</p><p>Using the Terraform <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/default_tags?ref=dept-engineering-blog.ghost.io"><strong>aws_default_tags</strong></a> data source, you can get programmatic access to an individual key and value of default tags.</p><p>This example shows how to use the user defined <code>environment</code><strong> </strong>default tag to determine whether to create an DynamoDB table.</p><pre><code class="language-hcl">provider "aws" {
  default_tags {
    tags = {
      owner       = "Rocket Insights"
      project     = "Project A"
      environment = "dev"  # Change dev to prod to add the prod DynamoDB
    }
  }
}

# Access individual default_tags via the data source
data "aws_default_tags" "provider" {}

resource "aws_dynamodb_table" "prod-only" {
  # Only create this DynamoDB table if the 
  # `environment` default tag value is `prod`
  count = data.aws_default_tags.provider.tags.environment == "prod" ? 1 : 0
  name  = "default-tags-prod-db"
}
</code></pre><h2 id="asg-and-lt-tags-tips-and-tricks">ASG and LT tags tips and tricks</h2><p>Terraform example of ASG and LT tagging here on <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/aws-default-tags-ds-examples/asg?ref=dept-engineering-blog.ghost.io">Rocket Insights GitHub repo</a>.</p><p>Auto Scaling Groups (<a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html?ref=dept-engineering-blog.ghost.io">ASG</a>) and Launch Templates (<a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/LaunchTemplates.html?ref=dept-engineering-blog.ghost.io">LT</a>) are tricky to tag correctly. Without the right configuration, the EC2 instance and attached storage volumes launched by the ASG and LT will not have the default tags attached.</p><p>Launch templates require the <strong><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template?ref=dept-engineering-blog.ghost.io#tag-specifications">tag_specifications</a> </strong>configuration and ASGs require the <strong><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group?ref=dept-engineering-blog.ghost.io#propagate_at_launch">propagate_at_launch</a></strong> tag configuration.</p><pre><code class="language-hcl">provider "aws" {
  default_tags {
    tags = {
      owner   = "Rocket Insights"
      project = "Project A"
    }
  }
}

# Access individual default_tags via the data source
data "aws_default_tags" "provider" {}

resource "aws_autoscaling_group" "default-tags-asg" {
  name = "default-tags-asg"

  # Loops through the AWS provider default_tags to generate
  # multiple aws_autoscaling_group specific propagate_at_launch tag.
  # Any EC2 instances launched by this ASG will have these tags
  # automatically attached.
  dynamic "tag" {
    for_each = data.aws_default_tags.provider.tags
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }

  launch_template {
    id      = aws_launch_template.default-tags-lt.id
    version = "$Latest"
  }
}

resource "aws_launch_template" "default-tags-lt" {
  name          = "default-tags-lt"

  # The EBS volume of the EC2 instances created 
  # by this launch template
  # will have these tags automatically attached
  tag_specifications {
    resource_type = "volume"
    # merge is a Terraform built-in function to combine maps
    tags = merge(
      {
        Name = "default-tags-lt-volume"
      },
      data.aws_default_tags.provider.tags
    )
  }
}
</code></pre><h2 id="ec2-ebs-volume-tags-tips-and-tricks">EC2 EBS volume tags tips and tricks</h2><p>Terraform example of EC2 EBS tagging here on <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/aws-default-tags-ds-examples/volume-tags?ref=dept-engineering-blog.ghost.io">Rocket Insights GitHub repo</a>.</p><p>When you create Elastic Compute (<a href="https://aws.amazon.com/ec2/?ref=dept-engineering-blog.ghost.io">EC2</a>) instances via Terraform, the Elastic Block Store (<a href="https://aws.amazon.com/ebs/?ref=dept-engineering-blog.ghost.io">EBS</a>) volumes attached to the EC2 are not automatically tagged. Untagged EBS volumes are cumbersome to administer.</p><p>You assign the EC2 default tags to the attached EBS storage volume with the aws_instance <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance?ref=dept-engineering-blog.ghost.io#volume_tags">volume_tags</a> .</p><pre><code class="language-hcl">provider "aws" {
  default_tags {
    tags = {
      owner   = "Rocket Insights"
      project = "Project A"
    }
  }
}

# Access individual default_tags via the data source
data "aws_default_tags" "provider" {}

resource "aws_instance" "default-tags-ec2" {
  ami = data.aws_ami.default-tags-ami.id
  instance_type = "t2.micro"

  # Assigns the default tags to volumes 
  # attached to this EC2 instance
  volume_tags = data.aws_default_tags.provider.tags
}
</code></pre><h2 id="aws-resource-groups-and-tag-editors">AWS Resource Groups and Tag Editors</h2><p>Have you ever wanted to do the following: "Find all AWS resources in all regions that have the tag `owner='Rocket Insights' " ?</p><p>To perform the above query, AWS introduced <strong><a href="https://docs.aws.amazon.com/ARG/latest/userguide/resource-groups.html?ref=dept-engineering-blog.ghost.io">Resource Groups and Tag Editors</a> </strong>. Resource groups are a way to manage the tags of multiple AWS resources in one place.</p><p>The topic of Resource Groups can be its own article. In a brief summary, here are some of the things you can do with Resource Groups and Tag Editors.</p><ul><li>Find AWS resources across regions with a specific tag</li><li>Find AWS resources without tags</li><li>Add or edit tags of multiple AWS resources at one time</li><li>Upgrade all AWS resources with a specific tag</li></ul><p>The AWS UI for Resource Groups is clunky but if you experiment with it, it can be a powerful tool in your AWS administration toolbox.</p><h2 id="conclusion">Conclusion</h2><p>Introducing a new technology into production is never easy. There are always bugs, workarounds, and traps. Terraform AWS default_tags are no exception to the above truth. This follow-up article provides solutions for easier implementation of Terraform AWS tags in production. Use it well!</p>]]></content:encoded></item><item><title><![CDATA[Software Engineers Rush to Write Bad Code]]></title><description><![CDATA[There is a better way. ]]></description><link>https://engineering.deptagency.com/software-engineers-rush-to-write-bad-code/</link><guid isPermaLink="false">62601adb1d0634003dc61ae2</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Travis Bumgarner]]></dc:creator><pubDate>Thu, 21 Apr 2022 15:14:54 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/preview_image.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/preview_image.jpg" alt="Software Engineers Rush to Write Bad Code"/><p>Software engineers are often forced to deliver value quickly for the customer. They move fast. What they create is a mixture of researching, brainstorming, learning, prototyping, and final drafting. </p><p>It is an absolute mess. </p><p>Software engineers must separate the mixture of activities. Do one activity. Do it well. Focus on getting your final draft into production and nothing else.</p><h2 id="how-does-an-engineering-mess-happen">How does an engineering mess happen?</h2><p>You are tasked with implementing some new library, <code>lib-foo</code>, into your codebase.</p><p>You install the package and immediately get faced with bugs. You play the game of whack-a-mole, solving one bug after the next. Are you learning anything about this new library?</p><p>After days of banging your head against the wall, you get something that compiles and does what you want. Was that the best way to do it?</p><p>You create a pull request and take a look over your code. What a mess. Will you be able to understand it six months in the future?</p><p>Two years later, you're at a new job and are again tasked with implementing <code>lib-foo</code>. Do you remember anything about the library? Probably not.</p><p>If this sounds familiar, it's time to make a change.</p><h2 id="there-is-a-better-way">There is a Better Way</h2><p>Engineers face a lot of frustrating situations. </p><p>Of all of these situations, though, nothing is more effective at killing enthusiasm and motivation than troubleshooting several bugs at the same time. When you're learning something new, you should focus on that one thing and nothing else. Everything else should get out of the way.</p><p>I do a lot of work with React. When I wanted to learn something new with React, I'd copy an old project, gut it, and use that to start.</p><p>At some point, I learned of <a href="https://github.com/cookiecutter/cookiecutter?ref=dept-engineering-blog.ghost.io">Cookiecutter</a>. It is a Python tool for generating a project's boilerplate from a template. No Python experience required. Tired of copying, pasting, and gutting, I wrote myself a <a href="https://github.com/TravisBumgarner/cookiecutter-react?ref=dept-engineering-blog.ghost.io">React Cookiecutter</a> to speed things up. I know the code well. Now, with a simple Python command, I've got a sandbox to start learning about a new React library.</p><p>Folks familiar with <a href="https://create-react-app.dev/?ref=dept-engineering-blog.ghost.io">Create React App</a> might be wondering: why not just use that? Well, I have been maintaining my React Cookiecutter for several years now and it is very familiar for me. If Create React App is a tool you know well, use it.</p><p><strong>Pick the tools that work best for you so you can focus only on your learning.</strong></p><p>Now that I have an environment to play around in, it is time to start learning.</p><p>My process is usually the following:</p><ol><li>Use one of my Cookiecutters to spin up a sandbox to play in.</li><li>Follow along with a tutorial and the documentation to learn the basics of the library.</li><li>Explore the library's ideas and concepts.</li><li>When I'm content with my learnings I'll add it to my <a href="https://github.com/TravisBumgarner/learning/tree/master/archives?ref=dept-engineering-blog.ghost.io">Learning Repo</a>. When possible, I will add notes in Markdown or comments to supplement the artifact.</li></ol><p>For more complicated tasks, I'll create a folder in my <a href="https://github.com/TravisBumgarner/proof-of-concepts?ref=dept-engineering-blog.ghost.io">Proof of Concepts Repo</a>. An example of such a task would be connecting two libraries I don't know with each other. I'll start by exploring each separately in the Learning Repo. Then I'll use those two explorations as references to integrate the two together.</p><p>Some might say that I'm writing throwaway code and it is a waste of time. Bad code is a liability. I'm throwing away code but it is only so that I can improve the quality of the code going to production. Furthermore, it is reference code. I have a place to look if I need to use that technology in a different codebase in the future. Finally, the majority of the code I write in a sandbox gets copied into the codebase.</p><p>As I start working in the actual codebase, I might get stuck on something. In this situation, I can jump back into any of my reference code and troubleshoot in isolation. I can figure out if the issue is from the new libraries or from pre-existing code.</p><p>Now imagine, you're two years down the road and you need to implement <code>lib-foo</code> again. You could try and take a look at where you originally implemented it, but maybe you don't have access to that codebase anymore. <strong>The great thing about creating a repository of references is that you can carry it with you throughout your career.</strong></p><p>Over time, the collection of references grows. You can share them with others who are learning what you've learned. You can implement those technologies more easily the second time around. You're moving at a better pace!</p><h2 id="fast-forward-two-years">Fast Forward Two Years</h2><p>You sit down to begin a new project integrating <code>lib-foo</code>, <code>lib-bar</code>, and <code>lib-buzz</code> into an existing code base. Will you rush to get a pull request open that is, at best, a rough draft?</p><p>Or will you have learning repos for <code>lib-foo</code> and <code>lib-bar</code>? A proof of concept on how the two connect together? Where you can build a solution more easily, elegantly, and quickly?</p><p>The choice is yours.</p>]]></content:encoded></item><item><title><![CDATA[Machine Learning: Explain it Like I’m Five [Shipit.io Podcast]]]></title><description><![CDATA[If you've heard a lot about AI and machine learning, but don't understand it, this month's episode is for you. ]]></description><link>https://engineering.deptagency.com/machine-learning-explain-it-like-im-five-podcast/</link><guid isPermaLink="false">6259bc621883db003d8128fb</guid><category><![CDATA[Platforms]]></category><category><![CDATA[AI]]></category><category><![CDATA[ShipIt Podcast]]></category><dc:creator><![CDATA[Kelsey Anderson]]></dc:creator><pubDate>Fri, 15 Apr 2022 20:01:27 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/machine-linkedin--1-.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html--><iframe title="Episode 14: Machine Learning - Explain it Like I'm Five" allowtransparency="true" height="300" width="100%" style="border: none; min-width: min(100%, 430px);" scrolling="no" data-name="pb-iframe-player" src="https://www.podbean.com/player-v2/?from=embed&i=76ptd-11b4fd0-pb&square=1&share=1&download=1&fonts=Arial&skin=1b1b1b&font-color=auto&rtl=0&logo_link=episode_page&btn-skin=1b1b1b&size=300" allowfullscreen=""/><!--kg-card-end: html--><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/machine-linkedin--1-.png" alt="Machine Learning: Explain it Like I’m Five [Shipit.io Podcast]"/><p>Hi, I'm Keegan Sands, and welcome back to <a href="https://shipit.io/?ref=dept-engineering-blog.ghost.io">ShipIt</a>. The podcast from DEPT® that's made by engineers for engineers. If you've heard a lot about AI and machine learning, but don't understand it, this month's episode is for you. </p><p>Machine learning: explain it like I'm five. </p><p>Special guest, <strong>Tyler Renelle</strong>, host of DEPT®'s machine learning guide podcast chats with DEPT® engineer <strong>Matt Merrill</strong> about the historical background and technical landscape of machine learning. So without further ado, let's get started. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/machine-linkedin--1-.jpg" class="kg-image" alt="Machine Learning: Explain it Like I’m Five [Shipit.io Podcast]" loading="lazy" width="1920" height="1080" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/04/machine-linkedin--1-.jpg 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/04/machine-linkedin--1-.jpg 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1600/2022/04/machine-linkedin--1-.jpg 1600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/machine-linkedin--1-.jpg 1920w" sizes="(min-width: 720px) 720px"/></figure><p><strong>Matt Merrill:</strong> It's the new year. It's probably a lot of people that are looking for some new stuff to learn in the new year. Maybe as the year goes along, I've been, mostly a backend developer, a Java and node.JS, and dabble in dev-ops for 15+  years.</p><p>I know little to nothing about AI/ML topics. Thought it would be really interesting to bring Tyler on here. </p><p><strong>Tyler Renelle:</strong> It’s named <a href="https://ocdevel.com/mlg?ref=dept-engineering-blog.ghost.io">Machine Learning Guide</a>. And it's part of our mutual podcast network now.</p><p><strong>Matt Merrill: </strong>I thought we'd have a conversation about machine learning and explain it to me, the dummy here like I'm five. I briefly introduced myself. So Tyler, why don't you tell us more about yourself and why we should trust you and listen to you on this topic?</p><p><strong>Tyler Renelle:</strong> my name is Tyler Renelle and I come from web dev mobile dev myself as well, about four or five years ago.</p><p>I switched to machine learning. So I'm definitely not the ultra expert on the topic, but I'd like to think of myself on an X, as an expert on teaching the topic as somebody who struggled with the transition from one field prep, probably primarily going to be a listening audience to machine learning. And at the time when I created the podcasts, there were no newbie resources in this, in the same way, that once upon a time, computer science was the only way into app development.</p><p>And then there were boot camps and it was very easy. At that time, it was the computer science days of machine learning. And I was like, this is actually a lot easier than they're portraying it in the textbooks. Let me take a stab at distilling this down to dummies like myself.</p><p>And now we've had the boot camp, the data science, boot camps, and the easy learning resources and all that stuff. But that was, it was a nascent market at the time. </p><p><strong>Matt Merrill:</strong> Yeah. But you still have a pretty good listenership as I understand it. So like obviously you've hit, you've struck a chord there, so that's cool.</p><p><strong>Tyler Renelle:</strong> Timing was timing. Like I said, there was nothing at the time, I was the first machine learning podcast. There was one out there. Was it called? Anyway, there was a professor who was teaching AI, but it was really mathy, really tacky.</p><p><strong>Matt Merrill:</strong> Yeah. That's going to be one of my questions. Because that scares me as a person who's not great at math.</p><p>So yeah. Let's just get into it like AI ML, NLP, tell, just give us like, give us the overview of this. Like how can we. How can we get through this and what do I Google? What do I ignore? What do I start with? I don't know, wherever you think we should start.</p><p><strong>Tyler Renelle:</strong> Okay, cool. So the master umbrella is data science. That's the umbrella term for everything we're going to be talking about in this episode, but we're going to hone in on machine learning, a sub field of data stuff. So data science is anything that deals with data. It can even extend it as far as why you might not even consider a financial advisor, a data scientist and anybody who's doing spreadsheets databases.</p><p>Somebody who has even a database administrator might be considered someone in the data science umbrella, but anything that deals with data is data science. Machine learning obviously deals with data. Therefore it's data science, the traditional data science roles at an organization are going to include things like feature engineering, data engineering, like pulling your data out of somewhere from cloud Twitter, whatever.</p><p>So data engineering also includes transforming that data into something that's usable downstream. It also includes data analysts. So people who are going to be looking at the data and making human-based decisions, using charts and graphs with programs like Tableau and Power BI, and then there's the machine learning people who are actually writing algorithms that make predictions based on the data.</p><p>So if a data analyst takes data from somewhere and looks at it to make a Schumann decision. Machine learning engineers are the same thing, but making robo decisions, they write algorithms that make predictions automated, with no human in the loop per se. Got it. AI artificial intelligence. It's bigger, so we got data science underneath data sciences, AI, and then underneath AI are machines.</p><p>So AI it's the field of automated intelligence. If you look it up on Wikipedia, it's just like automating things that would typically take human-level intelligence. That's going to include things like language processing, and natural language processing. So anything language-oriented, like Siri chatbots, computer vision knowledge representation.</p><p>And a decision and action planning. So a game-playing bots like Google's AlphaGo, zero, and all those things, anything that plays chess or predicts stock market or takes action on the stock market and then machine learning. And once upon a time machine learning was just a subfield within AI.</p><p>Nowadays that line is getting blurred. It's difficult to disambiguate the terms AI. And the reason is machine learning proved to be so valuable. So important learning on the data proved to be so important. It is subsuming the other spaces of AI. So once upon a time, computer vision was a bunch of dedicated algorithms that were handcrafted.</p><p>Using these things called, like these like patches that look for patterns in pixel values, these little square patches. It's almost, you've got his funny, you've got where's Waldo. We've got Waldo in the background. I always use Waldo as an example for my computer vision stuff. You might have red and white stripes in a square pack, a pixel patch that slides over an image and it looks for that pattern.</p><p>And then it says, ding ding, and then there's some score, but it's all handcuffed. Along came machine learning and they automated that process. Same concept still using the patch. It's called a convolution, but they use these networks called a convolutional neural network that actually learn the patterns that it's looking for.</p><p>So it's like one layer removed. So machine learning just swept through all the subdomains of AI. And that's why the line between AI and ML is blurry more than ever before. That's why some people have difficulty knowing the difference between the two terms. </p><p><strong>Matt Merrill:</strong> No, that's really cool. I liked the way you explain that, it's almost like abstracting up that, but at least in that image recognition example, abstracting an upper level.</p><p>So that like image recognition becomes just something you can do when you tell it what to look for, as opposed to like bespoke, crafting an algorithm to look for some specific thing. </p><p><strong>Tyler Renelle:</strong>  I like to think of machine learning as there are almost layers of machine learning. So let's say that the first level is to complete handcrafted scripts, like a Python file that you wrote, end to end yourself. The next layer above, that would be some part of that script.</p><p>Learn something on your behalf. So you don't quite code it into the script. You coded it. That part of the script is actually doing a little bit of knowledge gaining on its own learning patterns in data and that's machine learning. And then a layer above that is what's called learning. In machine learning, we have these machine learning algorithms or models where we tune what's called hyperparameters.</p><p>Don't worry about that for now, but there are still things that we like oversee in the machine learning process. There's a layer above that is called meta learning that does its own dial and knob me. A hyperparameter optimizes it. That's one step closer to AI. So like the master goal of AI, the thing completely manages itself.</p><p>We don't have to teach it how to learn.</p><p><strong>Matt Merrill:</strong> It's like autopilot, as opposed to like tweaking the knobs yourself as you go.</p><p><strong>Tyler Renelle:</strong> That's it? That's it. Cool. What, so basically, what is machine learning? I say machine learning is fuzzy logic, and I have to be careful with that phrase because it's actually a phrase used in machine learning and a subdomain of machine learning, but we're gonna roll with it.</p><p>If you write a script that says if-this, then-that, and it's very clear to you, it's, there are hard cuts that make for a very obvious A to Z scenario or solution. Then you write it yourself, but if there are no hard cuts, if it's fuzzy the in-betweens are different.</p><p>For you as a programmer to loop yourself in then it's fuzzy logic. That's a good use of machine learning. I'm going to get to an example for a project that we were actually working on slash considering machine learning implementation.</p><p>But we've talked about image recognition. Yep. That's sliding patches looking for patterns in the pixel values. Once upon a time, we did handcraft that, like you said, bespoke algorithms, and handwritten convolutions, but they were very scenario-based. So an expert in image theory would come in and write a handful of patches that are for car recognition, the handful of patches for car recognition.</p><p>It was fuzzy logic because we're because it's those, like delineators in pixel values to designate something as cat car tree or dog are fuzzy, but they were able to achieve it, nonetheless, because The Show Must Go On. Wasn't really machine learning at the time.</p><p>But that's a classic scenario. Once convolutional neural networks and learning algorithms for this type of scenario became available, they wiped out those old guard algorithms. That's a perfect situation in which there are no hard-cut if-then statements. So I say machine learning is fuzzy logic.</p><p>I also say it's learning patterns in data, and I'm going to pause in case you have any questions. </p><p><strong>Matt Merrill</strong>: One thing I think that we could talk about though, like how processing power shapes.</p><p><strong>Tyler Renelle:</strong> Let's talk about that. A little bit. Machine learning is quite old. Some people call it applied statistics.</p><p>What you do in machine learning is nothing but old textbook statistics, calculus in your algebra algorithms, and what we call it. So it's an algorithm is a math puzzle written in code. Once it's actually like the data that has learned gets fused into the algorithm.</p><p>These variables now. Numbers are called a model. So the model is like an algorithm, but the algorithms, the raw algorithms are nothing more than old textbook stacks, linear algebra, and calculus. Math problems. So in a way, machine learning has been around forever. It's just that, like you said, we didn't have the processing power.</p><p>We didn't have the know-how to turn it into tech. We really started hunkered down with the 60, 70s, and 80s. So machine learning as code is quite old, but it wasn't popular because at the time the algorithms in use were called shallow learning algorithms. There's a model called linear regression, a very simplistic model.</p><p>They just weren't that powerful, but the knowledge of neural networks, of deep learning, so shallow learning is basically like single math. Hydrate that into a model based on learning from data. Now you have a machine learning model. </p><p>Deep learning is stacked shallow learning algorithms. Okay. A neural network is actually nothing more than a network of linear regression nodes that are called neurons.</p><p>So it's stacked, shallow learning. Yeah, like in a Bayesian network, for example, would have been like naive Bayes as a shallow learning algorithm of Bayesian network is like that transformed into a graph. Any machine learning expert, listening to this episode is probably up in arms, but that's the general gist that was conceptualized a long time ago, but impossible to achieve until the computer power of our generation.</p><p><strong>Matt Merrill:</strong> That's a lot of data. That's a lot of data and you have to keep a lot of it in memory. And it also requires a lot of CPU power to be able to move it around and adjust it and stuff like that. And also on top of it, up until the 2010s, we weren't even capturing a lot of that data.</p><p>Like it was just able to be accessed, even if we did have the computing power. Yeah. It's a really cool way of looking at it. </p><p><strong>Tyler Renelle:</strong> It was like 20, I'm still like 2015, and it really all started popping off. And nowadays we use GPUs instead of CPUs for most machine learning. And why is that?</p><p>Qorvis CPU can handle a lot of computation. That's why there are only eight cores of a GPU. They are mainly tiny computations, but there are many more of them. And like I said, in the case of a neural network where each node of each neuron is basically just a linear regression unit, those are small computations.</p><p>They're very easy to distribute a crap ton of. And that's why GPUs is valuable in this case.</p><p><strong>Matt Merrill:</strong> Got it. So I'm going to take a crack at this. A general-purpose CPU could handle one very complicated operation, very quickly. Our, as a Kuta core, I think you said that could handle a simple operation very quickly.</p><p>Couldn't do a complicated one, but it's able to scale that out, which is exactly what these algorithms are.</p><p><strong>Tyler Renelle:</strong> Exactly. I think of a CPU, it was like an ECE two instance, a huge issue to a sense. And a GPU is like 50 Lambda functions normally. So, the computation became available, especially by way of GPS.</p><p>That made the neural networks feasible. And then, like you said data became available at scale and the way Google operates, right? Is it scraping the web? So now they have all the language, and data of the world. They went on to create all these natural language processing.</p><p>Pre-trained that are available through the hugging face library. Sorry, what was it called? Hugging face. Yeah. Let's go into, I'll talk about the different domains of machine-learning hugging face ties into the natural language domain. It's my favorite library actually, natural language is my favorite domain of machine learning.</p><p>I say machine learning is broken down into three parts: it's table space and time. Any time you're dealing with tables, whether they're spreadsheets or databases. You're going to be using tabled machine learning models. Let's say we have a spreadsheet. It's the housing costs of houses in Newburyport. You have a bunch of columns that are like the distance downtown on the square feet, the number of bedrooms, number of bathrooms.</p><p>And then what you're trying to predict is cost. So you take one of those columns in your CSV or spreadsheet pulled aside. That's the thing you're trying to predict a machine learning algorithm that deals with. We'll learn the patterns in those columns. We call these features the ones on the left X in order to predict the Y column, the cost of the house, a very popular algorithm here is called XG boost.</p><p>That's probably the most popular algorithm of the machine learning period. So that one's worth name-dropping interesting XG boost. It's a decision tree. It learns, then blanks. It's not actually, this is what's called a gradient boosted ensemble of decision trees. So it's multiple decision trees, all like voting against each other, but effectively it's a decision tree and what's cool is actually you, can you say like XG boost.fit?</p><p>And it can output the decision tree as generated and even look at the oh, okay. That makes sense. If its number of bedrooms is greater than two.  </p><p><strong>Matt Merrill:</strong> So you're saying like XG boost stuff fit. What is that? Is that in Python? Like Python. Okay, cool. So I'm thinking in my head right now.</p><p>This sounds like I'm straight ahead, enough use case that I, a machine learning moron might be able to use this in something that I'm working on. Is it really that simple? </p><p><strong>Tyler Renelle:</strong> Yes. But no, this is new. The idea of machine learning being available to people like us is very new. This revolution’s been going on.</p><p>Most people listening to this episode haven't heard the term machine learning. I didn't hear the term machine learning. </p><p><strong>Matt Merrill:</strong> if you're like me, like I heard AI at times, but not machine learning. Exactly. And I don't think I learned the difference between the two until similar to the timeframe you were talking about, 2015 or 2016. </p><p><strong>Tyler Renelle:</strong> Now, there was a revolution after the deep learning revolution, which became available again by, by way of lots of data, lots of. Compute power, even though the deep learning models themselves are available for a very long time sparked this huge revolution. And now there are just Python libraries for everything.</p><p>And you can just write some lines of code and get the job done very easily. It's crazy. So there are tablespace times. Let's start with. Time is like step, step you're learning. You're learning the steps that lead to the next step stock market predictions, a very obvious use case with that. So over the last week of fluctuations of the price of a stock, what is the likely next price of that stock?</p><p>What's going to be tomorrow and therefore, should I buy it myself today? Language. What's next? We're going to be, there's a lot of, there's a lot of really cool use cases of language. So language is. Stock market's time weather, predicting the weather budget forecasting for companies and all these things.</p><p>Yeah. So there are dedicated algorithms for four times and in the table stuff, the shallow learning algorithms, even the linear aggression, extra boosts, and stuff. There are also just neural networks. That was the deep learning revolution that introduced neural nets. A vanilla neural network. When somebody thinks of the word neural networks is actually almost like a class, like an abstract class to concept.</p><p>It's not really an algorithm. The neural network people are thinking of when they say that word is actually called a multilayer perceptron that's used for table data. And then for time series stuff like language, it's called a recurrent neural network. So subclass neural network, one is for table. One is for space.</p><p>One is for time is time. One is called a recurrent neural network. And the recurrent part is that it loops back on itself. Graph and you have to be able to, you have to do that because you have to be able to take an undefined number of steps in time. And so it has to be able to loop back on itself.</p><p>And then its space is like a pixel, an image. What we're talking about, a convolutional neural network. So subclass neural networks into convolutional neural nets. Convolution is that patch. I was talking about this while looking for Waldo. So it learns how to detect things in an image based on the pixel values.</p><p>So that space, interestingly, the difference between time and space has been a blurring. In the last few years, a series of models came out called  “Transformers”. There was a white paper called attention is all you need and introduced this like attention module into neural networks. So it pays attention to certain points either in time or space.</p><p>And what model did as strangely it blurred the lines between space and time. And now a lot of times for language stuff, people are using convolutional neural networks for tradition, traditionally used for space. So the line between space and time is being blurred and there's a little bit more universe, a universality to the neural networks that are being used in these cases by way of the. </p><p><strong>Matt Merrill:</strong> If I'm about to, start out and look at this stuff like, shit, is that a very advanced topic or is that actually a good place to start these days? </p><p><strong>Tyler Renelle:</strong> Because of how easy it is to get started as a developer, a question, tying that into you don't really need to know much of what I just said about like a 10, like a tension module and transformers and stuff.</p><p>A lot of days these Python libraries just handle the legwork for you and you just write a handful of lines of code to get the job done. So back to Hugging Fcce the library. Hugging Face is a suite of tools for natural language processing, your chatbot questioning, answering summarization, and sentiment analysis.</p><p>Anytime you do texts or spoken language, even when handling spoken language, you'll use this Hugging Face library. The goal of that library was to implement transformers that I just mentioned in its various ways, the different algorithms that were being pushed out in white paper land so that you, as a user, don't need to care about what's happening back there.</p><p>Let the scientists do their science. I just want to build this stuff spiraled out of control that the library does everything under the sun. It's incredible. You just write a one-liner to take a block of text, take a Wikipedia entry, and then you download it with a beautiful soup or something in Python, and you just say, Hugging Face dot summarize, and it's like a paragraph and it's incredible. </p><p><strong>Matt Merrill:</strong> So a couple of follow-ups there. You've mentioned beautiful soup. what's that? I never heard of that. </p><p><strong>Tyler Renelle:</strong> So beautiful soup is just a web scraping library for Python. Let's talk about languages a bit. So Python, that's the language of data science and machine learning. Other light popular languages for machine learning are.</p><p>Because of a spark and I guess Hadoop, because a lot of data science is data at scale parallelizing, data processing over multiple servers or whatever the case spark was a popular framework to use for that. So Scala became a popular language to use machine learning, but then they ported that to Python because more people were using Python and machine learning.</p><p>And it was such an uphill battle that they're like, okay, we give up, let's create pipe high spark. So now spark works. Python R is a popular language for machine learning, but it's a little bit more in the data analytics crowd and is fast becoming phased out in favor of Python, MATLAB. Matt lab is popular in the academic crowd for research purposes.</p><p>Again, starting to phase out the shows, becoming very, it's becoming very clear that there's a clear winner language. Yeah here. If you're going to learn machine learning, learn Python, honestly don't bother with any of the other languages. It's a bold statement, but I think most of my colleagues would agree.</p><p><strong>Matt Merrill:</strong> I think a lot of folks listen to this podcast, just because of what Rocket and DEPT® do. Like a lot of us are coming from a web background. And the nice thing about Python is it could do that too. It can't go wrong. Learning Python these days. It's not a waste of knowledge. </p><p>You could script dev ops stuff with that.</p><p>You could strip the random stuff. It's the language of stats. It's pretty great. So what you were saying about taking the Hugging Face library and just feeding some random text into it, there's your podcasts ML guide to do you have, do you walk through some of this stuff? </p><p><strong>Tyler Renelle:</strong> No, that's awesome.</p><p>I talk about a lot of these libraries and the technologies that are used in. Specific function calls you might make. I don't go too much into code because it's obviously audio format, but the podcast has a companion GitHub repository called no-fee this AI journal that I'm building open source. And I use a Hugging Face, through and through, but we're talking about how easy it is to do machine learning.</p><p>These days. I mentioned XG boost for tables. I mentioned a Hugging Face for language stuff. And more surprisingly, if you look at the Hugging Face models on their website, like what, I thought this was a language library. Does it work with table data? It works with images as well.  The first step to getting started with machine learning is to not use actually any Python libraries yourself, but to see how far you can get in the cloud-first and foremost, Oh, because cloud services on AWS, Google GCP, and Microsoft Azure, they have these machine learning tasks as hosted services that you can make calls to their endpoint.</p><p>And that's beneficial to you as a developer because many of these machine learning models do want a GPU about the only one that doesn't is X. But Hugging Face. If you're dealing with time and space, you're going to want a GPU. If you're developing on a Mac, you're out of luck because Macs don't have Nvidia graphics.</p><p>And Nvidia is pretty much, you can get machine learning, working with open cl and third-party libraries, something called plat ML, but it's just a pain in the neck. What you really want is a PC, either Linux or windows. If you're using Windows, use WSL two to use Linux. In other words, your only option is Linux pretty much.</p><p>It's not completely true, but pretty much you really want to be developing on Linux because you want to tap into your end video graphics. What if you don't have that, it's a pain in the neck to set up let's Kuda. There are ways around that by way of Docker. If you're developing a machine learning model, you're going to deploy it to the cloud eventually.</p><p>Anyway, you might as well get started one step ahead of the curve by working with the cloud-first. And these main three cloud offerings have machine learning models as hosted rest endpoints for almost everything you're going to be doing. Oh, that's amazing. Tablespace and data. Sentiment analysis, text classification, you can pipe it.</p><p>An image. AWS has its image classification services. You pipe it, an image. It'll tell you everything. That's in the image. You give it a table of data spreadsheets. There's a service called AWS autopilot. That's something worth knowing as well as GCPS auto ML. These are cloud services that you just give a spreadsheet and it does the number crunching.</p><p>You should tell it which column you're trying to. And it will look at the spreadsheet or the database and say, okay, this is the one who's trying to predict. It looks to be numeric. Maybe it has some out of bounds, like outliers or such. I'm going to try to dial that in using some magic. I'm going to take these columns.</p><p>This one's text, but I want numbers. So I'm going to turn that text into categories. It does all the magic for you behind the scenes. It selects which model to use, usually XG boost, but you don't have to care. It will select, tune and train the machine learning model for you. And then it will optionally host that as a rest endpoint for you to call for future data, which is the point of training, a machine learning model.</p><p><strong>Matt Merrill:</strong> The rest endpoint is a facade for whatever algorithm that this thing I don't want to say determines for you, but. Help it program and it just takes and compares it against the model that runs the algorithm. That's right.</p><p><strong>Tyler Renelle:</strong> That's amazing. That's autopilot, AWS autopilot. They also have a service called canvas autopilot and canvas, by the way, these are under the umbrella of AWS.</p><p>SageMaker is their suite of machine learning-oriented tools. What's cool about SageMaker? Obviously, the tools inside of SageMaker are incredibly powerful. Like I said, autopilot will automatically generate a machine learning model for you, but what's also cool about SageMaker is it pipelines them together as needed.</p><p>You can either use these tools, all a cart within SageMaker, or you can use them as part of a pipeline that you build and then deploy. And then as new data comes in, for example, It will send it through the pipeline of those, some feature engineering. It will do feature transformations and pipe it through autopilot and then redeploy a rest endpoint and all that stuff for me.</p><p><strong>Matt Merrill:</strong> So it almost manages the adjustments to the algorithm for you as it gets more data. Yup. Because even at the very little I've looked into this stuff, like I know that as of, even two years ago, that was a big deal of okay, I've got to retrain the model and redeploy it and dah, and this stuff will do it for you.</p><p><strong>Tyler Renelle:</strong> So within SageMaker there are tools, like clarify that sort of keeps an eye on drift and bias of data and can kick off certain processes that retrain and redeploy the model. If I were to say to this audience, how do we get started technologically? I would say, start with SageMaker actually, because SageMaker will use under the.</p><p>Much of what I've mentioned in this episode, like extra boosts and Hugging Face. They're using those technologies as open source technologies in their stack, but are a black box to you. You just send it data and it sends back a response. By becoming familiarized with SageMaker, we'll get you the furthest fastest.</p><p>You'll also be already in the cloud-ready to deploy when the time comes because the time will come.</p><p><strong>Matt Merrill:</strong> So one of the questions that I was going to ask and like at this point, I don't even think I need to ask it, but I am going to: can I still do this?</p><p>And based on everything that I've just heard you say the answer is a resounding yes. </p><p><strong>Tyler Renelle:</strong> Resounding yes. I suck at math as well.</p><p><strong>Matt Merrill:</strong> The best analogy that is coming to mind for me is I'm like, I'm a web developer. I like to do a lot of API. I don't need to know the details of the HTTP spec to make an API.</p><p>I can use a library like express or spring NBC in Java. And it handles all that stuff for me. But I do need to know some of the stuff happening under the covers, right? Like a simple example is if I'm going to throw a bad parameter, I want to throw a 400 level or code. Cause that's just a state.</p><p>So one of the things that are coming to mind is obviously you can't go into this blind, right? Like you have to know which knobs to tweak and what to dial-up and down. Or Ooh, SageMaker tried this for me, but it doesn't feel right. I'm not getting it right. How do I learn? Just enough to be able to do that.</p><p>Maybe you can speak to that.</p><p><strong>Tyler Renelle:</strong> So I'm a huge fan of top-down learning. Start by building an app. Now you have an app. Oh, this tiny module here. Doesn't get the job done from the auto tooling that I've been using. I want to unpack. And write it myself, start writing it yourself. Oh, this module, that module requires math, learn only that math needed and it will become obvious the process.</p><p>When I started machine learning, project processes started with. Is it a table auto autopilot. Is it text? I use SageMaker to comprehend images. I use their image to take risks. It'll get you 80, 80% of the way. There are clear gaps in SageMaker tools very quickly. You will discover things. You need to write yourself.</p><p>Yes. Okay. Let's crack open. Python was downloaded, Hugging Face or extra boost or something. For example, an AI journal gives book recommendations. You journal today: my boss yelled at me. Over time, it gets to know you and it will start recommending books. Okay. Maybe you should read this like emotion management books and it acts like Pandora.</p><p>So you can thumbs up and thumbs down. Its recommendations. The book recommendation is simple off the shelf. It's Hugging Face your journal hugging. Facebook's cool. Now you turn into vectors. Now I can compare those vectors using cosine similarity. A lot of that stuff is off the shelf, but the thumbs up thumbs down process gets a little hit.</p><p>That's handwritten in Python using a framework called care ass. So deep learning, Python's scikit-learn language is the shallow learning library. And then the deep learning libraries are TensorFlow and PI torch. Those are the two popular, deep learning libraries, and then add extra complexity.</p><p>There's a thing called care OSS that like sits on top of TensorFlow. That makes it easier to use. It's syntax. Overlay on top of TensorFlow specifically. So TensorFlow is by Google. It's like the angular of deep learning and PI torch is the React of deep learning. You know what I mean? I would so much rather use the PI torch and react most of the time.</p><p>Yeah. But there are times to use one or the other. Usually, if you're using another library, and they have already selected a deep learning preference, then you've just got to roll with it. So it pays to know both later after you've tried your SageMaker hand in and it didn't fully do the job. </p><p><strong>Matt Merrill:</strong> Plug that custom code into things that SageMaker already has done for you.</p><p><strong>Tyler Renelle:</strong> Yes. SageMaker has this concept of bring your own model, bring your own container and bring your own something else. I can't remember. There are three prongs. You can use them. Services like sentiment analysis. For example, you send it a string, a review and it'll be like sad, angry, happy block, easy peasy.</p><p>We want to get a little bit more complex. I want to classify this text. Is it, is it about baseball or is it about football? So you're going to do a little bit of training on your own, but because it's such a common type of scenario, SageMaker offers tools. Around that, but you do have to write your own code.</p><p>And so they call that bring your own code. You could still deploy it to SageMaker without worrying about GPS and scalability and the libraries that are pre-installed. But you do have to handcraft a little bit of code and it's going to subclass something. Got it. And then finally they have brought your own container.</p><p>You're like, no, I'm getting real customization here. I don't want to worry about the GPU that's it. But that and scalability. I want to handle all the libraries and the code myself. </p><p><strong>Matt Merrill:</strong> So that's "bring your own container." Cause maybe you need some kind of custom library or something like that you don't offer out of the box.</p><p><strong>Tyler Renelle:</strong> I'm with your subclass their Docker container, and then you do the rest of yourself.</p><p><strong>Matt Merrill:</strong> Like I'm drawing some parallels here to like Lambda where it's like, Lambda will give you the node runtime and you can run a JavaScript Lim. But you have to go within the bounds of whatever Lambda gives you.</p><p><strong>Tyler Renelle:</strong> Perfect analogy. Say SageMaker is the Lambda of machine learning. That makes sense. Plus, a bunch of extra with monitoring. Yeah. I be the pipelining and stuff like that. And monitoring to monitor. And then it sends you email alerts. Look, your data is starting to drift.</p><p>Look, your users aren't who you thought they were. It's incredible. It's super powerful.</p><p><strong>Matt Merrill:</strong> People are going to start thinking like where like selling SageMaker, we're legitimately not like we're just legitimately excited about that. </p><p><strong>Tyler Renelle:</strong> We talked earlier and I'm like, I'm not a DevOps guy and I'm taking an interest lately, but because AWS just seems to be the most popular.</p><p>It's the easiest place to. So just so happens. I don't know crap about GCP or Azure, and that's why I'm harping on SageMaker. It's a knowledge thing. It's because I'm taking the easy way out here, basically.</p><p><strong>Matt Merrill:</strong> As a counterpoint, even though I know nothing about this, I did work with a client at DEPT® that was using Google.</p><p>And they were doing all kinds of image, training records, image, training algorithms. I worked with their AI engineer or ML engineer, and he raved about GCP. In fact, they were using GCP for that reason. Like he drove the rest of their organization to use it. Interesting. So there's something to it, yeah, there's a really good, that's a really good kind of caveat to give people's this is just what we know, but that's through the lens of one person at one client, but what he was doing was pretty, pretty neat with image recognition. </p><p><strong>Tyler Renelle:</strong> That's really interesting to hear actually. Our parent organization DEPT and they're all GCP and they're like, yeah, they're mega data scientists and to Azure to actually, oh really? Okay.</p><p><strong>Matt Merrill:</strong> Yeah. Rocket is mostly AWS. Like all of our clients use AWS, a lot of DEPT clients, which are, right now, anyway, primarily in Europe, they're all using Azure and GCP.</p><p><strong>Tyler Renelle:</strong> Isn't it? Interesting.</p><p>I'll have to hit the drawing board one of these days and check out those offerings. Cause yeah, DEPT swears by it. </p><p><strong>Matt Merrill:</strong> with our head of DevOps Gerawan the other day. And I agree with him that GCP is much easier to use than AWS. AWS is an Interface can be very difficult to use.</p><p><strong>Tyler Renelle:</strong> Very difficult to use. AWS is so old. Everything is just band-aids on band-aids from like 2007, like the math to know our calculus statistics and linear rails. But don't learn it. </p><p><strong>Matt Merrill:</strong> I'm tapping out if that's chase.</p><p><strong>Tyler Renelle:</strong> Look, you'll know when you need it. So what I was getting at with that act, so I wrote about a  care OSS neural network that learns the thumb, the thumbing process, thumbs up a book, thumbs down a PR a book. Yeah. It's hard to explain why I use what I use there, but the last module of the neural network, the last layer is like layer dot soft, man.</p><p>But what is a soft max? And so that's not a super black box. You have to know what a soft max is to tack on that layer and understand what's coming out of it to use it. So you look up softmax oh, it's something in statistics. So then you deep dive into this concept of soft. You'll know when you need mare that the time will come if you really get into machine-learning, but punt for as long as you can.</p><p><strong>Matt Merrill:</strong> There's this series of YouTube videos from a guy from the New York Times who explains math concepts for adults in kind of very simple ways. I should find that and link it in the description because is this stack quest?</p><p><strong>Tyler Renelle:</strong> I don't know that doesn't sound familiar to blue one brown,</p><p><strong>Matt Merrill:</strong> That could be it though. Those are good.</p><p><strong>Tyler Renelle:</strong> Those are really stack quests that will get you mostly there as far as needed math knowledge for machine learning. Nice.</p><p><strong>Matt Merrill:</strong> Nice. I think we touched on a lot of really good stuff. I know, like I'm super interested. I'm charged up. I want to start now. So like a shameless plug here.</p><p>How do we find your podcast? Because that's probably a good place to start, right?</p><p><strong>Tyler Renelle:</strong> So just look at machine learning. In Spotify or iTunes or whatever you find it. The website is O C D E V E l.com forward-slash MLG C developer.com for session NLG.</p><p><strong>Matt Merrill:</strong> Cool. And it sounds if you're not, if you are interested, get yourself an AWS account, start playing around with SageMaker.</p><p>I know that you've got a couple of episodes and more of your latest episodes are on SageMaker. I'm going to go check those out. Nice. Anything else you'd recommend against final thoughts on ML?</p><p><strong>Tyler Renelle:</strong> So you're like in those podcasts, it's a bit of a, like a course, almost an audio course and more conversational format audio course, take a eudemonic class and turn it into.</p><p>And I have a forward slash resources section. And so if you even want to skip the podcast, go to the resources section, cause that's a step-by-step beginning to end for how to learn machine learning. And I keep it constantly to keep it up to date. So like the first course, everybody recommends on the internet this Andrew in Coursera course, but that's an example of following this curriculum.</p><p>And by the way, a little teaser for folks like w we're gonna actually going to put up an episode on the machine learning guide. Dev ops topics and machine learning. So if you already know all this stuff, I don't know why you would have listened to it this far but hope it's cool, thanks. And you can keep an eye out on the feed for that.</p><p>So that should be fun. Nice. Thanks for taking some time to explain this to me, like I was five. I appreciate it.</p><p><strong>Tyler Renelle:</strong> Thanks man. Thanks for having me on.</p>]]></content:encoded></item><item><title><![CDATA[Getting Started with Parallel Processing in Rust]]></title><description><![CDATA[Rust helps mitigate concurrency hazards by design, but it's still up to the programmer to construct their program logic thoughtfully so they can take advantage of the power of concurrent and parallel processing.]]></description><link>https://engineering.deptagency.com/parallel-processing-in-rust/</link><guid isPermaLink="false">6244a2231a8623003da8d48a</guid><category><![CDATA[Platforms]]></category><category><![CDATA[rust]]></category><dc:creator><![CDATA[Ashwin Sundar]]></dc:creator><pubDate>Wed, 06 Apr 2022 16:57:47 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/parallel-processing-rust.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/parallel-processing-rust.jpg" alt="Getting Started with Parallel Processing in Rust"/><p><a href="https://www.rust-lang.org/?ref=dept-engineering-blog.ghost.io">Rust</a> is a systems programming language that is quickly gaining traction at well-known companies including Amazon, Discord, Dropbox, Meta, Alphabet, and Microsoft. It is built for <a href="https://www.rust-lang.org/?ref=dept-engineering-blog.ghost.io">performance, reliability, and productivity</a> and has been voted the most loved programming language according to Stack Overflow's <a href="https://insights.stackoverflow.com/survey?ref=dept-engineering-blog.ghost.io">Annual Developer Survey</a> since 2016. Some large-scale commercial projects that have been built using Rust include:</p>
<ul>
<li>Mozilla's <a href="https://servo.org/?ref=dept-engineering-blog.ghost.io">Servo</a> parallel browser engine</li>
<li>Discord's <a href="https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f?ref=dept-engineering-blog.ghost.io">Read States</a> service</li>
<li>Polkadot’s <a href="https://github.com/paritytech/polkadot?ref=dept-engineering-blog.ghost.io">Substrate</a> blockchain engine</li>
<li>Figma's <a href="https://www.figma.com/blog/rust-in-production-at-figma/?ref=dept-engineering-blog.ghost.io">Multiplayer</a> service</li>
</ul>
<p>All of these real-world use cases of Rust utilize and benefit from concurrent and parallel processing, which can be daunting to implement on a good day, and pretty terrifying when implemented <a href="https://en.wikipedia.org/wiki/Therac-25?ref=dept-engineering-blog.ghost.io">badly</a>. Rust helps mitigate concurrency hazards by design, but it's still up to the programmer to construct their program logic thoughtfully so they can take advantage of the power of concurrent and parallel processing.</p>
<p><strong>When should I use concurrent or parallel processing, instead of serial processing?</strong></p>
<p>Most modern processors have multiple cores to work with, which means you can use these cores to achieve significant performance gains:</p>
<ul>
<li>When you have a lot of independent computations to process, such as a giant for-loop.</li>
<li>When some of your threads contain computations that are particularly lengthy to calculate. It's nice to run these on the &quot;backburner&quot; without blocking your program from performing other computations.</li>
<li>When you have low <a href="https://www.mathworks.com/help/parallel-computing/decide-when-to-use-parfor.html?ref=dept-engineering-blog.ghost.io">parallel overhead</a></li>
</ul>
<p><strong>How do I implement parallel processing in Rust?</strong><br>
My favorite way to learn new programming languages is by combining it with my love for math and solving problems in <a href="https://projecteuler.net/?ref=dept-engineering-blog.ghost.io">Project Euler</a>. To demonstrate parallelization in Rust, let's solve a <a href="https://projecteuler.net/problem=1?ref=dept-engineering-blog.ghost.io">simple problem</a> that I tweaked slightly so we can focus on the implementation of our solution:</br></p>
<p><em>If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1,000,000.</em></p>
<p><strong>Solution Methodology</strong><br>
While the mathematically elegant solution would be to use an <a href="https://en.wikipedia.org/wiki/Arithmetic_progression?ref=dept-engineering-blog.ghost.io">arithmetic series</a>, let's just focus on the simple solution, which is to figure out if each number in the range is divisible by 3 or 5. If it is, let's add it to a running sum we're keeping track of.</br></p>
<p><strong>Example 1 - No parallelization</strong></p>
<pre><code class="language-rust">fn euler1_unpar(input: i32) -&gt; i64 {
    let mut sum: i64 = 0;
    for i in 1..input {
        if i % 3 == 0 || i % 5 == 0 {
            sum += i as i64;
        }
    }
    sum
}
</code></pre>
<p><strong>Code walkthrough:</strong> In this example, we accept an <code>input</code>, and iterate on every number between <code>1..input</code> to determine if it is divisible by 3 or 5 using the modulo <code>%</code> operator. If it is, then add the value to a running <code>sum</code> we're keeping track of. At the end, return the sum. In Rust, you can return a value by simply calling it without a semicolon after the expression. Since Rust is a <a href="https://en.wikipedia.org/wiki/Strong_and_weak_typing?ref=dept-engineering-blog.ghost.io">strongly-typed</a> language, we need to tell the compiler to add the original <code>i32</code> input and convert the sum to an <code>i64</code>, so that we have enough space to store the answer.</p>
<p>Let's calculate a performance benchmark for this function so we can compare it to our multithreaded optimization that we'll write next. We can calculate this benchmark with the <a href="https://docs.rs/easybench/latest/easybench/?ref=dept-engineering-blog.ghost.io">easybench</a> crate, an importable package in Rust.</p>
<pre><code class="language-rust">use easybench::{bench};
let input = 1000000;
println!(&quot;euler1_unpar: {}&quot;, bench(|| euler1_unpar(input) ) );

&gt;&gt; euler1_unpar: 14.429298ms (R²=0.999, 70 iterations in 21 samples)
</code></pre>
<p>Our unparallelized function takes about <code>14.4</code> milliseconds to execute.</p>
<p><strong>Example 2 - Parallelized (2 threads)</strong></p>
<pre><code class="language-rust">fn euler1_par(input: i32) -&gt; i64 {
    use std::thread;

    let handle1 = thread::spawn(move || {
        let mut thread1_sum: i64 = 0;
        for i in 1..input / 2 {
            if i % 3 == 0 || i % 5 == 0 {
                thread1_sum += i as i64;
            }
        }

        thread1_sum
    });

    let handle2 = thread::spawn(move || {
        let mut thread2_sum: i64 = 0;

        for i in (input / 2)..input {
            if i % 3 == 0 || i % 5 == 0 {
                thread2_sum += i as i64;
            }
        }

        thread2_sum
    });

    handle1.join().unwrap() + handle2.join().unwrap()
}
</code></pre>
<p><strong>Code walkthrough:</strong> Here, we use the <code>thread</code> module so that we can take advantage of the native multithreading available in Rust. A new thread is created by calling <code>thread::spawn</code>, into which a <code>closure</code> is passed. <a href="https://doc.rust-lang.org/book/ch13-01-closures.html?ref=dept-engineering-blog.ghost.io">Closures</a> are anonymous functions that allow you to access environment variables, such as the <code>input</code> variable. This closure does the same mathematical computation as <code>euler1_unpar</code>, except we only process one half of the total range in the thread. The other half is saved for the second thread. We also need to <code>move</code> a copy of the input into the closure's scope so that the thread can take ownership of the data and use it. Writing code like this can seem tedious and time-consuming, but is required by Rust to help reduce the risk of <a href="https://doc.rust-lang.org/book/ch16-00-concurrency.html?ref=dept-engineering-blog.ghost.io">concurrency errors</a>.</p>
<p><code>thread::spawn</code> returns a <code>JoinHandle</code> type, which contains some convenience methods that allow us to take back control over the threads and handle their results. In this case, <code>JoinHandle::join()</code> halts execution of the function until our threads have finished their calculations. <code>.unwrap()</code> exposes the answers we've calculated in each thread, and then finally we sum those answers up.</p>
<p>Let's see how long this function takes to run:</p>
<pre><code>use easybench::{bench};
let input = 1000000;
println!(&quot;{}&quot;, euler1_par(input));

&gt; euler1_par:  7.345441ms (R²=0.998, 133 iterations in 27 samples)
</code></pre>
<p>The parallelized function takes about <code>7.3</code> milliseconds to execute.</p>
<p><strong>Conclusion</strong><br>
The parallelized code runs almost twice as fast as our unparallelized code, and we seem to only lose a little performance due to the overhead of setting up the threads. Nice!</br></p>
<p>This example demonstrates a way to get started with parallel processing in Rust. You often need to design your program with parallelization in mind from the get-go, as you are forced to think about the flow of your code and determine what pieces of the code take the longest to run and would benefit from parallelization.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Use Airflow to Project Confidence in Your Data]]></title><description><![CDATA[“Is the data ready?” Or more accurately, “Can I be confident that this data is correct?”]]></description><link>https://engineering.deptagency.com/use-airflow-to-project-confidence-in-your-data/</link><guid isPermaLink="false">642b1f3d9527e0003dca405e</guid><category><![CDATA[data]]></category><category><![CDATA[Airflow]]></category><dc:creator><![CDATA[Bob Briski]]></dc:creator><pubDate>Sun, 03 Apr 2022 04:00:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/airflow.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/04/airflow.png" alt="Use Airflow to Project Confidence in Your Data"/><p>A key tenet of Raybeam's (Now DEPT®) mission, whenever we start at a new client, is to deliver value quickly. This value often takes the form of marketing or product insights that we derive by combing through large amounts of data. As Raybeam is a software engineering firm <a href="https://www.deptagency.com/service/growth/data-intelligence/data-strategy/?ref=dept-engineering-blog.ghost.io">specializing in data and analytics</a>, we tend to work on the data backend and the analytical frontend simultaneously.</p><ul><li><a href="https://medium.com/raybeam/use-airflow-to-project-confidence-in-your-data-abd160f3dc8c?ref=dept-engineering-blog.ghost.io#e879">Introducing Raybeam’s status plugin</a></li><li><a href="https://medium.com/raybeam/use-airflow-to-project-confidence-in-your-data-abd160f3dc8c?ref=dept-engineering-blog.ghost.io#6106">Examples</a></li><li><a href="https://medium.com/raybeam/use-airflow-to-project-confidence-in-your-data-abd160f3dc8c?ref=dept-engineering-blog.ghost.io#5afe">Astronomer and Enterprise Airflow</a></li></ul><h3 id="is-the-data-ready"><strong><strong>Is the data ready?</strong></strong></h3><p>The most furious pace of engineering work tends to happen at the beginning of a project as we update or migrate the data and processing architecture. As data quality improves, we’re able to use it to deliver more and more insights. While we do pride ourselves on delivering sustained quality at a quick pace, not everything can happen at once. Inevitably, this leads to one of the most asked questions in analytics:</p><p>“Is the data ready?” Or more accurately, “Can I be confident that this data is correct?”</p><p>Modern data pipelines often consist of hundreds of external sources sending millions or billions of events per day. A good pipeline can collect, clean, process and serve that data to users quickly and on a regular schedule. When a customer marketing manager checks their dashboard in the morning, it should reflect the current state of customer marketing. There should be no surprises. If the data is meant to be less than 2 hours old, it should be.</p><p>But what happens if one source of data hasn’t been updated? It could be one of a million reasons; an API token expired, the source was late, or devops had an unscheduled outage. Anyone in a data organization has had this happen to them, probably multiple times. But a marketing manager, why should they know about this? They want to know one thing:</p><h3 id="%E2%80%9Ccan-i-be-confident-that-this-data-is-correct%E2%80%9D"><strong><strong>“Can I be confident that this data is correct?”</strong></strong></h3><p>Trust in data, like all trust, takes a long time to build and a moment to break. Once broken, you must start from scratch. Broken trust doesn’t just mean the data is wrong. It means the data is wrong <strong><strong>and no one was notified.</strong></strong> It’s an important distinction. It’s not possible to control all of your upstream sources so that data is never wrong or out-of-date. But it is possible to notify users they make a decision based on incorrect or outdated data.</p><h3 id="introducing-the-raybeam%E2%80%99s-status-plugin"><strong><strong>Introducing the Raybeam’s status plugin</strong></strong></h3><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-1.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="714" height="120" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-1.png 714w"/></figure><p>We wanted to create something that is simple for analysts or data engineers to incorporate into their workflow but also effective for reporting status to those colleagues that might not be as familiar with the underlying data process.</p><p>That’s why we’ve created the <a href="https://github.com/Raybeam/rb_status_plugin?ref=dept-engineering-blog.ghost.io">rb_status_plugin</a> for Airflow.</p><p>For most business users, it should answer the question, “Is my data ready?” quickly. If the answer is no, it should give the user some way to follow up.</p><p>For analysts or engineers, it should fit seamlessly into their Airflow data system and be quick and easy to set up.</p><p>Our plugin installs like any other Airflow plugin. There are detailed instructions for a variety of Airflow setups in the <a href="https://github.com/Raybeam/rb_status_plugin/blob/master/README.md?ref=dept-engineering-blog.ghost.io">README</a>.</p><p>Once installed, the <a href="https://github.com/Raybeam/rb_status_plugin?ref=dept-engineering-blog.ghost.io">rb_status_plugin</a> is incredibly easy to use. It allows you to choose a set of Airflow tasks and assign their result to a report. That report can be scheduled the same way an Airflow DAG is scheduled. In fact, that’s exactly what it is. The plugin will create a DAG to run it’s checks based on the tasks you care about.</p><h3 id="an-example"><strong><strong>An example</strong></strong></h3><p>The tasks can be anything, but we’ve found it works best with data quality checks. We tend to run many Pass/Fail tests against our data pipeline during and after its execution. In a very simple example, we could have a set of tasks that verifies:</p><ol><li>Our dataset is less than 2 hours old</li><li>Visitors and sales are positively correlated</li><li>The total number of rows added to table X is within 1 standard deviation of the last 2 weeks</li></ol><p>We could bundle these tasks into a report and schedule it for every morning at 5AM. Now, say this dataset contains all of the data used in the marketing acquisition team’s dashboards. We could make sure all of the marketing managers on that team were subscribed to this report. Each morning, when they log on, they’ll see an email showing them the status of the data that day. It would, presumably, be green the majority of the time. For the days where there was a problem, however, they would see that the data is not ready and have a quick way to follow up.</p><p>Our primary goal was to make it both simple to create reports and simple to use the reports. Our philosophy is to make small, simple tools that work well. Obviously there are many other parts that go into providing confidence, including scheduling, data quality testing, logging and alerting just to name a few. We build tools to handle those parts of the process as well. We’re working on a plan to make it easy for all of these tools to work together. For now though, we think we’ve created a simple tool that will make our lives and our clients’ lives, just a little easier. Maybe it will help you too.</p><h3 id="how-it-works"><strong><strong>How it works</strong></strong></h3><p>We’re going to walk through a couple of use cases to show how we’re using this plugin.</p><p>First, let’s assume we’re working with a social media marketing manager. They handle all social channels and their overriding goal is to make sure they’re getting the best ROI for their budget.</p><p>Let’s review a DAG that might be running for this type of work</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-2.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="718" height="190" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-2.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-2.png 718w"/></figure><p>I’ve just made this up but you can see that I’m loading data from some social channels, joining it, running a few tests and then building some downstream data sets.</p><p>From the green outlines, you can tell they’re all passing right now.</p><h3 id="creating-a-report"><strong><strong>Creating a report</strong></strong></h3><p>Now, let’s pretend I’m an analyst on the social media marketing team and I want to create a report so the people I support can feel confident about the data without asking me every day.</p><p>The <a href="https://github.com/Raybeam/rb_status_plugin/?ref=dept-engineering-blog.ghost.io">rb_status_plugin</a> adds a menu item in Airflow named “Status”. You have two options in the drop down. You can view all current statuses in the “Status Page” or you can go to the current report list.</p><p>We’re going to click into the “Status Page” in this example.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-3.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="1231" height="465" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-3.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/airflow-3.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-3.png 1231w" sizes="(min-width: 720px) 720px"/></figure><h3 id="empty-status-page"><strong><strong>Empty Status Page</strong></strong></h3><p>We haven’t created any reports yet, so we’ll be greeted with some helpful options and instructions.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-4.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="723" height="239" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-4.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-4.png 723w" sizes="(min-width: 720px) 720px"/></figure><p>We want to create a new report, so let’s click on “Create a new report”<br/></p><h3 id="new-report"><strong><strong>New report</strong></strong></h3><p>We’re taken to a straightforward page for adding a report.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-5.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="737" height="674" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-5.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-5.png 737w" sizes="(min-width: 720px) 720px"/></figure><p>You can name and describe the report so that anyone receiving it knows what the status relates to.</p><p>The owner and owner email will automatically be added to the list of subscribers. Emails for the main audience of this status report should be added in the “Subscribers” fields. In our example, that would be anyone in the social media marketing team.</p><h3 id="new-report-scheduling"><strong><strong>New report : Scheduling</strong></strong></h3><p>For scheduling, the options follow Airflow’s scheduling options. You can choose “None” if you’d like to manually trigger the report. This is good for when you’re first testing it. Otherwise, you can choose daily, weekly or, for more customized cases, cron notation.</p><h3 id="new-report-tests"><strong><strong>New report : Tests</strong></strong></h3><p>Now we’re going to choose the tests that will combine to determine the status of the report. Tests, in our case, could be any task in Airflow. A lot of times, that will be data quality tests. However, the rb_status_plugin library can use any Airflow task.</p><p>You’ll notice we try to make this easier by providing a type-ahead search and dropdown.</p><p><em>Something to note, if a task has not yet been run, it won’t be in Airflow’s database so it won’t show up in the dropdown. That means that you need to run the specific task at least once manually, or run the entire DAG that it’s in at least once.</em></p><h3 id="running-the-report"><strong><strong>Running the report</strong></strong></h3><p>If you go back to the status page, you’ll see a warning.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-6.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="1222" height="552" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-6.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/airflow-6.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-6.png 1222w" sizes="(min-width: 720px) 720px"/></figure><p>Reports are actually DAGs behind the scenes so they need to run.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-7.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="716" height="293" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-7.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-7.png 716w"/></figure><p>Since we set the schedule to “None”, it’s not going to run on its own. We’ll need to manually trigger it.</p><p>You could trigger it like any other DAG, by clicking the “play” button in the DAG list. We also provide a “run” and “delete” option in the report list UI.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-8.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="708" height="232" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-8.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-8.png 708w"/></figure><p><em>Note that all DAGs are created in the “Off” state for Airflow. We at Raybeam like to follow conventions of the underlying tool as much as possible, so reports will also be “Off” after creation. You’ll need to click on the “On/Off” toggle either in the report list or DAG list to turn the report on and run it.</em></p><h3 id="running-the-report-1"><strong><strong>Running the report</strong></strong></h3><p>Once the report is “On” and you’ve run it, you will need to wait until it’s picked up and run by Airflow. It should finish quickly since it’s mainly checking Airflow’s own underlying database.</p><p>Once the report DAG finishes, you’ll be able to see it in the Status page</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-9.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="717" height="344" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-9.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-9.png 717w"/></figure><p>It looks like all tests have passed and we have a successful status. If <a href="https://stackoverflow.com/questions/51829200/how-to-set-up-airflow-send-email?ref=dept-engineering-blog.ghost.io">you’ve set up email</a>, all of your subscribers will also receive the status in their inbox</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-10.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="740" height="491" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-10.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-10.png 740w" sizes="(min-width: 720px) 720px"/></figure><p>Clicking on “details” will take the user back to the Airflow status page.</p><h3 id="schedule-the-test"><strong><strong>Schedule the test</strong></strong></h3><p>Now is a good time to go back and schedule this report to go out on a regular basis. That way you can keep your users up to date automatically.</p><h3 id="second-report-failing-tests"><strong><strong>Second report : Failing tests</strong></strong></h3><p>Let’s create a second report. For this one, I’m going to pretend that I’m on the data engineering team. I don’t know exactly how the business is using every piece of data, but I do know that all of my external partners need to be loaded correctly, every day, on time.</p><p>For this example, I’m going to have our Facebook load fail. Maybe someone didn’t check and the API key expired.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-11.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="703" height="193" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-11.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-11.png 703w"/></figure><p>In this particular case, we’ve set up the DAG to continue, even if a particular job fails. That’s because we’ve set up other tests throughout that DAG that tell us if the final data set is valid.</p><p>Maybe, for the social marketing team, they can go one day without new Facebook data. Yeah, yeah, I know, pretty unlikely but it’s an example.</p><h3 id="create-the-loading-report"><strong><strong>Create the loading report</strong></strong></h3><p>For this report, I’m going to include all of the load tasks. In an actual Airflow instance, maybe I have load tasks scattered throughout a bunch of DAGs. That’s fine! The rb_status_plugin views everything at a task level. You can add tasks to a report, no matter which DAG they’re in.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-12.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="735" height="655" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-12.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-12.png 735w" sizes="(min-width: 720px) 720px"/></figure><p>Now you can see our report in the report list. We’ll need to turn it on and run it.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-13.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="715" height="278" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-13.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-13.png 715w"/></figure><h3 id="failed-status-report"><strong><strong>Failed status report</strong></strong></h3><p>Well, as you can see, our status report failed because the Facebook loading job failed.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-14.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="718" height="397" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-14.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-14.png 718w"/></figure><p>You’ll also notice that the social marketing report is still listed. We include a summary note at the top that “Some tests are failing”. If it’s red, you know to check the specific reports. If it’s green, you know the entire system is good to go.</p><p>The failed task is listed in the details and it links back to the job log for that task run. You can easily use the status report to click back into the task and find the underlying problem.</p><p>And once again, any subscribers will receive an email with the current status of the report.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-15.png" class="kg-image" alt="Use Airflow to Project Confidence in Your Data" loading="lazy" width="721" height="436" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/airflow-15.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/airflow-15.png 721w" sizes="(min-width: 720px) 720px"/></figure><h3 id="conclusion"><strong><strong>Conclusion</strong></strong></h3><p>By now, you’ve been thoroughly introduced to the new <a href="https://github.com/Raybeam/rb_status_plugin/?ref=dept-engineering-blog.ghost.io">rb_status_plugin</a>. We discussed the need for it and have gone through a couple of use cases.</p><p>Try it out and give us feedback.</p><h3 id="astronomer-and-enterprise-airflow"><strong><strong>Astronomer and Enterprise Airflow</strong></strong></h3><p>You may have noticed that our screenshots look a little different than your Airflow setup. That’s because we use <a href="https://www.astronomer.io/?ref=dept-engineering-blog.ghost.io">Astronomer</a> when we have the choice. <a href="https://www.astronomer.io/?ref=dept-engineering-blog.ghost.io">Astronomer</a> is the easiest way to run, develop and deploy Airflow that we’ve found.</p><p>We use their docker image for local development. Scaling to an enterprise capable deployment can be done in minutes. Give them a try if you haven’t yet.</p>]]></content:encoded></item><item><title><![CDATA[Why You Should Use React Native For Your Next Mobile App]]></title><description><![CDATA[Many seem to think that the only proper way to create a phone app is by engineering it natively. In this post, I break down why React Native is a great way to build an app. ]]></description><link>https://engineering.deptagency.com/why-you-should-use-react-native-for-your-next-mobile-app/</link><guid isPermaLink="false">6244b7241a8623003da8d53b</guid><category><![CDATA[react]]></category><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Matthew Brooks]]></dc:creator><pubDate>Wed, 30 Mar 2022 20:11:28 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/react-native.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/react-native.jpg" alt="Why You Should Use React Native For Your Next Mobile App"/><p>I believe there is a wide misconception about developing mobile applications. </p><p><em>Many seem to think that the only proper way to create a phone app is by engineering it natively. </em></p><p>This splits codebases, increases costs, and consumes more resources. Depending on your needs and how fast it’s required to be done, this is not always ideal. Herein lies the cross-platform and single-source solution: React Native.</p><p>There was not an instance where I directly experienced a conversation about choosing between React Native and native. Throughout my career, I’ve always used tools or frameworks like RN to avoid this conundrum. While I believe that a project developed within a single source is the best way to go, I began to wonder why others might trust in the opposite.</p><h2 id="setup-and-development-time">Setup and Development time</h2><p>You can iterate on development faster in React Native than in native development. I was surprised that setup time was basically negligible when I did it myself to compare native and RN. </p><p>React Native took just short of 5 minutes to get going while the collective time from Android and iOS app creation took about 20 minutes which excludes some environmental issues I ran into. RN springs your development time forward with all of the tools out there. There are many yarn and npm packages that solve a multitude of problems and/or provide utility, and they do it for both platforms. </p><p>For reference, I used a package that helped me manage push notifications for both platforms; Something that is normally specific to a platform. It saved me a lot of time because I didn’t have to develop two solutions for the same problem.</p><p>I think you’ll find that the fact that React Native lets you hit the ground running is a great benefit as time is the biggest factor when it comes to the cost of development. This lets us create a lot of momentum early which will lead to less hiccups and bottlenecks later.</p><h2 id="a-case-study">A Case Study</h2><p>Consider the following scenario: Your company needs a tool for internal use, and everyone uses their personal phones and/or you have a mixed bag of Android and iOS devices that are for work. Noone outside your workplace will be able to use it as it is made specifically for your business to do any number of things. In this case, I feel as though it is much simpler to go with RN. </p><p>Say everyone at your work has an iPhone, then you all might be inclined to go with the native approach which sounds easy enough. What if later on, during the development of the project, you hire someone with an Android device, or perhaps a current employee switches over. I would imagine that it’d be a tough sell to get them to switch devices, and now your <a href="https://www.deptagency.com/service/engineering/web-mobile-app-engineering/?ref=dept-engineering-blog.ghost.io">app development</a> time is doubled, assuming you don’t wanna start over from scratch with React Native.</p><p><strong>What About the Device Features?</strong></p><p>Also, if your app does need the capacity to perform low-level operations or get sensitive data, such as getting access to Location, Camera, Microphone, or even Bluetooth, you might think, “Native development should make this pretty easy.” While that is irrefutable, RN has tools that cover most of these options and are easily integratable.</p><blockquote class="kg-blockquote-alt">“..things are never as complicated as they seem. It is only our arrogance that prompts us to find unnecessarily complicated answers to simple problems.”</blockquote><p>- Muhammad Yunus, <a href="https://www.goodreads.com/work/quotes/1390141?ref=dept-engineering-blog.ghost.io">Banker to the Poor: Micro-Lending and the Battle Against World Poverty</a></p><p>I know what you’re thinking, “He said ‘most’, so there must be an exception.” </p><p>You would be correct, but that doesn’t mean if you suddenly need to add Bluetooth capabilities that it is impossible. Because of the fact that you have access to the individual platforms file structure (iOS and Android) within a React Native project, there are definitive ways to navigate around this situation; This most likely will not be as easy as it would be.</p><h2 id="the-development-team">The Development Team</h2><p>Having fewer people to coordinate with, when it comes to all stages of the software life cycle, is helpful because you’ll only field one team of developers for React Native versus having two for native projects.</p><p>A single band of engineers leads to the added benefit of maintaining a single source project and architecture. </p><p>As stated before, this is easier because you’ll only have one development team, so better communication, more fluid collaboration, and the ability to share resources between the engineers are all huge pluses. The two different teams that you would need for native development won’t be able to share what they’ve created. They’ll have to make everything on their own, yet you will come out with a look and feel that feels more natural for that device which some will argue is a benefit. With proper design in mind, this can be overcome when using RN because you still have the ability to use components conditionally depending on the platform, as the framework knows what device it’s being run on.</p><h2 id="testing-with-react-native">Testing with React Native</h2><p>Now, when it comes to testing the application with React Native, your test suite will need to run on both devices, but you don’t need to make two separate flows, you just use one. </p><p>This might seem obvious, but I think it’s important to mention. </p><p>Having separate test suites for native development requires that they are checking all of the same behaviors and functions to verify that there is as close of a match as possible. This just spells time consumption because depending on how you chop up the verification and independence of these pieces, you can end up waiting on the completion of one side. Having one source for your project severely reduces any bottleneck that would likely occur if you need to get both native apps out at the same time.</p><h2 id="future-of-react-native">Future of React Native</h2><p>There’s one more important question left to address. What about the future of React Native? </p><p>I think the longevity of this versatile technology is dependent upon the lifetime of the age of smartphones. Once our personal portable devices transcend our pockets and purses into the next thing, RN will continue to retain its usefulness in the mobile space. It’s been around for a long time, and people still continue to update it because there’s always something new that it will need to support. In fact, a new version of it was just released the last month. As we keep pushing phones further by adding new features and hardware, there will always be gaps to close which can take some time to fill in. Hopefully, that is irrelevant to your situation.</p><p>To summarize, if your next mobile app needs to work on iOS and Android devices, doesn’t have to support brand new hardware features to run, or is required to be stood up very quickly; then you should be using RN. </p><p>This technology is powerful and useful in its own right. Consider using it for your next project because there are so many use cases that it covers, and it will continue to be relevant as it is always being updated by its maintainers.</p>]]></content:encoded></item><item><title><![CDATA[Contentful Migrations]]></title><description><![CDATA[Migrations allow us to describe how to adjust the data model of an existing database. They make a scripted, surgical change to a database rather than replacing it entirely and blowing away any content changes.]]></description><link>https://brianschiller.com/blog/2022/03/24/contentful-migrations</link><guid isPermaLink="false">624342ce1a8623003da8d3bf</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Brian Schiller]]></dc:creator><pubDate>Tue, 29 Mar 2022 19:28:39 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/migration.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/migration.jpg" alt="Contentful Migrations"/><p>I've spent the last few days getting Contentful set up for a new project and have become frustrated with the migration story. As a developer, the primary way I interact with a CMS is by defining the data model. When project requirements change, I will need to update and grow the model to accommodate new feature requests. A good CMS, from my perspective, is one that makes this schema migration process safe and painless.</p><p><strong>Why we need migrations</strong></p><p>Some CMSs don't support migrations. The schema and articles all live in the same database and the only way to push changes up is to either</p><ol><li><strong>Do it live:</strong> click around in the interface, hoping you don't break something or delete something important.</li><li><strong>Stop the world</strong>: clone down production, make your changes, then completely replace the production data with your local stuff. Any data added in the meantime is lost, so make sure to warn the content editors not to do anything while you're working.</li></ol><p>For many sites, "Do it live" is too risky, and "Stop the world" requires too much coordination with the editors. Imagine a daily newspaper that couldn't publish any new articles (or even work on drafts!) because the devs were working on adding a field to the content model.</p><p>Migrations allow us to describe how to adjust the data model of an existing database. They make a scripted, surgical change to a database rather than replacing it entirely and blowing away any content changes.</p><p>Compared with the other two processes, writing a migration script is less discoverable. However, it doesn't require coordination with content authors. You're free to spend more time experimenting and testing your changes because no one is waiting for you to hurry up and let them get back to work.</p><p>Contentful claims to support scripted migrations. True enough, you can write a script, test it out in an isolated environment, and apply it to production when you're ready. But they're missing two important elements:</p><ol><li>A way to track migrations already applied, and</li><li>A way to inspect the current schema within a migration.</li></ol><p><strong>Track migrations</strong></p><p>When I write migrations, they're committed to the code repo. Once that branch is merged, the CI/CD server runs any migrations that haven't been applied yet. This should be a familiar model for anyone who's worked on a Rails or Django app.<br>Contentful's API for applying migrations is to use their CLI:</br></p><pre><code class="language-bash">contentful space migration some-migration-file.js</code></pre><p><br>This works great when executed interactively. It shows you a neat preview of the operations it's about to run, and in what order. There's even a way to <a href="https://github.com/contentful/contentful-migration/blob/8fce9244f81d97e0dbe18db32665e1a2008ae71d/examples/12-transform-content.js?ref=dept-engineering-blog.ghost.io">populate new fields based on old ones</a>. But there's no way to tell Contentful to only run new migrations. Most scripts are not idempotent: they'll fail if they're run a second time. Even worse, they might run without an error but break your data.</br></p><p><strong>Inspect current schema</strong></p><p>In trying out Contentful migrations, I wanted to try a relatively simple change: add a new option to an existing dropdown list.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/contentful-dropdown-options.png" class="kg-image" alt="Contentful Migrations" loading="lazy" width="944" height="313" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/03/contentful-dropdown-options.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/contentful-dropdown-options.png 944w" sizes="(min-width: 720px) 720px"/></figure><p><br>Unfortunately, there's no way to say "Add 'RPG' to the list of game types". Instead, you have to say "The list of game types should be 'Casino', 'Other', 'Solitaire', 'Trick Taking', and 'Rummy'", listing all allowable options. Imagine someone else working on a concurrent change to add "Tower Defense" to the list of game types. Whichever migration lands last will blow away the other person's changes.</br></p><p>It's actually worse than that. The list of options is specified as a validation. When you write the migration, you end up writing the code equivalent of "The only validation needed Game Type is to check that it appears in this list: <code>["Casino", "Other", "Solitaire", "Trick Taking", "Rummy", "RPG"]</code>". So your migration will conflict with a change to any other validation on that field. Here's the code:</p><pre><code class="language-javascript">module.exports = function (migration) {
  const rules = migration.editContentType('rules');
  const gameType = rules.editField('gameType');
  gameType.validations([
    // I wanted to say something like "existing allowed values, plus 'RPG'",
    // but there doesn't seem to be any way to do that easily.
    {
      in: ["Casino", "Other", "Solitaire", "Trick Taking", "Rummy", "RPG"],
    }
    // similarly, if there were other validations on this field, they'll be
    // gone after this migration runs.
  ]);
}`</code></pre><p>In a system more serious about supporting schema migrations, it would be possible to inspect the present state of a model <em>at the time the migration is run</em>. Django and Rails both have this facility. This would let you write something like this:</p><pre><code class="language-javascript">module.exports = function (migration, currentSchema) {
  const existingValidations = currentSchema.rules.gameType.validations;
  const existingPredefinedList = existingValidations.find(v =&gt; v.in);

  const rules = migration.editContentType('rules');
  const gameType = rules.editField('gameType');
  gameType.validations([
    ...existingValidations.filter(v =&gt; !v.in),
    {
      in: [...existingPredefinedList?.in, 'RPG'],
    }
  ]);
}</code></pre><p><strong>Papering over the problem</strong></p><p>Inspecting the current schema is a nice-to-have, but I wasn't willing to live without migrations that are executed at deploy-time. I hacked together a script that:</p><ol><li>Creates a new content type, `migrations` that tracks the migrations that have already run.</li><li>Queries the entries of this type, comparing the output to the migrations found in a <code>contentful/migrations</code> directory.</li><li>Applies any new migrations to the environment.</li></ol><p>As it happens, other people had the same problem. I came across <a href="https://github.com/deluan/contentful-migrate?ref=dept-engineering-blog.ghost.io">deluan/contentful-migrations</a>, which did nearly the same thing and switched to using that instead. It's been around for longer and is more battle-tested than my hacky bash script.<br/></p>]]></content:encoded></item><item><title><![CDATA[The art of polite code reviews]]></title><description><![CDATA[Taking the time to write meaningful comments while providing feedback to your coworkers can go a long way. Yves shares some of the principles he follows to articulate positive criticism during code reviews.]]></description><link>https://engineering.deptagency.com/the-art-of-polite-code-reviews/</link><guid isPermaLink="false">62434cf11a8623003da8d457</guid><category><![CDATA[People]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Yves Gurcan]]></dc:creator><pubDate>Tue, 29 Mar 2022 18:36:28 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/code-on-computer.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/code-on-computer.jpg" alt="The art of polite code reviews"/><p><strong>It can be hard to be on the receiving end of feedback.</strong></p><p><strong>If you're a developer, you probably submit and review pull requests every day or so. </strong>How many times have you felt a little hurt or frustrated by the comments of a fellow developer on a piece of code you wrote? Maybe a mistake you made gave you the wrong impression and triggered many <a href="https://en.wikipedia.org/wiki/Impostor_syndrome?ref=dept-engineering-blog.ghost.io">doubts about your abilities</a>?</p><p><strong>I've noticed that it can be a common occurrence in development teams to deliver criticism in a heavy-handed way throughout the process of code review. </strong>It can be challenging to provide constructive feedback when there is an urgent deadline to meet or if you're preoccupied with your own work. Sometimes, we clumsily engage in a hard conversation related to severe issues in the code at stake. Even worse, it can be tempting to adopt an approval process that does not really go beyond giving a thumbs up.</p><p><strong>Nobody's code is perfect. </strong>Having another pair of eyes take a closer look at the feature you just developed is crucial to delivering quality code, no matter how experienced you are. It’s unfortunate that we can not trust ourselves to always deliver faultless code, even if we are highly reliable. There will always be mistakes that slip the attention of the person who committed the code. However, these imperfections should not be an occasion to open fire on the author of the perceived defect.</p><p><strong>How do you provide this feedback in a way that stays professional, on point, and doesn't hurt anybody's feelings?</strong></p><p><strong>Backed by several years of experience in this field, I’ve noticed that the following approaches help tremendously.</strong></p><h2 id="the-feedback-sandwich"><strong>The feedback sandwich</strong></h2><p><strong>There is another line of work in which giving bad news is an important part of the job: medicine.</strong> In hospitals and clinics, doctors are often trained to use what is called the <a href="https://www.uwec.edu/files/246/Serving-Up-the-Feedback-Sandwich.pdf?ref=dept-engineering-blog.ghost.io">feedback sandwich</a>. The premise of this method is fairly simple and also very efficient: First, say something positive to the employee. Then, deliver the bad news. Finally, say another positive thing to the employee. In other words, the goal is to soften the blow and, yet, deliver the information that is crucial for the employee to grow.</p><p><strong>The aforementioned method is a good candidate to provide overall feedback on a pull request.</strong> Something as simple as "Nicely done! I noticed a couple of issues. Keep up the good work!" is a lot better to handle for the reviewee than a lacking "I commented on some issues." Not writing a review summary seems somewhat even worse since it does not provide a general impression of the code changes. The human mind often creates its own negative scenario due to our obsessions to understand things. We try to fill the gaps and we imagine the worst. Stating a few reassuring words can go a long way in giving the right impression.</p><h2 id="its-not-personal"><strong>It's not personal</strong></h2><p><strong>There’s no I in team. </strong>Avoid using terms such as "I think," "right" versus "wrong," and other phrases that put people on the spot. Instead, point to what you noticed precisely and make it impersonal to avoid confrontation or hurting someone's feelings. Whenever you’re tempted to say “I think” or “I believe,” consider using one of these alternatives:</p><ul><li>It seems that (this does not work as intended)...</li><li>It looks like (this is the wrong type for this variable)...</li><li>I noticed that (this line causes an error)...</li><li>I suspect that (this could be implemented differently)...</li><li>There might be (a more elegant solution to solve this problem)...</li><li>It could be beneficial (to write a function to avoid repeating the same code)...</li></ul><p><strong>It’s not </strong><em><strong>git blame</strong></em><strong>.</strong> Avoid blaming the author with phrases like "you should" when you see bad code. Instead, provide the reason why changing the code would be beneficial, preferably drawn from your own experience. For example, if you notice that the author of the pull request is not using the same case to name their variables, you can say something like this: “I've noticed that using the same case throughout a codebase helps with the readability of my code. How about using camelCase for function and variable names?”</p><h2 id="actionable-comments"><strong>Actionable comments</strong></h2><p><strong>Provide an alternative.</strong> Something that can be frustrating about criticism is when the person who finds the fault focuses on the problem. Instead, I would suggest looking for solutions right away in order to put the developer on what seems like a better track. You don’t like the name of a new function? Provide one or two options that float your boat in your comments. A piece of code seems to be located in the wrong file? Don’t bark at the author. Let them know what would be a more convenient home for this new class. Even better, GitHub and GitLab let reviewers add code suggestions that can be committed straight from the pull request.</p><h2 id="documented-feedback"><strong>Documented feedback</strong></h2><p><strong>Google It™. </strong>Sometimes, it can be hard to admit that what we did is not optimal. It can be tempting to respond with defiance and ask for proof. Before the conversation goes south, you might find it beneficial to take the time to check your assumptions about how, for example, that built-in method works. If your suspicion is correct, I would advise you not to take a lecturing tone about it. Instead, you can simply state that the documentation you consulted seems to suggest that the method in question works differently than what the added code implies. Make sure that you provide a link to a reliable source like the Mozilla Developer Network documentation or a trustworthy Stack Overflow question. Doing so can help provide more context about the issue and can be a great learning experience for both the reviewee and the reviewer. And, don’t hesitate to express when you discovered something new you didn’t expect!</p><h2 id="take-it-outside"><strong>Take it outside</strong></h2><p><strong>Instead of diving into a long conversation, offer to chat on Slack or hop on a video call.</strong> It can get a little frustrating when an exchange keeps going on and on without a resolution. It can also create misunderstandings or give the impression that something is more important than it seems. Taking it out outside of the code review can help everybody get on the same page. When you notice a problem that could snowball, don’t hesitate to write something like: “Hey! I have some thoughts about how we could refactor this function. I sent you a message on Slack. Let's put a video call on our calendars.” This can be a great way to de-escalate possible conflicts that might emerge due to different approaches to programming.</p><h2 id="give-the-benefit-of-the-doubt"><strong>Give the benefit of the doubt</strong></h2><p><strong>Avoid the clash of titans. </strong>You might have strong opinions about how to write code. The author of the pull request might as well. It does not seem like anybody would benefit from seeing two developers headbutt in a pull request. In doubt, ask the author why they wrote the code this way and look carefully at their answer. Maybe they did not think about the shortcoming of their approach. Hopefully, they will catch themselves before making the same mistake next time. Or, they might have some interesting suggestions you did not think about on how to resolve a problem you didn’t see.</p><h2 id="be-supportive"><strong>Be supportive</strong></h2><p><strong>It might be a little discouraging when there are a lot of issues with a pull request or when the same mistakes are repeated.</strong> No matter how horrible you think the code looks, show a little empathy. Don’t make it seem like it’s the end of the world for the author. If you’re dealing with an inexperienced developer, highlight the progress they’ve made.</p><h2 id="establish-guidelines"><strong>Establish guidelines</strong></h2><p><strong>Finally, the last piece of advice I would like to give is to set style and code review guidelines. </strong>Involve the entire team in this process. Make sure that you establish rules together instead of dictating them. Think hard about the reason behind each rule you set to determine whether it is justified or not. You might want to use an existing style guide to save yourself some work such as the <a href="https://github.com/airbnb/javascript?ref=dept-engineering-blog.ghost.io">Airbnb JavaScript Style Guide</a>. Set up linters to avoid spending too much time nitpicking.</p><p><strong>Specify the review process as well. </strong>Answer the questions that a new employee might have about it. Who reviews the code? How many reviews are necessary per pull request? When are these code reviews performed?</p><p><strong>As a result, you will have a guide you can refer to as a supporting document during code reviews and, hopefully, a team that fully adheres to it.</strong></p>]]></content:encoded></item><item><title><![CDATA[Introducing Web Blueprint - Our Practices for Web Development]]></title><description><![CDATA[This week we released a Github project called web-blueprint that documents our practices and opinions on web development. If you're a prospective client or hire, our hope is that you'll learn a little bit more about what web engineering at Rocket is all about.]]></description><link>https://engineering.deptagency.com/introducing-web-blueprint-our-practices-for-web-development/</link><guid isPermaLink="false">6241f29b1a8623003da8d38c</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:39:09 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/Matt_Blog_Header.png--1920-620-.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/Matt_Blog_Header.png--1920-620-.png" alt="Introducing Web Blueprint - Our Practices for Web Development"/><p>This week we released a Github project called <a href="https://github.com/rocketinsights/web-blueprint?ref=dept-engineering-blog.ghost.io">web-blueprint</a> that documents our practices and opinions on web development. If you're a prospective client or hire, our hope is that you'll learn a little bit more about what web engineering at DEPT® is all about. [<em>By the way, we're hiring!</em>]<br><br>I'm sure you've often found yourself asking "How do we usually...?" within your own team. I know we have.</br></br></p><p>Years ago, we were small. Back then, when someone asked "How do we usually...?" it was easy to yell across the desks and answer. We had fewer people and projects under our belt, so the answer was also probably somewhat straightforward. Flash forward to today, and now we're hundreds of engineers around the world. It's become a heck of a lot more difficult to answer the question "How do we usually...?"</p><p>We're also getting this question at a more frequent pace from new hires as well as people who are interviewing with us. Even experienced folks at DEPT® who are encountering a new situation ask the question. We also struggle with consolidating what we consider our principles and practices in one place.</p><p>We have plans to grow a lot more, and so we needed to get ahead of this issue before it got out of hand. How would we do it? A wiki page? Ergh, feels too businessy. A "Best Practices" Google Doc? In a company full of experienced engineers with <em>Strong Opinions, Loosely Held</em>, that's a tough one to nail down. Also, we happen to believe that there is no best practice, as each situation is pretty unique.</p><p>So, we decided that the best way to do it was in true engineer fashion:  By using a Github project and collaborating on it together. We call it <a href="https://github.com/rocketinsights/web-blueprint?ref=dept-engineering-blog.ghost.io">web-blueprint</a>.</p><p>The pull request process is a great way to collaborate and allow others to provide useful feedback before content goes live.  Github also allows us to have a public project on our DEPT® organization with an MIT license.  We wanted to share this project with the world so that you all get a better idea of how we work.  It's also nice to have this public-facing so that if you're thinking about working for or with us, you get an idea of what a project will be like with us.</p><p>Finally, how did we address the problem of everyone having an opinion and being heard? 😰 We settled on the idea of "Soapboxes" (as in, get up on your soapbox and shout). These are docs that any engineer at DEPT® can create on a particular topic. We vet them through a pull request process and it allows freedom of expression for our talented engineers and also allows a more diverse array of opinions.</p><p>So far, the feedback has been good, but we'd love more!  Let us know what you think in the issues on the project.</p>]]></content:encoded></item><item><title><![CDATA[Marketing Analytics with AWS and Power BI]]></title><description><![CDATA[We recently completed a project with one of the largest affiliate marketing consulting firms in the US which turned out to be a great success for everyone involved.  We asked one of the lead consultants on the project, Jason Lombardo, to answer a few questions. ]]></description><link>https://engineering.deptagency.com/marketing-analytics-with-aws-and-power-bi/</link><guid isPermaLink="false">6241f09f1a8623003da8d358</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:31:39 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/analytics.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/analytics.png" alt="Marketing Analytics with AWS and Power BI"/><p><em>We recently completed a project with one of the largest affiliate marketing consulting firms in the US which turned out to be a great success for everyone involved.  We asked one of the lead consultants on the project, Jason Lombardo, to answer a few questions which we hope will demonstrate the vast territory that is covered by a modern Data Architect / Engineer and how it all eventually comes together into a simple and friendly solution for the end users.</em></p><h2 id="how-did-this-project-start-out">How did this project start out?</h2><p><strong>Jason</strong>: When I first joined the project we knew the primary intermediary of the click-stream data that the client had been using to date was stepping out of that business so we needed to become self-sufficient in dealing with the data from its various sources directly.  This also meant we would now be fully responsible for all of the metrics and reports as well. </p><p>Essentially, we needed a full analytics stack from scratch and we had only a few months to deliver it.</p><p>The first thing I did when I joined the team was try to work backwards from the end-user experience to identify where we could simplify and potentially save time given our fairly lean team.  </p><p>The first area I observed was the reporting and the initial plan to build a configurable report designer and viewer in React/JavaScript.  While it may have yielded the most customizable branding and possibly endless visuals, there is a great deal of administration and automation that comes with modern Business Intelligence (BI) platforms that, while it’s not the sexy stuff on the front-end, it is essential functionality for any enterprise. </p><p>For example, having multiple stages of publishing and review before sharing out reports broadly, having security-trimmed access to datasets and calculations, and having automated data refresh are just a few of the back-end features that keep a professional BI operation running smoothly.</p><p>So, rather than reinvent, we pivoted to performing a vendor assessment of the market-leading data modeling and visualization platforms, which included Power BI, Qlik and Tableau.  We assisted in the calculation of the overall total cost of ownership for the embedded BI components, built demonstration models and reports based on real click-stream data as a proof of concept and finally presented our findings to the key project stakeholders to secure our direction moving forward.  In the end, we decided to use Power BI as our Business Intelligence platform for the analytical layer and the reports and dashboards.</p><h2 id="now-that-you-had-an-end-user-experience-in-mind-where-did-you-go-next">Now that you had an end-user experience in mind, where did you go next?</h2><p><strong>Jason</strong>: Well, next we turned to the design of the data warehouse itself.  We knew there were existing streams of data we needed to work with so we analyzed the data to understand what it looked like coming from its source.  We then collaborated with subject matter experts at our client to define an ideal business-level target data model for their analysis and reporting.  We leveraged data warehouse architecture best practices to map the target data model to a data warehouse star schema design pattern for efficiency, scale and performance.</p><h2 id="so-now-you-have-your-front-end-and-your-back-end-how-do-you-glue-them-together">So now you have your front-end and your back-end.  How do you glue them together?</h2><p><strong>Jason</strong>: With some good old fashioned SQL and some AWS Glue, of course.  </p><p>To bring our data architecture to life we used SQL scripts to create all of the tables, relationships, constraints, indexes, and views needed in the data warehouse.  For our data warehouse, we used AWS RedShift which is built on PostgreSQL.  We then used some more SQL to populate 85 years worth of temporal metadata so we could perform complex time-series and time-intelligence analytics.  </p><p>And finally, we used a bit more SQL to create all of the tables that we needed for our Master Data Management (MDM) relational database, for which we used PostgreSQL in AWS Relational Database Service (RDS).</p><h2 id="okay-so-databases-are-now-built-how-do-you-get-the-data-in-and-out">Okay, so databases are now built.  How do you get the data in and out?</h2><p><strong>Jason</strong>:  That’s where AWS Glue comes in which is an Extract Transform Load (ETL) platform built on top of Apache Spark.  Glue uses Python, so we created our own amazing Python library to abstract away the complexities of AWS Glue and Spark so we could be more efficient and promote consistency across all of our ETL scripts.  Most of our scripts ended up reading like plain English which worked well for troubleshooting and as self-documenting code.</p><p>We designed ETL processes for each of the data sources diagramming the transformations and the expected schema at each step through the process.  One of them ended up looking like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/aws-1.png" class="kg-image" alt="Marketing Analytics with AWS and Power BI" loading="lazy" width="1600" height="545" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/aws-1.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2023/09/aws-1.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/aws-1.png 1600w" sizes="(min-width: 720px) 720px"/></figure><h2 id="omg-that-is-both-beautiful-and-terrifying-how-complex-that-looks-do-you-really-understand-that">OMG, that is both beautiful and terrifying how complex that looks.  Do you really understand that?</h2><p><strong>Jason</strong>:  Yes, I made it.  Now we had a fully-working end-to-end data pipeline from source to target all modeled out the way the business wanted it.  Then we needed to manage that data and keep it tidy.  For that we built a layer of web services and an administrative user interface (UI) on top of the Master Data Management database we built earlier so Data Analysts at our client could edit the values of their various dimensions and have those dimensions reflected in their downstream reports and dashboards.  For the administrator portal we used Node.js and TypeScript for the web services and Sequelize for data access.</p><h2 id="all-that-is-left-is-the-reports-themselves-right">All that is left is the reports themselves, right?</h2><p>Jason:  I wish. First we needed to do some heavy math.  It turns out that counting the clicks themselves isn’t enough. But there is a whole field of statistics built around clicks which I won’t begin to explain because I still barely understand it myself.  Luckily, I knew how to look up the formulas in Power BI and the subject matter experts at our client knew how to use them to build the metrics we needed for our reports and visualizations.  For this step, the Data Analysts at our client who understand all of the math are the real heroes.</p><p>We just helped them make the connection between Power BI and the data warehouse in AWS so we could bring the data into Power BI’s in-memory analytics engine.  On top of that we helped build their semantic business data model and published their first certified dataset to the Power BI cloud service to be leveraged by the hundreds of custom client reports that would be built subsequently.</p><p><em>It's clear from talking to Jason that building a full analytics stack from the ground up is no mean feat. It's a potential minefield of complicated nuances and tools. But we, for one, admire how a data architect like Jason is able to pull everything together to create a seamless, seemingly simple, finished product.</em></p>]]></content:encoded></item><item><title><![CDATA[A Journey Moving the Monolith to Microservices]]></title><description><![CDATA[2020 was a hell of a year:  A global pandemic, a fraught US election, and ... Microservices!  😬]]></description><link>https://engineering.deptagency.com/a-journey-moving-the-monolith-to-microservices/</link><guid isPermaLink="false">6241f0581a8623003da8d34b</guid><category><![CDATA[Process]]></category><category><![CDATA[Teams]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:29:49 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/purple1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/purple1.jpg" alt="A Journey Moving the Monolith to Microservices"/><p>2020 was a hell of a year:  A global pandemic, a fraught US election, and ... Microservices!  😬</p><p>Myself and a team of DEPT  developers spent the entirety of 2020 at a client who wanted to move their Java based monolith into the 2020's with a spanking new Microservice / Single Page App (SPA) architecture.  Sound a little too good to be true? Here was the catch:   We needed to keep their old monolith running while we did it, and it is a big, complicated application.</p><p>It's now 2021, and I can confidently say that we did it! Rocket set them up for success!  But much like the rest of 2020, it wasn't without its perils.  Here's what we learned along the way along with all the good stuff and the bad stuff we encountered.</p><h2 id="why-microservices">Why Microservices?</h2><p>This particular client wanted Microservices but didn't seem to have scale problems. They had a modestly large user base, but nothing that was really stretching the scaling boundaries of their current app.  We found this peculiar.  What was their goal in using Microservices, then?  At this point, everyone likes Microservices, but most are cognizant of the overhead they can incur as well.</p><p>It took us a bit of time to figure out that they wanted Microservices to accelerate the pace of development. And they didn’t just want them, they wanted them NOW! Ah, this was a different beast that required a different weapon to take down.</p><p>Knowing the primary goal was time to market, we worked towards trying to scope new services with proper domain boundaries rather than simply just creating a new service for each portion of the domain.  Our challenge was to <em>keep things as simple as possible in a Microservices architecture</em>.</p><p>Mission impossible? Probably. 😃</p><h2 id="api-first-development">API first development</h2><p>The desire to quickly deliver a new feature set with this architecture forced our teams to work within an API contract driven development model.  This allowed some developers to run ahead on frontend development by agreeing on what the backend REST API would look like <em>while</em> we built it out.</p><p>We documented our API's with <a href="https://www.openapis.org/?ref=dept-engineering-blog.ghost.io">OpenAPI</a> specs from the very beginning.  This was crucial.  Our frontend team used these specs religiously.  Towards the end of the year, we were able to take the open API specs and use tools like <a href="https://www.npmjs.com/package/express-openapi-validator?ref=dept-engineering-blog.ghost.io">express-openapi-validator</a> to do automatic validation and <a href="https://www.npmjs.com/package/@manifoldco/swagger-to-ts?ref=dept-engineering-blog.ghost.io">swagger-to-ts</a> to generate Typescript interfaces from this (if you haven't gathered by now, a bunch of our services were in Node.js and Typescript).  Other teams were using Java, and because OpenAPI is, well... open, the tooling for other languages exists as well and can easily be supported.  Our initial investment in API driven development paid off in spades by avoiding writing a lot of boilerplate code.  This all worked brilliantly and we would repeat it again in a heartbeat.</p><h3 id="side-note-about-apis">Side note about API's:</h3><p>At the beginning of the project, we had a lot of debate about whether to use REST API's or GraphQL API's.  One thing that jumped out at us was that something like GraphQL didn't <em>need</em> OpenAPI, it was built in!  However, we ended up going with RESTful API's because we were running lean - We needed to get going fast.  We made the decision that the time necessary to setup GraphQL resolvers and other infrastructure was best spent elsewhere.  I'll save the debate about whether or not we were right for another post. 😜</p><h2 id="service-boundaries-are-kinda-important">Service boundaries are kinda important</h2><p>Since scaling wasn't our primary goal with this architecture, we set our sights on defining the right service boundaries to choose. Our first feature handled a somewhat broad swath of functionality, so we created a service with a larger set of capabilities than you’d typically see in a microservice.  This was instead of trying to carve off a small niche of functionality to allow it to scale independently.  We had plenty of groundwork to lay, so it was nice that we could start with just one new service and still complete a large feature set.</p><p>However, this time to market focused approach of determining service boundaries led to bad decisions (big surprise, huh?).  Because we were under the gun to deliver, we didn't get a chance to fully understand the business domain before we dove in and created a new service.  This led to us (honestly) confusing where various API's should be located:  Should they be in the new service we were creating?  Should we have created multiple services?  Should they be in the legacy application temporarily?  Should they be a completely separate service?</p><p>In hindsight:  We should have done more experimentation with service boundaries using <em>real working code</em>, and refactored as we went if we needed narrower service boundaries.</p><h2 id="using-your-cloud-provider-to-the-max">Using your cloud provider to the max</h2><p>One major factor of the success of our project was staying within one cloud provider's ecosystem.  In our case it was Amazon Web Services (AWS).  This same principle applies for any of the major cloud providers though: Using their utilities whenever we could saved us time and kept things smooth and moving quickly.</p><p>Our domain was hosted with Cloudfront.  We used Application Load Balancers (ALB's) to balance traffic.  We used S3 to host our SPA's static assets.</p><p>On the API side, we used CodePipeline to deploy our services into Docker containers using Elastic Container Service (ECS) which stored data in databases managed by Relational Data Service (RDS).  It worked splendidly.  The one place where we strayed from pure AWS was that we configured things with Terraform.  In our experience, this ended up being a much better choice than the de-facto standard of Cloudformation templates.  <a href="https://terragrunt.gruntwork.io/?ref=dept-engineering-blog.ghost.io">Terragrunt</a> proved invaluable in letting us piece together infrastructure components like lego blocks.</p><p>For running containers, we toyed with the idea of using Kubernetes, but decided that seemed like using a jackhammer when only a hammer was needed.  ECS is "just complex enough" for an orchestration engine, in our experience. A year out, that still seems like the right choice.</p><h2 id="pay-attention-to-the-glue">Pay attention to the glue</h2><p>Our client described transitioning off their old system while keeping it running as: “Changing the tires on a bus while it’s moving.”  Which is an apt metaphor. So, how were we going to share data between the systems and gracefully move off over time?</p><p>One thing that we did right was picking an API gateway that was endlessly customizable in one central place.  Because we were working in (mostly) a Node.js stack, we chose <a href="https://www.express-gateway.io/?ref=dept-engineering-blog.ghost.io">express-gateway</a> as our entrypoint to our Microservices.</p><p>In the year at this client, this has been the biggest question:  “If you’re so bought into AWS, why not just use AWS API Gateway?”  There’s a few reasons:</p><p>First, we were excited by the fact that express-gateway was basically just glorified E<a href="https://expressjs.com/?ref=dept-engineering-blog.ghost.io">xpress.js </a>middleware with a pre-existing set of routing policies built in. It was open source, backed by the Linux Foundation and <a href="https://auth0.com/blog/apigateway-microservices-superglue/?ref=dept-engineering-blog.ghost.io">recommended by Auth0</a>.  Using it, we could get the best of express along with not having to code our own proxying or filtering logic.  We were able to use existing policies like JWT authentication while also writing our own policies for those strange, unforeseen situations that inevitably come up during a major technical transition like this.</p><p>Second, we knew that authentication between the two systems was going to be challenging.  The old system used session based cookie authentication, and our new Single Page App needed token based authentication.  How were we going to bridge that gap while not propagating this issue to each of the underlying services?  We <em>could</em> customize the heck out of AWS API Gateway using Lambdas, but we decided that having a central point where we could keep this code and run pipelines was preferable.  We were able to use express-gateway <a href="https://www.express-gateway.io/docs/policies/?ref=dept-engineering-blog.ghost.io">custom policies</a> to bridge this gap in authentication nicely, all in one place in code, without having a bunch of Lambda code sprinkled around that was hard to track.</p><p>Third, we knew that this transition state between an old and new architecture was temporary.  We constantly told everyone that if we did things right in this “semi customized” API Gateway, that someday, when the old system was completely retired, we could completely get rid of it and move to an off the shelf solution like API Gateway.</p><p>A year later, I can say that this paid off and in a similar transitional situation we would use express-gateway again.</p><h2 id="dont-kid-yourself-the-old-monolith-isnt-going-anywhere-soon">Don't kid yourself:  The old Monolith isn't going anywhere soon</h2><p>The client wants a new system, and the product team is smart enough to know (and so are you) that "<a href="http://chadfowler.com/2006/12/27/the-big-rewrite.html?ref=dept-engineering-blog.ghost.io">The Big Rewrite</a>" approach is a recipe for disaster! You want to be lean and move your old system into the new world gradually, delivering value along the way.</p><p>But, you can't pretend like your old system is going anywhere soon, even if you really really want it to.  We spent a lot of time setting expectations around this with the client.  In our case, and I'm sure many others, the old system will still be the primary system the business runs on for a good while.  If the old system is shaky enough, it will take down the new system as well.</p><p>We spent a good amount of time shoring up the older system, which we had to work hard to justify.  And in my opinion it <em>still</em> wasn't enough. It's important to remember that moving an application to Microservices doesn't have to be all or nothing.  You can take steps in your existing Monolith to <em>get it ready to be split up</em> into Microservices.  It’s important that your product team understands this for roadmap planning.</p><h2 id="dont-short-change-your-investment-in-the-foundation">Don't short change your investment in the foundation</h2><p>A wise man at Rocket once told me:</p><blockquote>You can't fill a garbage truck with cement if you want to lay a foundation.</blockquote><p>Along those lines, our primary learning point in this project was around foundational Microservices architecture: You cannot build an "MVP" of Microservices infrastructure.  If you have even the faintest whiff there will be more than one service, you have to go all in and invest in the necessary infrastructure to support many services.</p><p>Because of the desire to deliver quickly, our Continuous Integration / Continuous Deployment (CI/CD) pipelines were created using an MVP approach.  This was a mistake.  The minute it became time to create services number 2 and 3, we felt the pain of the missing capabilities we didn’t add.  An example was sharing build artifacts across environments.  These missing capabilities caused each service to take a <em>long </em>time to build, and was repeated for each environment to save a little time at the beginning.  After a couple of services, this was a nightmare.</p><p>We didn't have time for things like standards on API formats from the get go, and instead learned in service 2 or 3 that we needed them.</p><p>The time spent up front building this foundation would have paid off in spades.</p><h2 id="wrapping-up">Wrapping up</h2><p>Rocket's past experience creating large scale systems for our customers served us well. We were able to apply patterns we've seen work before, and there was no difference here.  But we did have a few things reinforced and learned a few new things along the way:</p><ul><li>Microservices are <em>serious business</em>.  You can't shortchange the time and effort needed to do them right. of where the product is going and.</li><li>Use your cloud provider as leverage to move faster</li><li>Have a deep understanding of the business domain and the roadmap going forward in order to architect service boundaries properly</li><li>And, of course...</li></ul>]]></content:encoded></item><item><title><![CDATA[From RxJava to LiveData (and back?)]]></title><description><![CDATA[Here are some things that might be useful for someone else who has experience with RX and starts using LiveData for the first time.]]></description><link>https://engineering.deptagency.com/from-rxjava-to-livedata-and-back/</link><guid isPermaLink="false">6241efcd1a8623003da8d338</guid><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Kelsey Anderson]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:27:12 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/abstract-white.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/abstract-white.jpg" alt="From RxJava to LiveData (and back?)"/><p><strong><strong>Disclaimer</strong></strong>: I'm no <a href="http://reactivex.io/?ref=dept-engineering-blog.ghost.io">Reactive Extensions</a> (RX) expert.</p><p>I'm simply a happy user with some (~6 years) experience with it. I've seen people struggling with RX. They say it has a steep learning curve. In that light, I was super excited when I first heard about Google's <a href="https://developer.android.com/topic/libraries/architecture/?ref=dept-engineering-blog.ghost.io">Android Architecture Components</a> (AAC) and <a href="https://developer.android.com/topic/libraries/architecture/livedata?ref=dept-engineering-blog.ghost.io">LiveData</a>. A simplified version of RX with automatic lifecycle management! I immediately tried to use it in a pet project and it seemed simple indeed. The next real project I started, I chose LiveData without any hesitation: it is part of AAC, so future maintainers of the code should already be familiar with it. Fast forward 3-4 months and my excitement has been somewhat diminished. The familiarity point still sticks, but the simplicity part ... not so much. Here are some things that might be useful for someone else who has experience with RX and starts using LiveData for the first time.</p><h3 id="livedata-has-no-error-channel">LiveData has no error channel</h3><p>LiveData is designed for the happy path, a stream of successful results. But things tend to fail and especially so on mobile devices (connectivity issues, limited power and hardware resources, etc). So how do you handle these? One simple solution would be to split it up to two separate LiveData streams:</p><pre><code>results: LiveData&lt;Foo&gt;
errorMessages: LiveData&lt;String&gt;</code></pre><p>Another option would be to introduce a helper class Result and wrap the results in it:</p><pre><code>sealed class FooResult {
    data class Success(val data: Foo) : FooResult()
    data class Error(val error: Throwable) : FooResult()
}

results: LiveData&lt;FooResult&gt;</code></pre><h3 id="livedata-is-sticky">LiveData is sticky</h3><p>The first sentence from the official documentation for LiveData <a href="https://developer.android.com/topic/libraries/architecture/livedata?ref=dept-engineering-blog.ghost.io">says</a>:</p><blockquote>LiveData is an observable data holder class.</blockquote><p>So naturally enough, people coming from RX world think it's a simplified <code>Observable</code>. But actually, it is more similar to <code>BehaviourSubject</code>. It holds the last value and new observers would get that first. This fact makes error handling very cumbersome. Errors should be one time events, it makes no sense to cache them. Yes, it's nice to re-populate your <code>RecyclerView</code> with cached data on screen orientation changes, but showing the last REST API call error at the same place? Should I show the error or have I done it already? Since there's no API contract for error handling, it's unclear whether an error is fatal or not. Would there be more successful results after an error?</p><h3 id="livedata-has-no-future-like-apis">LiveData has no Future&lt;&gt; like APIs</h3><p>RX provides nice API for special cases of "streams of events" where there are either no results (<code>Completable</code>), a possible result (<code>Maybe</code>), or a result (<code>Single</code>). I think it makes the API very clear to understand. With LiveData, the caller would somehow have to know how many results to expect and stop observing at the right time, because the "stream" never ends.</p><h3 id="livedata-has-almost-no-operators">LiveData has (almost) no operators</h3><p>LiveData has only two operators (called "transformations"): <code>map()</code> and <code>switchMap()</code> (<code>flatMap()</code> in RX). I keep missing RX operators like <code>zip()</code>, <code>combineLatest()</code>, <code>distinctUntilChanged()</code>, etc. Sure, I have written my own versions of these for LiveData. But I'd always prefer the quality of RX operators over my home grown ones.</p><h3 id="livedata-has-no-operator-chaining">LiveData has no operator chaining</h3><p>I've heard complaints about RX being hard to read. But compare these two code samples:</p><pre><code>fun allPosts(): LiveData&lt;Post&gt; {
    val userIds = Transformations.map(allUsers()) { user: User? -&gt;
        user!!.id
    }

    return Transformations.switchMap(userIds) { id: String? -&gt;
        userPosts(id!!)
    }
}</code></pre><p>vs:</p><pre><code>fun allPosts(): Observable&lt;Post&gt; =
    allUsers()
        .map { user -&gt; user.id }
        .flatMap { userId -&gt; userPosts(userId) }</code></pre><h3 id="beware-of-converting-rx-streams-to-livedata">Beware of converting RX streams to LiveData</h3><p>There is a handy helper class to turn RX Publisher to LiveData:</p><pre><code>fun LiveDataReactiveStreams.fromPublisher(publisher: Publisher)</code></pre><p>Your RX chain needs to make sure to handle errors. Otherwise, the app dies with <code>RuntimeException</code>:</p><blockquote>LiveData does not handle errors. Errors from publishers should be handled upstream and propagated as state</blockquote><p>Another thing, what would you expect this code to print:</p><pre><code>fun bar(): LiveData&lt;Int&gt; {
    val p = Flowable.fromArray(1, 2, 3)
    return LiveDataReactiveStreams.fromPublisher(p)
}

bar().observe(this, Observer {
    Log.i("foobar", "got $it")
})
</code></pre><p>1, 2, 3, right? Wrong. It prints just 3.</p><h2 id="conclusions">CONCLUSIONS</h2><p><br>LiveData tries to be a simplified version of RX but fails because you need to handle the hard parts yourself. Yes, it has small API. Yes, writing a <code>zip()</code> operator is not very hard. But it sounds like the reasoning of a novice developer - "I don't understand the hard parts of this code, so I'll rewrite it, it'll be so simple!". Error handling is complicated. Corner cases are complicated. Code that looks complicated is (hopefully) complicated because it needs to handle these conditions (<a href="https://twitter.com/havocp/status/1032632650165616645?ref=dept-engineering-blog.ghost.io">https://twitter.com/havocp/status/1032632650165616645</a>). It's much simpler to ignore the RX operators you don't know about than to write the ones you do need yourself.</br></p><p>Note that I've completely ignored automatic lifecycle handling. It's great, no argument against that. I've just never understood the complaints (and projects like <a href="https://github.com/trello/RxLifecycle?ref=dept-engineering-blog.ghost.io">RxLifeCycle</a> or its "successor" <a href="https://github.com/uber/AutoDispose?ref=dept-engineering-blog.ghost.io">AutoDispose</a>). People say it requires manual work that is easy to forget. In my opinion the manual work is trivial, makes the intention very clear, and the editor already reminds you when you don't keep track of your <code>Disposable</code>s. I feel it's the perfect case of <a href="https://www.infoq.com/presentations/Simple-Made-Easy?ref=dept-engineering-blog.ghost.io">Simple Made Easy</a> (Side note: every programmer should be required to watch that talk at least once in their life).</p><p>But familiarity is also important. If your team has experience with LiveData and no experience with RX, the choice is already made for you.</p>]]></content:encoded></item><item><title><![CDATA[Organizing AWS microservices with Python, Serverless, and namespace packages]]></title><description><![CDATA[Our project began with a small ask: build a small Python back-end using AWS
Lambda and API Gateway (with just a handful of endpoints to support a few pages)
and use Serverless [https://serverless.com/] to manage the Cloud Formation
setup. 

Not very involved, not much code, not even much configuration for the deploy.

Next came authentication. In pursuing the joint dreams of DRTW ("don't reinvent
the wheel") and WALCAP ("write as little code as possible"), we selected AWS
Cognito over other opti]]></description><link>https://engineering.deptagency.com/organizing-aws-microservices-with-python-serverless-and-namespace-packages/</link><guid isPermaLink="false">6241ef8b1a8623003da8d328</guid><category><![CDATA[Platforms]]></category><category><![CDATA[AWS]]></category><category><![CDATA[python]]></category><dc:creator><![CDATA[Kevin Larrabee]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:26:14 GMT</pubDate><content:encoded><![CDATA[<p>Our project began with a small ask: build a small Python back-end using AWS Lambda and API Gateway (with just a handful of endpoints to support a few pages) and use <a href="https://serverless.com/?ref=dept-engineering-blog.ghost.io">Serverless</a> to manage the Cloud Formation setup. </p><p>Not very involved, not much code, not even much configuration for the deploy.</p><p>Next came authentication. In pursuing the joint dreams of <strong><strong>DRTW</strong></strong> (<em>"don't reinvent the wheel"</em>) and <strong><strong>WALCAP</strong></strong> ("<em>write as little code as possible"</em>), we selected AWS Cognito over other options such as Auth0 or rolling our own light authentication. Cognito has excellent integration with API Gateway and is surprisingly easy to set up and maintain, especially for an AWS product.</p><p>Let's pretend our project was called "Purplecat". The repository more or less looked something like...</p><figure class="kg-card kg-code-card"><pre><code> purplecat/
├── alembic/
│  └── ...
├── purplecat/
│  ├── api/
│  │  ├── handlers.py
│  │  └── models.py
│  ├── lib/
│  │  ├── aws/
│  │  │  ├── gateway.py
│  │  │  └── s3.py
│  │  └── db.py
│  └── config.py
├── tests/
│  ├── api/
│  │  ├── test_handlers.py
│  │  └── test_models.py
│  ├── lib/
│  │  ├── test_aws.py
│  │  └── test_db.py
│  ├── factories.py
│  └── fixtures.py
├── package.json
├── requirements-dev.txt
├── requirements.txt
├── serverless.yml
└── setup.py</code></pre><figcaption>Thank you,<a href="https://github.com/ogham/exa?ref=dept-engineering-blog.ghost.io" target="_blank" style="background-color: transparent; color: rgb(0, 150, 212); transition: color 0.3s ease 0s;">exa</a>, for the wonderful tree view</figcaption></figure><p>Not too bad; pretty standard Python. The <code>setup.py</code> excluded the <code>tests/</code> directory, of course, and the <code>serverless.yml</code> file defined all of the Lambda functions, API Gateway endpoints, and Cognito details.</p><h2 id="growing-pains">Growing pains</h2><p>Over the next few months, the API grew. Five endpoints jumped to dozens, SQLAlchemy models abounded, database queries got comically more complex as we added data versioning and user group-based access to different data, and the test suite was starting to get monstrous.</p><p>S3 buckets popped up for a variety of different purposes. We quickly broke the 200 resource limit on Cloud Formation and had to split the service into <em>nested stacks</em>.</p><p>Then we realized that we wanted to <em>manage Gateway and Lambda independently from Cognito and S3</em>, mostly so we wouldn't lose all user accounts if we tore down and re-deployed our API stack; a single <code>serverless.yml</code> file split into three. Still not the end of the world (or maintainability), though</p><p>Next came more endpoints and some non-API-related ETL pipelines, along with another Serverless file to manage Step Functions state machines and more Lambdas. Everything still sat in the same repo and same Python source directory. We started cooking spaghetti code - ETL imported from the API, and the API started importing from ETL. Different services began to require their own schemas and tables in the database, but they relied on the same <code>alembic</code> configuration and revision history.</p><p>Even worse, our <code>serverless.yml</code> files were handpicking directories of Python source to include in packaging (because why should a Lambda have all the code in the entire app?), so each included a block like...</p><figure class="kg-card kg-code-card"><pre><code class="language-yml"># Only include necessary files
# See "Optimizing packaging time": https://github.com/UnitedIncome/serverless-python-requirements
package:
  individually: false
  include:
    - "!./**"
    - "purplecat/__init__.py"
    - "purplecat/config.py"
    - "purplecat/lib/**"
    - "purplecat/api/**"
  exclude:
    - "**"
</code></pre><figcaption>So much repetitive config</figcaption></figure><p>Everything in this block was identical between Serverless configs except for the last <code>include</code> directory, which was the service-specific code. Forgetting to add a common file or directory would not cause a failure until the Lambda actually executed. And given that Cloud Formation stacks update with the rapidity of a dying, limbless tortoise, those were especially fun to fix. So... yay?</p><p>It was time to pay down some debt before it got out of hand, especially given the looming requirements on the roadmap.</p><h2 id="namespace-packages-services-microlibs">Namespace packages + Services = Microlibs</h2><p>We explored a few ways of reorganizing both the Python code as well as the Serverless stacks. We considered just <em>cleaning up the existing code</em> a bit. We thought about <em>multiple repositories</em>. Ultimately, we found <a href="https://medium.com/@jherreras/python-microlibs-5be9461ad979?ref=dept-engineering-blog.ghost.io">this inspiring article by Jorge Herrera</a> on setting up <em>"microlibs"</em> via Python's <strong><strong><a href="https://packaging.python.org/guides/packaging-namespace-packages/?ref=dept-engineering-blog.ghost.io">namespace packages</a></strong></strong>.</p><p><em>(<strong><strong>Note:</strong></strong> If you're like me and find Python docs to be comically opaque, the high-level view of namespace packages is that they provide a convenient way to have small, interrelated, <strong><strong>separately installable packages</strong></strong> living within the <strong><strong>same repository</strong></strong>. Each package will also only contain the code that it explicitly requires as a dependency. A wonderful system, despite looking something like Java's directory hell.)</em></p><p>We had an identical list of requirements so we decided to give it a go, adapting it to include our Serverless configurations. Any code imported by more than a single service was designated 'common', and no service was allowed to import from another. Moreover, the Python source (if  present) that supported a given Cloud Formation stack lived alongside the <code>serverless.yml</code> file that would deploy it.</p><p>We wound up with a structure something like the following (just much larger).</p><figure class="kg-card kg-code-card"><pre><code>purplecat/
├── common/
│  ├── alembic/
│  │  └── ...
│  ├── purplecat/
│  │  └── common/
│  │     ├── aws/
│  │     │  ├── gateway.py
│  │     │  └── s3.py
│  │     ├── db.py
│  │     └── models.py
│  ├── tests/
│  │  └── ...
│  ├── requirements-dev.txt
│  ├── requirements.txt
│  └── setup.py
└── service/
   ├── api/
   │  ├── alembic/
   │  │  └── ...
   │  ├── purplecat/
   │  │  └── service/
   │  │     └── api/
   │  │        ├── handlers.py
   │  │        └── models.py
   │  ├── tests/
   │  │  └── ...
   │  ├── package.json
   │  ├── requirements-dev.txt
   │  ├── requirements.txt
   │  ├── serverless.yml
   │  └── setup.py
   ├── auth/
   │  ├── package.json
   │  └── serverless.yml
   └── etl/
      ├── alembic/
      │  └── ...
      ├── purplecat/
      │  └── service/
      │     └── etl/
      │        ├── lambdas.py
      │        ├── models.py
      │        └── step_functions.py
      ├── tests/
      │  └── ...
      ├── package.json
      ├── requirements.txt
      ├── serverless.yml
      └── setup.py
</code></pre><figcaption>The greatest annoyance with Python's namespace packages is the repetitive directory structures</figcaption></figure><p>Each service had its own isolated Serverless config as well as a <code>package.json</code> that let each set up its own scripts. Each service also had its own <code>setup.py</code>  that specified it was part of the greater namespace package...</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from setuptools import setup

PACKAGE = "purplecat.service.api"

DEV = []
with open("requirements-dev.txt") as reqs:
    DEV = [l.strip() for l in reqs]

setup(
    name=PACKAGE,
    version='1.0.0',
    packages=[PACKAGE],
    namespace_packages=["purplecat"],
    install_requires=[
        "purplecat.common",
        "requests",
        # ...
    ],
    extras_require={"dev": DEV}
)
</code></pre><figcaption>service/api/setup.py</figcaption></figure><p>And each service had a <code>requirements.txt</code> file that specified the location of that <code>common</code> package...</p><figure class="kg-card kg-code-card"><pre><code>--index-url https://pypi.python.org/simple
../../common
-e .
</code></pre><figcaption>service/api/requirements.txt</figcaption></figure><p>The directory structure was more complex, but...</p><ul><li>... the services were<em> easier to isolate, package, and deploy</em></li><li>... the project structure ultimately had <em>less cognitive overhead</em></li><li>... separating concerns (even on database schemas &amp; migrations) enabled multiple teams to work on the same project more seamlessly</li><li>... no service exceeded Cloud Formation resource limits, so the complication of <em>nested/split stacks was no longer necessary</em></li><li>... the <a href="https://github.com/UnitedIncome/serverless-python-requirements?ref=dept-engineering-blog.ghost.io#readme"><strong><strong>serverless-python-requirements</strong></strong></a> plugin could <em>do its default packaging</em> without us specifying what directories to include or exclude, and Lambdas still only got what they needed</li></ul><p>Perhaps, best yet, this layout provided us an extensible scaffold with <strong><strong>a single, logical way to introduce additional services</strong></strong>, which we soon had to do.</p>]]></content:encoded></item><item><title><![CDATA[Deployments at Scale with AWS ECR and ECS]]></title><description><![CDATA[With DevOps at DEPT, one of our primary goals is to make the life of the developer easier. A specialty of ours is assisting in our customer's deployment process. ]]></description><link>https://engineering.deptagency.com/deployments-at-scale-with-aws-ecr-and-ecs/</link><guid isPermaLink="false">6241ef3b1a8623003da8d312</guid><category><![CDATA[Platforms]]></category><category><![CDATA[Process]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Benjamin Kincaid]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:25:08 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/geometric-shapes.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/geometric-shapes.jpg" alt="Deployments at Scale with AWS ECR and ECS"/><p>With DevOps at DEPT, one of our primary goals is to make the life of the developer easier. A specialty of ours is assisting in our customer's deployment process. In short, this is how a code change can safely make it from a developer's laptop all the way to a production environment where it can be seen by end-users.</p><p>In a typical software project, your environments will be something along the lines of dev, staging, and prod. Maintaining a release pipeline to these is simple enough in most cases, however, sometimes things can get hairy.</p><p>Let's say that a customer comes along who decides they need a separate set of environments. The new environments will need completely separate infrastructure, i.e, clusters, loadbalancers, and whatever other resources you happen to be using. Also, they have a whole list of features they want you to add to your application. Considering they have money to pay you for it, we'll call that customer Money Bags Inc, or MB for short.</p><p>This is a big request, but sometimes it does happen for various reasons. Now the environments you need to maintain look something like dev, mb-dev, staging, mb-staging, prod, and mb-prod. If you fork your code for the new customer's features, you will be effectively developing two separate apps. Instead, you opt to add MB's features to your existing codebase. This means you will need a way to deploy your code changes to all environments. The features need to work for the new customer without impacting functionality for your existing ones. Knowing what's deployed where, and testing new features in this scenario is a whole lot to think about!</p><p>If you are facing this, you're in luck because we're going to talk about a pipeline solution for exactly that. Our solution will allow you to maintain a high level of automation as well as visibility into the state of your environments. A developer should have little concern about their code ending up in the proper place. If however, they need to find some information about the state of the environment, it should be readily available, i.e, not obscured by our automation. As we are an AWS partner, the tooling focused on here will be AWS services, specifically AWS ECS and ECR. Keep in mind however that we are showing a paradigm, and the method can be implemented outside of these technologies.</p><h2 id="the-tools">The Tools</h2><p>Let's just briefly discuss the tools necessary to achieve this. I won't go into too much detail about them here, as the focus of this blog is the paradigm. I will talk about the minimum you need to understand, but links are included to find out more info should you be interested.</p><h3 id="docker">Docker</h3><p>Before we discuss the actual AWS services, let's just briefly touch on Docker, as the AWS services we'll be using are built on top of it. There are two main concepts related to Docker that we are interested in - containers and images. From Docker's documentation:</p><blockquote>Fundamentally, a container is nothing but a running process, with some added encapsulation features applied to it in order to keep it isolated from the host and from other containers. One of the most important aspects of container isolation is that each container interacts with its own private filesystem; this filesystem is provided by a Docker image. An image includes everything needed to run an application - the code or binary, runtimes, dependencies, and any other filesystem objects required.</blockquote><p>Also relevant to us is that images can be tagged. This is just a way of identifying a particular image. An image is not limited to just one tag, it can have many.</p><p>A lot of people introduce the concept of containers as light weight virtual machines. This isn't true, but the application of containers is similar. For more information on how to use Docker I suggest you look at their Q<a href="https://docs.docker.com/get-started/?ref=dept-engineering-blog.ghost.io#docker-concepts">uickstart</a> guide. If you want to know more about the actual architecture of containers I highly recommend <a href="https://www.youtube.com/watch?v=sK5i-N34im8&ref=dept-engineering-blog.ghost.io">this</a> video.</p><h3 id="aws-ecr">AWS ECR</h3><p>ECR is a Docker registry service fully managed by AWS. This is what we'll use to hold our container images after they've been built. The service itself is rather simple. You can check out the <a href="https://aws.amazon.com/ecr/getting-started/?ref=dept-engineering-blog.ghost.io">getting started</a> guide by AWS for a quick setup. Let's say we wanted to create an ECR repository for our project's api.  We could do that with the AWS CLI tool using the following command:</p><p><code>aws ecr create-repository --repository-name api</code></p><p>In our setup, ECR will be the only thing that is shared across MB and our multi-tenant environment. Each environment will make references to that single source of truth.</p><h3 id="aws-ecs">AWS ECS</h3><p>ECS is a container orchestration service provided by AWS. This will serve as the runtime for our containers. To make it simple, this is what will execute the <code>docker run</code> command for us. You can check out the AWS <a href="https://aws.amazon.com/ecs/getting-started/?ref=dept-engineering-blog.ghost.io">getting started </a>guide and <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html?ref=dept-engineering-blog.ghost.io">documentation</a> to learn more. What's most relevant here is that ECS uses a <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html?ref=dept-engineering-blog.ghost.io">task definition</a> to define how we want to run our containers. Here's a rather simple example of what one might look like.</p><pre><code>{
  "family": "api",
  "memory": "512",
  "cpu": "256",
  "networkMode": "awsvpc",
  "taskRoleArn": "arn:aws:iam::012345678922:role/api",
  "containerDefinitions": [
    {
      "name": "api",
      "image": "012345678922.dkr.ecr.us-east-1.amazonaws.com/api:dev",
      "portMappings": [
        {
          "hostPort": 80,
          "protocol": "tcp",
          "containerPort": 80
        }
      ]
    }
  ],
  "compatibilities": [
    "FARGATE"
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ]
}</code></pre><p>You can probably guess what most of these parameters do. The one that's really relevant to us here is <code>"image"</code>. This is set to track an image on the repository that we created earlier.</p><h2 id="the-method">THE METHOD</h2><h3 id="mutable-tags">Mutable Tags</h3><p>Now that we have a basic understanding of our technologies, let's discuss the core the method for keeping our environments in sync. The key to this is the mutable tag. In our ECS task definition example up above, we actually had a mutable tag set in the <code>"image"</code> value. Let's deconstruct what we have set in that field:</p><p><code>012345678922.dkr.ecr.us-east-1.amazonaws.com/api:dev</code></p><p>The first part just references our AWS account number and region. The next bit you may recognize as the name we gave to our repository earlier, <code>api</code>. Finally we have the name <code>dev</code>. That's our mutable tag. It's simple, but actually quite powerful.</p><p>Going back to the example environments that we introduced, it is ideal to have the api service in dev and mb-dev to stay up to date with one another. When we are testing a new feature, we want to know that all environments of a given tier have the feature deployed. Since we have the mutable tag, to update both our dev environments, we just push a new image to ECR, change the <code>dev</code> tag to that new image, and run a simple deployment command on both our dev ECS clusters. In code, this is just a few simple steps. Here are some scripts that accomplish our process:</p><ul><li>First, we build our image and push it to ECR. One thing to note is that we are first pushing with <code>COMMIT_HASH</code>. This creates an easy link between our images and the git commit that triggered their build.</li></ul><pre><code>#!/bin/bash

COMMIT_HASH=$(git rev-parse HEAD)

ECR_REPOSITORY="012345678922.dkr.ecr.us-east-1.amazonaws.com/api"

IMAGE_NAME="${ECR_REPOSITROY}:${COMMIT_HASH}"

docker build -t $IMAGE_NAME .

eval $(aws ecr get-login --no-include-email)

docker push $IMAGE_TAG</code></pre><ul><li>Next we query for the image we just pushed using the AWS CLI and attach the tag <code>$ENVIRONMENT</code> to it. We will want that value to vary depending on which environment we wish to push. This can be accomplished with any CI/CD tool. When we wish to push to the dev environments, we want <code>ENVIRONMENT=dev</code>.</li></ul><pre><code>#!/bin/bash

MANIFEST=$(aws ecr batch-get-image \
    --repository-name $IMAGE_NAME \
    --image-ids imageTag=$COMMIT_HASH \
    --query 'images[].imageManifest' \
    --output text)
    
aws ecr put-image \
  --region $AWS_DEFAULT_REGION \
  --repository-name $ECR_REPOSITORY\
  --image-tag $ENVIRONMENT \
  --image-manifest "$MANIFEST"</code></pre><ul><li>Finally we trigger an ECS deployment in all of our dev environments. This will make our clusters pull the new image. We need to set our access keys appropriately in order to point to the desired environment.</li></ul><pre><code>#!/bin/bash

export AWS_ACCESS_KEY_ID=$DEV_AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$DEV_AWS_SECRET_ACCESS_KEY

aws ecs update-service --force-new-deployment --cluster api --service api

export AWS_ACCESS_KEY_ID=$MB_DEV_AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$MBDEV_AWS_SECRET_ACCESS_KEY

aws ecs update-service --force-new-deployment --cluster api --service api</code></pre><p>Now let's say our new feature is passing tests in both <code>dev</code> and <code>mb-dev</code> and we're ready to push to <code>staging</code> and <code>mb-staging</code>. Our configuration in ECS should be almost identical. Let's just change the <code>dev</code> tag to <code>staging</code> in our task definition. Then we just need to re-run our second and third scripts, the only difference being that we need <code>ENVIRONMENT=staging</code> instead of <code>ENVIRONMENT=dev</code>.</p><h3 id="immutable-tags">Immutable Tags</h3><p>We can use this same method for our production environments as well, but you may want to maintain a higher level of control. A better approach here may be to use an immutable tag. For example, we can include a timestamp suffix on the tag, something like <code>prod-11/12/20-10:30</code>. The timestamp ensures that only one image is ever given this tag, hence "immutable". Also with the timestamp we will know exactly when an image was certified ready for production. If for some reason we need to roll back production to a specific date, we can easily identify the correct image. The tagging script for this is mostly the same, we just need to add in a new variable <code>$MASTER_TAG</code>.</p><pre><code>#!/bin/bash

MASTER_TAG="master-$(date +"%m/%d/%Y-%T")"

MANIFEST=$(aws ecr batch-get-image \
    --repository-name $IMAGE_NAME \
    --image-ids imageTag=$COMMIT_HASH \
    --query 'images[].imageManifest' \
    --output text)</code></pre><h2 id="conclusion">Conclusion</h2><p>Using a few simple scripts, we have a fully automated build and deployment process. Our method is highly scalable as well. If we have another customer that comes along similar to MB, the tagging mechanism remains the same. All we need to do is introduce a new deploy command for their ECS cluster. We also have easy visibility into what's deployed where. If we need to know the state of our API container within a given environment, all we have to do is check ECR and see which mutable tags are set to a particular image. You can easily see this from the AWS console. This paradigm has been quite successful for our customers.</p><p>If you need help implementing it, please reach out to us, as well as for many other awesome devops solutions.</p>]]></content:encoded></item><item><title><![CDATA[iOS Accessibility: Dynamic Type]]></title><description><![CDATA[Nowadays just about everyone, everywhere, regardless of age and ability, uses a smartphone.

With that in mind, it’s more important than ever to consider all the different ways to make your app accessible to everyone to ensure an inclusive experience. In this series, we’ll be discussing different ways iOS developers can make their apps more accessible.

First up, we’ll be talking about Dynamic Type.

Dynamic Type is a feature on iOS that enables the app’s content to scale based on the user's pre]]></description><link>https://engineering.deptagency.com/ios-accessibility-part-1-dynamic-type/</link><guid isPermaLink="false">6241eee41a8623003da8d303</guid><category><![CDATA[iOS]]></category><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Kelsey Anderson]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:23:36 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/iphone.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/iphone.jpg" alt="iOS Accessibility: Dynamic Type"/><p>Nowadays just about everyone, everywhere, regardless of age and ability, uses a smartphone. </p><p>With that in mind, it’s more important than ever to consider all the different ways to make your app accessible to everyone to ensure an inclusive experience.<strong> </strong>In this series, we’ll be discussing different ways iOS developers can make their apps more accessible. </p><p>First up, we’ll be talking about Dynamic Type.<br><br>Dynamic Type is a feature on iOS that enables the app’s content to scale based on the user's preferred content size. It helps users who need larger text for better readability. And it also accommodates those who can read smaller text, allowing for more information to appear on the screen. In this blog post I’ll be covering the following topics.</br></br></p><ul><li>Intro to Dynamic Type</li><li>The basic setup</li><li>Dynamic Type with custom fonts</li><li>UI tips</li></ul><p><strong>Intro to Dynamic Type</strong></p><p>Let’s first start with some information about Dynamic Type. Dynamic Type is a feature that enables the app’s content to scale based on the user’s preferred content size. The preferred content size can be found in Settings -&gt; Accessibility -&gt; Display &amp; Text Size -&gt; Larger Text. If you have never changed this setting before, you’ll see a screen that looks like the image on the left. If you want to use the Larger Accessibility Sizes, turn on the “Larger Accessibility Sizes” toggle in the upper right.</p><p>Once we flip the toggle, there are five more options, which are the Accessibility options.<br><br>When Larger Accessibility Sizes is turned off, the sizes from left to right are: xSmall, Small, Medium, Large (Default), xLarge, xxLarge, xxxLarge<br><br>When Larger Accessibility Sizes is turned on, the sizes from left to right are:<br>xSmall, Small, Medium, Large (Default), xLarge, xxLarge, xxxLarge, AX1, AX2, AX3, AX4, AX5.</br></br></br></br></br></p><p><strong>Basic setup for Dynamic Type</strong><br><br>If you’re using the <a href="https://developer.apple.com/documentation/swiftui/font?ref=dept-engineering-blog.ghost.io">iOS built-in text styles</a>, you get support for Dynamic Type out of the box. We’ll go into more detail about these built-in text styles later, as they will come into play a little more with supporting custom fonts. For now let’s jump into the most basic implementation.</br></br></p><p><strong>When building your views programmatically, use:</strong></p><p><code>label.font = .preferredFont(forTextStyle: .body)</code></p><p>Yep it’s that simple! However, one thing you don’t get out of the box is the text reacting to the user changing their preferred content size while your app is open. In order for your text to react to those changes, you have to set adjustsFontForContentSizeCategory to true.</p><p><code>label.adjustsFontForContentSizeCategory = true</code></p><p><strong>When building your views in Storyboard:</strong></p><p>The setup is just as easy when using Storyboards, it’s only two steps.</p><ol><li>Choose your built-in text style.</li></ol><p><br>2. Check the box “Automatically Adjusts Font” for your text to react to any preferred content size changes.</br></p><p>That wraps up the basic setup of supporting Dynamic Type in your app. However, in most apps, you won’t be using the built-in text styles, you’ll be using a custom font. In the next section we’ll go over how to support Dynamic Type using custom fonts.</p><p><strong>Dynamic Type using custom fonts</strong><br><br>The first thing we need to learn about when supporting Dynamic Type with custom fonts is Apple’s utility class UIFontMetrics. This came out in iOS 11 and is used to scale custom fonts. You create instances of this class by passing in one of the built-in text styles we went over in the first section. This part threw me off a little at first. I asked myself, <em>What does it matter what text style we use if we’re using a custom font?</em> It matters because these text styles scale differently from one another. The following spreadsheet shows the different font sizes for each text style at each content size category. In the far right column you can see how much each text style scales from the Default (Large) preference to the largest option (AX5).</br></br></p><p>With that in mind, you can see that it’s important to choose a text style that has a Default size close to the font size you’re scaling. For example, if you use the Body text style to scale a label that has a font size of say 28, it’s going to end up scaling much too large. Instead, using the Title 1 text style would be more appropriate as it’s Default size is 28.</p><p>Okay now that we’ve got that out of the way, let’s jump into the code for supporting dynamic type on UILabel.</p><pre><code>If let customFont = UIFont(name: "Noteworthy", size: 17) {
    label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
    label.adjustsFontForContentSizeCategory = true
}

</code></pre><p>You can also use the `default` class variable on UIFontMetrics which uses the Body text style.</p><pre><code>if let customFont = UIFont(name: "Noteworthy", size: 17) {
    label.font = UIFontMetrics.default.scaledFont(for: customFont)
    label.adjustsFontForContentSizeCategory = true
}
</code></pre><p>Pretty simple, in just a few lines of code your label now supports Dynamic Type and will adjust to any content size preference changes. There is one issue with this. If you have a screen with many labels, this code can add up quickly. There are ways that it can be isolated and reused, and, if you’re building your views programmatically, this is a non-issue. However, if you’re using Storyboards, there isn’t a straightforward way to support Dynamic Type in Interface Builder and it’s not ideal to have to create IBOutlets for every label to add support for Dynamic Type. Over time this was a big enough pain point for our team that we decided to create a framework for supporting Dynamic Type right from Interface Builder. I’ll be covering this framework on my next blog post.</p><p><strong>UI tips</strong><br><br>Lastly, I want to leave you with a few small tips on how to be proactive when building your views to account for font scaling.<br><br>1) Use scroll views early and often. Pretty much every screen will have enough content to where it will need to scroll when the text is set to an accessibility size. Even if there isn’t enough content at first, that could change in the future. And adding a scroll view after the fact can be painful.<br><br>2) Configure table view cells and collection view cells to be self sizing to account for text size changes.<br><br>3) In situations where you have views that are horizontally aligned, use stack views. With stack views, if the preferred content size changes to an accessibility size, you can change the axis to vertical to allow for more space. You can listen for changes to the preferred content size by overriding func traitCollectionDidChange(_ previousTraitCollection:<a href="https://developer.apple.com/documentation/uikit/uitraitcollection?ref=dept-engineering-blog.ghost.io"> UITraitCollection</a>?) in your view controller. This <a href="https://useyourloaf.com/blog/making-space-for-dynamic-type/?ref=dept-engineering-blog.ghost.io">blog post</a> gives a good example of how to accomplish this.<br><br>In conclusion, we learned about supporting Dynamic Type programmatically and in Interface Builder, for both system fonts and custom fonts. Although it would be nice to have a cleaner way to support Dynamic Type in IB when using custom fonts, it’s still relatively simple and goes a long way for the end user.<br><br>That’s it for now, be sure to check out the upcoming posts in our accessibility series!</br></br></br></br></br></br></br></br></br></br></br></br></p>]]></content:encoded></item><item><title><![CDATA[Create a Rust web service, from zero to cloud]]></title><description><![CDATA[This tutorial will describe how to...
install Rust, create a new project and manage dependencies, Set up a simple web server, and compile the app and deploy to a virtual server. I'll be working on Ubuntu 20.04 but most of the setup should be the same on macOS or a different flavor of Linux.]]></description><link>https://engineering.deptagency.com/create-a-rust-web-service-from-zero-to-cloud/</link><guid isPermaLink="false">6241ee531a8623003da8d2ef</guid><category><![CDATA[Platforms]]></category><category><![CDATA[rust]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Kevin Larrabee]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:21:53 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/clouds2.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/clouds2.jpg" alt="Create a Rust web service, from zero to cloud"/><p>Rust is a fantastic general-purpose language with a rich and expressive type system, but one of the reasons the language is <a href="https://insights.stackoverflow.com/survey/2021?ref=dept-engineering-blog.ghost.io#section-most-loved-dreaded-and-wanted-programming-scripting-and-markup-languages">so loved</a> is the overall <strong>developer experience</strong>.</p><p>Writing software can be very complex in any language. <em>Working with</em> the language and tools, however, should not be. This is an area where Rust shines!</p><p>This tutorial will describe how to...</p><ul><li>Install Rust</li><li>Create a new project and manage dependencies</li><li>Set up a simple web server</li><li>Compile the app and deploy to a virtual server</li></ul><p>I'll be working on Ubuntu 20.04 but most of the setup should be the same on macOS or a different flavor of Linux.</p><h2 id="getting-set-up">Getting set up</h2><h3 id="installing-rust">Installing Rust</h3><p>Rust is best installed by <a href="https://www.rust-lang.org/tools/install?ref=dept-engineering-blog.ghost.io">a shell script</a> that downloads the <code>rustup</code> tool, gives a brief overview on the components that will be installed (as well as <em>where </em>they'll go), and prompts us to confirm options.</p><pre><code>➜ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

info: downloading installer

Welcome to Rust!

This will download and install the official compiler for the Rust
programming language, and its package manager, Cargo.

... snip

Current installation options:


   default host triple: x86_64-unknown-linux-gnu
     default toolchain: stable (default)
               profile: default
  modify PATH variable: yes

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
</code></pre><p>The defaults are sensible so I'll accept them and let the script do its work...</p><pre><code>info: profile set to 'default'
info: default host triple is x86_64-unknown-linux-gnu
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
info: latest update on 2021-07-29, rust version 1.54.0 (a178d0322 2021-07-26)

... snip

Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).

To configure your current shell, run:
source $HOME/.cargo/env</code></pre><p>... and in about <strong>30 seconds </strong>(given 60-70 Mbps download speed) all of Rust's glorious tools will be installed, including:</p><ul><li>The <code>rustc</code> compiler</li><li>The <code>cargo</code> package, dependency, and build manager</li><li>The <code>rustfmt</code> code formatting tool</li><li>The <code>clippy</code> linter, named after <a href="https://en.wikipedia.org/wiki/Office_Assistant?ref=dept-engineering-blog.ghost.io">the best thing Microsoft ever created</a> (and made <a href="https://www.reddit.com/r/rust/comments/o7bede/make_clippy_sound_like_clippy/?ref=dept-engineering-blog.ghost.io">even better</a> by a third party)</li></ul><p>It also recommends running <code>source $HOME/.cargo/env</code> so the <code>PATH</code> variable in the current shell is updated - future shell sessions should just work, since <code>rustup</code> updates profiles like <code>~/.bash_profile</code> automatically.</p><p>If you're following along and have second thoughts at any point, you can simply run <code>rustup self uninstall</code> to clean your system so that it's free of Rust.</p><h3 id="creating-a-new-project">Creating a new project</h3><p>Now let's create a brand new project that does nothing more than print the typical <code>Hello, world!</code> to the screen.</p><pre><code>➜ cargo new hello
     Created binary (application) `hello` package

➜ cd hello

➜ cargo run
   Compiling hello v0.1.0 (/home/kevlarr/projects/know/rust-zero-to-cloud/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.56s
     Running `target/debug/hello`
Hello, world!</code></pre><p>We cheated a little bit, because <code>cargo</code> <em>automatically</em> creates a "Hello, world!" app for us by scaffolding a project...</p><pre><code>.
├── Cargo.lock
├── Cargo.toml
└── src
   └── main.rs</code></pre><p>... with a single source file.</p><figure class="kg-card kg-code-card"><pre><code class="language-rust">fn main() {
    println!("Hello, world!");
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/main.rs</span></p></figcaption></figure><p>We then just <code>cargo run</code> to compile our new app and run the executable at <code>target/debug/hello</code>. We <em>could </em>have created, compiled, and executed the file manually...</p><pre><code>➜ echo 'fn main() { println!("Hello, world!"); }' &gt; hello.rs
➜ rustc hello.rs
➜ ./hello

Hello, world!</code></pre><p>... but <code>cargo</code> enforces a standard way of laying out a project, managing dependencies, configuring the compiler, and even combining multiple related projects into a <a href="https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html?ref=dept-engineering-blog.ghost.io">workspace</a>.</p><p>The most useful <code>cargo</code> commands are:</p><ul><li><code>cargo run</code> to check for errors, compile, and then run our app</li><li><code>cargo build</code> to check for errors and compile</li><li><code>cargo check</code> to only check for errors</li></ul><p>Compiling can be pretty slow, so use <code>cargo check</code> while coding for the fastest feedback cycle.</p><h2 id="building-a-web-server">Building a web server</h2><p>Now let's be nice humans and create a web service that compliments whomever visits our site - feel free to <a href="https://github.com/kevlarr/bee-nice?ref=dept-engineering-blog.ghost.io">clone the repo</a>.</p><h3 id="adding-a-framework">Adding a framework</h3><p>We'll start by bootstrapping a new project.</p><pre><code>➜ cargo new bee-nice
     Created binary (application) `bee-nice` package
➜ cd bee-nice</code></pre><p><a href="https://actix.rs/?ref=dept-engineering-blog.ghost.io">Actix Web</a> is one of the most established web frameworks, so let's add that as a dependency to our <code>Cargo.toml</code> manifest file. (<a href="https://rocket.rs/?ref=dept-engineering-blog.ghost.io">Rocket </a>is also an <em>excellent </em>choice, and <a href="https://tokio.rs/blog/2021-07-announcing-axum?ref=dept-engineering-blog.ghost.io">Axum </a>looks very promising.)</p><figure class="kg-card kg-code-card"><pre><code class="language-toml">[package]
name = "bee-nice"
version = "0.1.0"
edition = "2018"

[dependencies]
actix-web = "3"</code></pre><figcaption><p><span style="white-space: pre-wrap;">Cargo.toml</span></p></figcaption></figure><p>And we'll update our app so that it starts a server with a single route.</p><figure class="kg-card kg-code-card"><pre><code class="language-rust">use std::io;

use actix_web::{get, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn hello() -&gt; impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -&gt; io::Result&lt;()&gt; {
    HttpServer::new(|| App::new().service(hello))
        .bind("127.0.0.1:8080")?
        .run()
        .await
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/main.rs</span></p></figcaption></figure><p>A few things:</p><ul><li><code>#[get("/")]</code> and <code>#[actix_web::main]</code> are attribute-like <a href="https://doc.rust-lang.org/book/ch19-06-macros.html?ref=dept-engineering-blog.ghost.io#procedural-macros-for-generating-code-from-attributes">procedural macros</a>, which transform our code and enable more succinct code than functions could</li><li><code>async</code> &amp; <code>await</code> can be complicated, but there's <a href="https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html?ref=dept-engineering-blog.ghost.io">a great 'book</a>' to help</li></ul><p>Now we <code>cargo build</code> to download dependencies and then compile our own application code.</p><pre><code>➜ cargo build
    Updating crates.io index
  Downloaded pin-project v1.0.8
  Downloaded unicode-bidi v0.3.6
  Downloaded miniz_oxide v0.4.4
  ... lots of other crates
   Compiling proc-macro2 v1.0.28
   Compiling unicode-xid v0.2.2
   Compiling syn v1.0.74
  ... lots of other crates
   Compiling actix-web v3.3.2
   Compiling bee-nice v0.1.0 (/home/kevlarr/projects/bee-nice)
    Finished dev [unoptimized + debuginfo] target(s) in 1m 21s
</code></pre><p>Most of that <code>1m 21s</code> was spent compiling dependencies. Unless we remove the <code>target/debug/deps</code> folder or update dependency versions, we won't need to recompile them again.</p><p>If we <code>cargo run</code> we won't see much output, since our application isn't logging anything....</p><pre><code>➜ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/bee-nice`</code></pre><p>... but <em>our browser</em> should at least be able to greet the world now!</p><h3 id="returning-a-compliment">Returning a compliment</h3><p>"Hello, world!" might be polite, but it's still not a compliment to the user.</p><p>Rust frameworks tend to be modular, and Actix Web is no exception - there is no built-in HTML templating and no automatic static file serving. We will set those up with the <a href="https://actix.rs/docs/static-files/?ref=dept-engineering-blog.ghost.io#directory">Actix Files</a> plugin and <a href="https://github.com/sunng87/handlebars-rust?ref=dept-engineering-blog.ghost.io">Handlebars</a> template engine.</p><p>Again, we'll add our new dependencies (including <a href="https://serde.rs/?ref=dept-engineering-blog.ghost.io">Serde</a> so we can make a custom object to hold the data for the HTML template) to <code>Cargo.toml</code>.</p><figure class="kg-card kg-code-card"><pre><code class="language-toml">[package]
name = "bee-nice"
version = "0.1.0"
edition = "2018"

[dependencies]
# We often just select simple versions for our dependencies...
actix-files = "0.5"
actix-web = "3"
serde = "1"

# ... or we might need to opt into certain "features" that aren't
# included by default
handlebars = { version = "3", features = ["dir_source"] }
</code></pre><figcaption><p><span style="white-space: pre-wrap;">Cargo.toml</span></p></figcaption></figure><p>And then we make some big changes to <code>src/main.rs</code>, including...</p><ul><li>... updating our server to optionally accept a <code>BIND_ADDRESS</code> environment variable, if we don't want <code>localhost:8080</code></li><li>... adding in a <code>Files</code> service that will map requests for <code>/public/:some/:file/:path</code> to files in the <code>./web/public</code> directory</li><li>... creating a <code>Handlebars</code> instance mapped to the <code>./web/templates</code> directory and injecting it into application state to be retrieved in request handlers</li></ul><figure class="kg-card kg-code-card"><pre><code class="language-rust">use std::{env, io};

use actix_files::Files;
use actix_web::{web::Data, get, App, HttpServer, HttpResponse, Responder};
use handlebars::Handlebars;
use serde::Serialize;

#[derive(Serialize)]
struct Compliment {
    adjective: &amp;'static str,
    verb: &amp;'static str,
}

#[get("/")]
async fn compliment(hb: Data&lt;Handlebars&lt;'_&gt;&gt;) -&gt; impl Responder {
    let compliment = Compliment {
        adjective: "most stellar",
        verb: "known and/or dreamed of",
    };
    let html = hb.render("compliment", &amp;compliment).unwrap();

    HttpResponse::Ok()
        .content_type("text/html")
        .body(html)
}

#[actix_web::main]
async fn main() -&gt; io::Result&lt;()&gt; {
    let address = env::var("BIND_ADDRESS")
        .unwrap_or_else(|_err| "localhost:8080".to_string());

    let template_service = {
        let mut handlebars = Handlebars::new();

        handlebars
            .register_templates_directory(".html", "web/templates")
            .unwrap();

        Data::new(handlebars)
    };

    let server = move || App::new()
        .app_data(template_service.clone())
        .service(Files::new("/public", "web/public").show_files_listing())
        .service(compliment);

    HttpServer::new(server)
        .bind(address)?
        .run()
        .await
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/main.rs</span></p></figcaption></figure><p>A few other things here:</p><ul><li><code>.unwrap()</code> is a great way to 'ignore' about <code>Result</code> and <code>Option</code> types during development, but we should prefer more robust error handling in production</li><li><code>async fn compliment(hb: Data&lt;Handlebars&lt;'_&gt;&gt;)</code> is an example of Actix Web's powerful <a href="https://actix.rs/docs/extractors/?ref=dept-engineering-blog.ghost.io">"extractor"</a> pattern, which allows request handlers to specify what data they want to extract from a request or application state</li><li><code>HttpResponse::Ok().content_type(..)</code> is an example of the <a href="https://doc.rust-lang.org/1.0.0/style/ownership/builders.html?ref=dept-engineering-blog.ghost.io">builder pattern</a>, a useful strategy for overcoming the lack of function overloads, optional arguments, keyword arguments, etc.</li></ul><p>Next, create a <a href="https://github.com/kevlarr/bee-nice/blob/master/web/public/main.css?ref=dept-engineering-blog.ghost.io">basic stylesheet</a> and an <a href="https://github.com/kevlarr/bee-nice/blob/master/web/templates/compliment.html?ref=dept-engineering-blog.ghost.io">HTML template</a> with placeholder variables for an <code>adjective</code> and a <code>verb</code> to be supplied at runtime.</p><figure class="kg-card kg-code-card"><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;You are the BEST&lt;/title&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
        &lt;meta charset="UTF-8"&gt;
        &lt;link rel="stylesheet" href="/public/main.css"&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h1&gt;Hey, you!&lt;/h1&gt;
        &lt;p&gt;You are simply the &lt;em&gt;{{ adjective }}&lt;/em&gt; person that I have EVER &lt;em&gt;{{ verb }}&lt;/em&gt;.&lt;/p&gt;
        &lt;p class="small"&gt;Don't ever stop being you!&lt;/p&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre><figcaption><p><span style="white-space: pre-wrap;">web/templates/compliment.html</span></p></figcaption></figure><p>Recompile and execute via <code>cargo run</code> and you <em>should</em> see a friendlier site!</p><h2 id="to-the-clouds">To the clouds</h2><p>We typically wrap our applications in a Docker image and deploy to some service that natively runs containers. With languages like Rust or <a href="https://jonathanmh.com/deploying-golang-app-without-docker/?ref=dept-engineering-blog.ghost.io">Go</a> that compile directly to binaries, we <em>often don't need that level of abstraction</em> because there's far less to manage when running an application:</p><ul><li>There's no underlying runtime or VM</li><li>Dependencies are often statically-linked into the binary</li><li>Rust doesn't even need to be installed to run an application</li></ul><p>For now, let's just deploy our app (which consists of the compiled binary, the HTML template, and the CSS file) to a <strong>virtual server running Ubuntu 20.04</strong> that...</p><ul><li>... can <strong>accept public traffic</strong></li><li>... we have <strong>root access</strong> to via SSH</li></ul><p>I'm using a DigitalOcean <a href="https://www.digitalocean.com/products/droplets/?ref=dept-engineering-blog.ghost.io">Droplet</a> because they're simple - and very cheap!</p><h3 id="a-better-binary">A better binary</h3><p>Because both my local machine and virtual server are running Ubuntu 20.04, I can simply run...</p><pre><code>➜ cargo build --release</code></pre><p>... to generate a more-optimized binary. As a comparison, the "release" version is less than 1/10th the size:</p><pre><code>➜ find . -name 'bee-nice' -exec ls -lh {} \;
-rwxr-xr-x 1 kevlarr kevlarr 8.1M Aug 19 11:20 ./target/release/bee-nice
-rwxr-xr-x 2 kevlarr kevlarr 100M Aug 19 13:42 ./target/debug/bee-nice</code></pre><p>If you are on a different OS than the remote server, you have several options:</p><ul><li>You can try your hand at <a href="https://rust-lang.github.io/rustup/cross-compilation.html?highlight=target&ref=dept-engineering-blog.ghost.io#cross-compilation">cross-compilation</a> (which might require <a href="https://stackoverflow.com/questions/40424255/cross-compilation-to-x86-64-unknown-linux-gnu-fails-on-mac-osx?ref=dept-engineering-blog.ghost.io">troubleshooting</a>)</li><li>You can use the <a href="https://hub.docker.com/_/rust?ref=dept-engineering-blog.ghost.io">official Docker image</a> to compile for a target platform, either with or without running the app inside a container (see the <em>"Compile your app inside the Docker container"</em> section)</li></ul><h3 id="deploying-to-the-remote-machine">Deploying to the remote machine</h3><p>Assuming we compiled directly (or used Docker just to compile), let's copy over the relevant files into <code>/opt/bee-nice</code> on the remote server, making sure that the binary is in the same directory as the <code>./web</code> folder.</p><pre><code>➜ ssh &lt;USER&gt;@&lt;HOST&gt; "mkdir /opt/bee-nice"
➜ scp target/release/bee-nice &lt;USER&gt;@&lt;HOST&gt;:/opt/bee-nice
➜ scp -r web &lt;USER&gt;@&lt;HOST&gt;:/opt/bee-nice</code></pre><p>Now let's remotely start the server in a detached session, binding to <code>0.0.0.0:80</code> so any HTTP traffic to our remote server's IP address will go straight to our app. Also, because we naively hard-coded relative paths like <code>web/templates</code> we need to make sure that the <em>current working directory of the process</em> is correct, hence running with <code>cd /opt/bee-nice &amp;&amp; ./bee-nice</code>  rather than <code>/opt/bee-nice/bee-nice</code>.</p><pre><code>➜ ssh &lt;USER&gt;@&lt;HOST&gt; "cd /opt/bee-nice &amp;&amp; BIND_ADDRESS=0.0.0.0:80 screen -d -m ./bee-nice"</code></pre><p>While this is a<em> terrible idea</em> for a production application, it's enough for now to show how simple it can be. Notice how there's nothing to install on the remote server, other than our compiled app? Pretty nice - and <a href="https://github.com/kevlarr/bee-nice/blob/master/Dockerfile?ref=dept-engineering-blog.ghost.io">easy to containerize</a> and deploy <em>properly</em>.</p><p>If all works as planned, you should now be able to share a link to your beautiful site with anyone whom you want to make marginally happier!</p><h2 id="conclusion">Conclusion</h2><p>Rust has modern, well-designed tooling that <strong>eases pain points</strong> seen in other languages, making it trivial to manage different language versions and pull third-party code into our projects. Additionally, working with <strong>compiled binaries can <em>greatly</em> simplify deploying</strong> and scaling web services.</p><p>Overall, beyond being an enjoyable language, Rust offers an <strong><em>enjoyable experience</em></strong> outside of the code itself, which can be as important to developer happiness - and project success - as having a good type system or an active open-source community.</p>]]></content:encoded></item><item><title><![CDATA[File Uploads Directly to S3 From the Browser]]></title><description><![CDATA[Uploading files to S3 directly from the browser is a great way to increase performance by removing the need to process and then re-upload files from your own server.]]></description><link>https://engineering.deptagency.com/file-uploads-directly-to-s3-from-the-browser/</link><guid isPermaLink="false">6241eb1f1a8623003da8d2e5</guid><category><![CDATA[Process]]></category><dc:creator><![CDATA[Matt DiDomenico]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:07:13 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/abstract-lines.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/abstract-lines.jpg" alt="File Uploads Directly to S3 From the Browser"/><p>Uploading files to S3 directly from the browser is a great way to increase performance by removing the need to process and then re-upload files from your own server.</p><p><em>Note: In this post we'll be examining the use of pre-signed POST requests. See <a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/PresignedUrlUploadObject.html?ref=dept-engineering-blog.ghost.io">pre-signed URLs</a> as an alternative (less powerful) method.</em></p><h2 id="overview">Overview</h2><p>A traditional approach to file uploading would typically involve the client making a POST request to an endpoint on your API server.  Your API server would then be responsible for processing the handling of the transfer of the file itself, and any subsequent processing you might want to perform (image resizing/thumbnail generation etc).</p><p>There's certainly nothing wrong with this approach, however these days many people opt to use third party storage solutions, like S3, to avoid having to implement a scalable storage solution of their own.  Great, right?  This now means your API server has to transfer the file(s) to S3 and incur additional resource costs.</p><p>What if you could avoid these resource costs, think RAM, CPU, bandwidth in and out, etc by having your client upload directly to S3?  Enter pre-signed POST requests!</p><p>Pre-signed POST requests allow the client to upload directly to S3, bypassing your server entirely.  Your API server is only tasked with generating the pre-signed POST request and providing it to the client, no file handling at all!  As you can imagine, this frees up significant resources which can then be utilized for handling additional API requests.</p><p>You might be wondering how we can perform any additional processes, like thumbnail generation for example, if we never actually get our hands on the file.  This is where lambda functions come in handy!</p><p><em>Although outside the scope of this particular post, I will look to address post upload processing with lambda functions in a future post!</em></p><h3 id="process-summary">Process summary</h3><ol><li>The client makes a request to an endpoint which responds with a URL and pre-signed post data.</li><li>The client then uses the provided URL and pre-signed POST data to form a request containing the file to be uploaded directly to S3.</li></ol><p>Now that we've gone over the benefits of utilizing pre-signed POST requests over the traditional approach, let's dive in further and take a look at how to implement them!</p><h2 id="s3-bucket-cors">S3 bucket CORS</h2><p>Assuming you already have an S3 bucket you'd like to upload your files to, the next step is to modify the bucket's CORS configuration to allow POST requests.</p><pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"&gt;
&lt;CORSRule&gt;
    &lt;AllowedOrigin&gt;*&lt;/AllowedOrigin&gt;
    &lt;AllowedMethod&gt;POST&lt;/AllowedMethod&gt;
    &lt;AllowedHeader&gt;*&lt;/AllowedHeader&gt;
&lt;/CORSRule&gt;
&lt;/CORSConfiguration&gt;
</code></pre><p>Once that's done it's time to create our pre-signed POST request!</p><h2 id="creating-the-pre-signed-post-request">Creating the pre-signed POST request</h2><p>An endpoint on your server that's accessible to the client would contain code similar to the following:</p><pre><code class="language-javascript">import aws from 'aws-sdk';
import { nanoid } from 'nanoid';

const s3 = new aws.S3();
const S3_BUCKET = 'launchpad';

const fileName = `uploads/${nanoid()}`; // create a unique file name
const fileType = contentType; // the content-type of the file
const s3Params = {
    Bucket: S3_BUCKET,
    Fields: {
        key: fileName
    },
    Conditions: [
        ['content-length-range', 0, 100000000],
        ['starts-with', '$Content-Type', 'image/'],
        ['eq', '$x-amz-meta-user-id', userId],
    ],
    ContentType: fileType
};

const data = s3.createPresignedPost(s3Params);</code></pre><p>Specific <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html?ref=dept-engineering-blog.ghost.io#sigv4-PolicyConditions">conditions</a> may be specified to add validation to the request.  In this case, we verify that the file size is less than 100000000 bytes and that the content-type of the file begins with 'image/'.</p><p>It's also worth noting that additional custom meta information can be specified.  Here we specify the user id of the person uploading the file which would be useful in post upload processing when we need to associate the file with the uploader.</p><p>The result of <strong><strong><a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html?ref=dept-engineering-blog.ghost.io#createPresignedPost-property">createPresignedPost</a></strong></strong> would look something like the following:</p><pre><code>{
  "url": "https://s3.amazonaws.com/launchpad",
  "fields": {
    "key": "uploads/ENirEL-xzcazcDjtD-EI0",
    "bucket": "launchpad",
    "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
    "X-Amz-Credential": "AKDSIAIO3EBJDSMZDNVBYYSD6MCA/21254530/us-east-1/s3/aws4_request",
    "X-Amz-Date": "20200530T020436Z",
    "Policy": "eyJleHBpcmFKJgKJHlowNDozNloiLCJjb25kaXRpb25zIjpbWyJjb250ZW50LWxlbmdfsdgrfgDBdLFsic3RhcnRzLXdpdGgiLCIkQ29udGVusdfsdfsdfaW1hZ2UvIl0sWyJlcfsdf4LWFtei1hY2wiLCJwdWJsaWMtcmVhZCJdLFsiZXEiLCIkeC1hbXotbWV0YS11c2VyLWlkIiwiMSJdLFsiZXEiLCIkeC1hbXotbWV0YS1pbWFnZWFibGUtdHlwZSIsImJlZCJdLFsiZXEiLCIkeC1hbXotbWV0YS1pbWFnZWFibGUtaWQiLCIxIl0seyJrZXkiOiJ1cGxvYWRzL28vRU5pckVMLXh6Y2F6Y0RqdEQtRUkwIn0seyJidWNrZXQiOiJzd3Rlc3RidWNrZXQzIn0seyJYLUFtei1BbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0seyJYLUFtei1DcmVkZW50aWFsIjoiQUtJQUlPM0VCSk1OVkJZWTZNQ0EvMjAyMDA1MzAvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsiWC1BbXotRGF0ZSI6IjIwMjAwNTMwVDAyMDQzNloifV19",
    "X-Amz-Signature": "fb0f381d8c9176f86g786gc30f5a25e236546723894dgf2cddb347"
  }
}
</code></pre><h2 id="uploading-the-file-to-s3">Uploading the file to S3</h2><p>Once we've written the code to create a pre-signed POST request, it's time to create the request itself and upload the file to S3!</p><pre><code class="language-javascript">import ky from 'ky';

// the selected file to upload
const file = &lt;FILE&gt;;

// get the pre-signed POST request
const presignedPost = await ky.post('/presignedpost', { contentType: file.type }).json();

const formData = new FormData();

formData.append("x-amz-meta-user-id", userId);
formData.append("Content-Type", file.type);

Object.entries(presignedPost.fields).forEach(([k, v]) =&gt; {
    formData.append(k, v);
});

formData.append("file", file);

await ky.post(presignedPost.url, {
    body: formData,
});
</code></pre><p>We use FormData to build out the POST request, appending all the necessary fields that were specified in our code to create the pre-signed POST request.  We then append all the fields provided by the pre-signed POST request response and lastly, we append the file itself.</p><p><strong><strong>Please note:  </strong></strong>the file <strong><strong>must</strong></strong> be the last field appended to the FormData.</p><p>Now that the form data has been prepared with all the necessary fields, all that's left to do is make the request!  In this example we use the popular HTTP request library <strong><strong><a href="https://github.com/sindresorhus/ky?ref=dept-engineering-blog.ghost.io">ky</a></strong></strong> but any will do!</p><p>If all went to plan S3 should reply with a 204 and if not, it should reply with one of the following <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html?ref=dept-engineering-blog.ghost.io">error codes</a> instead.</p><h2 id="security-concerns">Security Concerns</h2><p>Pre-signed POST requests are secure by nature.  The pre-signed POST request itself contains no sensitive information so there is no harm in allowing the client access to it.  That being said, it's important to note that anyone with access to a valid pre-signed POST request can use it to upload directly to your S3 bucket!</p><p>Be sure to only issue your pre-signed POST requests to trusted parties!  It's important to protect your endpoint responsible for the request generation like you would any other secure endpoint on your API.</p><h2 id="thats-it">That's it!</h2><p>I hope this post was helpful in getting a basic understanding of the process involved with uploading a file directly to S3 from the browser.</p><p>Utilizing this approach along with a lambda function to replace any file processing that would normally have been performed on your server makes for a really powerful combination.</p><h2 id="whats-next">What's next?</h2><p>We'll look to create a lambda function to process the file once it's been uploaded to S3 and retrieve the meta data we passed along with it.  Stay tuned!</p>]]></content:encoded></item><item><title><![CDATA[How to Build a full page calendar with React]]></title><description><![CDATA[Learn how to build a full-page calendar using React. ]]></description><link>https://engineering.deptagency.com/how-to-build-a-full-page-calendar-with-react/</link><guid isPermaLink="false">6241ea9e1a8623003da8d2cc</guid><category><![CDATA[Platforms]]></category><category><![CDATA[react]]></category><dc:creator><![CDATA[Nat Ring]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:05:54 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/calendar.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/calendar.jpg" alt="How to Build a full page calendar with React"/><p>As a developer, I can’t help but get excited when I see immersive apps built on the web. I love it when I get to build things that are a little bit “out of the ordinary”.</p><p>I recently built a full-page calendar widget. At first, I thought I’d reach for a library, but I found myself a little disappointed with the options in the ecosystem. More importantly, I realized that it’s actually not very hard to build a full-page calendar using modern web technologies.</p><p>Because it doesn’t take much code <em>and also because there are so many custom use cases for a calendar, </em>I think having a solid starting point that you can <em>copy into your project and adapt as necessary</em> will prove more useful than importing a third-party library and trying to extend it. So in this article, we’re going to build the calendar experience shown below. If you’re just interested in the code, feel free to open the code sandbox and view the entire project 🙂</p><figure class="kg-card kg-embed-card"><iframe width="1000" height="500" src="https://codesandbox.io/embed/full-page-calendar-part-one-veh84?fontsize=14&amp;hidenavigation=1&amp;theme=dark" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" style="display: block; margin: 1.75em 0px; width: 1000px; height: 500px; border: 0px; border-radius: 4px; overflow: hidden;"/></figure><p>Beyond using React, I’m going to try to keep the technology and patterns as non-controversial and “standard” as I can, in the hopes that this code will be useful to as many of you as possible! For example, I’ll be using global CSS to style the calendar because I know there are lots of different ways to encapsulate styles. I’ll let you adapt the style code as necessary in your own projects from the sandbox link. Some key technologies we’ll be using are:</p><ul><li>React</li><li><a href="https://css-tricks.com/snippets/css/complete-guide-grid/?ref=dept-engineering-blog.ghost.io" rel="noopener">CSS Grid</a> for the calendar grid</li><li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties?ref=dept-engineering-blog.ghost.io" rel="noopener">CSS Custom Properties</a> to keep the styles clean</li><li><a href="https://day.js.org/?ref=dept-engineering-blog.ghost.io" rel="noopener">Day.js</a> for date logic</li></ul><p>Note: This is not a step-by-step article. I’ll be detailing the less-obvious/ more-interesting patterns and approaches in this article, but you should open the sandbox and look at the code alongside this article to see how everything fits together!</p><hr><h3 id="component-structure-and-props-api">Component Structure and Props API</h3><p>The <code>&lt;Calendar /&gt;</code> component itself will handle :</p><ul><li>Displaying a month grid for the given month/year</li><li>Displaying headers for the days of the week</li><li>Displaying the current year/ month</li><li>Providing controls for changing year/month</li></ul><p>We’ll build the calendar as a controlled component with the props below. Also, see <code>&lt;App.js /&gt;</code> in the code sandbox.</p><pre><code>&lt;Calendar  
  // It's up to you to render the contents of each calendar day.  
  // Pass a function to renderCalendarDay which returns a  
  // node given an object with the following properties:  
  //  {  
  //    dateString,       e.g. 2021-08-30  
  //    dayOfMonth,       e.g. 30  
  //    isCurrentMonth,   true/false  
  //    isNextMonth,      true/false  
  //    isPreviousMonth,  true/false, 
  //  }  
  renderCalendarDay={(calendarDayObject) =&gt; ...}  

  // e.g. [2021, 08] for August 2021  
  yearAndMonth={[yearInt, monthInt]}  
  
  // onChange handler (you can just pass the setter  
  // from setState if you'd like)   
  onYearAndMonthChange={(yearAndMonth) =&gt; ...}
/&gt;</code></pre><p>Note: The calendar component does not render the day headers within each grid item. It’s up to you to do so. However, the code sandbox does export a simple component named <code>&lt;CalendarDayHeader /&gt;</code> to help with this. We’ll talk more about why we’ve opted to do this in the “Next Steps For You” section at the end of this article.</p><h3 id="rendering-the-grid-with-js">Rendering The Grid with JS</h3><p>We can use CSS Grid to do the heavy lifting of the layout for us, but we need to use javascript to figure out how many grid items to render. We also need to render some grid items for the previous/ next months as necessary to fill out the calendar.</p><p>In <code>calendar.js</code>, we’ll build out an array with an object for each day that needs to be rendered. The objects can encode the information necessary to render the day information.</p><p>Note: this strategy is adapted from another excellent article over on CSS Tricks! <a href="https://css-tricks.com/how-to-make-a-monthly-calendar-with-real-data/?ref=dept-engineering-blog.ghost.io" rel="nofollow noopener noopener">https://css-tricks.com/how-to-make-a-monthly-calendar-with-real-data</a></p><pre><code>// calendar.js
const [year, month] = yearAndMonth;

let currentMonthDays = createDaysForCurrentMonth(year, month);

let previousMonthDays = createDaysForPreviousMonth(  
  year, 
  month, 
  currentMonthDays
);

let nextMonthDays = createDaysForNextMonth(
  year,   
  month,   
  currentMonthDays
);

let calendarGridDayObjects = [ 
  ...previousMonthDays, 
  ...currentMonthDays,
  ... nextMonthDays
];</code></pre><p>A day object will look like:</p><pre><code>{  
  dateString,       // e.g. 2021-08-30  
  dayOfMonth,       // e.g. 30  
  isCurrentMonth,   // true/false  
  isNextMonth,      // true/false  
  isPreviousMonth,  // true/false
}</code></pre><p>Let’s take a look at <code>createDaysForCurrentMonth</code>. It’s actually pretty straightforward with <code>Day.js</code>!</p><pre><code>export function getNumberOfDaysInMonth(year, month) {
  return dayjs(`${year}-${month}-01`).daysInMonth();
}

export function createDaysForCurrentMonth(year, month) {
  return [ 
    ...Array(getNumberOfDaysInMonth(year, month))  
  ].map((_, index) =&gt; {    
    return { 
      dateString: dayjs(`${year}-${month}-${index + 1}`)
        .format("YYYY-MM-DD"),
      dayOfMonth: index + 1,
      isCurrentMonth: true,
      isNextMonth: false,
      isPreviousMonth: false
     };
   });
}</code></pre><p>See <code>helpers.js</code> for the implementation of all helper functions!</p><p>Now that we’ve got an array of day objects, rendering the grid is pretty simple in the component file. We don’t even need to create a nested DOM structure for the rows! CSS Grid will take care of this for us!</p><pre><code>&lt;div className="calendar-root"&gt;  
  &lt;div className="days-of-week"&gt;    
    {daysOfWeek.map((day, index) =&gt; (      
      &lt;div        
        key={day}        
        className="day-of-week-header-cell"    
      &gt;        
        {day}      
      &lt;/div&gt;    
    ))}  
  &lt;/div&gt;  
  &lt;div className="days-grid"&gt;    
    {calendarGridDayObjects.map((day) =&gt; (      
      &lt;div        
        key={day.dateString}        
        className={classNames(
          "day-grid-item-container", 
          { "current-month": day.isCurrentMonth }
        )}      
      &gt;        
        &lt;div className="day-content-wrapper"&gt;{renderDay(day)}&lt;/div&gt;     
      &lt;/div&gt;    
    ))}  
  &lt;/div&gt;
&lt;/div&gt;</code></pre><h3 id="styling-the-grid-with-css">Styling The Grid With CSS</h3><p>Alright, let’s get styling! We can start with a root class where we’ll define some helpful variables to keep our styles clean.</p><pre><code>.calendar-root { 
  --grid-gap: 2px; 
  --grid-height: 700px; 
  --grid-background-color: rgb(211, 205, 198);  
  --grid-foreground-color: white;
}</code></pre><p>Now, let’s define the styles for the grid!</p><pre><code>.days-grid {  
  position: relative; 
  width: 100%;  
  height: var(--grid-height);  
  box-sizing: border-box;  
  display: grid;  
  grid-template-columns: repeat(7, 1fr);  
  grid-column-gap: var(--grid-gap);  
  grid-row-gap: var(--grid-gap);  
  border: var(--grid-gap) solid var(--grid-background-color);   
  background-color: var(--grid-background-color);
}

.day-grid-item-container {  
  background-color: var(--grid-foreground-color);
}</code></pre><p><strong><strong>Thats it! 🎉 </strong></strong>There’s more css to look at in the code sandbox, but to style the bulk of the calendar, all we need is the code above. Some notes:</p><ul><li><code>grid-template-columns: repeat(7, 1fr)</code> tells the browser to layout 7 columns and distribute their widths equally.</li><li>We’ve given the grid a background color which is used as the item borders and explicitly defined the item foreground color. This lets us make the grid column and row gaps bigger/ smaller as we please.</li><li>The grid has a fixed height, so when a month needs only 5 rows, each row will be taller in comparison to when a month needs 6 rows.</li></ul><h3 id="next-steps-for-you">Next Steps For You</h3><p>Play around with the <a href="https://codesandbox.io/s/full-page-calendar-part-one-veh84?file=/src/App.js&ref=dept-engineering-blog.ghost.io" rel="noopener">code sandbox</a> and adapt the code as necessary in your project! You have full control over the day items, so you can do things like render interactive calendar item labels using your application data.</p><p>You’ll just want to make sure you render a <code>&lt;CalendarDayHeader /&gt;</code> inside the container element so that the calendar day is displayed. It’s structured as a separate component so that you can more easily attach an <code>onClick</code> handler to the container to catch click events bubbling up from the header. (This covers the use case of wanting to open a “new event” dialog when a user clicks somewhere inside the calendar day.)</p><p>If you think improvements can be made to this implementation or if you have any questions, please leave a comment! Happy coding!</p></hr>]]></content:encoded></item><item><title><![CDATA[Reusable Patterns in CloudFormation]]></title><description><![CDATA[At DEPT, we use a variety of tools to provision infrastructure in the cloud. In this post, we take a look at some of the reusable patterns we've developed using AWS CloudFormation.]]></description><link>https://engineering.deptagency.com/reusable-patterns-in-cloudformation/</link><guid isPermaLink="false">6241ea531a8623003da8d2bb</guid><category><![CDATA[Platforms]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:04:04 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/clouds.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/clouds.jpg" alt="Reusable Patterns in CloudFormation"/><p>At DEPT, we use a variety of tools to provision infrastructure in the cloud. In this post, we take a look at some of the reusable patterns we've developed using AWS CloudFormation.</p><h2 id="what-is-cloudformation">What is CloudFormation?</h2><p>If you've ever created infrastructure and/or resources in AWS then there's good chance you've used or at least heard of CloudFormation. For those who are unfamiliar, CloudFormation is an AWS service that allows you to provision and configure almost all AWS resources using yaml (or json) templates.</p><p>Here's an example template:</p><!--kg-card-begin: html--><table class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="fargate-service-template.yml" style="border-collapse: collapse; border-spacing: 0px; box-sizing: border-box; margin: 0px; width: 904.32px; max-width: 100%; background: var(--color-canvas-default); font-size: 12px; text-rendering: auto; padding: 0px; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-weight: 400; line-height: 1.4; color: rgb(51, 51, 51); border: 0px;"><tbody><tr><td id="file-fargate-service-template-yml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC1" class="blob-code blob-code-inner js-file-line" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">AWSTemplateFormatVersion</span>: <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">'</span>2010-09-09<span class="pl-pds" style="color: rgb(3, 47, 98);">'</span></span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC2" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Deploy a service on AWS Fargate, hosted in a private subnet.</span></td></tr><tr><td id="file-fargate-service-template-yml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC3" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">
</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC4" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">Parameters</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC5" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">
</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC6" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">ENV</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC7" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Name of the environment</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC8" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">AWS::SSM::Parameter::Value&lt;String&gt;</span></td></tr><tr><td id="file-fargate-service-template-yml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC9" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">/cloudformation/parameters/env</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC10" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC11" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">VPCStackName</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC12" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">AWS::SSM::Parameter::Value&lt;String&gt;</span></td></tr><tr><td id="file-fargate-service-template-yml-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC13" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">/cloudformation/parameters/vpc/stackname</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC14" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">The name of the parent VPC networking stack that you created. Necessary</span></td></tr><tr><td id="file-fargate-service-template-yml-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC15" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">                 <span class="pl-s" style="color: rgb(3, 47, 98);">to locate and reference resources created by that stack.</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC16" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC17" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">ECSStackName</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L18" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="18" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC18" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">AWS::SSM::Parameter::Value&lt;String&gt;</span></td></tr><tr><td id="file-fargate-service-template-yml-L19" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="19" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC19" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">/cloudformation/parameters/ecs/stackname</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L20" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="20" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC20" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">The name of the parent ECS stack that you created. Necessary</span></td></tr><tr><td id="file-fargate-service-template-yml-L21" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="21" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC21" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">                 <span class="pl-s" style="color: rgb(3, 47, 98);">to locate and reference resources created by that stack.</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L22" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="22" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC22" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L23" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="23" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC23" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">ServiceName</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L24" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="24" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC24" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">String</span></td></tr><tr><td id="file-fargate-service-template-yml-L25" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="25" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC25" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">"</span>CHANGE-ME<span class="pl-pds" style="color: rgb(3, 47, 98);">"</span></span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L26" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="26" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC26" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">A name for the service</span></td></tr><tr><td id="file-fargate-service-template-yml-L27" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="27" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC27" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">
</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L28" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="28" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC28" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">ContainerPort</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L29" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="29" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC29" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Number</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L30" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="30" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC30" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">8443</span></td></tr><tr><td id="file-fargate-service-template-yml-L31" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="31" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC31" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">What port number the application inside the docker container is binding to</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L32" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="32" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC32" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">ContainerCpu</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L33" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="33" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC33" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Number</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L34" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="34" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC34" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">2048</span></td></tr><tr><td id="file-fargate-service-template-yml-L35" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="35" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC35" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">How much CPU to give the container. 1024 is 1 CPU</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L36" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="36" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC36" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">ContainerMemory</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L37" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="37" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC37" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Number</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L38" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="38" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC38" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">4096</span></td></tr><tr><td id="file-fargate-service-template-yml-L39" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="39" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC39" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">How much memory in megabytes to give the container</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L40" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="40" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC40" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L41" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="41" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC41" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">DesiredCount</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L42" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="42" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC42" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Number</span></td></tr><tr><td id="file-fargate-service-template-yml-L43" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="43" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC43" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Default</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">1</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L44" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="44" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC44" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Description</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">How many copies of the service task to run</span></td></tr><tr><td id="file-fargate-service-template-yml-L45" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="45" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC45" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">
</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L46" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="46" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC46" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L47" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="47" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC47" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">Resources</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L48" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="48" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC48" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L49" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="49" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC49" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">LogGroup</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L50" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="50" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC50" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">AWS::Logs::LogGroup</span></td></tr><tr><td id="file-fargate-service-template-yml-L51" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="51" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC51" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Properties</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L52" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="52" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC52" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">LogGroupName</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ServiceName }</span></td></tr><tr><td id="file-fargate-service-template-yml-L53" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="53" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC53" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">RetentionInDays</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">365</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L54" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="54" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC54" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L55" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="55" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC55" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">ECRRepo</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L56" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="56" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC56" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">AWS::ECR::Repository</span></td></tr><tr><td id="file-fargate-service-template-yml-L57" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="57" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC57" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Properties</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L58" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="58" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC58" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">RepositoryName</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{"Fn::Sub" : "my-test-org/${ServiceName}"}</span></td></tr><tr><td id="file-fargate-service-template-yml-L59" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="59" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC59" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">
</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L60" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="60" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC60" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-fargate-service-template-yml-L61" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="61" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC61" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-c" style="color: rgb(106, 115, 125);"><span class="pl-c" style="color: rgb(106, 115, 125);">#</span> The task definition.</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L62" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="62" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC62" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">TaskDefinition</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L63" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="63" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC63" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">AWS::ECS::TaskDefinition</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L64" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="64" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC64" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Properties</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L65" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="65" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC65" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">Family</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ServiceName }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L66" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="66" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC66" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">Cpu</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ContainerCpu }</span></td></tr><tr><td id="file-fargate-service-template-yml-L67" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="67" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC67" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">Memory</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ContainerMemory }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L68" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="68" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC68" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">NetworkMode</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">awsvpc</span></td></tr><tr><td id="file-fargate-service-template-yml-L69" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="69" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC69" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">RequiresCompatibilities</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L70" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="70" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC70" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">        - <span class="pl-s" style="color: rgb(3, 47, 98);">FARGATE</span></td></tr><tr><td id="file-fargate-service-template-yml-L71" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="71" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC71" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">ExecutionRoleArn</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{"Fn::ImportValue" : {"Fn::Sub" : "${ECSStackName}-ECSTaskExecutionRole"}}</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L72" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="72" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC72" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">TaskRoleArn</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: TaskRole }</span></td></tr><tr><td id="file-fargate-service-template-yml-L73" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="73" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC73" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">ContainerDefinitions</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L74" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="74" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC74" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">        - <span class="pl-ent" style="color: rgb(34, 134, 58);">Name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ServiceName }</span></td></tr><tr><td id="file-fargate-service-template-yml-L75" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="75" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC75" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">          <span class="pl-ent" style="color: rgb(34, 134, 58);">Environment</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L76" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="76" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC76" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">            - <span class="pl-ent" style="color: rgb(34, 134, 58);">Name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">"</span>APP_ENV<span class="pl-pds" style="color: rgb(3, 47, 98);">"</span></span></td></tr><tr><td id="file-fargate-service-template-yml-L77" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="77" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC77" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">              <span class="pl-ent" style="color: rgb(34, 134, 58);">Value</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ENV }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L78" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="78" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC78" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">            - <span class="pl-ent" style="color: rgb(34, 134, 58);">Name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">"</span>AWS_DEFAULT_REGION<span class="pl-pds" style="color: rgb(3, 47, 98);">"</span></span></td></tr><tr><td id="file-fargate-service-template-yml-L79" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="79" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC79" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">              <span class="pl-ent" style="color: rgb(34, 134, 58);">Value</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: "AWS::Region" }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L80" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="80" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC80" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">            - <span class="pl-ent" style="color: rgb(34, 134, 58);">Name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">"</span>PORT<span class="pl-pds" style="color: rgb(3, 47, 98);">"</span></span></td></tr><tr><td id="file-fargate-service-template-yml-L81" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="81" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC81" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">              <span class="pl-ent" style="color: rgb(34, 134, 58);">Value</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ContainerPort }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L82" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="82" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC82" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">          <span class="pl-ent" style="color: rgb(34, 134, 58);">Image</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepo}:${ENV}" }</span></td></tr><tr><td id="file-fargate-service-template-yml-L83" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="83" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC83" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">          <span class="pl-ent" style="color: rgb(34, 134, 58);">PortMappings</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L84" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="84" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC84" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">            - <span class="pl-ent" style="color: rgb(34, 134, 58);">ContainerPort</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ContainerPort }</span></td></tr><tr><td id="file-fargate-service-template-yml-L85" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="85" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC85" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">          <span class="pl-ent" style="color: rgb(34, 134, 58);">LogConfiguration</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L86" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="86" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC86" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">            <span class="pl-ent" style="color: rgb(34, 134, 58);">LogDriver</span>: <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">"</span>awslogs<span class="pl-pds" style="color: rgb(3, 47, 98);">"</span></span></td></tr><tr><td id="file-fargate-service-template-yml-L87" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="87" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC87" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">            <span class="pl-ent" style="color: rgb(34, 134, 58);">Options</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L88" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="88" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC88" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">              <span class="pl-ent" style="color: rgb(34, 134, 58);">awslogs-group</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: LogGroup }</span></td></tr><tr><td id="file-fargate-service-template-yml-L89" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="89" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC89" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">              <span class="pl-ent" style="color: rgb(34, 134, 58);">awslogs-region</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: "AWS::Region" }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L90" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="90" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC90" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">              <span class="pl-ent" style="color: rgb(34, 134, 58);">awslogs-stream-prefix</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ENV }</span></td></tr><tr><td id="file-fargate-service-template-yml-L91" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="91" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC91" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">
</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L92" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="92" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC92" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-c" style="color: rgb(106, 115, 125);"><span class="pl-c" style="color: rgb(106, 115, 125);">#</span> The ECS service.</span></td></tr><tr><td id="file-fargate-service-template-yml-L93" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="93" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC93" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">Service</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L94" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="94" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC94" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">AWS::ECS::Service</span></td></tr><tr><td id="file-fargate-service-template-yml-L95" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="95" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC95" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">DependsOn</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">LoadBalancerRule</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L96" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="96" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC96" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">Properties</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L97" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="97" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC97" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">ServiceName</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: ServiceName }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L98" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="98" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC98" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">Cluster</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{"Fn::ImportValue" : {"Fn::Sub" : "${ECSStackName}-ClusterName"}}</span></td></tr><tr><td id="file-fargate-service-template-yml-L99" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="99" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC99" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">LaunchType</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">FARGATE</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L100" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="100" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC100" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">DeploymentConfiguration</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L101" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="101" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC101" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">        <span class="pl-ent" style="color: rgb(34, 134, 58);">MaximumPercent</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">200</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L102" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="102" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC102" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">        <span class="pl-ent" style="color: rgb(34, 134, 58);">MinimumHealthyPercent</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">100</span></td></tr><tr><td id="file-fargate-service-template-yml-L103" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="103" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC103" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">DesiredCount</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: DesiredCount }</span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L104" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="104" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC104" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">NetworkConfiguration</span>:</td></tr><tr><td id="file-fargate-service-template-yml-L105" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="105" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC105" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">        <span class="pl-ent" style="color: rgb(34, 134, 58);">AwsvpcConfiguration</span>:</td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L106" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="106" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC106" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">          <span class="pl-ent" style="color: rgb(34, 134, 58);">SecurityGroups</span>: </td></tr><tr><td id="file-fargate-service-template-yml-L107" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="107" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC107" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">            - <span class="pl-s" style="color: rgb(3, 47, 98);">{"Fn::ImportValue" : {"Fn::Sub" : "${ECSStackName}-FargateContainerSecurityGroup"}} </span></td></tr><tr style="background-color: transparent;"><td id="file-fargate-service-template-yml-L108" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="108" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-fargate-service-template-yml-LC108" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">          <span class="pl-ent" style="color: rgb(34, 134, 58);">Subnets</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ "Fn::Split": [ "," , {"Fn::ImportValue" : {"Fn::Sub" : "${VPCStackName}-PrivateSubnetList"}}]}</span></td></tr><tr><td id="file-fargate-service-template-yml-L109" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="109" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 21.9766px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-fargate-service-template-yml-LC109" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">TaskDefinition</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">{ Ref: TaskDefinition }</span></td></tr></tbody></table><!--kg-card-end: html--><p><a href="https://gist.github.com/kevinkarwaski/afdf3cab9ff5c52926161d4189d96017/raw/df044798d59f9fe6b3eaff816bb350395def83ed/fargate-service-template.yml?ref=dept-engineering-blog.ghost.io">view raw</a><a href="https://gist.github.com/kevinkarwaski/afdf3cab9ff5c52926161d4189d96017?ref=dept-engineering-blog.ghost.io#file-fargate-service-template-yml">fargate-service-template.yml </a>hosted with ❤ by <a href="https://github.com/?ref=dept-engineering-blog.ghost.io">GitHub</a></p><p>The template above creates a simple ECS (EC2 Container Service) resource with one ECS Fargate container task.</p><p>The template itself isn't revolutionary but there are few references within the template that make structuring reusable templates easy.</p><h3 id="pseudo-parameters-and-intrinsic-functions"><a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html?ref=dept-engineering-blog.ghost.io">Pseudo Parameters</a> and <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html?ref=dept-engineering-blog.ghost.io">Intrinsic Functions</a></h3><p>Pseudo parameters are basically aliases for common AWS specific configuration data. Utilizing pseudo parameters is critical for composing AWS account and regionally agnostic templates.</p><p>Intrinsic functions are helpers that resolve different bits of data within a template. This is useful for a number of things including importing values from other CloudFormation stacks or applying simple string substitutions.</p><p>Both pseudo parameters and intrinsic functions can be used together to dynamically resolve more complex configs. Here are some examples.</p><p>Render the AWS region using the <code>Ref</code> intrinsic function and the pseudo parameter <code>AWS::Region</code>:</p><pre><code>- Name: "AWS_DEFAULT_REGION"
  Value: { Ref: "AWS::Region" }
</code></pre><p>Simple string substitution using the <code>Fn::Sub</code> intrinsic function and the pseudo parameter <code>AWS::AccountId</code> and <code>AWS::Region</code>:</p><pre><code>Image: { "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepo}:${ENV}" }
</code></pre><p>Import a value from the ECS Cluster CloudFormation Stack using the <code>Fn::ImportValue</code> intrinsic function:</p><pre><code>Cluster: {"Fn::ImportValue" : {"Fn::Sub" : "${ECSStackName}-ClusterName"}}
</code></pre><p>Import and split a list of subnets from the VPC CloudFormation Stack using the <code>Fn::ImportValue</code> and <code>Fn::Split</code> intrinsic functions:</p><pre><code>Subnets: { "Fn::Split": [ "," , {"Fn::ImportValue" : {"Fn::Sub" : "${VPCStackName}-PrivateSubnetList"}}]}
</code></pre><h3 id="resolvedvalue-parameters"><a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_Parameter.html?ref=dept-engineering-blog.ghost.io">ResolvedValue Parameters</a></h3><p>A more recent feature we've leveraged in CloudFormation templates is ResolvedValue parameters. This special parameter type allows for referencing parameter values in the the <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html?ref=dept-engineering-blog.ghost.io">AWS Systems Manager (SSM) Parameter Store</a>.</p><p>From the example template above:</p><pre><code>ENV:
  Description: Name of the environment
  Type: AWS::SSM::Parameter::Value&lt;String&gt;
  Default: /cloudformation/parameters/env
</code></pre><p>This renders the value of the SSM Parameter located at the param store path <code>/cloudformation/parameters/env</code>.</p><p>By leveraging SSM parameter values in CloudFormation templates, you can easily separate environments across multiple AWS accounts and re-use the same CloudFormation template to provision infrastructure in each account without having to alter or override any template parameters!</p><h2 id="final-thoughts">Final thoughts</h2><p>By putting each of these features to use, composing reusable account and regional agnostic CloudFormation templates becomes trivial.</p><p>This is extremely useful and efficient if you'd like to create isolated and identical environments (think development, QA, staging, production, etc) in separate AWS accounts.</p><p>If building cloud infrastructure and supporting tools interests you, please reach out! We're always looking for talented and impassioned software engineers to work with at Rocket.</p>]]></content:encoded></item><item><title><![CDATA[GitOps with Terraform, Terragrunt and GitHub Workflows]]></title><description><![CDATA[In this post, we'll define GitOps, review a sample Terraform app module repo and an associated GitHub Workflow to lint and version the module repo, and review a sample Terragrunt live repo and a GitHub Workflow to apply infrastructure changes.]]></description><link>https://engineering.deptagency.com/gitops-with-terraform-terragrunt-and-github-workflows/</link><guid isPermaLink="false">6241e9b11a8623003da8d2ab</guid><category><![CDATA[People]]></category><category><![CDATA[Platforms]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Jirawat Uttayaya]]></dc:creator><pubDate>Mon, 28 Mar 2022 17:01:27 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/code.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/code.jpg" alt="GitOps with Terraform, Terragrunt and GitHub Workflows"/><p>At DEPT, we love pushing the envelope of new technologies and tooling to deliver best-in-class products and solutions that delight our clients.</p><p>We use Terraform and Terragrunt to build and provision as much as we possibly can, and recently we've begun leveraging GitHub Workflows for CI/CD. In this post, we'll take a look at how we combine these tools to create a GitOps centric workflow for managing cloud infrastructure.</p><p>Specifically we'll:</p><ul><li>Define GitOps</li><li>Review a sample Terraform app module repo and an associated GitHub Workflow to lint and version the module repo</li><li>Review a sample Terragrunt live repo and a GitHub Workflow to apply infrastructure changes</li></ul><h2 id="gitops">GitOps</h2><p>So what's GitOps?</p><p>The fundamentals are pretty straight forward:</p><ul><li>Git as the single source of truth of a system</li><li>Git as the single place where we create, change and destroy all environments</li><li>All changes are observable / verifiable</li></ul><p>As we'll see below, combining Terraform, Terragrunt and GitHub Workflows is as GitOps as it gets.</p><h2 id="terraform">Terraform</h2><p>Terraform is a declarative, cloud agnostic tool for provisioning immutable infrastructure.</p><p>Terraform <strong><strong>modules</strong></strong> are a fundamental component. Any set of Terraform configuration files in a folder is a module. That's it.</p><p>Within a module, you leverage <strong><strong>providers</strong></strong> (AWS is a provider) to create <strong><strong>resources</strong></strong> (EC2 is a resource). Dynamic configuration data is defined in <strong><strong>variables </strong></strong>(EC2 instance class is a variable)<strong><strong> </strong></strong>and <strong><strong>provisioners</strong></strong> can be used to execute specific actions (like installing and configuring software) on hosts to prepare them for service. Each of these are present in the sample repo.</p><p>Here's our sample module repo:</p><p><a href="https://github.com/rocketinsights/terraform-blog-sample-module?ref=dept-engineering-blog.ghost.io">https://github.com/rocketinsights/terraform-blog-sample-module</a></p><p>This module leverages the AWS provider to create the following resources:</p><ul><li>VPC</li><li>Load Balancer</li><li>Security Groups</li><li>EC2 instance</li><li>Installs/starts nginx on the EC2 instance (via a provisioner)</li></ul><h2 id="linting-and-versioning-with-a-github-workflow">Linting and Versioning with a GitHub Workflow</h2><p>In addition to the Terraform module code, we also have two GitHub Workflows defined; one for pull requests and one for merges to master.</p><p>The pull request workflow lints and validates formatting of the Terraform code:</p><!--kg-card-begin: html--><table class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="github-workflow-lint-terraform.yml" style="border-collapse: collapse; border-spacing: 0px; box-sizing: border-box; margin: 0px; width: 1358.45px; max-width: 100%; background: var(--color-canvas-default); font-size: 12px; text-rendering: auto; padding: 0px; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-weight: 400; line-height: 1.4; color: rgb(51, 51, 51); border: 0px;"><tbody><tr><td id="file-github-workflow-lint-terraform-yml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC1" class="blob-code blob-code-inner js-file-line" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Lint and Validate Terraform Code</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC2" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC3" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">on</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC4" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">pull_request</span>:</td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC5" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">branches</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC6" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      - <span class="pl-s" style="color: rgb(3, 47, 98);">master</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC7" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">jobs</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC8" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">build</span>:</td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC9" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">runs-on</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">ubuntu-latest</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC10" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">env</span>:</td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC11" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">AWS_DEFAULT_REGION</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">us-east-1</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC12" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">steps</span>:</td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC13" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">uses</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">actions/checkout@v1</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC14" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Install Terraform and Terragrunt</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC15" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC16" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        brew tap rocketinsights/tgenv</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC17" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        brew install tfenv tgenv</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L18" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="18" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC18" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        tfenv install</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L19" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="19" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC19" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        tgenv install</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L20" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="20" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC20" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L21" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="21" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC21" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Get Versions</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L22" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="22" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC22" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L23" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="23" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC23" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        terragrunt --version</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L24" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="24" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC24" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        terraform --version</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L25" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="25" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC25" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L26" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="26" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC26" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Terraform Init</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L27" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="27" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC27" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">find . -type f -name "*.tf" -exec dirname {} \;|sort -u | while read m; do (cd "$m" &amp;&amp; terraform init -input=false -backend=false) || exit 1; done</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L28" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="28" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC28" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L29" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="29" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC29" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Validate Terraform configs</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L30" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="30" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC30" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">find . -name ".terraform" -prune -o -type f -name "*.tf" -exec dirname {} \;|sort -u | while read m; do (cd "$m" &amp;&amp; terraform validate &amp;&amp; echo "√ $m") || exit 1 ; done</span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L31" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="31" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC31" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">
</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-lint-terraform-yml-L32" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="32" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-lint-terraform-yml-LC32" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Check Terraform config formatting </span></td></tr><tr><td id="file-github-workflow-lint-terraform-yml-L33" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="33" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-lint-terraform-yml-LC33" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">terraform fmt -write=false -recursive</span></td></tr></tbody></table><!--kg-card-end: html--><p><a href="https://gist.github.com/kevinkarwaski/a57fd43f36c0e0eb4deb06d05b887545/raw/84e38d0d8059c8dd99763d05b76da4f03ee55569/github-workflow-lint-terraform.yml?ref=dept-engineering-blog.ghost.io">view raw</a><a href="https://gist.github.com/kevinkarwaski/a57fd43f36c0e0eb4deb06d05b887545?ref=dept-engineering-blog.ghost.io#file-github-workflow-lint-terraform-yml">github-workflow-lint-terraform.yml </a>hosted with ❤ by <a href="https://github.com/?ref=dept-engineering-blog.ghost.io">GitHub</a></p><p>The master workflow uses a special action to create and apply an auto-incremented SemVer version tag:</p><!--kg-card-begin: html--><table class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="github-workflow-apply-tag.yml" style="border-collapse: collapse; border-spacing: 0px; box-sizing: border-box; margin: 0px; width: 708px; max-width: 100%; background: var(--color-canvas-default); font-size: 12px; text-rendering: auto; padding: 0px; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-weight: 400; line-height: 1.4; color: rgb(51, 51, 51); border: 0px;"><tbody><tr><td id="file-github-workflow-apply-tag-yml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC1" class="blob-code blob-code-inner js-file-line" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Apply/Increment Tag</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC2" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">on</span>:</td></tr><tr><td id="file-github-workflow-apply-tag-yml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC3" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">push</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC4" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">branches</span>:</td></tr><tr><td id="file-github-workflow-apply-tag-yml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC5" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      - <span class="pl-s" style="color: rgb(3, 47, 98);">master</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC6" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">jobs</span>:</td></tr><tr><td id="file-github-workflow-apply-tag-yml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC7" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">build</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC8" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">runs-on</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">ubuntu-latest</span></td></tr><tr><td id="file-github-workflow-apply-tag-yml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC9" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">steps</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC10" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">uses</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">actions/checkout@master</span></td></tr><tr><td id="file-github-workflow-apply-tag-yml-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC11" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">with</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC12" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">        <span class="pl-ent" style="color: rgb(34, 134, 58);">fetch-depth</span>: <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">'</span>0<span class="pl-pds" style="color: rgb(3, 47, 98);">'</span></span></td></tr><tr><td id="file-github-workflow-apply-tag-yml-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC13" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Bump version and push tag</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC14" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">uses</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">anothrNick/github-tag-action@1.19.0</span></td></tr><tr><td id="file-github-workflow-apply-tag-yml-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC15" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">env</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-apply-tag-yml-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-apply-tag-yml-LC16" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">        <span class="pl-ent" style="color: rgb(34, 134, 58);">GITHUB_TOKEN</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">${{ secrets.GITHUB_TOKEN }}</span></td></tr><tr><td id="file-github-workflow-apply-tag-yml-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-apply-tag-yml-LC17" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">        <span class="pl-ent" style="color: rgb(34, 134, 58);">WITH_V</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">true</span></td></tr></tbody></table><!--kg-card-end: html--><p><a href="https://gist.github.com/kevinkarwaski/a1c6caa4ed868d84aea23e2038f126be/raw/b0799f596d7d71a38bc838274537f9d5464d13bd/github-workflow-apply-tag.yml?ref=dept-engineering-blog.ghost.io">view raw</a><a href="https://gist.github.com/kevinkarwaski/a1c6caa4ed868d84aea23e2038f126be?ref=dept-engineering-blog.ghost.io#file-github-workflow-apply-tag-yml">github-workflow-apply-tag.yml </a>hosted with ❤ by <a href="https://github.com/?ref=dept-engineering-blog.ghost.io">GitHub</a></p><p>Why do we care about versioning? Enter Terragrunt...</p><h2 id="terragrunt">Terragrunt</h2><p>Terragrunt is thin wrapper around Terraform.</p><p>It makes writing DRY Terraform modules easy and facilitates targeted control over when and where module updates are deployed.</p><p>As discussed above, our sample Terraform module is in its own repo and every time we merge to master, we automatically get a SemVer tag thanks to our fancy GitHub Workflow.</p><p>Now let's look at our Terragrunt live sample repo:</p><p><a href="https://github.com/rocketinsights/terraform-blog-sample-live?ref=dept-engineering-blog.ghost.io">https://github.com/rocketinsights/terraform-blog-sample-live</a></p><p>This repo contains our Terragrunt configuration files for each environment; dev, staging and prod. Each environment specific Terragrunt file references our module at a <strong><strong>specific version</strong></strong> and sets any module variables required as inputs.</p><p>Because we are pinning each environment to a specific version of the module, we can make changes without affecting any running environment.</p><p>When we're ready to deploy a module change to a given environment, we simply increment the version tag in the source reference defined in the environment specific Terragrunt file. (This is usually the "ah-ha!" moment.)</p><p>Ok, great, but how do we continuously deploy this across different environments in AWS?</p><h2 id="applying-infrastructure-changes-with-a-github-workflow">Applying infrastructure changes with a Github Workflow</h2><p>Just as we saw in our module repo, our Terragrunt live repo also has two GitHub Workflows defined - one for pull requests and another for merges to master.</p><p>The pull request workflow outputs the plan (think dry run) of what Terragrunt is going to do (what AWS resources Terraform <em>intends</em> to create/update/delete):</p><!--kg-card-begin: html--><table class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="github-workflow-terragrunt-plan-all.yml" style="border-collapse: collapse; border-spacing: 0px; box-sizing: border-box; margin: 0px; width: 708px; max-width: 100%; background: var(--color-canvas-default); font-size: 12px; text-rendering: auto; padding: 0px; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-weight: 400; line-height: 1.4; color: rgb(51, 51, 51); border: 0px;"><tbody><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC1" class="blob-code blob-code-inner js-file-line" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Terragrunt Plan All (dry run)</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC2" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">on</span>:</td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC3" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">pull_request</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC4" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">branches</span>:</td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC5" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      - <span class="pl-s" style="color: rgb(3, 47, 98);">master</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC6" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">jobs</span>:</td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC7" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">build</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC8" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">runs-on</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">ubuntu-latest</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC9" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">env</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC10" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">AWS_ACCESS_KEY_ID</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">${{ secrets.AWS_ACCESS_KEY_ID }}</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC11" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">AWS_SECRET_ACCESS_KEY</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">${{ secrets.AWS_SECRET_ACCESS_KEY }}</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC12" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">AWS_DEFAULT_REGION</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">us-east-1</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC13" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">steps</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC14" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">uses</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">actions/checkout@v1</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC15" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Install Terraform and Terragrunt</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC16" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC17" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        brew tap rocketinsights/tgenv</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L18" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="18" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC18" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        brew install tfenv tgenv</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L19" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="19" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC19" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        tfenv install</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L20" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="20" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC20" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        tgenv install</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L21" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="21" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC21" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L22" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="22" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC22" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Get Versions</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L23" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="23" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC23" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L24" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="24" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC24" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        terragrunt --version</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L25" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="25" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC25" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        terraform --version</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L26" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="26" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC26" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L27" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="27" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC27" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Setup infra modules deploy key</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L28" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="28" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC28" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L29" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="29" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC29" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        mkdir ~/.ssh</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L30" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="30" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC30" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        echo "${{ secrets.INFRA_MODULES_DEPLOY_KEY }}" &gt; ~/.ssh/id_rsa</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L31" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="31" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC31" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        chmod 600 ~/.ssh/id_rsa</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L32" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="32" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC32" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        ssh-keyscan -t rsa github.com</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L33" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="33" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC33" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-plan-all-yml-L34" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="34" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC34" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Terragrunt plan-all</span></td></tr><tr><td id="file-github-workflow-terragrunt-plan-all-yml-L35" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="35" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-plan-all-yml-LC35" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">terragrunt plan-all</span></td></tr></tbody></table><!--kg-card-end: html--><p><a href="https://gist.github.com/kevinkarwaski/6312f20ed8505b0de2671a6b118409b7/raw/09f4857939e0c83f61b1adfe9cdad0eeb9d105a0/github-workflow-terragrunt-plan-all.yml?ref=dept-engineering-blog.ghost.io">view raw</a><a href="https://gist.github.com/kevinkarwaski/6312f20ed8505b0de2671a6b118409b7?ref=dept-engineering-blog.ghost.io#file-github-workflow-terragrunt-plan-all-yml">github-workflow-terragrunt-plan-all.yml </a>hosted with ❤ by <a href="https://github.com/?ref=dept-engineering-blog.ghost.io">GitHub</a></p><p>The master workflow takes that plan and applies it (actually creates/updates/deletes the resources in AWS based on the plan):</p><!--kg-card-begin: html--><table class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="github-workflow-terragrunt-apply-all.yml" style="border-collapse: collapse; border-spacing: 0px; box-sizing: border-box; margin: 0px; width: 708px; max-width: 100%; background: var(--color-canvas-default); font-size: 12px; text-rendering: auto; padding: 0px; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-weight: 400; line-height: 1.4; color: rgb(51, 51, 51); border: 0px;"><tbody><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC1" class="blob-code blob-code-inner js-file-line" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Terragrunt Apply All (deploy)</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC2" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">on</span>:</td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC3" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">push</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC4" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">branches</span>:</td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC5" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      - <span class="pl-s" style="color: rgb(3, 47, 98);">main</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC6" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-ent" style="color: rgb(34, 134, 58);">jobs</span>:</td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC7" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">build</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC8" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    <span class="pl-ent" style="color: rgb(34, 134, 58);">runs-on</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">ubuntu-latest</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC9" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">env</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC10" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">AWS_ACCESS_KEY_ID</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">${{ secrets.AWS_ACCESS_KEY_ID }}</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC11" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">AWS_SECRET_ACCESS_KEY</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">${{ secrets.AWS_SECRET_ACCESS_KEY }}</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC12" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">AWS_DEFAULT_REGION</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">us-east-1</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC13" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">steps</span>:</td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC14" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">uses</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">actions/checkout@v1</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC15" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Install Terraform and Terragrunt</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC16" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC17" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        brew tap rocketinsights/tgenv</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L18" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="18" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC18" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        brew install tfenv tgenv</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L19" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="19" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC19" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        tfenv install</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L20" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="20" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC20" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        tgenv install</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L21" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="21" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC21" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L22" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="22" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC22" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Get Versions</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L23" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="23" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC23" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L24" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="24" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC24" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        terragrunt --version</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L25" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="25" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC25" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        terraform --version</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L26" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="26" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC26" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L27" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="27" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC27" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Setup infra modules deploy key</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L28" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="28" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC28" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">|</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L29" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="29" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC29" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        mkdir ~/.ssh</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L30" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="30" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC30" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        echo "${{ secrets.INFRA_MODULES_DEPLOY_KEY }}" &gt; ~/.ssh/id_rsa</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L31" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="31" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC31" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);">        chmod 600 ~/.ssh/id_rsa</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L32" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="32" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC32" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);">        ssh-keyscan -t rsa github.com</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L33" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="33" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC33" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-s" style="color: rgb(3, 47, 98);"/></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L34" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="34" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC34" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);"><span class="pl-s" style="color: rgb(3, 47, 98);"/>    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Terragrunt plan-all</span></td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L35" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="35" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC35" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">terragrunt plan-all</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L36" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="36" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC36" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-github-workflow-terragrunt-apply-all-yml-L37" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="37" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC37" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-ent" style="color: rgb(34, 134, 58);">name</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">Terragrunt apply-all</span></td></tr><tr style="background-color: transparent;"><td id="file-github-workflow-terragrunt-apply-all-yml-L38" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="38" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-github-workflow-terragrunt-apply-all-yml-LC38" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">run</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">terragrunt apply-all --terragrunt-non-interactive</span></td></tr></tbody></table><!--kg-card-end: html--><p><a href="https://gist.github.com/kevinkarwaski/65a3ae0c0ecc25b50d9e586a0aeb6853/raw/c1367b3d696a00a28b43aebf68c7fa8bd5645263/github-workflow-terragrunt-apply-all.yml?ref=dept-engineering-blog.ghost.io">view raw</a><a href="https://gist.github.com/kevinkarwaski/65a3ae0c0ecc25b50d9e586a0aeb6853?ref=dept-engineering-blog.ghost.io#file-github-workflow-terragrunt-apply-all-yml">github-workflow-terragrunt-apply-all.yml </a>hosted with ❤ by <a href="https://github.com/?ref=dept-engineering-blog.ghost.io">GitHub</a></p><p>This downloads the module from the source reference in the terraform block of the Terragrunt configuration file, sets the inputs (which correlate to the module variables) and creates the resources in AWS.</p><h3 id="secrets">Secrets</h3><p>Something to take note of regarding these workflows is their use of <strong><strong>secrets</strong></strong>.</p><p>From the <a href="https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets?ref=dept-engineering-blog.ghost.io">GitHub</a>:</p><blockquote><a href="https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes?ref=dept-engineering-blog.ghost.io">libsodium sealed box</a></blockquote><p>Once you create a secret, it can only be decrypted in the workflow (you can't decrypt it in the UI or via the GitHub API) and GitHub automatically redacts secrets printed to the log.</p><p>Let's look at each secret we leverage:</p><p><strong><strong>INFRA_MODULES_DEPLOY_KEY</strong></strong>:<br>Terragrunt requires the source reference for a private GitHub repo to use the <code>ssh://</code> format.</br></p><p>To accommodate this, we create a GitHub deploy key under the module repo and add the private key as a secret in the Terragrunt live repo. We then render the private key so it can be used when Terragrunt is called. (Our example repos are public so this is would ONLY be required if you were using private GitHub repos.)</p><p><strong><strong>AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY</strong></strong>:<br>These are the keys for the IAM user used by Terragrunt/Terraform to provision resources in AWS.</br></p><h2 id="final-thoughts">Final thoughts</h2><p>Terraform, Terragrunt and GitHub Workflows are incredibly powerful tools that work extremely well together to facilitate a GitOps continuous delivery model.</p><p>At DEPT, we've used patterns like this to great effect, resulting in resilient and reliable workflows for efficiently replicating cloud infrastructure and application delivery pipelines on client projects.</p><p>We are always looking to work with both existing and new clients to apply these patterns to help evolve and adopt modern, robust and reliable patterns to their application delivery and cloud infrastructure management processes.</p><p>If you are interested in learning more about how we can help your organization or you'd like to work with us to apply these patterns to client projects, please let us know!</p>]]></content:encoded></item><item><title><![CDATA[CI/CD with CodeBuild and CodePipeline]]></title><description><![CDATA[Recently, we've used a few of AWS's services to create full integration and delivery solutions. In this post we'll look at how we've used AWS CodeBuild and CodePipeline to create a cost effective, performant and code driven end-to-end CI/CD solution.]]></description><link>https://engineering.deptagency.com/ci-cd-with-codebuild-and-codepipeline/</link><guid isPermaLink="false">6241e6d41a8623003da8d28b</guid><category><![CDATA[Platforms]]></category><category><![CDATA[Process]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Mon, 28 Mar 2022 16:49:48 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/code-on-computer2--1-.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/code-on-computer2--1-.jpg" alt="CI/CD with CodeBuild and CodePipeline"/><p>At DEPT we use a variety of hosted continuous integration / continuous delivery (CI/CD) platforms to help our clients deliver great products and experiences to their customers.</p><p>Recently, we've used a few of AWS's services to create full integration and delivery solutions. In this post, we'll look at how we've used AWS CodeBuild and CodePipeline to create a cost-effective, performant and code-driven end-to-end CI/CD solution.</p><h3 id="what-is-codebuild-and-codepipeline">What is CodeBuild and CodePipeline?</h3><p>AWS has a number of services they've wrapped up under the label of "Developer Tools"; these include CodeCommit, CodeBuild, CodeDeploy and CodePipeline.</p><p>For this post, we're going to focus on CodeBuild and CodePipeline. So what are they?</p><p>As defined by AWS:</p><blockquote>AWS CodeBuild is a fully managed continuous integration service that compiles source code, runs tests, and produces software packages that are ready to deploy. With CodeBuild, you don’t need to provision, manage, and scale your own build servers. CodeBuild scales continuously and processes multiple builds concurrently, so your builds are not left waiting in a queue.</blockquote><blockquote>AWS CodePipeline is a continuous integration and continuous delivery service for fast and reliable application and infrastructure updates. CodePipeline builds, tests, and deploys your code every time there is a code change, based on the release process models you define.</blockquote><p>So CodeBuild jobs are the individual units of work that provide an open and flexible execution environment and CodePipeline is the orchestration layer to connect and execute CodeBuild jobs in a specific order.</p><p>Before we dig into a full-blown example of these tools working together, let's talk a little bit about what makes a CodeBuild tick: buildspecs, images and runtimes, and artifact management and caching.</p><h3 id="configuration-with-buildspecs">Configuration with buildspecs</h3><p>Like other CI/CD-as-a-Service providers, AWS CodeBuild jobs are configured using a buildspec yaml file that lives with the application code.</p><p>Here's an example buildspec that executes tests for a react app:</p>
<!--kg-card-begin: html-->
<table class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="codebuild-buildspec-react-example.yml" style="border-collapse: collapse; border-spacing: 0px; box-sizing: border-box; margin: 0px; width: 708px; max-width: 100%; background: var(--color-canvas-default); font-size: 12px; text-rendering: auto; padding: 0px; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-weight: 400; line-height: 1.4; color: rgb(51, 51, 51); border: 0px;"><tbody><tr><td id="file-codebuild-buildspec-react-example-yml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC1" class="blob-code blob-code-inner js-file-line" style="padding-top: 4px; padding-right: 10px !important; padding-bottom: 1px !important; padding-left: 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">version</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">0.2</span></td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC2" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC3" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">phases</span>:</td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC4" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">install</span>:</td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC5" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">runtime-versions</span>:</td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC6" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      <span class="pl-ent" style="color: rgb(34, 134, 58);">nodejs</span>: <span class="pl-c1" style="color: rgb(0, 92, 197);">10</span></td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC7" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">commands</span>:</td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC8" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      - <span class="pl-s" style="color: rgb(3, 47, 98);">echo Installing app dependencies...</span></td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC9" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      - <span class="pl-s" style="color: rgb(3, 47, 98);">yarn install</span></td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC10" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">build</span>:</td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC11" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    <span class="pl-ent" style="color: rgb(34, 134, 58);">commands</span>:</td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC12" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      - <span class="pl-s" style="color: rgb(3, 47, 98);">echo Building static site...</span></td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC13" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      - <span class="pl-s" style="color: rgb(3, 47, 98);">yarn build</span></td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC14" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">      - <span class="pl-s" style="color: rgb(3, 47, 98);">echo Run tests...</span></td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC15" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">      - <span class="pl-s" style="color: rgb(3, 47, 98);">CI=true yarn test</span></td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC16" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC17" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">artifacts</span>:</td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L18" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="18" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC18" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">type</span>: <span class="pl-s" style="color: rgb(3, 47, 98);">zip</span></td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L19" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="19" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC19" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">  <span class="pl-ent" style="color: rgb(34, 134, 58);">paths</span>:</td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L20" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="20" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC20" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">    - <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">'</span>./build/**/*<span class="pl-pds" style="color: rgb(3, 47, 98);">'</span></span></td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L21" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="21" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC21" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">'</span>./cicd/**/*<span class="pl-pds" style="color: rgb(3, 47, 98);">'</span></span></td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L22" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="22" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC22" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">
</td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L23" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="23" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC23" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;"><span class="pl-ent" style="color: rgb(34, 134, 58);">cache</span>:</td></tr><tr style="background-color: transparent;"><td id="file-codebuild-buildspec-react-example-yml-L24" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="24" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none; background: rgba(0, 0, 0, 0);"/><td id="file-codebuild-buildspec-react-example-yml-LC24" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre; background: rgba(0, 0, 0, 0);">  <span class="pl-ent" style="color: rgb(34, 134, 58);">paths</span>:</td></tr><tr><td id="file-codebuild-buildspec-react-example-yml-L25" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="25" style="padding: 1px 10px !important; line-height: 20px; text-align: right; vertical-align: top; border-top: 1px solid rgb(239, 239, 239); background: rgba(0, 0, 0, 0); position: relative; width: 14.6562px; min-width: inherit; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-subtle); white-space: nowrap; cursor: pointer; user-select: none;"/><td id="file-codebuild-buildspec-react-example-yml-LC25" class="blob-code blob-code-inner js-file-line" style="padding: 1px 10px !important; line-height: 20px; text-align: left; vertical-align: top; border: 0px; background: rgba(0, 0, 0, 0); position: relative; overflow: visible; font-family: ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;, monospace; font-size: 12px; color: var(--color-fg-default); overflow-wrap: normal; white-space: pre;">    - <span class="pl-s" style="color: rgb(3, 47, 98);"><span class="pl-pds" style="color: rgb(3, 47, 98);">'</span>./node_modules/**/*<span class="pl-pds" style="color: rgb(3, 47, 98);">'</span></span></td></tr></tbody></table>
<!--kg-card-end: html-->
<p><a href="https://gist.github.com/kevinkarwaski/6b5a6d6685523434ed6372ba01806a60/raw/2e1fc7937f22cdb8fa6107fbf4f5ffa03f104358/codebuild-buildspec-react-example.yml?ref=dept-engineering-blog.ghost.io">view raw</a><a href="https://gist.github.com/kevinkarwaski/6b5a6d6685523434ed6372ba01806a60?ref=dept-engineering-blog.ghost.io#file-codebuild-buildspec-react-example-yml">codebuild-buildspec-react-example.yml </a>hosted with ❤ by <a href="https://github.com/?ref=dept-engineering-blog.ghost.io">GitHub</a></p><p>Buildspecs support many phases (above, we're using install and build) and other options which you can read more about <a href="https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html?ref=dept-engineering-blog.ghost.io">here</a>.</p><h3 id="execution-with-images-and-runtimes">Execution with Images and Runtimes</h3><p>Under the hood, CodeBuild jobs are containerized execution environments. AWS provides <a href="https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html?ref=dept-engineering-blog.ghost.io">managed Docker images</a> (for Linux and Windows) to execute CodeBuild jobs but you are free to use any Docker image you'd like hosted at Dockerhub or in any private registry of your choosing.</p><p>We've stuck with using AWS's official CodeBuild images as they are actively maintained, known to work with CodeBuild and are supported by AWS.</p><p>Conveniently, the official AWS CodeBuild images also come prepackaged with a variety of runtime environments for android, docker, dotnet, golang, nodejs, java, php, python and ruby.</p><p>You simply specify the runtime(s) you'd like in the buildspec install phase and, well, you're off and running.</p><p>From our react app example buildspec, we specify the <code>nodejs</code> runtime:</p><pre><code>phases:
  install:
    runtime-versions:
      nodejs: 10
</code></pre><h3 id="artifacts-and-caching">Artifacts and Caching</h3><p>CodeBuild jobs allow for caching and build artifact management in S3.</p><p>From our react app example buildspec:</p><pre><code>artifacts:
  type: zip
  paths:
    - './build/**/*'
    - './cicd/**/*'

cache:
  paths:
    - './node_modules/**/*'
</code></pre><p>Here we're saying we'd like the files in both the <code>build</code> and <code>cicd</code> directories compressed into a zip file and stored in S3. We'd also like the <code>node_modules</code> directory cached in S3.</p><p>As we'll see below, when many CodeBuild jobs are strung together in a CodePipeline, an output artifact from one job can be used as input for another job. Compile once, reuse later.</p><h2 id="pull-requests-and-pipelines">Pull Requests and Pipelines</h2><p>While CodeBuild and CodePipeline provide a ton of flexibility for creating robust delivery patterns and solutions, we're going to focus on two of the most common uses of any CI/CD platform; handling pull requests and providing continuous delivery of a specific branch like <code>master</code>.</p><h3 id="pull-requests">Pull Requests</h3><p>We're going to use a single CodeBuild job to test all Pull Requests.</p><p>We'll use CloudFormation to create the CodeBuild job in AWS and use a GitHub webhook trigger and event filter to ensure this job only runs when a pull request is created, updated or re-opened.  There's no limit to the number of CodeBuild jobs that can run in parallel so you're never left waiting for results due to job queueing.</p><p>When the CodeBuild job is triggered, the PR is cloned and whatever instructions are written in the buildspec file are executed using the runtime environment defined.</p><p>See the CloudFormation template for our example pull request CodeBuild job <a href="https://gist.github.com/kevinkarwaski/57f286d663a90aa269f603cdf62cdbaa?ref=dept-engineering-blog.ghost.io">here</a>. (It's too big to embed here.)</p><p>From CloudFormation template above, we're using an EVENT FilterGroup on the Webhook trigger to limit what events should trigger this job:</p><pre><code>  Triggers:
    Webhook: True
    FilterGroups:
      -
        - Type: EVENT
          Pattern: PULL_REQUEST_CREATED, PULL_REQUEST_UPDATED, PULL_REQUEST_REOPENED
</code></pre><p><em>What about build success/failure notifications and logs?</em></p><p>Great question, from the template above, we're creating a CloudWatch Events Rule to capture the SUCCEEDED, FAILED and STOPPED build status and post the results to an SNS topic (which can be customized to send email or post to Slack, etc.). The notification includes the project name, build status and a link to the output logs from job in CloudWatch Logs:</p><pre><code>  ### Notifications
  CodebuildStateFailureEventRule:
    Type: "AWS::Events::Rule"
    Properties:
      Description: "Rule for sending failure notifications to SNS topic"
      EventPattern:
        source:
          - aws.codebuild
        detail-type:
          - CodeBuild Build State Change
        detail:
          project-name:
            - !Ref CodeBuildProject
          build-status:
            - SUCCEEDED
            - FAILED
            - STOPPED
      State: "ENABLED"
      Targets:
        - Arn: !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:CodebuildNotifications"
          Id: "CodeBuildNotifications"
          InputTransformer:
            InputTemplate: '"Pull Request has &lt;build-status&gt; for &lt;project-name&gt;. Logs are here: &lt;deep-link&gt;"'
            InputPathsMap:
              project-name: "$.detail.project-name"
              build-status: "$.detail.build-status"
              deep-link: "$.detail.additional-information.logs.deep-link"
</code></pre><p>Surprisingly, the AWS Console provides a great view of CodeBuild jobs; you can watch it progress through the phases and tail the log output.</p><h3 id="pipelines">Pipelines</h3><p>Now that we have our pull requests taken care of, we're going to use CodePipeline to string together a few simple CodeBuild jobs and create a continuous integration and delivery pipeline.</p><p>Again, let's use CloudFormation to create the CodePipeline, CodeBuild jobs and all required AWS resources. We'll target the <code>master</code> branch of a GitHub repo. The pipeline will only fire when merges to <code>master</code> occur.</p><p>See the CloudFormation template for our example CodePipeline <a href="https://gist.github.com/kevinkarwaski/00aa01826d88650ae5c0da54ae93f258?ref=dept-engineering-blog.ghost.io">here</a>. (It's too big to embed here.)</p><p>Once provisioned with CloudFormation, the pipeline will appear in the AWS console and look like this:</p><p>CodePipelines are defined by a series of stages. From the example above, we've got three stages: <strong>Source</strong>, <strong>Test</strong> and <strong>Deploy</strong>.</p><p>Within each stage you can run actions (serially or in parallel). Actions have providers and for the most part, we're using the CodeBuild action provider but there are <a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html?ref=dept-engineering-blog.ghost.io#actions-valid-providers">many</a> to choose from to suit your specific needs.</p><p>When merges to the <code>master</code> branch occur, the <strong>Source</strong> stage is triggered. This clones the repo, zips up the codebase, posts it to the artifact S3 bucket and triggers the Test stage.</p><p>In the <strong>Test</strong> stage, our CodeBuild test job is triggered; it pulls and decompresses the source archive from S3, sets up the execution environment and runs the tests. If the tests succeed, the Deploy stage is triggered.</p><p>In the <strong>Deploy</strong> stage, our CodeBuild deploy job is triggered; it pulls and decompresses the same tested source archive from S3 and deploys the app.</p><p>If the deployment succeeds, we trigger the TagRepo action which applies a deploy tag to the Github repo at the commit that was cloned from the Source action.</p><p>For a deeper dive, see the <a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/concepts.html?ref=dept-engineering-blog.ghost.io">CodePipeline concepts</a>.</p><h2 id="costs">Costs</h2><p>In addition to the features/flexibility of CodeBuild and CodePipeline, the cost is exceedingly cheap.</p><p>From AWS, <a href="https://aws.amazon.com/codepipeline/pricing/?ref=dept-engineering-blog.ghost.io">CodePipeline cost explained</a>:</p><blockquote>With AWS CodePipeline, there are no upfront fees or commitments. You pay only for what you use. AWS CodePipeline costs $1.00 per active pipeline* per month. To encourage experimentation, pipelines are free for the first 30 days after creation.<br><br>*An active pipeline is a pipeline that has existed for more than 30 days and has at least one code change that runs through it during the month. There is no charge for pipelines that have no new code changes running through them during the month. An active pipeline is not prorated for partial months.<br><br/></br></br></br></blockquote><p>and <a href="https://aws.amazon.com/codebuild/pricing/?ref=dept-engineering-blog.ghost.io">CodeBuild cost explained</a>:</p><blockquote>AWS CodeBuild uses simple pay-as-you-go pricing. There are no upfront costs or minimum fees. You pay only for the resources you use. You are charged for compute resources based on the duration it takes for your build to execute. The per-minute rate depends on the selected compute type.</blockquote><p>To put that in perspective, if you executed 100 builds in one month using <code>build.general1.small</code> CodeBuild compute type and each build ran for 5 minutes, your monthly charges would be <strong>$2.00</strong>! (Add another <strong>$1</strong> for each CodePipeline.)</p><p>That's insanely cheap!</p><p>Keep in mind, unlike other CI/CD-as-a-service providers, there's no concurrent build limit! You'd be hard pressed to find a more cost effective, flexible and feature rich CICD solution.</p><h2 id="final-thoughts">Final Thoughts</h2><p>While there are many many options for building robust CI/CD solutions we've found AWS CodeBuild and CodePipeline to be rich in features, functionality and exceedingly cost effective.</p><p>If you want complete control of your continuous integration and delivery environment, you run apps and infrastructure in AWS and you'd like to limit the number of third party providers accessing your data, these services check all the boxes and for pennies on the dollar.</p><p>If building modern integration and delivery systems is something you are interested in, we'd love to speak with you. We're always looking for talented and impassioned software engineers to work with at DEPT.</p>]]></content:encoded></item><item><title><![CDATA[Securely Storing Data on iOS]]></title><description><![CDATA[Our mobile devices and the apps that run on them have a unique insight into our
lives. We use them for communication, entertainment, shopping, photography,
navigation, and myriad other purposes. Consequently, apps have access to a great
deal of information about our preferences, our contacts, where we go, what we
buy, and who we are. As app developers, we need to be cognizant of the
information being handled by our apps and to safeguard it accordingly.

The protection of sensitive information is]]></description><link>https://engineering.deptagency.com/securely-storing-data-on-ios/</link><guid isPermaLink="false">6241e52f1a8623003da8d27e</guid><category><![CDATA[Platforms]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Paul Calnan]]></dc:creator><pubDate>Mon, 28 Mar 2022 16:42:11 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/iphone--1-.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/iphone--1-.jpg" alt="Securely Storing Data on iOS"/><p>Our mobile devices and the apps that run on them have a unique insight into our lives. We use them for communication, entertainment, shopping, photography, navigation, and myriad other purposes. Consequently, apps have access to a great deal of information about our preferences, our contacts, where we go, what we buy, and who we are. As app developers, we need to be cognizant of the information being handled by our apps and to safeguard it accordingly.</p><p>The protection of sensitive information is a broad topic. This article will focus specifically on data storage on the user's device. Almost all apps require some data to be stored in a manner that will persist across app launches. However, simply storing data in the clear on the file system or in <code>UserDefaults</code> does not provide adequate security for sensitive user information.</p><p>This article will show you how to use the safeguards built into iOS to protect user data. We will present how to encrypt data stored on the file system, how to use the keychain to store sensitive data like usernames and passwords, and how to add biometric authentication to keychain items for added security.</p><h2 id="file-system">File System</h2><p>iOS provides data protection facilities to secure an app's files and to prevent unauthorized access to them. This is all handled automatically by the operating system — the system encrypts and decrypts files on the fly without requiring any special code in the app.</p><p>Each file has an associated content protection attribute that determines when the file is encrypted and when it can be accessed. There are four content protection modes available:</p><ul><li><strong><strong>Complete</strong></strong> The file is stored in an encrypted format and may be read from or written to only while the device is unlocked. At all other times, attempts to read and write the file will fail. This is the most restrictive file protection available.</li><li><strong><strong>Complete unless open</strong></strong> You can open existing files only when the device is unlocked. If a file is opened while the device is unlocked, you may continue to access that file after the device is locked. New files may be created at any time, regardless of whether the device is locked.</li><li><strong><strong>Complete until first user authentication</strong></strong> <em>This is the default protection mode.</em> The file is inaccessible until the first time the user unlocks the device. After the user first unlocks the device, the file remains accessible until the device is shut down or rebooted.</li><li><strong><strong>No protection</strong></strong> The file is not stored in an encrypted format and may be accessed at any time.</li></ul><p>To encrypt a file at creation time, pass a content protection attribute under the <code>options</code> parameter when calling <a href="https://developer.apple.com/documentation/foundation/data/1779858-write?ref=dept-engineering-blog.ghost.io"><code>Data.write(to:options:)</code> </a>. To change the content protection of an existing file, you can use the <a href="https://developer.apple.com/documentation/foundation/nsurl/1413819-setresourcevalue/?ref=dept-engineering-blog.ghost.io"><code>NSURL.setResourceValue(_:forKey:)</code></a> function, passing a <code>URLFileProtection</code> value for the <code>.fileProtectionKey</code> key.</p><p>You can read from a protected file the same way you would read from any other file. No additional parameters are required to handle the data protection. However, if the file is not currently available due to data protection (for example, if the file uses the complete file protection mode and the device is currently locked) the operation will fail.</p><p>Your app delegate will be notified when files with complete file protection become unavailable and available. You can use the <code>applicationProtectedDataWillBecomeUnavailable(_:)</code> and <code>applicationProtectedDataDidBecomeAvailable(_:)</code> delegate functions to close and reopen those files as necessary.</p><p>When choosing a content protection mode, it is important to keep in mind when a file is available and when your app may need to access it. Apple recommends the following:</p><blockquote>Assign the complete protection level to files that your app accesses only when it is in the foreground. If your app supports background capabilities, such as handling location updates, assign a different protection level for files that you might access while in the background. For example, a fitness app might use the complete unless open protection level on a file that it uses to log location events in the background.<br><br>Files containing personal information about the user, or files created directly by the user, always warrant the strongest level of protection. Assign the complete protection level to user data files and manage access to those files using the app delegate methods. The app delegate methods give you time to close the files before they become inaccessible to your app.</br></br></blockquote><h3 id="excluding-files-from-backup">Excluding files from backup</h3><p>When considering secure storage options for your app's data, you should also take into consideration iOS backups. When a user backs up his or her device, everything in the app's home directory is backed up, with the exception of the app bundle itself, the caches directory, and temp directory. It is possible for the app to programmatically exclude particular files from these backups.</p><p>When creating files containing sensitive data, you should consider whether to include them in the user's backups. If it is possible to recreate the file or re-download the file from your app's backend, consider excluding it from backup.</p><p>You can exclude a file from backup using the <a href="https://developer.apple.com/documentation/foundation/nsurl/1413819-setresourcevalue/?ref=dept-engineering-blog.ghost.io"><code>NSURL.setResourceValue(_:forKey:)</code></a> API as follows:</p><pre><code class="language-swift">func excludeFromBackup(fileAt url: URL) throws {
    try (url as NSURL).setResourceValue(NSNumber(booleanLiteral: true),
                                        forKey: .isExcludedFromBackupKey)
}
</code></pre><p>Apple recommends setting this property each time you save the file because some common file operations reset the property to <code>false</code>.</p><h2 id="keychain">Keychain</h2><p>The iOS keychain is ideal for storing small secrets like account information and access tokens. The keychain is an encrypted database managed by the system. Apps can create, read, update, and delete keychain items using a query-based API provided in the <code>Security</code> framework. The API is C-based, so it is not as easy to interact with as other Objective-C or Swift APIs. That said, it is also well documented by Apple and many code examples are provided to help.</p><h3 id="accessing-the-keychain">Accessing the keychain</h3><p>All access to the keychain is done via queries. You build a dictionary of key-value pairs containing the parameters of the query and pass it to the API functions to create, read, update, and delete keychain items.</p><p>The keychain supports multiple classes of item: <a href="https://developer.apple.com/documentation/security/ksecclassgenericpassword?ref=dept-engineering-blog.ghost.io"><code>kSecClassGenericPassword</code></a>, <a href="https://developer.apple.com/documentation/security/ksecclassinternetpassword?ref=dept-engineering-blog.ghost.io"><code>kSecClassInternetPassword</code></a>, <a href="https://developer.apple.com/documentation/security/ksecclasscertificate?ref=dept-engineering-blog.ghost.io"><code>kSecClassCertificate</code></a>, <a href="https://developer.apple.com/documentation/security/ksecclasskey?ref=dept-engineering-blog.ghost.io"><code>kSecClassKey</code></a>, and <a href="https://developer.apple.com/documentation/security/ksecclassidentity?ref=dept-engineering-blog.ghost.io"><code>kSecClassIdentity</code></a>. Each item class supports a different set of item attributes. Follow the links to see Apple's documentation on each class and its associated attributes.</p><p>We will look at the <code>kSecClassInternetPassword</code> as that includes fields for account (i.e., username), server, and password.</p><p>To add an item to the keychain, you call the <a href="https://developer.apple.com/documentation/security/1401659-secitemadd?ref=dept-engineering-blog.ghost.io"><code>SecItemAdd(_:_:)</code></a> function. To retrieve an item from the keychain, you call the <a href="https://developer.apple.com/documentation/security/1398306-secitemcopymatching?ref=dept-engineering-blog.ghost.io"><code>SecItemCopyMatching(_:_:)</code></a> function. For example:</p><pre><code class="language-swift">enum KeychainError: Error {
    case encodingError
    case notFound
    case unexpectedPasswordData
    case unhandledError(OSStatus)
}

func savePassword(forServer server: String,
                  account: String,
                  password: String) throws {

    guard let passwordData = password.data(using: .utf8) else {
        throw KeychainError.encodingError
    }

    let query: [String: Any] = [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrAccount as String: account,
        kSecAttrServer as String: server,
        kSecValueData as String: passwordData
    ]

    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.unhandledError(status)
    }
}

func loadPassword(forServer server: String,
                  account: String) throws -&gt; String {

    let query: [String: Any] = [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrServer as String: kSecMatchLimitOne,
        kSecReturnAttributes as String: true,
        kSecReturnData as String: true
    ]

    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &amp;item)

    guard status != errSecItemNotFound else {
        throw KeychainError.notFound
    }

    guard status == errSecSuccess else {
        throw KeychainError.unhandledError(status)
    }

    guard
        let existingItem = item as? [String: Any],
        let passwordData = existingItem[kSecValueData as String] as? Data,
        let password = String(data: passwordData, encoding: .utf8)
    else {
        throw KeychainError.unexpectedPasswordData
    }

    return password
}
</code></pre><h3 id="keychain-item-accessibility">Keychain item accessibility</h3><p>Similar to the file system content protection attributes discussed above, you can configure the accessibility of a keychain item to define when it can be accessed. This is done by including one of the following accessibility values in the query dictionary under the <a href="https://developer.apple.com/documentation/security/ksecattraccessible?ref=dept-engineering-blog.ghost.io"><code>kSecAttrAccessible</code></a> key. In order of decreasing restrictiveness:</p><ul><li><strong><strong>When passcode set</strong></strong></li><li>These items can only be accessed when the device is unlocked. This requires a passcode to be set on the device. If the user removes the passcode from the device, any items with this setting are automatically deleted.</li><li>See: <a href="https://developer.apple.com/documentation/security/ksecattraccessiblewhenpasscodesetthisdeviceonly?ref=dept-engineering-blog.ghost.io"><code>kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly</code></a></li><li><strong><strong>When unlocked</strong></strong> (Default)</li><li>These items are only accessible when the device is unlocked. If the device does not have a passcode set, it is always considered to be unlocked.</li><li>See: <a href="https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlockedthisdeviceonly?ref=dept-engineering-blog.ghost.io"><code>kSecAttrAccessibleWhenUnlockedThisDeviceOnly</code></a>, <a href="https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked?ref=dept-engineering-blog.ghost.io"><code>kSecAttrAccessibleWhenUnlocked</code></a>.</li><li><strong><strong>After first unlock</strong></strong></li><li>These items cannot be accessed until the user has first unlocked the device after a reboot. As above, if the device does not have a passcode set, it is always considered to be unlocked.</li><li>See: <a href="https://developer.apple.com/documentation/security/ksecattraccessibleafterfirstunlockthisdeviceonly?ref=dept-engineering-blog.ghost.io"><code>kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly</code></a>, <a href="https://developer.apple.com/documentation/security/ksecattraccessibleafterfirstunlock?ref=dept-engineering-blog.ghost.io"><code>kSecAttrAccessibleAfterFirstUnlock</code></a></li></ul><p>Several of the accessibility values above have variants with the suffix <code>ThisDeviceOnly</code>, indicating that the associated item should not be restored from backup when migrating to a new device.</p><h3 id="user-presence-and-biometrics">User presence and biometrics</h3><p>Beyond the protection provided by a device passcode, you can configure keychain items to require biometric authentication. This is specified by including an access control instance in the query dictionary when creating a keychain item.</p><p>An access control instance is created using the <a href="https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility?ref=dept-engineering-blog.ghost.io"><code>SecAccessControlCreateWithFlags</code></a> function, which takes an optional argument to specify the accessibility as discussed above (otherwise the accessibility can be specified directly in the query dictionary). It also takes a set of <a href="https://developer.apple.com/documentation/security/secaccesscontrolcreateflags?ref=dept-engineering-blog.ghost.io"><code>SecAccessControlCreateFlags</code></a> allowing you to restrict access, requiring one of the following forms of authentication before you can read the item:</p><ul><li><code>devicePasscode</code> requires the user to enter the device passcode.</li><li><code>biometryAny</code> requires Touch ID or Face ID.</li><li><code>biometryCurrentSet</code> also requires Touch ID or Face ID. Additionally, this item is invalidated if the user's biometric enrollment is changed (for example, fingers added or removed from Touch ID).</li><li><code>userPresence</code> requires either biometric authentication or passcode entry.</li><li><code>watch</code> requires a nearby, paired Apple watch running watchOS 6 or later.</li></ul><p>For example, to create an access control instance requiring biometrics:</p><pre><code class="language-swift">func createAccessControlRequiringBiometrics() throws -&gt; SecAccessControl? {
    var error: Unmanaged&lt;CFError&gt;?
    let access =
        SecAccessControlCreateWithFlags(nil, 
                                        kSecAttrAccessibleWhenUnlocked,
                                        .userPresence,
                                        &amp;error)

    if let error = error?.takeUnretainedValue() {
        throw error
    }

    return access
}
</code></pre><p>You can then include the resulting <code>SecAccessControl</code> instance in the query dictionary passed to <code>SecItemAdd(_:_:)</code> under the <code>kSecAttrAccessControl</code> key.</p><h3 id="keychain-persistence-across-installs">Keychain persistence across installs</h3><p>It is worth noting that the keychain persists across app installs. This can lead to otherwise unexpected behavior. If you cache the user's username and password to the keychain, users will likely expect that data to be deleted when the app is deleted. Users, including product owners and QA engineers, often find it surprising that data remains available after deleting and reinstalling the app.</p><p>While the keychain persists across installs, the app's <code>UserDefaults</code> do not. You can take advantage of this fact by storing a flag in <code>UserDefaults</code> indicating whether the app has run before. If that flag comes back as <code>false</code>, you can delete any keychain items from a previous installation, if they exist.</p><pre><code class="language-swift">class AppDelegate: UIResponder, UIApplicationDelegate {

    private let hasRunBeforeKey = "hasRunBefore"

    private var hasRunBefore: Bool {
        get {
            return UserDefaults.standard.bool(forKey: hasRunBeforeKey)
        }

        set {
            UserDefaults.standard.setValue(newValue, forKey: hasRunBeforeKey)
        }
    }

    ...

    func application(_ application: UIApplication, 
         didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -&gt; Bool {

        if !hasRunBefore {
            // TODO: Delete any keychain items
            hasRunBefore = true
        }

        ...
</code></pre><h2 id="conclusion">Conclusion</h2><p>We have seen how to protect user data on the file system and using the keychain. We have also seen the different data protection options provided by iOS that allow you to restrict when protected user data can be accessed.</p><p>When it comes time to implement this, it is generally a good idea to consider the most restrictive protection options possible while still allowing your app to properly function. Specifying too restrictive options can lead to some pretty hard to track down bugs. For example, if your app needs to access the protected data while running in the background, it is important that the protection level does not require the device to be unlocked.</p><p>This article has focused solely on data at rest on your device. In a future article, we will look at how to protect your data in transit between your device and a server.</p><h2 id="references">References</h2><ul><li><a href="https://developer.apple.com/documentation/security/keychain_services/keychain_items/adding_a_password_to_the_keychain?ref=dept-engineering-blog.ghost.io">Adding a Password to the Keychain</a></li><li><a href="https://developer.apple.com/documentation/uikit/protecting_the_user_s_privacy/encrypting_your_app_s_files?ref=dept-engineering-blog.ghost.io">Encrypting Your App's Files</a></li><li><a href="https://developer.apple.com/icloud/documentation/data-storage/?ref=dept-engineering-blog.ghost.io">iOS Data Storage Guidelines</a></li><li><a href="https://developer.apple.com/documentation/foundation/nsurlisexcludedfrombackupkey?ref=dept-engineering-blog.ghost.io"><code>NSURLIsExcludedFromBackupKey</code></a></li><li><a href="https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility?ref=dept-engineering-blog.ghost.io">Restricting Keychain Item Accessibility</a></li><li><a href="https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items?ref=dept-engineering-blog.ghost.io">Searching for Keychain Items</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Using Unity with Swift for AR]]></title><description><![CDATA[In this post, I’ll dive into some of the technical challenges of AR as well as our preferred solution, focusing on integrating Unity into an iOS project.]]></description><link>https://engineering.deptagency.com/using-unity-with-swift-for-ar/</link><guid isPermaLink="false">6241e4851a8623003da8d264</guid><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Matt Merrill]]></dc:creator><pubDate>Mon, 28 Mar 2022 16:40:03 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/white-geometric.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/white-geometric.jpg" alt="Using Unity with Swift for AR"/><p>Since the introduction of ARKit with iOS 11, we've seen an increasing demand for applications incorporating some form of Augmented Reality (AR) experience. AR is a relatively new technology that enhances the user's real world with virtual audio/visual objects, and a mobile phone or tablet is a perfect platform for this tech. </p><p>In this post, I’ll dive into some of the technical challenges of AR as well as our preferred solution, <strong><strong>focusing on integrating Unity into an iOS project</strong></strong>.</p><h3 id="the-first-technical-requirement-for-a-compelling-ar-experience-is-an-understanding-of-where-the-user-is-in-space">The first technical requirement for a compelling AR experience is an understanding of where the user is in space. </h3><p>Apple's ARKit and Google's ARCore offer positional tracking and spatial mapping to construct a 3D representation of the user's environment, using just the camera and sensors built into the phone. These technologies are fairly similar in that they provide a context for how to render augmented content in a way that looks convincing; for example, a 3D rendered coffee mug at the correct position and perspective so that it appears to rest on a physical table.<br/></p><p>Apple offers SceneKit as a rendering solution that closely integrates with ARKit, but I've found it very cumbersome to use, and of course, it's iOS only. <a href="https://unity3d.com/?ref=dept-engineering-blog.ghost.io">Unity</a> is a simpler and more powerful cross-platform alternative, and is one of the most prominent 3rd party rendering engines for mobile applications. </p><p>One of the primary reasons for its popularity is the extensive developer community, and therefore accessibility of learning content. It's incredibly easy to get started building a convincing 3D scene, and then deploy it to both iOS and Android. </p><p>Unfortunately, it's difficult to integrate that scene into an existing Xcode project due to the Unity build process, which generates an Xcode project that expects to control the entire app.</p><p>There are several advantages, however, to embedding Unity in an existing project. Primarily, almost all new projects these days use Swift, but the generated Unity project is written in Objective-C++. Additionally, 3D or AR experiences are often just a single feature in a much larger app, meaning it would be convenient to treat Unity as any other screen to be presented modally or in a navigation controller. There's no official support for including Unity in another app in this manner, but that doesn't mean it's impossible!</p><h3 id="weve-put-together-a-template-for-a-swift-project-that-embeds-a-unity-project-as-a-single-view-controller">We've put together a template for a Swift project that embeds a Unity project as a single view controller. </h3><p>You can download the template, which includes instructions for starting or configuring your own projects <a href="https://github.com/rocketinsights/Unity-iOS?ref=dept-engineering-blog.ghost.io">here</a>. This repository provides Xcode build configs and Unity pre- and post-build scripts that keep the two projects in sync automatically. Simply building the Unity project will build for iOS device and simulator, and link those build products in the Swift project. From there, the native Swift code can present or dismiss the Unity scene just like any other view controller.</p><p>The included sample project also shows how to integrate with <a href="https://www.vuforia.com/?ref=dept-engineering-blog.ghost.io">Vuforia</a>, a powerful, cross-platform AR platform with advanced features such as object recognition and some of the most accurate model placement available. It uses ARKit or ARCore under the hood, depending on the platform, and is also built into Unity, allowing developers to enable it with a single checkbox.</p><p>The use cases of Unity integration into a larger Swift project certainly aren't limited to AR. Unity’s cross-platform support (which is especially helpful for Android… but more on that in a later post!) and unique features such as the ability to seamlessly integrate UI into both 2D and 3D space (a challenging task in native iOS because UIKit can't render into SceneKit contexts) make it an extremely powerful tool. In addition, anything requiring 3D rendering, physics simulation, or even complex audio or video features is often easier to implement in Unity than in the corresponding native frameworks.</p><p>Have fun!<br>You can find the repo, along with detailed integration instructions, here: <a href="https://github.com/rocketinsights/Unity-iOS?ref=dept-engineering-blog.ghost.io">https://github.com/rocketinsights/Unity-iOS</a></br></p>]]></content:encoded></item><item><title><![CDATA[Heroku Review Apps with Automated Custom Domains on Route 53]]></title><description><![CDATA[We were recently transitioning a client to use Heroku build pipelines for their
Next.js project that handles most business logic through an external API,
including authentication and cookie handling. This pipeline uses Heroku's 
review
apps [https://devcenter.heroku.com/articles/github-integration-review-apps] as
well as separate instances for staging and production environments. The client
handled their domains through AWS's Route 53 [https://aws.amazon.com/route53/] 
DNS service. Pointing thei]]></description><link>https://engineering.deptagency.com/heroku-review-apps-with-automated-custom-domains-on-route-53/</link><guid isPermaLink="false">6241e3e61a8623003da8d251</guid><category><![CDATA[Platforms]]></category><dc:creator><![CDATA[Nat Ring]]></dc:creator><pubDate>Mon, 28 Mar 2022 16:37:34 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Heroku-Review-Apps.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/06/Heroku-Review-Apps.png" alt="Heroku Review Apps with Automated Custom Domains on Route 53"/><p>We were recently transitioning a client to use Heroku build pipelines for their Next.js project that handles most business logic through an external API, including authentication and cookie handling. This pipeline uses Heroku's <a href="https://devcenter.heroku.com/articles/github-integration-review-apps?ref=dept-engineering-blog.ghost.io">review apps</a> as well as separate instances for staging and production environments. The client handled their domains through <a href="https://aws.amazon.com/route53/?ref=dept-engineering-blog.ghost.io">AWS's Route 53</a> DNS service. Pointing their CNAME records for staging and production to the Heroku instances was easy enough, but enabling review apps is a different story.</p><h2 id="review-apps">Review apps</h2><p>When working on a feature it's incredibly helpful to be able to poke around a standalone instance of the feature branch outside of the developer's local machine. This is useful for code reviewers and stakeholders to sign off on functionality and design as well as for developers to ensure their code works in a staging environment. Heroku allows for automatic creation of a review app when a pull request is opened on GitHub. Once that pull request is merged or closed, the review app is destroyed.</p><p>In order for our review apps to successfully share cookies and talk to the API server at <code>https://api.clientdomain.com</code>, we needed to ensure that they shared the same domain. By default, review app domains are assigned a name based off of the branch name, like <code>http://my-great-feature.herokuapp.com</code>. We needed to make sure we could access this branch at <code>http://my-great-feature.clientdomain.com</code>. You can configure this in the UIs of Heroku and AWS but doing this manually for every review app is untenable.</p><p>Furthermore, Chrome recently pushed a change where cookies cannot be shared across protocols, even if the domain is the same. Now we have to make sure that we have a secure review app at <code>https://my-great-feature.clientdomain.com</code> to match up with the secure API at <code>https://api.clientdomain.com</code>.</p><h2 id="cool-backstory-show-me-the-code">Cool backstory, show me the code</h2><p><a href="https://github.com/rocketinsights/devops-heroku/tree/master/nodejs/custom-domain-review-apps?ref=dept-engineering-blog.ghost.io">Sample project repo</a>.</p><p>If you're using "new" review apps, we take care of all this configuration in an <a href="https://devcenter.heroku.com/articles/app-json-schema?ref=dept-engineering-blog.ghost.io">app.json</a> at the root of your project by using the <code>postdeploy</code> and <code>pr-predestroy</code> keys. Here we specify scripts to be run once after an app is created and when the review app is destroyed.</p><blockquote><em><em>Note: The <code>postdeploy</code> script will be run once after <em>any</em> app is created, including non-review apps. If the app already exists, this script will not be run on subsequent pushes.</em></em></blockquote><p><code>app.json</code></p><pre><code class="language-json">{
  "scripts": {
    "postdeploy": "node bin/postdeploy.js",
    "pr-predestroy": "node bin/pr-predestroy.js"
  }
}
</code></pre><h3 id="binpostdeployjs">bin/postdeploy.js</h3><p>If you're using Node, you'll need to install <code>aws-sdk</code> and <code>heroku-client</code> as dependencies because Heroku will prune <code>devDependencies</code> before running these scripts. You'll also need to set up your API keys in the config vars for Review Apps through the Heroku UI.</p><pre><code class="language-js">const AWS = require('aws-sdk');
const Heroku = require('heroku-client');

const accessKeyId = process.env['AWS_ACCESS_KEY_ID'];
const secretAccessKey = process.env['AWS_SECRET_ACCESS_KEY'];
const heroku = new Heroku({ token: process.env['HEROKU_API_TOKEN'] });

AWS.config.update({
  accessKeyId,
  secretAccessKey,
  region: 'us-east-1'
});

const route53 = new AWS.Route53();

run().catch(err =&gt; console.log(err));
</code></pre><p>Once we get into the main <code>run</code> function, Heroku makes a few other configuration variables available to us automatically: <code>HEROKU_APP_NAME</code>, <code>HEROKU_BRANCH</code>, and <code>HEROKU_PR_NUMBER</code>. We'll only make use of <code>HEROKU_APP_NAME</code>.</p><pre><code class="language-js">async function run() {
  const appName = process.env['HEROKU_APP_NAME']; 
  const hostName = `${appName}.yourdomain.com`;

  // Asign new domain in Heroku for your review app
  heroku.post(`/apps/${appName}/domains`, {
    body: {
      hostname: hostName,
      sni_endpoint: null // Not needed since we'll have Heroku manage this for us
    }
  }).then(app =&gt; {
    const res = await route53.listHostedZones().promise();
    const zoneId = res.HostedZones.find(zone =&gt; zone.Name === 'iconnections.io.').Id;

    // Create new CNAME in Route 53
    const changeRes = await route53.changeResourceRecordSets({
      HostedZoneId: zoneId,
      ChangeBatch: {
        Changes: [{ 
          Action: 'CREATE',
          ResourceRecordSet: {
            Name: hostName,
            Type: 'CNAME',
            TTL: 60, // 1 minute
            ResourceRecords: [{ Value: newCname }] // domain from Heroku
          }
        }]
      }
    }).promise();
    console.log(changeRes);

    // Turns on automatic certificate management
    heroku.post(`/apps/${appName}/acm`).then(async (appAcm) =&gt; {
      console.log(appAcm);
    });
  });
}
</code></pre><p>There might be a delay between when Heroku is able to assign your review app a certificate and when the Route 53 DNS updates with the new CNAME. If that's the case, give it a few minutes as Heroku will automatically retry to assign the certificate.</p><h3 id="binpr-predestroyjs">bin/pr-predestroy.js</h3><p>The configuration setup is the same as <code>postdeploy.js</code>. Since Heroku will handle destroying of our review app, we're mostly concerned with clearing the old CNAME record in Route53 so we don't have unused records piling up.</p><blockquote><em><em>The same 3 Heroku-injected configuration variables that are available to us in postdeploy.js are also available in pr-predestroy. The Heroku documentation does not make that clear.</em></em></blockquote><pre><code class="language-js">async function run() {
  const appName = process.env['HEROKU_APP_NAME']; // This is available to us in pr-predestroy too!
  const hostName = `${appName}.yourdomain.com`;

  heroku.get(`/apps/${appName}/domains/${hostName}`).then(async (app) =&gt; {
    const newCname = app.cname;

    const res = await route53.listHostedZones().promise();
    const zoneId = res.HostedZones.find(zone =&gt; zone.Name === 'yourdomain.com.').Id;

    // Destroy CNAME record in Route 53
    const changeRes = await route53.changeResourceRecordSets({
      HostedZoneId: zoneId,
      ChangeBatch: {
        Changes: [{ 
          Action: 'DELETE', // Now this is DELETE
          ResourceRecordSet: {
            Name: hostName,
            Type: 'CNAME',
            TTL: 60, // 1 minute
            ResourceRecords: [{ Value: newCname }] // domain from Heroku
          }
        }]
      }
    }).promise();
    console.log(changeRes);
  });
}
</code></pre><p>Many thanks to the work of these people for helping document this process:</p><ul><li><a href="https://medium.com/clutter-engineering/heroku-review-apps-with-custom-domains-8edfc0a2b153?ref=dept-engineering-blog.ghost.io">Heroku Review Apps: Custom Domains</a></li><li><a href="https://thecodebarbarian.com/working-aws-route-53-in-node-js.html?ref=dept-engineering-blog.ghost.io">Working With AWS Route 53 in Node.js</a></li></ul>]]></content:encoded></item><item><title><![CDATA[SQL 101 for Product Managers]]></title><description><![CDATA[The following presentation was transcribed from “SQL and Python for Product Managers” webinar, presented by Colorado Product Meetup and senior software engineer, Brian Schiller. It has been edited for clarity.]]></description><link>https://engineering.deptagency.com/sql-101-for-product-managers/</link><guid isPermaLink="false">6241df601a8623003da8d238</guid><category><![CDATA[Platforms]]></category><category><![CDATA[sql]]></category><dc:creator><![CDATA[Kelsey Anderson]]></dc:creator><pubDate>Mon, 28 Mar 2022 16:18:10 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/sql-101-1050x700-1.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/sql-101-1050x700-1.jpeg" alt="SQL 101 for Product Managers"/><p><em>The following presentation was transcribed from “SQL and Python for Product Managers” webinar, presented by Colorado Product Meetup and senior software engineer, Brian Schiller. It has been edited for clarity.</em></p><h2 id="what-is-sql">What is SQL?</h2><p>SQL is a family of programming languages used to communicate with a database. These languages are different, but they’re similar enough that we can call them a family. One thing to note is that pronouncing it “sequel” or “SQL” are both totally valid options so continue saying it the way you prefer!</p><h2 id="what-does-sql-look-like">What does SQL look like?</h2><p>We’re going to walk through some examples of what SQL looks like and what it can do. We’ll look at:</p><ul><li>Select example (most common for product managers)</li><li>Insert example</li><li>Update example</li><li>Crazy CTE example</li></ul><p><strong><strong>Select</strong></strong>: a select query allows you to extract specific data from your database using set parameters. Here’s an example.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT firstName, familyName, joinDate
FROM mailing_list
WHERE joinDate &lt; '2020-03-01';
</code></pre>
<!--kg-card-end: markdown--><p>In this simple query, we can assume that there is a table in the database called <em>mailing list</em> and we want to see first name, family name, and join date. There may be additional columns, but these are the only ones we’re asking for.</p><p>While this is the most common SQL query for product managers, there are a few other examples worth understanding.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">INSERT INTO books(isbn, title, author_id)
VALUES
  ('9780780797086',
   'Harry Potter and the Philosopher''s Stone', 123),
  ('9781732102200',
   'A Philosophy of Software Design', 345);
</code></pre>
<!--kg-card-end: markdown--><p><strong><strong>Insert</strong></strong>: this is an example of an <em>insert</em> query. Developers use this for inserting and updating existing records in the database.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">UPDATE users u SET
  u.last_active_date = CURRENT_TIMESTAMP,
  u.accepted_tos = TRUE
WHERE u.id = 510292;
</code></pre>
<!--kg-card-end: markdown--><p><strong><strong>Update</strong></strong>: You can also do an <em>update</em> (for example, update users with the alias “U”).  This allows us to change the data associated with one or more users depending on the conditions we specify in the query. In the example above, we’ve specified their active date and that they’ve accepted our terms of service.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/sql-4.png" class="kg-image" alt="SQL 101 for Product Managers" loading="lazy" width="580" height="700"/></figure><p>Finally (just for a little bit of variety) here is a somewhat-more-complex example.</p><p>Even though the first couple of SQL examples are fairly simple, it can get pretty crazy. The feature I want to point out here is this idea of a <em>join.</em> What this basically does is allow different tables to be related to one another via one column.</p><p><em>This is a little over a 101 course, but if you want to learn more about this, watch the video (starting at 3:00).</em></p><h2 id="what%E2%80%99s-so-great-about-sql">What’s So Great About SQL?</h2><p><strong><strong>Huge community</strong></strong> – SQL has been around so long and it is the most common and popular way to put together a web app. There is also a big community – people who understand SQL and how to use it. They can give you pointers.</p><p><strong><strong>Excellent tools</strong></strong> – Because of the huge community, there are excellent tools for every operating system for every database you’re interacting with. Our personal favorite is one called Postico (Mac OS).</p><p><strong><strong>Well-understood operational needs</strong></strong> – The other thing that’s nice about SQL is that it has well-understood operational needs. Even among newer technologies, none of them are as well understood as plain old relational databases.</p><p><strong><strong>If you can ask a question about your data, you can ask it in SQL</strong></strong> – One point that’s incredibly useful when a product is evolving (which is true of every product) is: if you can ask a question about your data, you can ask it in SQL. Data foresight is not required.</p><p>“<strong><strong>Normalization</strong></strong>” (guidance on how to layout your data) – Normalization patterns were developed at the same time as ID of databases were developed. They turn out to be really useful and powerful in making sure that that you can ask increasingly complex questions about your data.</p><p><strong><strong>Structure up front, figure out query patterns later</strong></strong>  – This gives you the freedom to figure out queries later. Compared to some other database technologies where you need to know all of the queries and questions at the beginning. This is important because it allows us to change our minds as our product develops.</p><h2 id="what%E2%80%99s-not-so-great-about-sql">What’s Not So Great About SQL</h2><p><strong><strong>Different dialects</strong></strong> – Remember how SQL is a family of programming languages? These families each have different dialects which means that MS SQL server won’t work quite the same as MySQL, which won’t work quite the same as PostgreSQL.</p><p><strong><strong>Friend of a friend</strong></strong> – It’s a bit awkward for “friend-of-a-friend” queries (though they’re possible with “recursive common-table-expressions”).</p><p><strong><strong>Big data</strong></strong> – When your data gets REALLY BIG (no, bigger than that. Bigger. Still bigger), you can run into scaling problems. However, by the time you reach this point, you understand the problem better than at first. You’re ready to move to a column-oriented or distributed NoSQL database. You also have the money to hire people who can help you set up.</p><h2 id="is-sql-outdated">Is SQL outdated?</h2><p>Yes, SQL is an old technology, but it’s still the most popular database technology used today. The NoSQL folks, especially MongoDB, spent a lot of marketing dollars to plant the idea that newer databases=better. However, we still think that relational databases, accessed via SQL, have many benefits over newer NoSQL DBs.</p><h2 id="why-developers-love-sql">Why Developers Love SQL</h2><p>To summarize, SQL forces developers to think about how domain entities relate to one another. You can make changes, but you don’t end up with new data structured one way and old data another.</p><p>In return for this bit of discipline, you are free to query your data any way you like. That’s easy to underestimate, so I’ll give a brief example.</p><p>Sarah Mei describes a project she worked on as “the best use case [she’s] ever seen for a document database.” It was a web app displaying information about tv shows. Each TV show has many seasons, each season had many episodes, and so on (see diagram below).</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/sql-5.png" class="kg-image" alt="SQL 101 for Product Managers" loading="lazy" width="612" height="281" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2023/09/sql-5.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/sql-5.png 612w"/></figure><p>The SQL DB way of storing this would be to put everything in different tables, with foreign keys linking an episode to a season, a season to a show. The resulting structure required joining all of these tables together to pull out all the information we care about.</p><p>In MongoDB, we can store the data nested exactly as we would use it when rendering the webpage. Seems like a win, right? But Mei’s article goes on to describe a later feature request: “We want to be able to click on an actor’s name and see that person’s entire television career.” Uh oh. Accomplishing this with the existing MongoDB structure would require examining every show in the database, every season in each show, every episode in each season, looking for appearances of the requested actor.</p><p>This request eventually prompted the conversion of the entire project to a PostgreSQL database. NoSQL databases like MongoDB can be made extremely fast for specific queries, but SQL databases are better when you don’t yet know the types of questions you’ll need to ask of the data.</p><h2 id="interested-in-learning-more-about-sql">Interested in Learning More About SQL?</h2><p>Here are our favorite resources for product managers wanting to learn beyond SQL 101.</p><ul><li><a href="https://selectstarsql.com/?ref=dept-engineering-blog.ghost.io">Select Star SQL</a></li><li><a href="https://sqlpd.com/?ref=dept-engineering-blog.ghost.io">Learn SQL while solving crimes! SQL Police Department</a></li><li><a href="https://sqlbolt.com/?ref=dept-engineering-blog.ghost.io">SQLBolt – Learn SQL</a></li><li><a href="https://github.com/bgschiller/join-and-window-function-tutorial?ref=dept-engineering-blog.ghost.io">Brian’s GitHub: Join and Window Function Tutorial</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Compliance in Codebases - How to Use Clang LibASTMatchers for Compliance]]></title><description><![CDATA[The following article was created from a 2021 CppCon talk given by Jonah Jolley.

One of our clients has a software product that must comply with federal
regulations. For them, they must submit technical documents to a governing body.
However, in their case, the codebase didn’t accurately reflect what was
documented. This led to an unmitigated, unsafe condition. 

To solve this issue, we used Clang LibASTMatchers to check the codebase and
ensure they were indeed compliant.

Why Compliance in Cod]]></description><link>https://engineering.deptagency.com/compliance-in-codebases-how-to-use-clang-libastmatchers-for-compliance/</link><guid isPermaLink="false">6241d8971a8623003da8d219</guid><dc:creator><![CDATA[Jonathan Whiteside]]></dc:creator><pubDate>Mon, 28 Mar 2022 15:54:10 GMT</pubDate><content:encoded><![CDATA[<p><em>The following article was created from a 2021 CppCon talk given by Jonah Jolley.</em></p><p>One of our clients has a software product that must comply with federal regulations. For them, they must submit technical documents to a governing body. However, in their case, the codebase didn’t accurately reflect what was documented. This led to an unmitigated, unsafe condition. </p><p>To solve this issue, we used Clang LibASTMatchers to check the codebase and ensure they were indeed compliant.</p><h2 id="why-compliance-in-codebases-matter">Why Compliance in Codebases Matter</h2><p>Compliance is adhering to rules and regulations set by an authority. In the professional world, compliance will generally fall into two buckets</p><ol><li><strong><strong>Regulatory</strong></strong>: Companies have to follow relevant laws and regulations for their industry. This is important as these external guidelines are in place for safety, whether personal safety or data safety.</li><li><strong><strong>Organizational</strong></strong>: While this is typically HR or security departments, this also applies to coding practices. Measures set by the engineering team to ensure quality and easier collaboration</li></ol><p>The biggest concern is that safety can be jeopardized, whether it’s a standard to use a certain grade of material or process for testing and auditing software.</p><p>Without compliance, quality suffers. Trust with customers erodes. You also face the possibility of fines and disciplinary action.</p><p>To reduce the risk of noncompliance, it’s important for organizations to practice corrective and preventive action (CAPA). CAPA involves:</p><ul><li><strong><strong>Investigation</strong></strong>: investigate the situation, understand the risks, make a correction plan, and a timeline for implementation</li><li><strong><strong>Correction</strong></strong>: Fix the issue (for some industries, this would be a recall). It could also be personal and shift change or documentation update</li><li><strong><strong>Root Cause Analysis</strong></strong>: Find out the underlying cause</li><li><strong><strong>Preventative Action:</strong></strong> understand how this happened and change processes to avoid it in the future</li></ul><h2 id="tips-for-compliance">Tips for Compliance</h2><p>Disclaimer: We’re not compliance officers. However, there are some basic steps we take in industries that require it.</p><ul><li>Ensure employees are up to date and understanding regulations and practices</li><li>Ensure up to date documentation of the design and architecture of a product</li></ul><p>What about automation? Generally, this is a well-defined problem. Codebases can use static analysis tools. But what about custom checkers to ensure certain things? With Clang, there is an opportunity to increase the efficiency of this process</p><h2 id="an-example">An Example</h2><p>For this example, we’re going to pretend we’re making a candy dispenser. We have an LED touchscreen where we can select the candy we want. Right now it’s showing its screensaver.</p><p>We have another LED light that will blink different lights for different statuses. And we have a door we can open to grab our candy.</p><h3 id="device-architecture">Device Architecture</h3><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/device-architecture-clang.png" class="kg-image" alt="" loading="lazy" width="1460" height="1064" srcset="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w600/2022/03/device-architecture-clang.png 600w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/size/w1000/2022/03/device-architecture-clang.png 1000w, https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/device-architecture-clang.png 1460w" sizes="(min-width: 720px) 720px"/></figure><ul><li>GUI where you can select your candy, it also will show statuses</li><li>Driver process, which holds all the microcontroller code and sensor information. It provides an API to the Baker Process to control them</li><li>Baker Process That takes a recipe and will command the different components inside to make it based on that recipe.</li><li>Also, in the baker process, there are monitors that watch sensor information based on the recipe to ensure conditions stay in a safe range.</li><li>If a monitor sees an unsafe condition it raises an alarm,</li><li>The Alarm Manager will look up  that alarm that is defined in a config file and in there we have the correct action a device should take, whether full shutdown or pause the flame</li></ul><p>So we need alarms that describe an unsafe condition, monitors that ensure device conditions stay nominal, and an alarm manager that will enforce corrective action. As a part of getting certified to be able to release to the public, we need to submit documentation on the device. Part of that is submissions of all of the alarms as they describe potential unsafe situations.</p><h2 id="how-do-we-solve-this">How Do We Solve This?</h2><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/clang-code1.png" class="kg-image" alt="" loading="lazy" width="512" height="121"/></figure><p>This alarm client is Instantiated with a string, coming from a literal or a config file. It will eventually get its raise function called which is an event dispatched to the Alarm Manager. Somewhere else we have every alarm defined in a config and severity rating and other metadata we might need.</p><p>We see an alarm client. We see the alarm it is and that it is raised. So the two main things we need to grab are the variable name, and what it was initialized with.</p><p>We can’t do this manually, because it would take a person hours to maintain, it’s error-prone, and existing tools are cumbersome.</p><p>We can’t use regex either. Regex is brittle and the underlying representation would be the same even though the code text is different. It’s also complex to maintain. After I write the most “amazing” beautiful regex ever. Coming back to it after an extended period of time it looks like gibberish. Finally, C++ has more going on than just the textual representation. We need context. If we had a local variable with the same name in two different functions. Now we need to capture the function name to confidently mark an alarm as defined and raised</p><h2 id="what-about-clang-tooling">What About Clang Tooling?</h2><p>Leveraging Clang-Tidy would be really great, we could write a custom checker. However, this falls short because we need to post-process the information we are getting. We could leverage this if we only wanted to make sure that every AlarmClient had its raise function called. If you’re thinking about this in your own codebases you could leverage that.</p><p>With LibTooling, we could control the output of the program, specialize it for what we need, and enable being able to post-process with additional tooling. Clang LibTooling is a library to support writing standalone tools. It allows us to run tools over single files or subsets of files while giving us full control and access to the Clang AST. It also allows us to share code with Clang Plugins.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/icvNkYextOo?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""/></figure><h3 id="additional-resources-for-clang">Additional Resources for Clang</h3><ul><li><a href="https://clang.llvm.org/docs/LibASTMatchersTutorial.html?ref=dept-engineering-blog.ghost.io">AST Matcher Tutorial</a></li><li><a href="https://clang.llvm.org/docs/LibASTMatchersReference.html?ref=dept-engineering-blog.ghost.io">LibASTMatchers reference</a></li><li><a href="https://clang.llvm.org/doxygen/?ref=dept-engineering-blog.ghost.io">Clang documentation</a></li><li><a href="https://github.com/WatchMeCalculate/Talks/tree/main/Using_Clang_LibASTMatchers_For_Compliance_In_Codebases/code?ref=dept-engineering-blog.ghost.io">Code used</a></li></ul><p>If your codebase needs to comply with certain regulations, reach out to our software development team. We'd be happy to brainstorm the best solution and implement Clang or another tool.</p>]]></content:encoded></item><item><title><![CDATA[FreeRTOS Tutorial: Getting Started with ESP32 on Linux]]></title><description><![CDATA[To help others as they make their way through this process, we decided to document and release our findings as a FreeRTOS tutorial for developers. Here is how to get started with ESP32 on Linux.]]></description><link>https://engineering.deptagency.com/freertos-tutorial-getting-started-with-esp32-on-linux/</link><guid isPermaLink="false">6241d8161a8623003da8d207</guid><category><![CDATA[Platforms]]></category><category><![CDATA[iot]]></category><dc:creator><![CDATA[Kelsey Anderson]]></dc:creator><pubDate>Mon, 28 Mar 2022 15:46:46 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/freertos-esp32-1.jpg" medium="image"/><content:encoded><![CDATA[<h3 id="updated-august-24-2021">Updated August 24, 2021</h3><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/freertos-esp32-1.jpg" alt="FreeRTOS Tutorial: Getting Started with ESP32 on Linux"/><p>While building our latest IoT product, our development team found that documentation for FreeRTOS was dated and often flat-out wrong. To help others as they make their way through this process, we decided to document and release our findings as a FreeRTOS tutorial for developers. Here is how to get started with ESP32 on Linux.</p><h2 id="initial-observations">Initial Observations</h2><p>One can set up and install AWS FreeRTOS on numerous microcontrollers. For most of these steps, you will need either a Linux, Mac, or Windows computer.</p><ul><li>Ideally a Linux environment would work best with setup and work. Mac’s also work well with setup and flashing the boards</li><li>Windows requires additional setup and might require more troubleshooting depending on experience.</li></ul><h2 id="finding-documentation">Finding Documentation</h2><p>AWS offers different documentation for different boards. Select from this list of <a href="http://amazon.com/freertos/latest/userguide/getting-?ref=dept-engineering-blog.ghost.io">microcontrollers</a> depending on what you bought. In the context of this example, we are using this architecture of the <a href="https://www.digikey.com/en/products/detail/schtoeta-engineering-limited/ESP32-DEVKITC-32U/9357002?utm_adgroup=RF%20Evaluation%20and%20Development%20Kits%2C%20Boards&utm_source=google&utm_medium=cpc&utm_campaign=Shopping_Product_RF%2FIF%20and%20RFID_NEW&utm_term=&utm_content=RF%20Evaluation%20and%20Development%20Kits%2C%20Boards&gclid=CjwKCAjwoZWHBhBgEiwAiMN66QWpAB8by92edD-wuj8mEid7UUkG2YE5bGd4oTFU_93Fm1C535wz0BoCbLAQAvD_BwE">ESP32</a> so we will be following the <a href="https://docs.aws.amazon.com/freertos/latest/userguide/getting_started_espressif.html?ref=dept-engineering-blog.ghost.io">ESP32-DevKitC</a> guide. We will also be using Ubuntu 20.04 to set up and flash the ESP32. Either a full installation or a VM will work. Ubuntu iso download <a href="https://ubuntu.com/download/desktop?version=20.04&architecture=amd64&ref=dept-engineering-blog.ghost.io">here</a>. Virtual Box Download <a href="https://www.virtualbox.org/?ref=dept-engineering-blog.ghost.io">here</a> (if using VM).</p><h2 id="ubuntu-prerequisites">Ubuntu Prerequisites</h2><p>Virtual Box setup</p><ul><li>Click new. Give name of choosing to the VM, set Type to <strong><strong>Linux</strong></strong>, and version to <strong><strong>Ubuntu (64-bit)</strong></strong> and click next</li><li>Set Memory (RAM) size to whatever fits. I recommend to ~4 Gigabytes of RAM and click next</li><li>Select <strong><strong>Create a virtual hard disk now</strong></strong> and click next</li><li>Select <strong><strong>VDI</strong></strong> and click next</li><li>Select <strong><strong>Fixed size</strong></strong> and click next</li><li>Set disk size for your virtual machine. I recommend around 25GB and click create. (wait until completion might take a while)</li></ul><p>Select your virtual machine and click settings</p><ul><li>Go to storage and next to <strong><strong>Controller: IDE</strong></strong> click the <strong><strong>blue cd icon with a green plus sign</strong></strong></li><li>Click add and navigate to the place where you downloaded the Ubuntu ISO</li></ul><p>In settings click the <strong><strong>USB</strong></strong> tab</p><ul><li>In the right side click the usb icon with a green plus sign</li><li>Add anything with Silicon Labs USB to Uart Bridge controllers, or anything with Cygnal Integrated products CP210x Uart bridge. If you don't see these options you many need to download drivers for your platform <a href="https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers?ref=dept-engineering-blog.ghost.io">here</a></li></ul><p>Once USB and storage settings are set up, start the virtual machine</p><ul><li>Select language and click install Ubuntu</li><li>Click Normal Installation and select <strong><strong>Download updates while installing Ubuntu</strong></strong></li><li>Select <strong><strong>Erase disk and install Ubuntu</strong></strong> and click install now</li></ul><p>For all Ubuntu distributions</p><ul><li><strong><strong>sudo apt install gcc g++ cmake putty unzip curl</strong></strong></li><li><strong><strong>sudo apt update</strong></strong></li><li><strong><strong>sudo apt upgrade</strong></strong></li></ul><p>If you are on a virtual machine (Virtual Box), at the top of the window click <strong><strong>Devices</strong></strong></p><ul><li>For shared clipboard set to <strong><strong>bidirectional</strong></strong></li><li>For drag and drop set to <strong><strong>bidirectional</strong></strong></li></ul><p>Finally click <strong><strong>Insert guest additions CD Image</strong></strong></p><ul><li>Once done installing in terminal input reboot</li><li>These steps are not mandatory. They just make working inside the virtual box easier</li></ul><h2 id="aws-prerequisites">AWS Prerequisites</h2><ul><li>Have access to AWS services</li></ul><p>Go to the Identity and Access Management (IAM) and grant your IAM user permission to use AWS IoT and FreeRTOS services.</p><p>Attach the following Policies to IAM user:</p><ul><li>AmazonFreeRTOSFullAccess</li><li>AwsIoTFullAccess</li></ul><h2 id="adding-policies">Adding Policies</h2><p><a href="https://console.aws.amazon.com/iam/home??ref=dept-engineering-blog.ghost.io">IAM Console</a></p><p>AmazonFreeRTOSFullAccess</p><ul><li>In the navigation pane, choose <strong><strong>Users</strong></strong></li><li>Input your username in search box, and select your name</li><li>Choose <strong><strong>Add permissions</strong></strong></li><li>Choose <strong><strong>Attach existing policies directly</strong></strong></li><li>Search <strong><strong>AmazonFreeRTOSFullAccess</strong></strong> and select it from the list. Then choose <strong><strong>Next: Review</strong></strong></li><li>Choose <strong><strong>Add permissions</strong></strong></li></ul><p>AWSIoTFullAccess</p><ul><li>In the navigation pane, choose <strong><strong>Users</strong></strong></li><li>Input your username in search box, and select your name</li><li>Choose <strong><strong>Add permissions</strong></strong></li><li>Choose <strong><strong>Attach existing policies directly</strong></strong></li><li>Search <strong><strong>AWSIoTFullAccess</strong></strong> and select it from the list. Then choose <strong><strong>Next: Review</strong></strong></li><li>Choose <strong><strong>Add permissions</strong></strong></li></ul><h2 id="setting-up-esp32">Setting up ESP32</h2><p>Depending on the platform (Linux, Mac, Windows) select the following documentation and install the needed modules/ software.</p><ul><li><a href="https://docs.espressif.com/projects/esp-idf/en/release-v4.2/esp32/get-started/windows-setup.html?ref=dept-engineering-blog.ghost.io">Windows</a></li><li><a href="https://docs.espressif.com/projects/esp-idf/en/release-v4.2/esp32/get-started/macos-setup.html?ref=dept-engineering-blog.ghost.io">Mac</a></li><li><a href="https://docs.espressif.com/projects/esp-idf/en/release-v4.2/esp32/get-started/linux-setup.html?ref=dept-engineering-blog.ghost.io">Linux</a></li></ul><p>If you haven’t downloaded a FreeRTOS configuration go to the <a href="https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2&ref=dept-engineering-blog.ghost.io#/software/freertos">FreeRTOS console</a>. Here you can download a pre-configured FreeRTOS that has some prebuilt modules depending on the services you might want to use and depending on chip architecture.</p><p>Under <strong><strong>My configurations,</strong></strong> you can make your own FreeRTOS version that can include (or exclude) certain libraries.</p><p>For our ESP32 in search bar enter <strong><strong>ESP32</strong></strong> download the row that has configuration <strong><strong>Connect to AWS Greengrass</strong></strong> with hardware platform <strong><strong>ESP32-DevKitC</strong></strong></p><p>Once downloaded unzip and make note where the extract folder is (we will be using the command line in this directory)</p><ul><li>Rename the folder with a name with no special characters, if you don’t rename the folder and it still has special characters it may mess with cmake</li></ul><p>In command line/ Terminal. Navigate to the extracted freeRTOS file.</p><p>Navigate to vendors/espressif/esp-idf/</p><ul><li>Enter ls -al and check to see if the execute command is enabled (+x) for both install.sh and export.sh</li></ul><p>If not (or you aren’t sure) enter the command:</p><ul><li><strong><strong>chmod +x install.sh export.sh</strong></strong></li><li>This will enable the scripts to be run.</li></ul><p>Now navigate back to the freeRTOS directory and run the following commands</p><ul><li><strong><strong>source vendors/espressif/esp-idf/install.sh</strong></strong></li></ul><p><strong><strong>source vendors/espressif/esp-idf/export.sh</strong></strong></p><ul><li>Everytime you restart terminal, you need to run export.sh or else flashing and building won’t work.</li></ul><p>If you are using Windows or Mac you also need to install the drivers for this platforms from silicon labs check <a href="https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers?ref=dept-engineering-blog.ghost.io">here</a></p><ul><li>Don't do this if you are working within a linux virtual machine</li></ul><p>At this point try to establish a serial connection with your ESP32</p><ul><li>To figure out for your machine check <a href="https://docs.espressif.com/projects/esp-idf/en/release-v4.2/esp32/get-started/establish-serial-connection.html?ref=dept-engineering-blog.ghost.io">here</a></li></ul><p>For linux use <strong><strong>ls /dev/tty</strong></strong>*</p><ul><li>You should see /dev/ttyUSBX (X is usually a number)</li><li>In a new terminal enter <strong><strong>sudo putty</strong></strong> (make sure your esp32 is connected to your computer.</li><li>For connection type select serial. For serial line input the port you found in the previous step. For speed input <strong><strong>115200</strong></strong></li><li>If everything works you should see a black terminal</li><li>If this is the case press the buttons on your ESP32 to restart the board, this will cause some text to print to the screen</li><li>You won’t be able to connect to the ESP32 unless you use sudo putty. To change this enter <strong><strong>sudo usermod -a -G dialout $USER</strong></strong> and then reboot the machine/VM</li><li>After rebooting don’t forget to rerun <strong><strong>source vendor/espressif/esp-idf/export.sh</strong></strong> from the FreeRTOS directory</li></ul><h2 id="flashing-esp32">Flashing ESP32</h2><ul><li>First in terminal/command line check the version of python using <strong><strong>python3 --version.</strong></strong> Make sure you have python 3.0.1 or higher installed</li></ul><p>If you haven't installed aws cli follow the doc here for your system <a href="https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html?ref=dept-engineering-blog.ghost.io">here</a></p><p>For Ubuntu run the following</p><p><strong><strong>curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"</strong></strong></p><ul><li>Careful not to download this in freeRTOS directory</li><li><strong><strong>unzip awscliv2.zip</strong></strong></li><li><strong><strong>sudo ./aws/install</strong></strong></li></ul><p>Enter <strong><strong>aws configure</strong></strong> and enter your account information public key, private key, region, format. For format you could leave it empty or set it to <strong><strong>json</strong></strong></p><ul><li>This would be helpful if you want to use IoT Core directly from the ESP32 without using Greengrass. Check the documentation <a href="https://docs.aws.amazon.com/freertos/latest/userguide/getting_started_espressif.html?ref=dept-engineering-blog.ghost.io#download-and-configure-espressif-idf42">here</a> and scroll down to the Download and configure FreeRTOS section</li><li>Enter <strong><strong>pip install tornado nose</strong></strong></li><li>Enter <strong><strong>pip install boto3</strong></strong></li><li>Make sure you are inside the FreeRTOS directory and enter <strong><strong>source vendors/espressif/esp-idf/export.sh</strong></strong></li></ul><p>Navigate to <strong><strong>vendors/espressif/esp-idf/tools</strong></strong> there should be a file called <strong><strong>idf.py</strong></strong></p><ul><li>Execute <strong><strong>chmod +x idf.py</strong></strong>  This will enable us to run the script</li></ul><p>Then in the FreeRTOS directory execute the following:</p><ul><li><strong><strong>idf.py -DVENDOR=espressif -DBOARD=esp32_wrover_kit -DCOMPILER=xtensa-esp32 build</strong></strong></li></ul><p><strong><strong>idf.py erase_flash flash monitor</strong></strong></p><ul><li>Each one of these can be run individually the first wipes the esp32, the second flashes binaries on it, and the third launches a terminal to see what the esp32 is printing</li><li>If everything works fine you will see a bunch of lines and addresses being printed.</li></ul><p><strong><strong>Next Up</strong></strong>: <a href="engineering.deptagency.com/freertos-tutorial-create-a-hello-world-application-with-esp32/">FreeRTOS Tutorial #3: Creating a Hello World Application</a></p>]]></content:encoded></item><item><title><![CDATA[FreeRTOS Tutorial: Create a Hello World Application with ESP32]]></title><description><![CDATA[To help others as they make their way through this process, we decided to document and release our findings as a FreeRTOS tutorial for developers. Here is how to create a Hello World application.]]></description><link>https://engineering.deptagency.com/freertos-tutorial-create-a-hello-world-application-with-esp32/</link><guid isPermaLink="false">6241d72f1a8623003da8d1e8</guid><category><![CDATA[Platforms]]></category><category><![CDATA[iot]]></category><dc:creator><![CDATA[Kelsey Anderson]]></dc:creator><pubDate>Mon, 28 Mar 2022 15:44:47 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/freertos-esp32.jpg" medium="image"/><content:encoded><![CDATA[<h3 id="updated-august-20-2021">Updated August 20, 2021</h3><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/freertos-esp32.jpg" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32"/><p>While building our latest IoT product, our development team found that documentation for FreeRTOS was dated and often flat-out wrong. To help others as they make their way through this process, we decided to document and release our findings as a FreeRTOS tutorial for developers. Here is how to create a Hello World application.</p><p>You can always view our <a href="https://github.com/devetry/internal-iot-embedded/commit/0583412d992abb93a1b5caa76bf940ec01aafbdd?ref=dept-engineering-blog.ghost.io">open-source IoT project here</a> on Github.</p><h2 id="prerequisites">Prerequisites</h2><p>You have followed and completed the instructions laid out in <a href="https://engineering.deptagency.com/freertos-tutorial-getting-started-with-esp32-on-linux?ref=dept-engineering-blog.ghost.io">FreeRTOS tutorial #2</a> and confirmed that you are able to talk, build and flash the ESP32.</p><p>You have your machine (virtual or actual) on and with the ESP32 connected, and your device can connect to the ESP32 via Putty or some other shell.</p><h2 id="where-to-start">Where to Start</h2><p>Navigate to your FreeRTOS download directory.</p><ul><li>Using <strong><strong>ls</strong></strong> the only directory you should see is <strong><strong>FreeRTOS</strong></strong></li></ul><p>To compile a program (hello world or otherwise) there are a couple of mandatory steps.</p><ul><li>First we need to make a <strong><strong>CMakeLists.txt</strong></strong> file, this explains to the cross compiler where to find libraries, what chip it is compiling on, and how/where to generate the binary executables. For now enter touch CMakeLists.txt this will make the file but with nothing in it.</li><li>We also need a src folder. In the same directory as your CMakeLists.txt file enter <strong><strong>mkdir src</strong></strong></li><li>Inside src we need to make a <strong><strong>main.c</strong></strong> file. This is where we will create our hello world program.</li></ul><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/03/freertos-tutorial-app1.png" class="kg-image" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32" loading="lazy" width="512" height="441"/></figure><p>The above is a sample CMakeLists.txt file that will work for hello world and most applications. If you want to add extra files, libraries, and dependencies you can add them after the last line <strong><strong>add_subdirectory</strong></strong>(FreeRTOS).  Check the specific documentation, and setup <a href="https://docs.aws.amazon.com/freertos/latest/userguide/getting_started_espressif.html?ref=dept-engineering-blog.ghost.io">here</a> and scroll down to <strong><strong>Add Custom Components to ESP-IDF</strong></strong></p><ul><li>Line 1 is mandatory and forces the compiler to use a minimum version of CMake</li><li>Lines 3 - 11 try to figure out which type of ESP32 board you are working with (ESP32s2 or ESP32). Depending on which tool chains it finds it will set the <strong><strong>SOC_NAME</strong></strong> variable to be either <strong><strong>esp32s2 or esp32</strong></strong></li><li>Line 12 is how we set the name of the project. This is custom and up to the user to decide</li><li>Line 15 - 19 tell the tool chain what to link against and where to find the main.c executable.</li><li>Line 30 set which directories to include</li><li>Lines 33 - 37 This line helps us figure out which tool chains to use esp32_devkitc or esp32_saola_1</li><li>Line 38 we are adding/compiling the entire FreeRTOS OS along with all its libraries and files. This is for the most part also a mandatory line.</li><li>With all this we now have a configured CMakeList.txt be sure to save before closing vim or the IDE.</li></ul><h2 id="creating-the-helloworld-application">Creating the Hello_World application</h2><h3 id="setting-up-file">Setting up file</h3><p>Applications in FreeRTOS are very similar to a typical C application.</p><ul><li>There is a <strong><strong>main</strong></strong> function.</li><li>You can design functions and modularize your code.</li><li>You can include certain libraries into your namespace and use them.</li><li>In freeRTOS instead of having an <strong><strong>int main()</strong></strong> function we use <strong><strong>void app_main()</strong></strong> this is essentially your main function and anything you would like to be executed should go here.</li><li>In terms of libraries we will need the C standard library, FreeRTOS library, esp_systems library, and task library are what we need. It should look like this.</li></ul><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/freertos-2.png" class="kg-image" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32" loading="lazy" width="202" height="57"/></figure><h3 id="implementing-interface">Implementing Interface</h3><p>Along with the app_main() we also need to implement these 5 functions.</p><ul><li><strong><strong>vApplicationTickHook()</strong></strong></li><li><strong><strong>vApplicationIdleHook()</strong></strong></li><li><strong><strong>vApplicationDaemonTaskStartupHook()</strong></strong></li><li><strong><strong>vApplicationGetIdleTaskMemory()</strong></strong></li><li><strong><strong>vApplicationGetTimerTaskMemory()</strong></strong></li></ul><p>The reason being is that inside the aws Free_RTOS kernel configs Amazon has enabled/enforced some “conditions” to be set to true (1). Depending on what gets set to 1 this enforces the user to implement their own versions of these functions and the program will not compile unless they are implemented. Other versions of FreeRTOS might have these dependencies disabled and depending on the online documentation you may see different and more concise versions of <strong><strong>hello world</strong></strong>.</p><p>While this might be slightly annoying for a simple hello world project, for people who like tinkering with OSs and implementing their own task handler this can be fun/useful.</p><p>If you have no idea how to implement these functions, most if not all of the freertos demos implement them and you can use those implementations.</p><p>What your current implementation should look like so far:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/freertos-3.png" class="kg-image" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32" loading="lazy" width="118" height="58"/></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/freertos-4.png" class="kg-image" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32" loading="lazy" width="448" height="512"/></figure><h3 id="creating-hello-world-task">Creating Hello World Task</h3><p>Now with app_main declared and the 5 functions implemented it is time to write a hello task.</p><p>Actions are executed in the FreeRTOS in the form of <strong><strong>tasks</strong></strong>. Tasks are functions or behaviors you want your microcontroller to perform. This could be:</p><ul><li>Reading sensor data</li><li>Printing to screen</li><li>Sending data over wifi</li><li>Computation</li><li>Etc. more on tasks here</li></ul><h4 id="so-why-tasks-and-how-do-they-work-in-freertos">So why Tasks and how do they work in FreeRTOS?</h4><p>Your ESP32 comes with a dual core processor. Meaning it can execute 2 tasks at once. Since this is a very barebone OS we as developers need to tell FreeRTOS which tasks take priority over others.</p><p>Let us say we have 3 Tasks. <strong><strong>Task 1</strong></strong> reads data from <strong><strong>sensor 1</strong></strong> every second. <strong><strong>Task 2</strong></strong> reads and adjusts an output signal based on the sensor readings from Task 1. And <strong><strong>Task 3</strong></strong> sends the collected sensor data to some online database every 30 seconds.</p><p>We can only run 2 Tasks at any given time. In this scenario, it makes sense to give Tasks 1 and 2 a higher priority than task 3 since we are reading and adjusting every second. While only sending data every 30 seconds. If we give Task 3 a higher or equal priority to tasks 1 and 2 we would be wasting clock cycles doing nothing and effectively wasting resources and power.</p><h4 id="how-do-we-make-a-task">How do we make a task?</h4><p>Tasks are essentially the same as functions in C. They are of type void and take one parameter of **void *** which are optional parameters you can pass if your tasks need or depend on something external.</p><p>Our hello world task will look like this</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/freertos-5.png" class="kg-image" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32" loading="lazy" width="255" height="61"/></figure><p>From here the code looks very similar in C. We will try to print hello world infinitely every second. It should look something like this.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/freertos-6.png" class="kg-image" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32" loading="lazy" width="287" height="108"/></figure><ul><li>vTaskDelay() is FreeRTOS’ way of delaying execution for a second</li><li>pdMS_TO_TICKS is how we convert from ticks to milliseconds.</li></ul><p>With this, our hello world task is implemented.</p><p>Back in main we need to tell FreeRTOS which tasks we want to execute. We do use by using the <strong><strong>xTaskCreate()</strong></strong> function.</p><p>This creates the task and hands it over to the scheduler so that it can be executed onto a processor. It should look something like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2023/09/freertos-7.png" class="kg-image" alt="FreeRTOS Tutorial: Create a Hello World Application with ESP32" loading="lazy" width="453" height="77"/></figure><ul><li>The first parameter is the task name we created earlier</li><li>The second parameter is a description of the task</li><li>The third parameter is how much memory do we want to allow this task to use</li><li>The fourth parameter is a pointer to the parameters you want to pass into the task</li><li>The fifth parameter is the priority of the task (the lower the number the higher the priority)</li><li>The sixth parameter is optional and is used to pass back a handle if we want to reference the task</li></ul><p>With this we can now run our main.c.</p><h2 id="flashing-and-building">Flashing and building</h2><p>In order to execute your implementation make sure you are in the same directory as the CMakeLists.txt file that you created earlier. If this is a new terminal make sure you run:</p><ul><li><strong><strong>source FreeRTOS/vendors/espressif/esp-idf/export.sh</strong></strong></li></ul><p>Then run <strong><strong>cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=freertos/tools/cmake/toolchains/xtensa-esp32.cmake -GNinja</strong></strong></p><ul><li>If you moved your tool chain to a different folder you need to update -<strong><strong>DCMAKE_TOOLCHAIN_FILE</strong></strong> to point to the directory where xtensa-esp32.cmak is residing.</li><li>This will build the build directory that will hold and generate the binaries that will run on your ESP32</li></ul><p>Finally, run <strong><strong>cmake --build build</strong></strong> (if your build directory is named something different like my_build then the command should look like <strong><strong>cmake --build my_build</strong></strong>).</p><p>If there are no errors and it compiles, you can use idf.py flash to flash the code onto the board (remember to hold the reset button for a second while it is trying to connect).</p><p>You can use <strong><strong>idf.py</strong></strong> monitor or <strong><strong>putty</strong></strong> to see the terminal print <strong><strong>Hello world</strong></strong> every second.</p>]]></content:encoded></item><item><title><![CDATA[State of Swift WebSockets]]></title><description><![CDATA[Apple delivered several exciting and innovative new APIs for developers at WWDC this year. You might have heard or read about the SwiftUI and Combine revamp, but I want to address the new world of native WebSockets! The historically complicated connection protocol is now delivered as a first class citizen! Before we dive in on how awesome this is, it's important to have a basic understanding of Sockets and the road we've traveled to reach this point.


What is a socket?

By definition a socket i]]></description><link>https://engineering.deptagency.com/state-of-swift-websockets/</link><guid isPermaLink="false">6240f377ce0c8f004d7dcf20</guid><dc:creator><![CDATA[Mike Neas]]></dc:creator><pubDate>Sun, 27 Mar 2022 23:30:37 GMT</pubDate><content:encoded><![CDATA[<p>Apple delivered several exciting and innovative new APIs for developers at WWDC this year. You might have heard or read about the SwiftUI and Combine revamp, but I want to address the new world of native WebSockets! The historically complicated connection protocol is now delivered as a first class citizen! Before we dive in on how awesome this is, it's important to have a basic understanding of Sockets and the road we've traveled to reach this point.</p><h2 id="what-is-a-socket">What is a socket?</h2><p>By definition a <strong>socket</strong> is one end of a two-way communication link between two programs running on a network. When a program runs on a network it is assigned an ip address and a port number (ie. 10.0.0.1:80). Using this data we can identify and locate the application we want to communicate with. Let's break it down with an example!</p><p>When we connect to a website like "www.google.com" through a web browser we have two programs running. Those two programs are Google's web server application and the web browser application installed on our machine. There is an ip address and port number mapped to the web address behind "www.google.com" and a unique address for our web browser. Our computers have two sockets created to be able to <strong>transport</strong> information between each other.</p><p>A multitude of different protocols have been created that allow us to communicate in different ways over a network. Most commonly we hear of <strong>TCP/IP</strong>, which is exactly the protocol that permits us to communicate between our web browser and Google's servers. An <strong>HTTP</strong> request typically occurs over a TCP connection and brings us a few significant benefits including guaranteed delivery acknowledgement and error handling. However, the most important aspect of this protocol is its nature of "connectedness". With TCP we know exactly who we're trying to communicate with, the only thing left to do is keep a connection open.</p><h2 id="what-are-websockets">What are WebSockets?</h2><p>So we know that a socket is made when we need to communicate between applications. We have a high level understanding of the benefits of a TCP connection. Now we can talk about what a WebSocket is!</p><pre><code>A WebSocket is a communication protocol that provides full duplex communication over a single TCP connection.
</code></pre><p>Let's break that statement down with an example.</p><p>If we request anything from google.com using <em>HTTP</em> we would make a <em>request</em> to Google's server. <strong>Wait</strong>. Hopefully we would receive a <em>reply</em> with the information we wanted. Then the connection is closed because we got what we needed.</p><p>With WebSockets on the other hand, we make a connection with a server and request for that connection to stay open until it times out or explicitly exited. While that connection is persisted, both the server and client are free to pass messages over the wire. There's no need to request and wait for a reply anymore because we leave the connection open!</p><p>It's kind of like passing notes in class versus having a string telephone with two cups on the ends to verbally communicate with someone.</p><p>WebSockets allow us to receive live updates in either direction easily. Common use cases include:</p><ul><li>Multiplayer games</li><li>Messaging platforms</li><li>Stock market tickers</li><li>Sports scores</li></ul><h2 id="but-whats-going-on">But what's going on??</h2><p>At a high level there are 5 major requirements of a WebSocket connection 😄</p><p>🤝 - Handshake, when a client request to open a WebSocket with a server a negotiation happens with a <code>HTTP/1.1 101 Switching Protocol</code>. The server and client need to check out that they want to and are allowed to open up to each other.</p><p>🔐🔗 - Connection, once the handshake is complete, a connection is made, and a bidirectional stream of data can occur between the client and server.</p><p>👂💬 - Messaging, now that the connection is up, both applications can send and receive data to each other! We'll see shortly that the Swift API supports <code>String</code> and <code>Data</code> types.</p><p>🏓 - Ping, another important part of WebSockets is the ability to Ping/Pong each other. A client can tell the server "Hey, I know I haven't sent a message in a while, but I'm still around so don't close me off yet!" Pinging isn't directly required by spec but it certainly has it's common use cases.</p><p>❌ - Closure, when we're all done, a client can tell the server it wants to close off the connection and whatever reason it may have for doing so.</p><p>If you're interested in the full specification of WebSockets please checkout <a href="https://tools.ietf.org/html/rfc6455?ref=dept-engineering-blog.ghost.io">RFC6455</a>, it covers all the nitty gritty details of how WebSockets should be constructed and used.</p><h2 id="how-could-we-use-websockets-pre-ios13">How could we use WebSockets pre iOS13?</h2><p>Back to Swift development. Before XCode 11/iOS13 was released, WebSockets were only able to be implemented using CFNetwork or Webkit. Where you'd either have to implement low level details or rely on JavaScript's WebSocket implementation.</p><p>Many developers opt for 3rd party library's:</p><ul><li><a href="https://github.com/daltoniam/Starscream?ref=dept-engineering-blog.ghost.io">Starscream</a></li><li><a href="https://github.com/facebook/SocketRocket?ref=dept-engineering-blog.ghost.io">SocketRocket</a></li><li><a href="https://github.com/socketio/socket.io-client-swift?ref=dept-engineering-blog.ghost.io">Socket.io</a></li><li><a href="https://github.com/tidwall/SwiftWebSocket?ref=dept-engineering-blog.ghost.io">SwiftWebSocket</a></li></ul><p>These are all still acceptable and relevant options for various use cases, but now we can move forward with a truly native Swift solution!</p><h2 id="what-can-we-do-now">What can we do now!?</h2><ul><li>A webSocketTask can be created with a URL from a URLSession.</li><li>In order to kick off the handshake and connection we call <code>.resume()</code>.</li><li>From there we can use <code>URLSessionWebSocketDelegate</code> methods to confirm a successful or nonsuccessful attempt.</li><li>If we want to send a message <code>URLSessionWebSocketTask.Message</code> has a <code>.string</code> or <code>.data</code> method to wrap up our data to pass to the <code>task.send</code> method.</li><li>If we want to listen to messages that could be coming in to us we have <code>task.receive</code> that gives us a Result type which will either be <code>.success</code> with a String or Data message or <code>.failure</code> with an error.</li><li>Sending a ping is as simple as calling <code>task.sendPing</code> and I would advise wrapping a call to that method in a <code>Timer</code> if you have a WebSocket server that specifies a ping timeout.</li><li>Lastly, we have <code>.cancel</code> which takes a type of cancellation you want and an optional reason.</li></ul><p>A more thorough implementation using <code>URLSessionWebSocketDelegate</code> can be found <a href="https://github.com/MichaelNeas/perpetual-learning/blob/master/ios-sockets/SwiftWebSockets/SwiftWebSockets/Networking/NativeWebSocket.swift?ref=dept-engineering-blog.ghost.io">here</a>.</p><hr><h4 id="but-theres-more">But there's more!</h4><h5 id="or-less-securely">or less securely:</h5><p>We now have the ability to go lower level in the Network framework and create our own Swift WebSocket Server or provide custom client side WebSocket implementations. Please view my server <a href="https://github.com/MichaelNeas/perpetual-learning/blob/master/ios-sockets/SwiftWebSockets/SwiftWebSocketServer/SwiftWebSocketServer/SwiftWebSocketServer.swift?ref=dept-engineering-blog.ghost.io">implementation</a> based off the Advances in Networking WWDC talk and a wonderful generic TCP Listener found in the credits.</p><h2 id="final-words">Final Words</h2><p>I have made <a href="https://github.com/MichaelNeas/perpetual-learning/tree/master/ios-sockets?ref=dept-engineering-blog.ghost.io">a few projects</a> recently with the new WebSocket API's. To see an end to end implementation please open the <a href="https://github.com/MichaelNeas/perpetual-learning/blob/master/ios-sockets/SwiftWebSocketClientServer.xcworkspace?ref=dept-engineering-blog.ghost.io">WebSocket Workspace</a>. While some of the Apple developer documentation is limited, it is fully usable and I'm excited to keep playing around! Feel free to borrow my implementation of a <a href="https://github.com/MichaelNeas/perpetual-learning/blob/master/ios-sockets/SwiftWebSockets/SwiftWebSockets/Networking/NativeWebSocket.swift?ref=dept-engineering-blog.ghost.io">WebSocket wrapper</a>. Please check out the <a href="https://github.com/MichaelNeas/perpetual-learning/tree/master/ios-sockets/StarscreamComparison?ref=dept-engineering-blog.ghost.io">comparison between Starscream and Apple's implementation of client side WebSockets</a>, <em>(Scarily similar)</em>. If you see anything you would like to add, have an issue with, or want to help out in any way, please submit a pull request or issue!</p><h3 id="credits">Credits</h3><ul><li><a href="https://github.com/MichaelNeas/perpetual-learning/tree/master/ios-sockets?ref=dept-engineering-blog.ghost.io">Source Code</a></li><li><a href="https://developer.apple.com/documentation/foundation/urlsessionwebsockettask?ref=dept-engineering-blog.ghost.io">WebSocketTask documentation</a></li><li><a href="https://developer.apple.com/documentation/network/nwprotocolwebsocket?ref=dept-engineering-blog.ghost.io">NWProtocolWebSocket documentation</a></li><li><a href="https://developer.apple.com/videos/play/wwdc2019/712/?ref=dept-engineering-blog.ghost.io">WWDC Advances in Networking, part 1</a></li><li><a href="https://developer.apple.com/videos/play/wwdc2019/713/?ref=dept-engineering-blog.ghost.io">WWDC Advances in Networking, part 2</a></li><li><a href="https://rderik.com/blog/building-a-server-client-aplication-using-apple-s-network-framework/?ref=dept-engineering-blog.ghost.io">Generic TCP NWListener</a></li></ul></hr>]]></content:encoded></item><item><title><![CDATA[Best Practices for Terraform AWS Tags]]></title><description><![CDATA[AWS tags are key-value labels you can assign to AWS resources that give extra information about it.]]></description><link>https://engineering.deptagency.com/best-practices-for-terraform-aws-tags/</link><guid isPermaLink="false">6240f2dece0c8f004d7dcf17</guid><category><![CDATA[AWS]]></category><category><![CDATA[Platforms]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[DevOps]]></category><dc:creator><![CDATA[Jirawat Uttayaya]]></dc:creator><pubDate>Sun, 27 Mar 2022 23:28:27 GMT</pubDate><media:content url="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/AdobeStock_113700278_small.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/47/37/4737bc20-e86f-4872-8e5d-fdeb1e0e0c6b/content/images/2022/04/AdobeStock_113700278_small.jpg" alt="Best Practices for Terraform AWS Tags"/><p><em>Update Jan 1, 2022: Thank you for making this blog post so popular. It is a first page Google and Bing search result for <code><a href="https://www.google.com/search?q=aws+terraform+tags&ref=dept-engineering-blog.ghost.io">aws terraform tags</a></code> .</em><br><br><em>Originally we coded the default tags examples for <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/default-tags?ref=dept-engineering-blog.ghost.io">Terraform 0.12</a> . We updated the example code for <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/default-tags-tf1.0?ref=dept-engineering-blog.ghost.io">Terraform 1.0</a> and <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/default-tags-terragrunt?ref=dept-engineering-blog.ghost.io">Terragrunt</a>.</em></br></br></p><p><em>Check out <a href="https://engineering.deptagency.com/best-practices-for-terraform-aws-tags-part-2?ref=dept-engineering-blog.ghost.io">Part 2</a> for solutions to bugs and issues using Terraform AWS Tags in production.</em></p><p><em>Update Feb 17, 2022: I gave a <a href="https://www.hashicorp.com/resources/best-practices-for-terraform-aws-tags?ref=dept-engineering-blog.ghost.io">talk at HashiCorp 2022</a> about this blog post.</em></p><p><a href="https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html?ref=dept-engineering-blog.ghost.io">AWS tags</a> are key-value labels you can assign to AWS resources<br>that give extra information about it.</br></p><p>DEPT® Cloud Engineering practice recommends the following AWS tags best practices:</p><ul><li>Define <a href="https://docs.aws.amazon.com/whitepapers/latest/tagging-best-practices/adopt-a-standardized-approach-for-tag-names.html?ref=dept-engineering-blog.ghost.io">an explicit ruleset</a> for tag key naming and stick with it.Inconsistent tags keys such as "appid", "Application ID", and "App_ID" are frustrating to use.</li><li>Strive for more tags, not less. Tag all AWS resources.</li></ul><p>The minimum Rocket Insights recommended set of tags are</p><ul><li><strong><strong>Name</strong></strong>: human-readable resource name. Note that the AWS Console UI displays the case-sensitive "Name" tag.</li><li><strong><strong>app-id</strong></strong>: the application using the resource</li><li><strong><strong>app-role</strong></strong>: the resource's technical function, e.g. webserver, database</li><li><strong><strong>app-purpose</strong></strong>: the resource's business purpose, e.g. "frontend ui", "payment processor"</li><li><strong><strong>environment</strong></strong>: dev, test, or prod</li><li><strong><strong>project</strong></strong>: what projects use the resource</li><li><strong><strong>owner</strong></strong>: who to contact about the resource</li><li><strong><strong>cost-center</strong></strong>: who to bill for the resource usage</li><li><strong><strong>automation-exclude</strong></strong>: a true/false value for automation to not modify the resource</li><li><strong><strong>pii</strong></strong>: a true/false value if the resource stores personal identifiable information</li><li>Tag all Terraform created AWS resources with code path info.Tagging all AWS resources with the Terraform base and module path results in easier infrastructure maintenance.</li></ul><p>Starting with <a href="https://www.terraform.io/?ref=dept-engineering-blog.ghost.io">Terraform</a> 0.12.31 and <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs?ref=dept-engineering-blog.ghost.io">AWS provider</a> v3.38.0, HashiCorp added the default tags feature.<br>You can now <strong><strong>configure AWS tag in one place</strong></strong> and automatically <strong><strong>apply them to all AWS resources</strong></strong>. You no longer have to manually add <code>tags</code> blocks to each individual Terraform AWS resource.</br></p><p>This Terraform AWS default tags tutorial demonstrates for both Terraform resources and modules:</p><ul><li>Basic usage</li><li>Providing alternate default tags</li><li>Adding and overriding tags to the default</li></ul><p>For the full Terraform code, please visit the Rocket Insights <a href="https://github.com/rocketinsights/terraform-blog-default-tags/tree/main/default-tags?ref=dept-engineering-blog.ghost.io">Github repo</a>.</p><h2 id="basic-usage">Basic Usage</h2><p>The default tags are defined in the AWS provider section. It automatically applies to all AWS resources.</p><pre><code class="language-hcl">locals {
  // Change the local variable to match the git repo name
  terraform-git-repo = "terraform-blog-default-tags"
}

provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = {
	  owner    = "Rocket Insights"	
      project  = "Project A"
      // This regex results in the terraform git 
      // repo name and any sub-directories.
      // For this repo, terraform-base-path is 
      // terraform-blog-default-tags/default-tags
      // This tag helps AWS UI users discover what 
      // Terraform git repo and directory to modify
      terraform-base-path = replace(path.cwd, 
		"/^.*?(${local.terraform-git-repo}\\/)/", "$1")
    }
  }
}

resource "aws_dynamodb_table" "default-tags-basic" {
  name = "default-tags-basic"
  .......
}</code></pre><p>The final tags for <code>aws_dynamodb_table.default-tags-basic</code> are<br><strong><strong>owner </strong></strong>= "Rocket Insights"<br><strong><strong>project </strong></strong>= "Project A"<br><strong><strong>terraform-base-path </strong></strong>= "terraform-blog-default-tags/default-tags"</br></br></br></p><h2 id="alternate-default-tags">Alternate Default Tags</h2><p>If an AWS resource requires alternate default tags, define an alternate AWS provider with new default tags.</p><pre><code class="language-hcl">provider "aws" {
  alias  = "alt-tags"
  region = "us-east-1"
  default_tags {
    tags = {
      app-id = "Terraform Default Tags"
      owner  = "Alt Owner"
    }
  }
}

resource "aws_dynamodb_table" "default-tags-alternate" {
  provider = aws.alt-tags
  name     = "default-tags-alternate"
  ........
}</code></pre><p>The final tags for <code>aws_dynamodb_table.default-tags-alternate</code> are<br><strong><strong>app-id</strong></strong> = "Terraform Default Tags"<br><strong><strong>owner</strong></strong>  = "Alt Owner"</br></br></p><h2 id="add-and-override-default-tags">Add and Override Default Tags</h2><p>If an AWS resource requires more tags in addition to the default tags, simply add the tag to the built-in resource <code>tags</code> block.<br><br>If an AWS resource requires to override a default tag, define a tag with the same key in the <code>tags</code> block.</br></br></p><pre><code class="language-hcl">locals {
  terraform-git-repo = "terraform-blog-default-tags"
}

provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = {
	  owner    = "Rocket Insights"	
      project  = "Project A"
      terraform-base-path = replace(path.cwd, 
		"/^.*?(${local.terraform-git-repo}\\/)/", "$1")
    }
  }
}

resource "aws_dynamodb_table" "default-tags-add" {
  name     = "default-tags-add"
  .......
  
  tags = {
    cost-center = "Rocket Insights Billing"
    project     = "Project Override"
  }
}</code></pre><p>The final tags for <code>aws_dynamodb_table.default-tags-add</code> are<br><strong><strong>cost-center </strong></strong>= "Rocket Insights Billing"<br><strong><strong>owner </strong></strong>= "Rocket Insights"<br><strong><strong>project </strong></strong>= "Project Override"<br><strong><strong>terraform-base-path</strong></strong>  = "terraform-blog-default-tags/default-tags"</br></br></br></br></p><h2 id="modules-basic-usage">Modules Basic Usage</h2><p>The AWS default tags apply to existing Terraform AWS modules without any changes needed.</p><pre><code class="language-hcl">locals {
  terraform-git-repo = "terraform-blog-default-tags"
}

provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = {
	  owner    = "Rocket Insights"	
      project  = "Project A"
      terraform-base-path = replace(path.cwd, 
		"/^.*?(${local.terraform-git-repo}\\/)/", "$1")
    }
  }
}

module "default-tags-dynamodb" {
  source = "./tfmodules/default-tags-dynamodb"
}</code></pre><pre><code class="language-hcl">// In ./tfmodules/default-tags-dynamodb
resource "aws_dynamodb_table" "default-tags-module" {
  name = "default-tags-module"
  ......
}</code></pre><p>The final tags for <code>module.default-tags-dynamodb.aws_dynamodb_table.default-tags-module</code> are<br><strong><strong>owner</strong></strong> = "Rocket Insights"<br><strong><strong>project </strong></strong>= "Project A"<br><strong><strong>terraform-base-path</strong></strong> = "terraform-blog-default-tags/default-tags"</br></br></br></p><h2 id="modules-and-alternate-tags">Modules and Alternate Tags</h2><p>If an AWS module requires alternate default tags, define an alternate AWS provider with new default tags<br>and pass the new provider to the module.</br></p><pre><code class="language-hcl">provider "aws" {
  alias  = "alt-tags"
  region = "us-east-1"
  default_tags {
    tags = {
      app-id = "Terraform Default Tags"
      owner  = "Alt Owner"
    }
  }
}

module "default-tags-dynamodb-alternate" {
  source = "./tfmodules/default-tags-dynamodb"
  providers = {
    aws = aws.alt-tags
  }
  .......
}
</code></pre><pre><code class="language-hcl">// In ./tfmodules/default-tags-dynamodb
resource "aws_dynamodb_table" "default-tags-module" {
  name = "default-tags-module"
  ......
}</code></pre><p>The final tags for <code>module.default-tags-dynamodb-alternate.aws_dynamodb_table.default-tags-module</code> are<br><strong><strong>app-id</strong></strong> = "Terraform Default Tags"<br><strong><strong>owner</strong></strong>  = "Alt Owner"</br></br></p><h2 id="modules-with-both-default-and-alternate-tags">Modules with both Default and Alternate Tags</h2><p>If an AWS module requires both the default and alternate default tags, define the default and alternate AWS providers<br>and pass both providers to module. Then the module assigns the correct AWS provider to the AWS resource.<br><br>This is valuable since certain AWS resources like aws_s3_bucket_object have a <strong><strong>maximum limit of 10 tags</strong></strong>.</br></br></br></p><pre><code class="language-hcl">locals {
  terraform-git-repo = "terraform-blog-default-tags"
}

provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = {
      owner               = "Rocket Insights"
      project             = "Project A"
      terraform-base-path = replace(path.cwd, 
        "/^.*?(${local.terraform-git-repo}\\/)/", "$1")
    }
  }
}

provider "aws" {
  alias  = "alt-tags"
  region = "us-east-1"
  default_tags {
    tags = {
      app-id = "Terraform Default Tags"
      owner  = "Alt Owner"
    }
  }
}
module "default-and-alternate-tags-dynamodb" {
  source = "./tfmodules/default-and-alternate-tags-dynamodb"
  providers = {
    aws          = aws
    aws.alt-tags = aws.alt-tags
  }
}</code></pre><pre><code class="language-hcl">// In ./tfmodules/default-and-alternate-tags-dynamodb
provider "aws" {
  alias = "aws"
}
provider "aws" {
  alias = "alt-tags"
}

resource "aws_dynamodb_table" "default-and-alternate-tags-module-basic" {
  name         = "default-and-alternate-tags-module-basic"
  .......
}

resource "aws_dynamodb_table" "default-and-alternate-tags-module-alternate" {
  provider     = aws.alt-tags
  name         = "default-and-alternate-tags-module-alternate"
  .......
} </code></pre><p>The final tags for the two DynamoDB resources in the default-and-alternate-tags-dynamodb TF modules uses the assigned AWS provider tags<br><br>The final tags for <code>module.default-and-alternate-tags-dynamodb.aws_dynamodb_table.default-and-alternate-tags-module-basic</code> are<br><strong><strong>owner</strong></strong> = "Rocket Insights"<br><strong><strong>project</strong></strong>  = "Project A"<br><strong><strong>terraform-base-path</strong></strong> = "terraform-blog-default-tags/default-tags"<br><br>The final tags for <code>module.default-and-alternate-tags-dynamodb.aws_dynamodb_table.default-and-alternate-tags-module-alternate</code> are<br><strong><strong>app-id</strong></strong> = "Terraform Default Tags"<br><strong><strong>owner</strong></strong>  = "Alt Owner"</br></br></br></br></br></br></br></br></br></p><h2 id="modules-and-adding-and-overriding-default-tags">Modules and Adding and Overriding Default Tags</h2><p>If an AWS module requires more tags in addition to the default tags, simply define the module tags variable and add the tag to the module.</p><pre><code class="language-hcl">locals {
  terraform-git-repo = "terraform-blog-default-tags"
}

provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = {
      owner               = "Rocket Insights"
      project             = "Project A"
      terraform-base-path = replace(path.cwd, 
        "/^.*?(${local.terraform-git-repo}\\/)/", "$1")
    }
  }
}

module "default-tags-dynamodb-add" {
  source = "./tfmodules/default-tags-dynamodb-add"

  tags = {
    app-purpose = "Adding Module Tags"
  }
}</code></pre><pre><code class="language-hcl">// In ./tfmodules/default-tags-dynamodb-add
variable "tags" {
  type        = map(string)
  description = "Additional tags for DynamoDB table"
}
resource "aws_dynamodb_table" "default-tags-module-add" {
  name         = "default-tags-module-add"
  .......
  tags = merge(
      // path.module is a built-in Terraform variable that
      // describes the path of Terraform module
      // For this repo, the terraform-module-path is
      // tfmodules/default-tags-dynamodb-add
      // Along with the terraform-base-path tag,
      // this tag helps AWS UI users
      // discover what module repo to modify
    { terraform-module-path = path.module },
    var.tags
  )
}</code></pre><p>The final tags for <code>module.default-tags-dynamodb-add.aws_dynamodb_table.default-tags-module-add</code> are<br><strong><strong>app-purpose</strong></strong> = "Adding Module Tags"<br><strong><strong>owner </strong></strong>= "Rocket Insights"<br><strong><strong>project </strong></strong>= "Project A"<br><strong><strong>terraform-base-path</strong></strong>   = "terraform-blog-default-tags/default-tags"<br><strong><strong>terraform-module-path</strong></strong> = "tfmodules/default-tags-dynamodb-add"</br></br></br></br></br></p><h2 id="conclusion">Conclusion</h2><p>Terraform default tags for AWS are an easy way to <strong><strong>add metadata to all AWS resources</strong></strong>.<br>Defining the default tags in one code location follows the best practice of <strong><strong>DRY (Don't Repeat Yourself)</strong></strong>.<br>If you have complex tagging requirements, <strong><strong>Terraform is customizable</strong></strong> to meet your needs.</br></br></p>]]></content:encoded></item></channel></rss>