<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>The Ops Community ⚙️: briancaffey</title>
    <description>The latest articles on The Ops Community ⚙️ by briancaffey (@briancaffey).</description>
    <link>https://community.ops.io/briancaffey</link>
    <image>
      <url>https://community.ops.io/images/QnT7cEo3WDld7zIEalJPNVgUMBIxBk4AIYas0XvNHks/rs:fill:90:90/g:sm/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL3Vz/ZXIvcHJvZmlsZV9p/bWFnZS85MDUvNmZh/MWM4ZTctZjkwNi00/YzIyLWI5M2ItOGU1/YjZlMDcwODIwLmpw/ZWc</url>
      <title>The Ops Community ⚙️: briancaffey</title>
      <link>https://community.ops.io/briancaffey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://community.ops.io/feed/briancaffey"/>
    <language>en</language>
    <item>
      <title>My Infrastructure as Code Rosetta Stone - Deploying the same web application on AWS ECS Fargate with CDK, Terraform and Pulumi</title>
      <dc:creator>briancaffey</dc:creator>
      <pubDate>Sat, 07 Jan 2023 20:40:55 +0000</pubDate>
      <link>https://community.ops.io/briancaffey/my-infrastructure-as-code-rosetta-stone-deploying-the-same-web-application-on-aws-ecs-fargate-with-cdk-terraform-and-pulumi-job</link>
      <guid>https://community.ops.io/briancaffey/my-infrastructure-as-code-rosetta-stone-deploying-the-same-web-application-on-aws-ecs-fargate-with-cdk-terraform-and-pulumi-job</guid>
      <description>&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;I wrote three infrastructure as code libraries for deploying containerized 3-tier web apps on AWS ECS Fargate using CDK, Terraform and Pulumi. This article will provide an overview of my experience working with these three IaC tools and will show how I use my libraries in automated infrastructure deployment pipelines with GitHub Actions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CDK Construct Library&lt;/strong&gt;: &lt;a href="https://github.com/briancaffey/cdk-django"&gt;github.com/briancaffey/cdk-django&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Modules&lt;/strong&gt;: &lt;a href="https://github.com/briancaffey/terraform-aws-django"&gt;github.com/briancaffey/terraform-aws-django&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pulumi Component Library&lt;/strong&gt;: &lt;a href="https://github.com/briancaffey/pulumi-aws-django"&gt;github.com/briancaffey/pulumi-aws-django&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mono repo with a sample Django micro blogging app (μblog) and frontend app (Vue SPA written with Quasar), GitHub Action workflows for infrastructure and (separate) application deployment pipelines, IaC code that &lt;em&gt;consumes&lt;/em&gt; each of the libraries listed above, &lt;a href="https://briancaffey.github.io/django-step-by-step/"&gt;VuePress documentation site&lt;/a&gt; and miscellaneous items (k6 load testing scripts, Cypress tests, docker-compose, etc.): &lt;a href="https://github.com/briancaffey/django-step-by-step"&gt;github.com/briancaffey/django-step-by-step&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  eli5
&lt;/h2&gt;

&lt;p&gt;Pretend we are at the beach building sandcastles. We can build sandcastles using our hands, but this takes a lot of time, and we might bump into each other and accidentally knock over part of our sandcastle. I made some tools for building sandcastles. We have one tool for building a sand castle base that includes the wall around the outside, the moat, the door and different sections inside the walls. And I made another tool for deploying smaller sand castle houses inside the walls of the sandcastle base. We fill the tool with sand and water and then turn it over inside of our base and we can build an entire city of sandcastles. Also, the tool lets us carefully remove parts of our sandcastle without knocking over any of the other parts. We can share the tool with all of our friends and they can make cool sandcastles too, and the tool is free for them to use.&lt;/p&gt;

&lt;p&gt;Instead of sandcastles, I'm working with computer systems that can power internet applications, like YouTube for example. I'm building tools that can allow me or anyone else to build really awesome internet applications using computers.&lt;/p&gt;

&lt;p&gt;The tools are not physical tools like the ones for building sandcastles, but instead these tools are made with code. The code for websites like YouTube allow you upload videos &lt;em&gt;to YouTube&lt;/em&gt;, but the code I'm writing allows you to upload any type of website (even on like YouTube) &lt;em&gt;to the internet&lt;/em&gt;. When we run this code, it creates applications on the internet. Also, sand is very expensive and Jeff Bezos owns the beach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made an Infrastructure as Code Rosetta Stone with CDK, Terraform and Pulumi
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/SOJv-3M53Z1cHBCv__Qzqa4fOSpDkZyHCtUjEsUM-fY/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L2lhY19yb3NldHRh/X3N0b25lX29nX2lt/YWdlLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/SOJv-3M53Z1cHBCv__Qzqa4fOSpDkZyHCtUjEsUM-fY/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L2lhY19yb3NldHRh/X3N0b25lX29nX2lt/YWdlLnBuZw" alt="IaC Rosetta Stone" width="880" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To push myself to learn more about AWS, IaC, CI/CD, automation and Platform Engineering&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Learn differences between major IaC tools and how to use them to do exactly the same thing (build a web app) on the same Cloud (AWS) in the same way (serverless container technology using ECS Fargate).&lt;/li&gt;
&lt;li&gt;Get more experience publishing software packages (npm) and finding the right level of abstraction for IaC libraries that is both dynamic and straightforward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To fail as many times as possible&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every time I fail when I think I have things right, I learn something new&lt;/li&gt;
&lt;li&gt;Failed IaC pipelines can sometimes be scary, and every failure I have on these project can teach me about potential failure modes for live projects running in production&lt;/li&gt;
&lt;li&gt;You can often times be "stuck" where you have a set of resources that you can't update or delete. Learning to get unstuck from these scenarios is important&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To take an application-first approach to DevOps&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application developers are increasingly being tasked with operational duties&lt;/li&gt;
&lt;li&gt;While learning about IaC, I had a hard time finding in-depth materials covering application development, CI/CD pipelines and automation and Infrastructure as Code and how these three knowledge domains work together. There are important considerations to make when  between a Hello World docker image&lt;/li&gt;
&lt;li&gt;You could probably use another framework with these IaC libraries like Flask or Rails, but for now I'm building these projects with Django first-in-mind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To develop a project I can reference when helping myself and others&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;companies and projects that do IaC and CI/CD for the most part have things in private repos for obvious reasons, there isn't any good reason to share this type of code unless you are sharing it with an auditor&lt;/li&gt;
&lt;li&gt;Hopefully the sample application, IaC and CI/CD pipelines &lt;em&gt;aren't overly complex&lt;/em&gt;. There are more complex examples of open source companies out there, but their repos have steep learning curves and a lot going on&lt;/li&gt;
&lt;li&gt;People often ask about how to split up IaC deployments and application deployments. I want to be able to use this project to &lt;strong&gt;show&lt;/strong&gt; people how it can be done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To encourage others (specifically Developer Advocates / Developer Relations / Solutions Architects in the CDK, Terraform and Pulumi communities) to share complete and non-trivial examples of IaC software **in use&lt;/strong&gt; with an actual application.**&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are many ways one could create "IaC Rosetta Stone" (&lt;code&gt;public cloud providers x CI/CD providers x IaC tools&lt;/code&gt; is a big number)&lt;/li&gt;
&lt;li&gt;This takes a lot of effort and time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;I have nothing to sell you&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;So many articles about Cloud/DevOps are trying to sell you a tool. Outside of what I consider to be mainstream vendors like GitHub and AWS, there are no products that I'm promoting here&lt;/li&gt;
&lt;li&gt;I'm also not trying to sell anyone on using my IaC packages&lt;/li&gt;
&lt;li&gt;Hopefully my IaC packages can serve as helpful reference or starting point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Walk before running&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I want to build up confidence with vanilla use cases before getting too fancy&lt;/li&gt;
&lt;li&gt;With a solid foundation in these tools, I want to learn about some of the more advanced patterns teams are adopting (Pulumi Automation API, Terragrunt for Terraform, self-mutating CDK Pipelines)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;12 Factor App, DevOps and Platform Engineering&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://12factor.net/"&gt;12 Factor App&lt;/a&gt; is great, and has guided how I approach both Django application development and IaC library development&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://platformengineering.org/"&gt;platformengineering.org&lt;/a&gt; community has some good guiding principles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CDK/Terraform/Pulumi terminology
&lt;/h2&gt;

&lt;h3&gt;
  
  
  constructs, modules and components
&lt;/h3&gt;

&lt;p&gt;A CDK construct, Terraform module and Pulumi component generally mean the same thing: an abstract grouping of one or more cloud resources.&lt;/p&gt;

&lt;p&gt;In this article I will refer to &lt;strong&gt;constructs/modules/components&lt;/strong&gt; as &lt;strong&gt;c/m/c&lt;/strong&gt; for short, and the term &lt;strong&gt;stack&lt;/strong&gt; can generally be used to refer to either a CloudFormation stack, a Pulumi Stack or a Terraform group of resources that are part of a module that has had &lt;code&gt;apply&lt;/code&gt; ran against it.&lt;/p&gt;

&lt;h3&gt;
  
  
  what is a stack?
&lt;/h3&gt;

&lt;p&gt;AWS has a resource type called CloudFormation Stacks, and Pulumi also has a concept of stacks. Terraform documentation doesn't refer to stacks, and instead in Terraform docs use the words "Terraform configuration" to refer to some group of resources that were built using a module.&lt;/p&gt;

&lt;p&gt;CDK Constructs and Pulumi Components are somewhat similar, however CDK Constructs map to CloudFormation and the Pulumi components I'm using from the &lt;code&gt;@pulumi/aws&lt;/code&gt; package generally map directly to Terraform resources from the AWS Provider (the Pulumi AWS Provider uses much of the same code that the Terraform AWS Provider uses).&lt;/p&gt;

&lt;h3&gt;
  
  
  verbs
&lt;/h3&gt;

&lt;p&gt;In CDK you &lt;code&gt;synth&lt;/code&gt; CDK code to generate CloudFormation templates. You can also run &lt;code&gt;diff&lt;/code&gt; to see what changes would be applied during a stack update.&lt;/p&gt;

&lt;p&gt;In Terraform you &lt;code&gt;init&lt;/code&gt; to download all providers and modules. This is sort of like running &lt;code&gt;npm install&lt;/code&gt; in CDK and Pulumi. You then run &lt;code&gt;terraform plan&lt;/code&gt; to see the changes that would result. &lt;code&gt;terraform apply&lt;/code&gt; does CRUD operations on your cloud resources.&lt;/p&gt;

&lt;p&gt;In Pulumi you run &lt;code&gt;pulumi preview&lt;/code&gt; to see what changes would be made to a stack. You can use the &lt;code&gt;--diff&lt;/code&gt; flag to see the specifics of what would change.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In CDK you synth CloudFormation and use these templates to deploy stacks made up of constructs. An "app" can contain multiple stacks, and you can deploy one or more stacks in an app at a time&lt;/li&gt;
&lt;li&gt;In Terraform you plan a configuration made up of modules, and then run &lt;code&gt;terraform apply&lt;/code&gt; to build the configuration/stack (&lt;a href="https://discuss.hashicorp.com/t/what-is-a-terraform-stack/31985"&gt;discuss.hashicorp.com/t/what-is-a-terraform-stack/31985&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Pulumi: You preview a Pulumi stack made up of components, and then run &lt;code&gt;pulumi up&lt;/code&gt; to build the resources&lt;/li&gt;
&lt;li&gt;To tear down a stack in all three tools, you run &lt;code&gt;destroy&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Infrastructure as Code library repos
&lt;/h2&gt;

&lt;p&gt;Let's look at the three repos that I wrote for deploying the same type of 3-tier web application to AWS using ECS Fargate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDK: &lt;a href="https://github.com/briancaffey/cdk-django"&gt;&lt;code&gt;cdk-django&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Terraform: &lt;a href="https://github.com/briancaffey/terraform-aws-django"&gt;&lt;code&gt;terraform-aws-django&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pulumi: &lt;a href="https://github.com/briancaffey/pulumi-aws-django"&gt;&lt;code&gt;pulumi-aws-django&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Language
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cdk-django&lt;/code&gt; and &lt;code&gt;pulumi-aws-django&lt;/code&gt; are both written in TypeScript. &lt;code&gt;terraform-aws-django&lt;/code&gt; is written in HCL, a domain specific language created by HashiCorp. The &lt;code&gt;cdk-django&lt;/code&gt; is published to both npm and PyPI, so you can use it in JavaScript, TypeScript and Python projects, other languages are supported as well, but you need to write your library in TypeScript so it can be transpiled to other languages using &lt;a href="https://github.com/aws/jsii"&gt;jsii&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My Pulumi library is written in TypeScript and is published to NPM. For now it can only be used in JavaScript and TypeScript projects. There is a way in Pulumi to write in any language and then publish to any other major language, but I haven't  done this yet. See &lt;a href="https://github.com/pulumi/pulumi-component-provider-ts-boilerplate"&gt;this GitHub repo&lt;/a&gt; for more information on this.&lt;/p&gt;

&lt;p&gt;The HCL is pretty simple when you get used to it. I find that I don't like adding lots of logic in Terraform code because it takes away from the readability of a module. There is a tool called &lt;a href="https://developer.hashicorp.com/terraform/cdktf"&gt;CDKTF&lt;/a&gt; which allows you to write HCL Terraform in TypeScript, but I haven't used it yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Release management, versioning and publishing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pulumi-aws-django&lt;/code&gt; and &lt;code&gt;terraform-aws-django&lt;/code&gt; both use &lt;code&gt;release-please&lt;/code&gt; for automatically generating a changelog file and bumping versions. &lt;code&gt;release-please&lt;/code&gt; is an open source tool from Google that they use to version their Terraform GCP modules. Whenever I push new commits to &lt;code&gt;main&lt;/code&gt;, a new PR is created that adds changes to the CHANGELOG.md file, bumps the version of the library in &lt;code&gt;package.json&lt;/code&gt; and adds a new git tag (e.g. &lt;code&gt;v1.2.3&lt;/code&gt;) based on commit messages.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cdk-django&lt;/code&gt; uses &lt;a href="https://github.com/projen/projen"&gt;&lt;code&gt;projen&lt;/code&gt;&lt;/a&gt; for maintaining the changelog and bumping versions and publishing to npm. It is popular among developers in the CDK community and is a really awesome tool since it basically uses one file (&lt;code&gt;.projenrc.ts&lt;/code&gt;) to configure your entire repo, including files like &lt;code&gt;tsconfig.json&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, and even GitHub Action workflows. It has a lot of configuration options, but I'm using it in a pretty simple way. It generates a new release and items to the changelog when I manually trigger a GitHub Action.&lt;/p&gt;

&lt;p&gt;These tools are both based on &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/"&gt;conventional commits&lt;/a&gt; to automatically update the Changelog file.&lt;/p&gt;

&lt;p&gt;I'm still manually publishing my &lt;code&gt;pulumi-aws-django&lt;/code&gt; package from the CLI. I need to add a GitHub Action to do this for me. This and other backlog items are listed at the end of the article!&lt;/p&gt;

&lt;h3&gt;
  
  
  Makefile, examples and local development
&lt;/h3&gt;

&lt;p&gt;Each repo has a Makefile that includes commands that I frequently use when developing new features or fixing bugs. Each repo has commands for the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synthesizing CDK to CloudFormation / running &lt;code&gt;terraform plan&lt;/code&gt; / previewing pulumi up for both the base and app stacks&lt;/li&gt;
&lt;li&gt;creating/updating an ad hoc base stack called &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;destroying resources in the ad-hoc base stack called &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creating an ad hoc app stack called &lt;code&gt;alpha&lt;/code&gt; that uses resources from &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;destroying an ad hoc app stack called &lt;code&gt;alpha&lt;/code&gt; that uses resources from &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creating/updating a prod base stack called &lt;code&gt;stage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;destroying resources in the prod base stack called &lt;code&gt;stage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creating a prod app stack using called &lt;code&gt;stage&lt;/code&gt; that uses resources from the &lt;code&gt;stage&lt;/code&gt; base stack&lt;/li&gt;
&lt;li&gt;destroying resources in the prod app stack called &lt;code&gt;stage&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example of what these commands look like in &lt;code&gt;pulumi-aws-django&lt;/code&gt; for prod infrastructure base and app stacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;prod-base-preview&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/base &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; preview

&lt;span class="nl"&gt;prod-base-up&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/base &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; up &lt;span class="nt"&gt;--yes&lt;/span&gt;

&lt;span class="nl"&gt;prod-base-destroy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/base &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; destroy &lt;span class="nt"&gt;--yes&lt;/span&gt;

&lt;span class="nl"&gt;prod-app-preview&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; preview

&lt;span class="nl"&gt;prod-app-preview-diff&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; preview &lt;span class="nt"&gt;--diff&lt;/span&gt;

&lt;span class="nl"&gt;prod-app-up&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; up &lt;span class="nt"&gt;--yes&lt;/span&gt;

&lt;span class="nl"&gt;prod-app-destroy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; destroy &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I currently don't have tests for all of these libraries, but for now the most effective way of testing that things are working correctly is to use the &lt;code&gt;c/m/c&lt;/code&gt;s to create environments and smoke check the environments to make sure everything works correctly.&lt;/p&gt;

&lt;p&gt;Adding unit tests is another item for the backlog.&lt;/p&gt;

&lt;h3&gt;
  
  
  ad-hoc vs prod
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://briancaffey.github.io/2022/03/27/ad-hoc-developer-environments-for-django-with-aws-ecs-terraform-and-github-actions"&gt;the last article I wrote was about ad hoc environments&lt;/a&gt;. Also known as "on-demand" environments or "preview" environments.&lt;/li&gt;
&lt;li&gt;the motivation for using ad-hoc environments is speed and cost (you can stand up an environment in less time and you share the costs of the base environment, including VPC, ALB, RDS)&lt;/li&gt;
&lt;li&gt;you can completely ignore "ad-hoc" environments and use the "prod" infrastructure for any number of environments (such as dev, QA, RC, stage and prod)&lt;/li&gt;
&lt;li&gt;prod can be used for a production environment and any number of pre-production environments&lt;/li&gt;
&lt;li&gt;multiple environments built with "prod" infrastructure can be configured with a "knobs and dials" (e.g., how big are app and DB instances, how many tasks to run in a service, etc.)&lt;/li&gt;
&lt;li&gt;the "prod" infrastructure should be the same for the "production" environment and the "staging" environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Directory structure
&lt;/h3&gt;

&lt;p&gt;The directory structures for each repo are all similar with some minor differences.&lt;/p&gt;

&lt;p&gt;There are two types of environments: &lt;code&gt;ad-hoc&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt;. Within ad-hoc and production, there are two directories &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each repo has a directory called &lt;code&gt;internal&lt;/code&gt; which contain building blocks used by the &lt;code&gt;c/m/c&lt;/code&gt;s that are exposed. The contents of the &lt;code&gt;internal&lt;/code&gt; directories are not intended to be used by anyone who is using the libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDK construct library repo structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/cdk-django$ tree -L 4 -d src/
src/
├── constructs
│   ├── ad-hoc
│   │   ├── app
│   │   └── base
│   ├── internal
│   │   ├── alb
│   │   ├── bastion
│   │   ├── customResources
│   │   │   └── highestPriorityRule
│   │   ├── ecs
│   │   │   ├── iam
│   │   │   ├── management-command
│   │   │   ├── redis
│   │   │   ├── scheduler
│   │   │   ├── web
│   │   │   └── worker
│   │   ├── rds
│   │   ├── sg
│   │   └── vpc
│   └── prod
│       ├── app
│       └── base
└── examples
    └── ad-hoc
        ├── app
        │   └── config
        └── base
            └── config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Terraform module library repo structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/terraform-aws-django$ tree -L 4 -d modules
modules
├── ad-hoc
│   ├── app
│   └── base
├── internal
│   ├── alb
│   ├── autoscaling
│   ├── bastion
│   ├── ecs
│   │   ├── ad-hoc
│   │   │   ├── celery_beat
│   │   │   ├── celery_worker
│   │   │   ├── cluster
│   │   │   ├── management_command
│   │   │   ├── redis
│   │   │   └── web
│   │   └── prod
│   │       ├── celery_beat
│   │       ├── celery_worker
│   │       ├── cluster
│   │       ├── management_command
│   │       └── web
│   ├── elasticache
│   ├── iam
│   ├── rds
│   ├── route53
│   ├── s3
│   ├── sd
│   └── sg
└── prod
    ├── app
    └── base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pulumi component library repo structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/pulumi-aws-django$ tree -L 3 src/
src/
├── components
│   ├── ad-hoc
│   │   ├── README.md
│   │   ├── app
│   │   └── base
│   └── internal
│       ├── README.md
│       ├── alb
│       ├── bastion
│       ├── cw
│       ├── ecs
│       ├── iam
│       ├── rds
│       └── sg
└── util
    ├── index.ts
    └── taggable.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pulumi examples directory&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/pulumi-aws-django$ tree -L 3 examples/
examples/
└── ad-hoc
    ├── app
    │   ├── Pulumi.alpha.yaml
    │   ├── Pulumi.yaml
    │   ├── index.ts
    │   ├── node_modules
    │   ├── package-lock.json
    │   ├── package.json
    │   └── tsconfig.json
    └── base
        ├── Pulumi.yaml
        ├── bin
        ├── index.ts
        ├── package-lock.json
        ├── package.json
        └── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CLOC
&lt;/h3&gt;

&lt;p&gt;Let's use CLOC (count lines of code) to compare the lines of code used in the &lt;code&gt;c/m/c&lt;/code&gt; of CDK/CloudFormation/Terraform/Pulumi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cdk-django&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/cdk-django$ cloc src/constructs/
      14 text files.
      14 unique files.
       0 files ignored.

github.com/AlDanial/cloc v 1.94  T=0.04 s (356.1 files/s, 30040.9 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
TypeScript                      13            155             59            908
Python                           1             18              8             33
-------------------------------------------------------------------------------
SUM:                            14            173             67            941
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;terraform-aws-django&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/terraform-aws-django$ cloc modules/
      68 text files.
      58 unique files.
      11 files ignored.

github.com/AlDanial/cloc v 1.94  T=0.15 s (385.9 files/s, 20585.1 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
HCL                             55            472            205           2390
Markdown                         3              7              0             20
-------------------------------------------------------------------------------
SUM:                            58            479            205           2410
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;pulumi-aws-django&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/pulumi-aws-django$ cloc src/components/
      15 text files.
      15 unique files.
       0 files ignored.

github.com/AlDanial/cloc v 1.94  T=0.11 s (134.5 files/s, 12924.2 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
TypeScript                      13            110            176           1119
Markdown                         2              6              0             30
-------------------------------------------------------------------------------
SUM:                            15            116            176           1149
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Communities
&lt;/h2&gt;

&lt;p&gt;The CDK, Terraform and Pulumi communities are all great and a lot of people helped when I got stuck on issues writing these libraries. Thank you!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cdk.dev/"&gt;cdk.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discuss.hashicorp.com/c/terraform-core/27"&gt;Terraform Section of HashiCorp Discuss Forum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://slack.pulumi.com/"&gt;Pulumi Slack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  μblog
&lt;/h2&gt;

&lt;p&gt;μblog is a micro blogging application that I have written using Django and Vue.js. Here's a screenshot of the homepage:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/7-Zue-N3cYCdQOp5yPtZP0_NI_zKWvfR0m38D9E8rrs/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L3VibG9nX3NjcmVl/bnNob3QucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/7-Zue-N3cYCdQOp5yPtZP0_NI_zKWvfR0m38D9E8rrs/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L3VibG9nX3NjcmVl/bnNob3QucG5n" alt="ublog" width="880" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is a pretty simple app. Users can write posts with text and an optional images. Logged in users can write posts and like posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mono-repo structure
&lt;/h3&gt;

&lt;p&gt;It lives in a GitHub mono repo called &lt;code&gt;django-step-by-step&lt;/code&gt;. This mono repo contains a few different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend Django application&lt;/li&gt;
&lt;li&gt;frontend Vue.js application&lt;/li&gt;
&lt;li&gt;IaC code that uses c/m/c from &lt;code&gt;cdk-django&lt;/code&gt;, &lt;code&gt;terraform-aws-django&lt;/code&gt; and &lt;code&gt;pulumi-aws-django&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions workflows for both Infrastructure deployments and application deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;μblog is the reference application that I deploy to infrastructure created with CDK, Terraform and Pulumi. μblog is meant to represent a generic 12 Factor application that uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gunicorn for a backend API&lt;/li&gt;
&lt;li&gt;Vue.js for a client that consumes the backend API&lt;/li&gt;
&lt;li&gt;celery for async task processing&lt;/li&gt;
&lt;li&gt;celery beat for scheduling tasks&lt;/li&gt;
&lt;li&gt;Postgres for relational data&lt;/li&gt;
&lt;li&gt;Redis for caching and message brokering&lt;/li&gt;
&lt;li&gt;S3 for object storage&lt;/li&gt;
&lt;li&gt;Django admin for a simple admin interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a lot more I could add on μblog. For now I'll just mention that it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;has a great local development environment (supports both docker-compose and virtual environments)&lt;/li&gt;
&lt;li&gt;demonstrates how to use Django in different ways. It implements the same application using Function Based View and Class Based Views, and implements both a REST API (both with FBV and CBV) and GraphQL.&lt;/li&gt;
&lt;li&gt;GitHub Actions for running unit tests&lt;/li&gt;
&lt;li&gt;k6 for load testing&lt;/li&gt;
&lt;li&gt;contains a documentation site deployed to GitHub pages (made with VuePress) can be found here: &lt;a href="https://briancaffey.github.io/django-step-by-step/"&gt;https://briancaffey.github.io/django-step-by-step/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Infrastructure Deep Dive
&lt;/h1&gt;

&lt;p&gt;Let's go through each of the &lt;code&gt;c/m/c&lt;/code&gt;s used in the three libraries. I'll cover some of the organizational decisions, dependencies and differences between how things are done between CDK, Terraform and Pulumi.&lt;/p&gt;

&lt;p&gt;I'll first talk about the two stacks used in ad hoc environments: &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;app&lt;/code&gt;. Then I'll talk about the prod environments which are also composed of &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;app&lt;/code&gt; stacks.&lt;/p&gt;

&lt;p&gt;Keep in mind that there aren't that many differences between the ad hoc environment base and app stacks and the prod environment app and base stacks. A future optimization could be to use a single base and app stack, but I think there is a trade-off between readability and DRYness of infrastructure code, &lt;strong&gt;especially with Terraform&lt;/strong&gt;. In general I try to use very little conditionals and logic with Terraform code. It is much easier to have dynamic configuration in CDK and Pulumi, and probably also for other tools like CDKTF (that I have not yet tried).&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting up the stacks
&lt;/h2&gt;

&lt;p&gt;While it is possible to put all resources in a single stack with both Terraform, CDK and Pulumi, it is not recommended to do so.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform enables this with outputs and &lt;code&gt;terraform_remote_state&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pulumi encourages the use of &lt;a href="https://www.pulumi.com/docs/guides/organizing-projects-stacks/"&gt;micro stacks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CDK has an article on how to &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/stack_how_to_create_multiple_stacks.html"&gt;create an app with multiple stacks&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My design decision was to keep things limited to 2 stacks. Later on it would be interesting to try splitting out another stack.&lt;/p&gt;

&lt;p&gt;Also, on-demand environments really lends itself to stacks that are split up.&lt;/p&gt;

&lt;p&gt;In the section &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/resources.html"&gt;"Passing unique identifiers"&lt;/a&gt;, the CDK recommends that we keep the two stacks in the same app. In Terraform and Pulumi, each stack environment is in its own app.&lt;/p&gt;

&lt;p&gt;There is a balance to be found between single stacks vs micro stacks. Both the base and app &lt;code&gt;c/m/c&lt;/code&gt;s could be split out further. For example, the &lt;code&gt;base&lt;/code&gt; &lt;code&gt;c/m/c&lt;/code&gt;s could be split into &lt;code&gt;networking&lt;/code&gt; and &lt;code&gt;rds&lt;/code&gt;. The &lt;code&gt;app&lt;/code&gt; stack could be split into different ECS services so that their infrastructure can be deployed independently, like &lt;code&gt;cluster&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;frontend&lt;/code&gt;. The more resources that a stack has, the longer it takes to deploy and the more risky it gets, but adding lots of stacks can add to mental overhead, and pipeline complexity. Each tool has ways of dealing with these complexities (CDK Pipelines, Terragrunt, Pulumi Automation API), but I won't be getting into any of these options in this article. I would like to try these out and share in a future article.&lt;/p&gt;

&lt;p&gt;My rules of thumbs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single stacks are bad because you don't want to put all your eggs in one basket, however your IaC tool should give you confidence about what is going to change when you try to make a change&lt;/li&gt;
&lt;li&gt;Lots of small stacks can cause overhead and make things more complex than they need to be&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ad hoc base overview
&lt;/h2&gt;

&lt;p&gt;Here's an overview of the resources used in an ad hoc base environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;(Inputs)&lt;/li&gt;
&lt;li&gt;(Optional environment configs)&lt;/li&gt;
&lt;li&gt;VPC and Service Discovery&lt;/li&gt;
&lt;li&gt;S3&lt;/li&gt;
&lt;li&gt;Security Groups&lt;/li&gt;
&lt;li&gt;Load Balancer&lt;/li&gt;
&lt;li&gt;RDS&lt;/li&gt;
&lt;li&gt;Bastion Host&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Visualization
&lt;/h3&gt;

&lt;p&gt;Here's a dependency graph showing all of the resources in ad hoc base stack. This can be found on the &lt;code&gt;Resources&lt;/code&gt; tab of the ad hoc base stack in the Pulumi console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/8eAoVEESsKVAF00aYRxBWmz9VM_rNWIm-IYSBH4GuXM/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L3B1bHVtaV9hZF9o/b2NfYmFzZV9kZXBf/Z3JhcGgucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/8eAoVEESsKVAF00aYRxBWmz9VM_rNWIm-IYSBH4GuXM/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L3B1bHVtaV9hZF9o/b2NfYmFzZV9kZXBf/Z3JhcGgucG5n" alt="Graph view of ad hoc base infrastructure" width="880" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Inputs
&lt;/h3&gt;

&lt;p&gt;There are only two required inputs for the ad hoc base stack&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ACM certificate ARN&lt;/li&gt;
&lt;li&gt;Domain Name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I store these values in environment variables for the pipelines in CDK, Terraform and Pulumi. When running pipelines from my local environment, they are exported in my shell before running deploy/apply/up or synth/plan/preview.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC
&lt;/h3&gt;

&lt;p&gt;The VPC is the first resource that is created as part of the &lt;code&gt;base&lt;/code&gt; stack. There official, high-level constructs in each IaC tool for building VPCs and all related networking resources.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;awsx&lt;/code&gt; has a VPC module&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform-aws-vpc&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;L2 VPC Construct in CDK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setting in the Terraform VPC module &lt;code&gt;one_nat_gateway_per_az = false&lt;/code&gt; doesn't seem to exist on the &lt;code&gt;awsx.ec2.Vpc&lt;/code&gt; module. This will add to cost savings since it will use 1 NAT Gateway instead of 2 or 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Groups
&lt;/h3&gt;

&lt;p&gt;Pulumi and Terraform can be used in a similar way to define security groups. CDK has a much more concise option for defining ingress and egress rules for security groups.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;albSecurityGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SecurityGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AlbSecurityGroup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;albSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addIngressRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anyIpv4&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTPS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;albSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addIngressRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anyIpv4&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Load Balancer Resources
&lt;/h3&gt;

&lt;p&gt;There's not much to comment on here. In each library I have resource group that defines the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application Load Balancer&lt;/li&gt;
&lt;li&gt;A default target group&lt;/li&gt;
&lt;li&gt;An HTTP listener that redirects to HTTPS&lt;/li&gt;
&lt;li&gt;An HTTPS listener with a default "fixed-response" action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Properties from these resources are used in the "app" stack to build listener rules for ECS services that are configured with load balancers, such as the backend and frontend web services.&lt;/p&gt;

&lt;p&gt;Ad hoc app environments all share a common load balancer from the base stack that they use.&lt;/p&gt;

&lt;h3&gt;
  
  
  RDS Resources
&lt;/h3&gt;

&lt;p&gt;All three libraries have the RDS security group and Subnet Group in the same &lt;code&gt;c/m/c&lt;/code&gt; as the RDS instance. The SG and DB Subnet group could alternatively be grouped closer to the other network resources.&lt;/p&gt;

&lt;p&gt;Currently the RDS resources are part of the "base" stack for each library. A future optimization may be to break the RDS instance out of the "base" stack and put it in its own stack. The "RDS" stack would be dependent on the "base" stack, and then "app" stack would then be dependent on both the "base" stack and the "RDS" stack. More stacks isn't necessarily a bad thing, but for my initial implementation of these libraries I have decided to keep the "micro stacks" approach limited to only 2 stacks for an environment.&lt;/p&gt;

&lt;p&gt;The way that database secrets are handled is another difference between CDK and Terraform and Pulumi. I am currently "hardcoding" the RDS password for Terraform and Pulumi, and in CDK I am using a Secrets Manager Secret for the database credential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dbSecret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dbSecretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret for rds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;generateSecretString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;secretStringTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;generateStringKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;excludePunctuation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;includeSpace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;DatabaseInstance&lt;/code&gt; props we can then use this secret like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    credentials: Credentials.fromSecret(secret),
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the application deployed with CDK, I use a Django settings module package that uses a package called &lt;code&gt;aws_secretsmanager_caching&lt;/code&gt; to get and cache the secrets manager secret for the database, whereas in the apps deployed with Terraform and Pulumi I read in the password from an environment variable.&lt;/p&gt;

&lt;p&gt;The Terraform and Pulumi database instance arguments simply accept a &lt;code&gt;password&lt;/code&gt; field. This will be another item for the backlog for Terraform and Pulumi. The &lt;a href="https://www.pulumi.com/registry/packages/random/api-docs/randompassword/"&gt;&lt;code&gt;randompassword&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.pulumi.com/registry/packages/aws/api-docs/secretsmanager/secretversion/"&gt;&lt;code&gt;secretversion&lt;/code&gt;&lt;/a&gt; components can be used to do this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bastion Host
&lt;/h3&gt;

&lt;p&gt;There are two main use cases for the bastion host in ad-hoc environments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When creating a new ad hoc app environment, the bastion host is used to create a new database called &lt;code&gt;{ad-hoc-env-name}-db&lt;/code&gt; that the new ad hoc environment will use. (There might be another way of doing this, but using a bastion host is working well for now).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you using a database management tool on you local machine like DBeaver, the bastion host can help you connect to the RDS instance in a private subnet. The bastion host instance is configured to run a service that forwards traffic on port 5432 to the RDS instance. If you port forward from your local machine to the bastion host on port 5432, you can connect RDS by simple connecting to &lt;code&gt;localhost:5432&lt;/code&gt; on your local machine.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to manage SSH keys since you connect to the instance in a private subnet using SSM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm start-session &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Outputs
&lt;/h3&gt;

&lt;p&gt;Here are the outputs for the ad hoc base stack used in Terraform and Pulumi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vpc_id&lt;/li&gt;
&lt;li&gt;assets_bucket_name&lt;/li&gt;
&lt;li&gt;private_subnet_ids&lt;/li&gt;
&lt;li&gt;app_sg_id&lt;/li&gt;
&lt;li&gt;alb_sg_id&lt;/li&gt;
&lt;li&gt;listener_arn&lt;/li&gt;
&lt;li&gt;alb_dns_name&lt;/li&gt;
&lt;li&gt;task_role_arn&lt;/li&gt;
&lt;li&gt;execution_role_arn&lt;/li&gt;
&lt;li&gt;rds_address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In CDK, the stack references in the app stack don't reference the unique identifiers from the base stack (such as the VPC id or bastion host instance id), but instead they reference the properties of the stack which have types like &lt;code&gt;Vpc&lt;/code&gt; and &lt;code&gt;RdsInstance&lt;/code&gt;. More on this later in the following section &lt;strong&gt;Passing data between stacks&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ad hoc app overview
&lt;/h2&gt;

&lt;p&gt;The ad hoc app is an group of resources that powers an on-demand environment that is meant to be short lived for testing, QA, validation, demos, etc.&lt;/p&gt;

&lt;p&gt;This visualization shows all of the resources in the ad hoc app stack. It also comes from the Pulumi console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/TTFn_cywXKjSknB16FZoXHUeK2ktQ8AAD44Y2eD41sQ/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L3B1bHVtaV9hZF9o/b2NfYXBwX2RlcF9n/cmFwaC5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/TTFn_cywXKjSknB16FZoXHUeK2ktQ8AAD44Y2eD41sQ/w:880/mb:500000/ar:1/aHR0cHM6Ly9icmlh/bmNhZmZleS5naXRo/dWIuaW8vc3RhdGlj/L3B1bHVtaV9hZF9o/b2NfYXBwX2RlcF9n/cmFwaC5wbmc" alt="Graph view of ad hoc app infrastructure" width="880" height="746"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ECS Cluster
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;This is a small component that defines both ECS Cluster and the default capacity providers&lt;/li&gt;
&lt;li&gt;It defaults to not using &lt;code&gt;FARGATE_SPOT&lt;/code&gt;; ad hoc environments do use &lt;code&gt;FARGATE_SPOT&lt;/code&gt; for cost savings&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: defaultCapacityProviderStrategy on cluster not currently supported. (&lt;a href="https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.CapacityProviderStrategy.html"&gt;link&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Shared environment variables
&lt;/h3&gt;

&lt;p&gt;The backend containers should all have the same environment variables, so I define them once in the app stack and pass these into the service resource &lt;code&gt;c/m/c&lt;/code&gt;s.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I struggled to get this right in pulumi. A lot of Pulumi examples used &lt;code&gt;JSON.stringify&lt;/code&gt; for containerDefinitions in task definitions. I was able to get help from the Pulumi Slack channel; someone recommended that I use &lt;code&gt;pulumi.jsonStringify&lt;/code&gt; which was added in a relatively recent version of &lt;code&gt;pulumi/pulumi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;CDK allows you to declare environment variables for a containerDefinition like &lt;code&gt;{ FOO: "bar" }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pulumi and Terraform require that values are passed like &lt;code&gt;{ name: "FOO", value: "bar"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You could transform &lt;code&gt;{ FOO: "bar" }&lt;/code&gt; into the name/value format, but I didn't bother to do this&lt;/li&gt;
&lt;li&gt;extra env vars in Terraform to allow for dynamically passing extra environment variables, and I used the &lt;code&gt;concat&lt;/code&gt; function to add these to the list of default environment variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what the code looks like for joining extra environment variables to the default environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// CDK&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extraEnvVars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;environmentVariables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;extraEnvVars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;environmentVariables&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # terraform
    env_vars = concat(local.env_vars, var.extra_env_vars)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    // Pulumi
    if (extraEnvVars) {
      envVars = envVars.apply(x =&amp;gt; x.concat(extraEnvVars!))
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Route53 Record
&lt;/h3&gt;

&lt;p&gt;This is pretty straightforward in each library. Each ad hoc environment gets a Route 53 record, and listener rules for the web services (Django and Vue.js SPA) match on a combination of the host header and path patterns.&lt;/p&gt;

&lt;p&gt;This part is pretty opinionated in that it assumes you want to host the frontend and backend services on the same URL. For example, requests matching &lt;code&gt;example.com/api/*&lt;/code&gt; are routed to the backend API and all other requests matching &lt;code&gt;example.com/*&lt;/code&gt; are routed to the frontend service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis
&lt;/h3&gt;

&lt;p&gt;I go into more depth about why I run a Redis instance in an ECS service in my other article. This is only for the ad hoc environments. Production environments are configured with ElastiCache running Redis.&lt;/p&gt;

&lt;p&gt;I decided to not make this service use any persistent storage. It may be a good idea to not use FARGATE_SPOT for this service, since restarts to the redis service could cause issues in ad hoc environments. For example, you may get a lot of celery errors in ad hoc environments if redis is not reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Service
&lt;/h3&gt;

&lt;p&gt;The web service is what defines the main Django application as well as the frontend website (JavaScript SPA or SSR site). I designed the Web Service resources group to be able to support both traditional Django apps (powered by templates), or for Django apps that service only a limited number of endpoints. This &lt;code&gt;c/m/c&lt;/code&gt; has an input parameter called &lt;code&gt;pathPatterns&lt;/code&gt; which determines which paths it serves. For example, the API container may serve traffic for &lt;code&gt;/api/*&lt;/code&gt; and &lt;code&gt;/admin/*&lt;/code&gt; only, or it may want to serve all traffic (&lt;code&gt;/*&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The way I use these components in ad hoc and prod environments is heavily opinionated in that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it assumes that the frontend SPA/SSR site should have a lower priority rule than the backend service and should route request paths matching &lt;code&gt;/*&lt;/code&gt;, while the backend service routes requests for a specific list of path patterns (&lt;code&gt;/api/*&lt;/code&gt;, &lt;code&gt;/admin/*&lt;/code&gt;, &lt;code&gt;/graphql/*&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may want Django to handle most of your routes and 404 pages, in which case you would want the SPA to only handle requests matching certain paths. This would require some more consideration and careful refactoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Celery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The reason for having a celery service is to be able to have potentially multiple workers that scale independently&lt;/li&gt;
&lt;li&gt;I use the same Pulumi component for both works and schedulers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The terminology for this resource group could be better. Celery is one of many options for running async task workers, so it should probably be called something like &lt;code&gt;AsyncWorker&lt;/code&gt; across the board rather than using the term &lt;code&gt;celery&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Management Command
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Defines a task that can be used to run commands like &lt;code&gt;collectstatic&lt;/code&gt; and &lt;code&gt;migrate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;These tasks are ran both after the initial &lt;code&gt;app&lt;/code&gt; stack deployment and before rolling application upgrades&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my Django app I have a single management command that calls &lt;code&gt;migrate&lt;/code&gt; and &lt;code&gt;collectstatic&lt;/code&gt; and runs them in the same process one after another. This management command could also be used for clearing caches during updates, loading fixtures, etc.&lt;/p&gt;

&lt;p&gt;One other thing to note about this &lt;code&gt;c/m/c&lt;/code&gt; is that it outputs a complete script that can be used in GitHub Actions (or on your CLI when testing locally) that does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;saves the &lt;code&gt;START&lt;/code&gt; timestamp&lt;/li&gt;
&lt;li&gt;runs the task with the required settings&lt;/li&gt;
&lt;li&gt;waits for the task to complete&lt;/li&gt;
&lt;li&gt;saves the &lt;code&gt;END&lt;/code&gt; timestamp&lt;/li&gt;
&lt;li&gt;collects the logs for the task between &lt;code&gt;START&lt;/code&gt; and &lt;code&gt;END&lt;/code&gt; and prints them to &lt;code&gt;stdout&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example of what the script looks like in Pulumi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;executionScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`#!/bin/bash
START_TIME=$(date +%s000)
TASK_ID=$(aws ecs run-task --cluster &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecsClusterId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --task-definition &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;taskDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --launch-type FARGATE --network-configuration "awsvpcConfiguration={subnets=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateSubnetIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;],securityGroups=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appSgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;],assignPublicIp=ENABLED}" | jq -r '.tasks[0].taskArn')
aws ecs wait tasks-stopped --tasks $TASK_ID --cluster &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecsClusterId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
END_TIME=$(date +%s000)
aws logs get-log-events --log-group-name &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cwLoggingResources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwLogGroupName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --log-stream-name &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{TASK_ID##*/} --start-time $START_TIME --end-time $END_TIME | jq -r '.events[].message'
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executionScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;executionScript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In GitHub Actions we get this command as a stack output, save it to a file, make it executable and then run it. This is what it looks like with CDK as a CloudFormation stack output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;backend&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command"&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run_backend_update&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# get the script from the stack output with an output key that contains the string `backendUpdate`&lt;/span&gt;
          &lt;span class="s"&gt;BACKEND_UPDATE_SCRIPT=$(aws cloudformation describe-stacks \&lt;/span&gt;
            &lt;span class="s"&gt;--stack-name $AD_HOC_APP_NAME \&lt;/span&gt;
            &lt;span class="s"&gt;| jq -r '.Stacks[0].Outputs[]|select(.OutputKey | contains("backendUpdate")) | .OutputValue' \&lt;/span&gt;
          &lt;span class="s"&gt;)&lt;/span&gt;

          &lt;span class="s"&gt;echo "$BACKEND_UPDATE_SCRIPT" &amp;gt; backend_update_command.sh&lt;/span&gt;
          &lt;span class="s"&gt;cat backend_update_command.sh&lt;/span&gt;
          &lt;span class="s"&gt;sudo chmod +x backend_update_command.sh&lt;/span&gt;
          &lt;span class="s"&gt;./backend_update_command.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Passing data between stacks
&lt;/h3&gt;

&lt;p&gt;Pulumi uses stack references, Terraform uses remote state and CDK uses Stack Outputs or Stack References.&lt;/p&gt;

&lt;p&gt;Here's what this looks like in Terraform&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "terraform_remote_state" "this" {
  backend = "local"

  config = {
    path = "../base/terraform.tfstate"
  }
}

module "main" {
  source = "../../../modules/ad-hoc/app"

  vpc_id                         = data.terraform_remote_state.this.outputs.vpc_id
  assets_bucket_name             = data.terraform_remote_state.this.outputs.assets_bucket_name
  private_subnet_ids             = data.terraform_remote_state.this.outputs.private_subnet_ids
  app_sg_id                      = data.terraform_remote_state.this.outputs.app_sg_id
  alb_sg_id                      = data.terraform_remote_state.this.outputs.alb_sg_id
  listener_arn                   = data.terraform_remote_state.this.outputs.listener_arn
  alb_dns_name                   = data.terraform_remote_state.this.outputs.alb_dns_name
  service_discovery_namespace_id = data.terraform_remote_state.this.outputs.service_discovery_namespace_id
  rds_address                    = data.terraform_remote_state.this.outputs.rds_address
  domain_name                    = data.terraform_remote_state.this.outputs.domain_name
  base_stack_name                = data.terraform_remote_state.this.outputs.base_stack_name
  region                         = var.region
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocBaseStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;baseStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocAppStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AdHocBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AdHocBase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;certificateArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domainName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addHocApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AdHocApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AdHocApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;baseStackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;serviceDiscoveryNamespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceDiscoveryNamespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;rdsInstance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;databaseInstance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;assetsBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assetsBucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in Pulumi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ad-hoc-base/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vpcId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vpcId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetsBucketName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assetsBucketName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privateSubnets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;privateSubnetIds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appSgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appSgId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;albSgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;albSgId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listenerArn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listenerArn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;albDnsName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;albDnsName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serviceDiscoveryNamespaceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceDiscoveryNamespaceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rdsAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rdsAddress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domainName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domainName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseStackName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baseStackName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ad hoc app env&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adHocAppComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AdHocAppComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AdHocAppComponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpcId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;assetsBucketName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;privateSubnets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;appSgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;albSgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;listenerArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;albDnsName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serviceDiscoveryNamespaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rdsAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;baseStackName&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CLI scaffolding
&lt;/h2&gt;

&lt;p&gt;CDK and Pulumi have some good options for how to scaffold a project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulumi has &lt;code&gt;pulumi new aws-typescript&lt;/code&gt; among lots of other options (run &lt;code&gt;pulumi new -l&lt;/code&gt; to see over 200 project types). I used this to create the library itself, the examples and the pulumi projects that I use in &lt;code&gt;django-step-by-step&lt;/code&gt; that consume the library.&lt;/li&gt;
&lt;li&gt;CDK has &lt;code&gt;projen&lt;/code&gt; CLI commands which can help set up either library code or project code&lt;/li&gt;
&lt;li&gt;The major benefits of these tools is setting up &lt;code&gt;tsconfig.json&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt; correctly&lt;/li&gt;
&lt;li&gt;Terraform is so simple that it doesn't really need tooling for scaffolding&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;p&gt;For &lt;code&gt;terraform-aws-django&lt;/code&gt;, I tried to follow the recommendations from &lt;a href="https://www.terraform-best-practices.com/"&gt;terraform-best-practices.com&lt;/a&gt; which helped me a lot with things like consistent naming patterns and directory structures. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the name &lt;code&gt;this&lt;/code&gt; for resources in a module where that resource is the only resource of its type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CDK and Pulumi lend themselves to more nesting and abstractions because they can be written in more familiar programming languages with better abstractions, functions, loops, classes, etc., so there are some differences in directory structure of my libraries when comparing Terraform to both CDK and Pulumi.&lt;/p&gt;

&lt;p&gt;For Pulumi and CDK, I mostly tried to follow along with recommendations from their documentation and example projects. While working with Pulumi I struggled a bit with the concepts of &lt;code&gt;Inputs&lt;/code&gt;, &lt;code&gt;Outputs&lt;/code&gt;, &lt;code&gt;pulumi.interpolate&lt;/code&gt;, &lt;code&gt;apply()&lt;/code&gt;, &lt;code&gt;all()&lt;/code&gt; and the differences between &lt;code&gt;getX&lt;/code&gt; and &lt;code&gt;getXOutput&lt;/code&gt;. There is a little bit of a learning curve here, but the documentation and examples go a long way in showing how to do things the right way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment configuration
&lt;/h2&gt;

&lt;p&gt;Environment configuration allows for either a base or app stack to be configured with non-default values. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you may decide to start a new base environment but you want to provision a powerful database instance class and size. You would change this using environment configuration&lt;/li&gt;
&lt;li&gt;You might want to create an ad hoc app environment but you need it to include some special environment variables, you could set these in environment config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the examples above, our IaC can optionally take environment configuration values that overwrite default values, or extend default values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulumi defines environment-specific config in files called &lt;code&gt;Pulumi.{env}.yaml&lt;/code&gt; (&lt;a href="https://www.pulumi.com/docs/intro/concepts/config/"&gt;Pulumi article on configuration&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Terraform uses &lt;code&gt;{env}.tfvars&lt;/code&gt; for this type of configuration&lt;/li&gt;
&lt;li&gt;CDK has several options for this type of configuration (&lt;code&gt;cdk.context.json&lt;/code&gt;, extending stack props, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CDK I have been using &lt;code&gt;setContext&lt;/code&gt; and the &lt;code&gt;tryGetContext&lt;/code&gt; method:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setContext&lt;/code&gt; needs to be set on the node before any child nodes are added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocBaseStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;baseStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocAppStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the config objects are read from JSON files like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`src/examples/ad-hoc/base/config/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`src/examples/ad-hoc/app/config/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;adHocAppEnvName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context can be used in constructs like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extraEnvVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tryGetContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;extraEnvVars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulumi has similar functions for getting context values, here's an example of how I get extra environment variables for app environments using Pulumi's config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EnvVar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;extraEnvVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EnvVar&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extraEnvVars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my &lt;code&gt;Pulumi.alpha.yaml&lt;/code&gt; file I have the &lt;code&gt;extraEnvVars&lt;/code&gt; set like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;aws:region: us-east-1&lt;/span&gt;
  &lt;span class="s"&gt;extraEnvVars&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FOO&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BAR&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BIZ&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BUZ&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I haven't done too much with configuration, but it seems like the right place to build out all of the dials and switches for optional settings in stack resources that you want people to be able to change in their ad hoc environments, or that you want to set per "production" environment (QA, stage, prod, etc.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Local development
&lt;/h2&gt;

&lt;p&gt;Using the Makefile targets in each library repo, my process for developing &lt;code&gt;c/m/c&lt;/code&gt;s involves making code changes followed by Makefile targets that preview/plan/diff against my AWS account, then running deploy/apply/up and waiting for things to finish deploying. Once I can validate that things are looking correct in my account, I run the destroy command and make sure that all of the resources are removed successfully. RDS instances can take up to 10 minutes to create, which means that the base stack takes some time to test. The app environment is able to be spun up quickly, but it can sometimes get stuck and take some time to delete services.&lt;/p&gt;

&lt;p&gt;Here are some sample times for deploying ad hoc stacks with CDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CDK ad hoc base deployment time

 ✅  ExampleAdHocBaseStack (dev)

✨  Deployment time: 629.64s

# CDK ad hoc app deployment time

 ✅  ExampleAdHocAppStack (alpha)

✨  Deployment time: 126.62s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is an example of what the &lt;code&gt;pulumi preview&lt;/code&gt; commands shows for the ad-hoc base stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Pulumi preview
~/git/github/pulumi-aws-django$ pulumi -C examples/ad-hoc/base --stack dev preview
Previewing update (dev)

View Live: https://app.pulumi.com/briancaffey/ad-hoc-base/dev/previews/718625b2-48f5-4ef4-8ed4-9b2694fda64a

     Type                                                    Name                        Plan
 +   pulumi:pulumi:Stack                                     ad-hoc-base-dev             create
 +   └─ pulumi-contrib:components:AdHocBaseEnv               myAdHocEnv                  create
 +      ├─ pulumi-contrib:components:AlbResources            AlbResources                create
 +      │  ├─ aws:alb:TargetGroup                            DefaultTg                   create
 +      │  ├─ aws:alb:LoadBalancer                           LoadBalancer                create
 +      │  ├─ aws:alb:Listener                               HttpListener                create
 +      │  └─ aws:alb:Listener                               HttpsListener               create
 +      ├─ pulumi-contrib:components:BastionHostResources    BastionHostResources        create
 +      │  ├─ aws:iam:Role                                   BastionHostRole             create
 +      │  ├─ aws:iam:RolePolicy                             BastionHostPolicy           create
 +      │  ├─ aws:iam:InstanceProfile                        BastionHostInstanceProfile  create
 +      │  └─ aws:ec2:Instance                               BastionHostInstance         create
 +      ├─ pulumi-contrib:components:RdsResources            RdsResources                create
 +      │  ├─ aws:rds:SubnetGroup                            DbSubnetGroup               create
 +      │  ├─ aws:ec2:SecurityGroup                          RdsSecurityGroup            create
 +      │  └─ aws:rds:Instance                               DbInstance                  create
 +      ├─ pulumi-contrib:components:SecurityGroupResources  SecurityGroupResources      create
 +      │  ├─ aws:ec2:SecurityGroup                          AlbSecurityGroup            create
 +      │  └─ aws:ec2:SecurityGroup                          AppSecurityGroup            create
 +      ├─ aws:s3:Bucket                                     assetsBucket                create
 +      ├─ awsx:ec2:Vpc                                      dev                         create
 +      │  └─ aws:ec2:Vpc                                    dev                         create
 +      │     ├─ aws:ec2:InternetGateway                     dev                         create
 +      │     ├─ aws:ec2:Subnet                              dev-private-1               create
 +      │     │  └─ aws:ec2:RouteTable                       dev-private-1               create
 +      │     │     ├─ aws:ec2:RouteTableAssociation         dev-private-1               create
 +      │     │     └─ aws:ec2:Route                         dev-private-1               create
 +      │     ├─ aws:ec2:Subnet                              dev-private-2               create
 +      │     │  └─ aws:ec2:RouteTable                       dev-private-2               create
 +      │     │     ├─ aws:ec2:RouteTableAssociation         dev-private-2               create
 +      │     │     └─ aws:ec2:Route                         dev-private-2               create
 +      │     ├─ aws:ec2:Subnet                              dev-public-1                create
 +      │     │  ├─ aws:ec2:RouteTable                       dev-public-1                create
 +      │     │  │  ├─ aws:ec2:RouteTableAssociation         dev-public-1                create
 +      │     │  │  └─ aws:ec2:Route                         dev-public-1                create
 +      │     │  ├─ aws:ec2:Eip                              dev-1                       create
 +      │     │  └─ aws:ec2:NatGateway                       dev-1                       create
 +      │     └─ aws:ec2:Subnet                              dev-public-2                create
 +      │        ├─ aws:ec2:RouteTable                       dev-public-2                create
 +      │        │  ├─ aws:ec2:RouteTableAssociation         dev-public-2                create
 +      │        │  └─ aws:ec2:Route                         dev-public-2                create
 +      │        ├─ aws:ec2:Eip                              dev-2                       create
 +      │        └─ aws:ec2:NatGateway                       dev-2                       create
 +      └─ aws:servicediscovery:PrivateDnsNamespace          PrivateDnsNamespace         create


Outputs:
    albDnsName                 : output&amp;lt;string&amp;gt;
    albSgId                    : output&amp;lt;string&amp;gt;
    appSgId                    : output&amp;lt;string&amp;gt;
    assetsBucketName           : output&amp;lt;string&amp;gt;
    baseStackName              : "dev"
    bastionHostInstanceId      : output&amp;lt;string&amp;gt;
    domainName                 : "example.com"
    listenerArn                : output&amp;lt;string&amp;gt;
    privateSubnetIds           : output&amp;lt;string&amp;gt;
    rdsAddress                 : output&amp;lt;string&amp;gt;
    serviceDiscoveryNamespaceId: output&amp;lt;string&amp;gt;
    vpcId                      : output&amp;lt;string&amp;gt;

Resources:
    + 44 to create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running infrastructure pipelines in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I don't currently have GitHub Actions working for all tools in all environments, this part is still a WIP but is working at a basic level. Another item for the backlog!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;.github/workflows&lt;/code&gt; directory of the &lt;code&gt;django-step-by-step&lt;/code&gt; repo, I will have the following &lt;code&gt;2 * 2 * 2 * 3 = 24&lt;/code&gt; pipelines for running infrastructure as code pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;ad_hoc,prod&lt;span class="o"&gt;}&lt;/span&gt;_&lt;span class="o"&gt;{&lt;/span&gt;base,app&lt;span class="o"&gt;}&lt;/span&gt;_&lt;span class="o"&gt;{&lt;/span&gt;create_update,destroy&lt;span class="o"&gt;}&lt;/span&gt;_&lt;span class="o"&gt;{&lt;/span&gt;cdk,terraform,pulumi&lt;span class="o"&gt;}&lt;/span&gt;.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For CDK I'm using CDK CLI commands&lt;/li&gt;
&lt;li&gt;For Terraform I'm also using terraform CLI commands&lt;/li&gt;
&lt;li&gt;For Pulumi I'm using the official Pulumi GitHub Action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pulumi has &lt;a href="https://www.pulumi.com/docs/guides/continuous-delivery/github-actions/"&gt;a great article&lt;/a&gt; about how to use their official GitHub Action. This action calls the Pulumi CLI under the hood with all of the correct flags.&lt;/p&gt;

&lt;p&gt;The general pattern that all of these pipelines use is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do a synth/plan/preview, and upload the synth/plan/preview file to an artifact&lt;/li&gt;
&lt;li&gt;Pause and wait on manual review of the planned changes&lt;/li&gt;
&lt;li&gt;download the artifact and run deploy/apply/up against it, or optionally cancel the operation if the changes you see in the GitHub Actions pipeline logs are not what you expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do this by having two jobs in each GitHub Action: one for synth/plan/preview and one for deploy/apply/up.&lt;/p&gt;

&lt;p&gt;The job for deploy/apply/up includes an &lt;code&gt;environment&lt;/code&gt; that is configured in GitHub to be a protected environment that requires approvals. Even if you are the only approver (which I am on this project), it is the easiest and safest way preview infrastructure changes before they happen. If you see something in the plan and it isn't what you wanted to change, you cancel the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application deployments
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;There are two GitHub Actions pipelines for deploying the frontend and the backend. Both of these pipelines run bash scripts that call AWS CLI commands to perform rolling updates on all of the services used in the application (frontend, API, workers, scheduler)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The backend deployment script runs database migrations, the &lt;code&gt;collectstatic&lt;/code&gt; command and any other commands needed to run before the rolling update starts (clearing the cache, loading fixtures, etc.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is important to note here is that application deployments are not dependent on the IaC tool we use. Since we are tagging things consistently across CDK, Terraform and Pulumi, we can look up resources by tag rather than getting "outputs" of the app stacks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Interacting with AWS via IaC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CDK interacts directly with CloudFormation (and custom resources which allow for running arbitrary SDK calls and lambda functions) and provides &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/constructs.html"&gt;L1, L2 and L3 constructs&lt;/a&gt; which offer different levels of abstraction over CloudFormation.&lt;/li&gt;
&lt;li&gt;Terraform has the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs"&gt;AWS Provider&lt;/a&gt; and the &lt;a href="https://registry.terraform.io/namespaces/terraform-aws-modules"&gt;&lt;code&gt;terraform-aws-modules&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Pulumi has AWS Classic (&lt;code&gt;@pulumi/aws&lt;/code&gt;) &lt;strong&gt;provider&lt;/strong&gt; and &lt;code&gt;AWSx&lt;/code&gt; (&lt;a href="https://www.pulumi.com/registry/packages/awsx/"&gt;Crosswalk for Pulumi&lt;/a&gt;) &lt;strong&gt;library&lt;/strong&gt; and &lt;code&gt;aws_native&lt;/code&gt; &lt;strong&gt;provider&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;aws_native&lt;/code&gt; "manages and provisions resources using the AWS Cloud Control API, which typically supports new AWS features on the day of launch."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;aws_native&lt;/code&gt; looks like a really interesting option, but it is currently in public preview so I have not decided to use it. I am using the AWSx library only for my VPC and associated resources, everything else uses the AWS Classic provider.&lt;/p&gt;

&lt;p&gt;For CDK I use mostly L2 constructs and some L1 constructs.&lt;/p&gt;

&lt;p&gt;Fot Terraform I use the VPC from the &lt;code&gt;terraform-aws-modules&lt;/code&gt;, and everything else uses the AWS Terraform Provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not put in IaC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ECR (Elastic Container Registry)&lt;/li&gt;
&lt;li&gt;ACM (Amazon Certificate Manager)&lt;/li&gt;
&lt;li&gt;(Roles used for deployments)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I created the Elastic Container Registry &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;frontend&lt;/code&gt; repos manually in the AWS Console. I also manually requested an ACM certificate for &lt;code&gt;*.mydomain.com&lt;/code&gt; for the domain that I use for testing that I purchased through Route53 domains.&lt;/p&gt;

&lt;p&gt;I currently am using another less-than best practice of using Administrative Credentials stored in GitHub secrets. The better approach here is to make roles for different pipelines and use OIDC to authenticate instead of storing credentials. This is another good item for the backlog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tagging
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Terraform and CDK both make it easy to automatically tag all resources in a stack&lt;/li&gt;
&lt;li&gt;It is possible to do this in Pulumi, but you need to write a little bit of code.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/automatically-enforcing-aws-resource-tagging-policies/"&gt;https://www.pulumi.com/blog/automatically-enforcing-aws-resource-tagging-policies/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/joeduffy/aws-tags-example/tree/master/autotag-ts"&gt;https://github.com/joeduffy/aws-tags-example/tree/master/autotag-ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tagging is important since I look up resources by tag in GitHub Actions pipelines (for example, the Bastion Host is looked up by tag)&lt;/li&gt;
&lt;li&gt;Automatically tagging resources works through &lt;a href="https://www.pulumi.com/docs/intro/vs/terraform/"&gt;stack transformations&lt;/a&gt; are unique to Pulumi&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Smoke checking application environments
&lt;/h2&gt;

&lt;p&gt;Here's the list of things I check when standing up an application environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[x] Run the init/tsc, synth/plan/preview and deploy/apply/up commands successfully&lt;/li&gt;
&lt;li&gt;[x] Access the bastion host (&lt;code&gt;make aws-ssm-start-session&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Run ECSExec to access a shell in a backend container (&lt;code&gt;make aws-ecs-exec&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Test database connectivity (&lt;code&gt;python manage.py showmigrations&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Run the migrations (&lt;code&gt;python manage.py migrate&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Run collectstatic (&lt;code&gt;python manage.py collectstatic&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Visit the site (&lt;code&gt;alpha.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Publish a blog post&lt;/li&gt;
&lt;li&gt;[x] Publish a blog post with an image&lt;/li&gt;
&lt;li&gt;[x] Check celery worker logs for successfully complete scheduled tasks&lt;/li&gt;
&lt;li&gt;[x] Trigger an autoscaling event by running k6 load tests against an environment&lt;/li&gt;
&lt;li&gt;[x] Optionally deploy another backend or frontend image tag using the GitHub Actions pipelines for backend and frontend updates&lt;/li&gt;
&lt;li&gt;[x] Destroy the app stack&lt;/li&gt;
&lt;li&gt;[x] Destroy the base stack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backlog and next steps
&lt;/h2&gt;

&lt;p&gt;Here are some of the next things I'll be working on in these project, roughly in order of importance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduce manual approvals in GitHub Actions for all deployments and allow for the previewing or "planning" before proceeding with an live operations in infrastructure pipelines&lt;/li&gt;
&lt;li&gt;Switch to using OIDC for AWS authentication from GitHub Actions and remove AWS secrets from GitHub&lt;/li&gt;
&lt;li&gt;Show how to do account isolation (different accounts for prod vs pre-prod environments)&lt;/li&gt;
&lt;li&gt;GitHub Actions deployment pipeline for publishing &lt;code&gt;pulumi-aws-django&lt;/code&gt; package&lt;/li&gt;
&lt;li&gt;Complete all GitHub Action deployment pipelines for base and app stacks (both ad hoc and prod)&lt;/li&gt;
&lt;li&gt;For Pulumi and Terraform, use a Secrets Manager secret for the database instead of hardcoding it. Use the &lt;code&gt;random&lt;/code&gt; functions to do this&lt;/li&gt;
&lt;li&gt;Refactor GitHub Actions and make them reusable across different projects&lt;/li&gt;
&lt;li&gt;Writing tests for Pulumi and CDK. Figure out how to write tests for Terraform modules&lt;/li&gt;
&lt;li&gt;Use graviton instances and have the option to select between different architectures&lt;/li&gt;
&lt;li&gt;Standardize all resources names across CDK, Terraform and Pulumi&lt;/li&gt;
&lt;li&gt;The Pulumi components that define the resources associated with each ECS service are not very dry&lt;/li&gt;
&lt;li&gt;Interfaces could be constructed with inheritance (base set of properties that is extended for different types of services)&lt;/li&gt;
&lt;li&gt;Fix the CDK issue with priority rule on ALB listeners. I need to used a custom resource for this which is currently a WIP. Terraform and Pulumi look up the next highest listener rule priority under the hood, so you are not required to provide it, but CDK requires it, which means that you can't do ad hoc environments in CDK without a custom resource that looks up what the next available priority number is.&lt;/li&gt;
&lt;li&gt;Make all three of the libraries less opinionated. For example, the celery worker and scheduler should be optional and the frontend component should also be optional&lt;/li&gt;
&lt;li&gt;experiment with using a frontend with SSR. This is supported by Quasar, the framework I'm currently using to build my frontend SPA site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to get involved or help with any of the above, please let me know!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I first started out with IaC following this project &lt;a href="https://github.com/aws-samples/ecs-refarch-cloudformation"&gt;aws-samples/ecs-refarch-cloudformation&lt;/a&gt; (which is pretty old at this point) and wrote a lot of CloudFormation by hand. The pain of doing that lead me to explore the CDK with Python. I learned TypeScript by rewriting the Python CDK code I wrote in TypeScript. I later worked with a team that was more experienced in Terraform and learned how to use that. I feel like Pulumi takes the best of the two tools and has a really great developer experience. There is a little bit of a learning curve with Pulumi, and you give up some of the simplicity of Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing
&lt;/h2&gt;

&lt;p&gt;Thank you for reading to the end! I have posted the article on the following channels, please like and share the article and follow me wherever you are active. Also please share your feedback!&lt;/p&gt;

&lt;p&gt;(links pending)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://briancaffey.github.io/2023/01/07/i-deployed-the-same-containerized-serverless-django-app-with-aws-cdk-terraform-and-pulumi"&gt;my personal blog (briancaffey.github.io)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DEV.to&lt;/li&gt;
&lt;li&gt;Ops Community&lt;/li&gt;
&lt;li&gt;Medium&lt;/li&gt;
&lt;li&gt;Substack&lt;/li&gt;
&lt;li&gt;My MailChimp mailing list&lt;/li&gt;
&lt;li&gt;Hacker News&lt;/li&gt;
&lt;li&gt;Twitter&lt;/li&gt;
&lt;li&gt;LinkedIn&lt;/li&gt;
&lt;li&gt;Facebook&lt;/li&gt;
&lt;li&gt;HashNode&lt;/li&gt;
&lt;li&gt;Hackernoon&lt;/li&gt;
&lt;li&gt;Reddit (r/aws, r/Terraform, r/pulumi, r/django, r/devops)&lt;/li&gt;
&lt;li&gt;Terraform Forum&lt;/li&gt;
&lt;li&gt;Pulumi Slack channel&lt;/li&gt;
&lt;li&gt;CDK.dev Slack channel&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cdk</category>
      <category>terraform</category>
      <category>pulumi</category>
    </item>
    <item>
      <title>Setting up ad hoc development environments for Django applications with AWS ECS, Terraform and GitHub Actions</title>
      <dc:creator>briancaffey</dc:creator>
      <pubDate>Sat, 11 Jun 2022 21:40:56 +0000</pubDate>
      <link>https://community.ops.io/briancaffey/setting-up-ad-hoc-development-environments-for-django-applications-with-aws-ecs-terraform-and-github-actions-1kac</link>
      <guid>https://community.ops.io/briancaffey/setting-up-ad-hoc-development-environments-for-django-applications-with-aws-ecs-terraform-and-github-actions-1kac</guid>
      <description>&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;This article will show how software development teams can build on-demand instances of a web application project for dog-food testing, quality review, internal and external demos and other use cases that require short-lived but feature-complete environments. It will focus on the technical implementation of building ad hoc environments using a specific set of tools (including AWS ECS, Terraform and GitHub Actions). I will also be giving context on high-level implementation decisions based on what I think are best practices guided by the &lt;a href="https://12factor.net/"&gt;12-Factor Application methodology&lt;/a&gt;. If any of this interests you, please have a read and let me know what you think in the comments on the outlets where I'll be sharing this article (links at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Links
&lt;/h2&gt;

&lt;p&gt;This article references three open-source code repositories on GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/briancaffey/django-step-by-step"&gt;django-step-by-step&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;this repo contains an example microblogging application called μblog built with Django&lt;/li&gt;
&lt;li&gt;the same application is implemented as a traditional Model Template View (MTV) site, a decoupled REST API and Javascript web application and a GraphQL API&lt;/li&gt;
&lt;li&gt;it is a monorepo that also includes a frontend Vue.js application, CI/CD pipelines, a VuePress documentation site as well as tooling and instructions for settings up a local development environments (both with and without docker)&lt;/li&gt;
&lt;li&gt;it includes a complete set of GitHub Action examples for automating the processes of creating, updating and destroying ad hoc environments that will be an important part of what is covered in this article&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;&lt;a href="https://github.com/briancaffey/terraform-aws-django"&gt;terraform-aws-django&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a collection of modules for running Django applications on AWS using Terraform&lt;/li&gt;
&lt;li&gt;one of the submodules can be used for creating ad hoc environments which will be what we use to create ad hoc environments&lt;/li&gt;
&lt;li&gt;this module has been published to Terraform Registry and is used in the &lt;code&gt;terraform/live/ad-hoc&lt;/code&gt; directory of the &lt;code&gt;django-step-by-step&lt;/code&gt; repo&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;&lt;a href="https://github.com/briancaffey/terraform-aws-ad-hoc-environments"&gt;terraform-aws-ad-hoc-environments&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Terraform module that provides shared infrastructure used by ad hoc environments (including VPC, RDS instance, bastion host, security groups and IAM roles, etc.)&lt;/li&gt;
&lt;li&gt;this module has also been published to Terraform Registry and is also used in &lt;code&gt;django-step-by-step&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;this module is designed to be used with the &lt;code&gt;terraform-aws-django&lt;/code&gt; Terraform module&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;p&gt;There are all sorts of applications, and all sort of engineering teams. For some context on what I'm describing in this article, here are some basic assumptions that I'm making about the type of engineering team and software application product that would be a good fit for this type of development workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;engineering team is composed of a backend team, a frontend team, a devops team and works closely with a product team&lt;/li&gt;
&lt;li&gt;backend team primarily develops a REST API&lt;/li&gt;
&lt;li&gt;frontend team develops a JavaScript SPA (frontend website)&lt;/li&gt;
&lt;li&gt;SPA consumes backend REST API&lt;/li&gt;
&lt;li&gt;product team frequently needs to demo applications to prospective clients&lt;/li&gt;
&lt;li&gt;development teams don't have deep expertise in infrastructure, containers, CI/CD or automation&lt;/li&gt;
&lt;li&gt;devops team has been tasked with building automation that will allow anyone on the team to quickly spin up a complete environment for testing and demoing purposes within minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are assumptions about specific tools and technologies used at the company:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend is a REST API developed with Django and a Postgres database&lt;/li&gt;
&lt;li&gt;backend is packaged into a docker container&lt;/li&gt;
&lt;li&gt;frontend is also packaged into a docker container using multi-stage builds and NGINX&lt;/li&gt;
&lt;li&gt;frontend does not require any build-time configuration (all configuration needed by frontend is fetched from backend)&lt;/li&gt;
&lt;li&gt;backend application's configuration is driven by plain-text environment variables at run-time&lt;/li&gt;
&lt;li&gt;engineering team uses AWS&lt;/li&gt;
&lt;li&gt;automation pipeline exists for building, tagging and pushing backend and frontend container images to an ECR repository&lt;/li&gt;
&lt;li&gt;devops team uses AWS ECS for running containerized workloads&lt;/li&gt;
&lt;li&gt;devops team uses Terraform for provisioning infrastructure&lt;/li&gt;
&lt;li&gt;devops team uses GitHub Actions for building automation pipelines&lt;/li&gt;
&lt;li&gt;team is somewhat cost-conscious&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are ad hoc environments?
&lt;/h2&gt;

&lt;p&gt;Ad hoc environments are short-lived environments that are designed to be used for testing a specific set of features or for demoing a specific application configuration in an isolated environment. It is intended to be a functional duplicate of the main production environment. An ad hoc environment is the first cloud environment that the application code will be deployed to after a developer has been working on it in a local development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs to make when designing ad hoc environment infrastructure and automation
&lt;/h2&gt;

&lt;p&gt;Now that we have a sense of what we are building and the team we are working with, let's think about the high-level trade-offs that we will face as we build a solution for providing on-demand ad hoc environments. When building infrastructure and workflows for ad hoc environments, there are a few things to solve for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simplicity of the end-user interface and process for requesting an ad hoc environment&lt;/li&gt;
&lt;li&gt;startup speed&lt;/li&gt;
&lt;li&gt;total cost of ownership&lt;/li&gt;
&lt;li&gt;degree of similarity to production environments&lt;/li&gt;
&lt;li&gt;shared vs isolated resources&lt;/li&gt;
&lt;li&gt;automation complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at these items by considering how we can set up the main infrastructure components that will be used to run our ad hoc application environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relational Databases
&lt;/h3&gt;

&lt;p&gt;Startup speed can be measured by the time between when an environment is requested and when that environment can be used by whoever requested it. In this period of time, an automation pipeline may do some of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; to build infrastructure&lt;/li&gt;
&lt;li&gt;run scripts to prepare the application such as database migrations&lt;/li&gt;
&lt;li&gt;seeding initial sample data with a script or database dump&lt;/li&gt;
&lt;li&gt;message the user with information about the environment (URLs, commands for accessing an interactive shell, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RDS instances can take a long time to create relative to other AWS resources such as S3 buckets and IAM roles. RDS instances are also more costly than other resources. We could use a single, shared RDS instance placed in a private subnet of a shared VPC. Each ad hoc environment could use a different named database in the RDS instance in the form &lt;code&gt;{ad-hoc-env-name}-db&lt;/code&gt;. Using one RDS instance per ad hoc environment would be slow to startup and tear down and also costly if there are many developers using ad hoc environments simultaneously.&lt;/p&gt;

&lt;p&gt;If we choose to isolate the application's relational database at the database level (and not the RDS instance level), then we will need our automation workflow to create a database per ad hoc environment.&lt;/p&gt;

&lt;p&gt;Let's spin up a simple example to illustrate how this would would work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A developer is working on &lt;code&gt;feature-abc&lt;/code&gt; that involves a significant refactor of the data model.&lt;/li&gt;
&lt;li&gt;The developer decides to spin up an ad hoc environment called &lt;code&gt;feature-abc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Our automation will need to create a database in the RDS instance called &lt;code&gt;feature-abc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We can configure a bastion host with &lt;code&gt;psql&lt;/code&gt; installed that has network and security group access to our RDS instance, and we can give our GitHub Actions an SSH key that can be used to run &lt;code&gt;createdb&lt;/code&gt; over SSH.&lt;/li&gt;
&lt;li&gt;The automation also runs database migrations once the application has started, and we can view the logs of the database migration to check for any irregularities or other issues.&lt;/li&gt;
&lt;li&gt;This will give the developer and the rest of team confidence that the promoting &lt;code&gt;feature-abc&lt;/code&gt; to the next pre-production environments will not have any errors.&lt;/li&gt;
&lt;li&gt;The developer may even choose to load a SQL dump of the next pre-production environment into their &lt;code&gt;feature-abc&lt;/code&gt; database get even more confidence that there will be no data integrity errors.&lt;/li&gt;
&lt;li&gt;When the developer's PR is merged and approved, the ad hoc environment &lt;code&gt;feature-abc&lt;/code&gt; can be destroyed, including the &lt;code&gt;feature-abc&lt;/code&gt; database in the shared RDS instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this approach we won't incur the costs of multiple RDS instances. Ad hoc environments will start up faster because an RDS instance per environment is not required. We do have slightly less resource isolation, and we need to introduce a bastion host, but I consider this an acceptable trade-off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis (key-value database)
&lt;/h3&gt;

&lt;p&gt;Redis is another database used in the application and it plays a few different roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;primarily, it is a caching layer that can cache request responses to reduce load on the database and speed up our application&lt;/li&gt;
&lt;li&gt;it is a message broker for our async task workers (celery)&lt;/li&gt;
&lt;li&gt;it can be used as a backend for other 3rd party Django apps that our main application may need to use (such as django-constance, cache-ops, django-channels, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS offers a managed Redis service called ElastiCache. Redis running on an ElastiCache instance can do database isolation similar to how Postgres running on RDS can do database isolation as we discussed previously, but there are some key differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redis databases are numbered, not named&lt;/li&gt;
&lt;li&gt;the backend application uses isolated numbered databases for the different 3rd party apps that I just mentioned (for example: celery can use database &lt;code&gt;0&lt;/code&gt;,  API caching layer can use database &lt;code&gt;1&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it difficult to use a single ElastiCache instance for our ad hoc environments since we would need to figure out which numbered database to assign to a specific role for each ad hoc environment (e.g. how do we know which numbered database to use for the API caching for the &lt;code&gt;feature-abc&lt;/code&gt; ad hoc environment).&lt;/p&gt;

&lt;p&gt;So how can we approach providing isolated redis instances for multiple ad hoc environments? Spoiler: my solution is to run redis as a stateful service in ECS. Before we dig into how to do this, we need to talk about another important part of our application: compute.&lt;/p&gt;

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

&lt;p&gt;Our backend application is composed of a few different services that all share the same code base. In other words, our backend's services uses the same docker image but run different processes for each component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gunicorn for the core API&lt;/li&gt;
&lt;li&gt;celery for the task workers&lt;/li&gt;
&lt;li&gt;celerybeat for task scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If our application used websockets, we could have another service that runs an asgi server process (like daphne or uvicorn).&lt;/p&gt;

&lt;p&gt;Since our backend application is packaged into a container and we are using AWS as our cloud provider, ECS is a great choice for running our backend services. ECS is a container orchestration tool that I usually describe as a nice middle ground between docker swarm and Kubernetes. Simply put, it is a flexible option for running our containerized services that make up our backend application.&lt;/p&gt;

&lt;p&gt;With ECS you can choose to run containers directly on EC2 instances that you manage, or you can run containers using Fargate. Fargate is a serverless compute option that takes care of managing both the underlying "computer" and operating system that run our application's containers. All of our backend dependencies are defined in our Dockerfile, so we do not to maintain or update the underlying operating system that runs our containers -- AWS handles all of this for us. To use Fargate, we simply tell AWS which containers to run and how much CPU and memory to use in the ECS Task that runs the containers. To scale our app horizontally, the ECS service that managed ECS tasks simply increases the number of tasks that run.&lt;/p&gt;

&lt;p&gt;Since we are going to use the Fargate launch type for our ECS Tasks, let's talk about the ergonomics of these serverless compute instances compared to running our services directly on an EC2 instances.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We can't SSH into Fargate compute instances. We can instead use AWS Systems Manager and EcsExec to open an interactive shell in a running backend container. This can be useful for developers who might need to run a management command or access an interactive Django shell to verify behavior in their ad hoc environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We can't simply change code on the server and restart services. This can sometimes be a useful pattern for debugging something that can only be tested on a cloud environment (e.g. something that can't easily be reproduced on your local machine), so this requires that developers push new images to their backend services for every change they want to see reflected on their ad hoc environment. Later on I'll discuss how we can provide tooling for developers to quickly update the image used in their backend services.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With AWS Fargate, you will pay more than you would for a comparable amount of CPU and memory on EC2 instances. Similar to EC2 spot instances, Fargate offers interruptable instances called Fargate Spot which costs significantly less than regular Fargate instances. Fargate spot is appropriate for our ad hoc environments since ad hoc environments are non-critical workloads. In the event that a Fargate spot instance is interrupted, the ECS service will automatically launch another Fargate task to replace the task that was stopped.&lt;/p&gt;

&lt;p&gt;In my opinion, ECS with Fargate is ideal for running the stateless services that make up our backend application. In terms of parity with our production environment, we can keep almost everything the same, except use regular Fargate instances instead of Fargate spot instances.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis, revisited
&lt;/h3&gt;

&lt;p&gt;We can run redis as an ECS service instead of using ElastiCache. In order to do this, we will need our backend services (gunicorn, celery and celerybeat) to be able to communicate with a fourth ECS service that will be running redis (using an official redis image from Docker Hub, or a redis image that we define in ECR).&lt;/p&gt;

&lt;p&gt;By default, &lt;strong&gt;there is no way for our backend services to know how to communicate with any other service in our ECS cluster&lt;/strong&gt;. If you have used docker-compose, you may know that you can use the service name &lt;code&gt;redis&lt;/code&gt; in a backend service to easily communicate with a redis service called &lt;code&gt;redis&lt;/code&gt;. This networking convenience is not available to use out of the box with ECS. To achieve this in AWS, we need some way to manage a unique ad hoc environment-specific Route 53 DNS record that points to the private IP of the Fargate task that is running redis in an ECS cluster for a given ad hoc environment. Such a service exists in AWS and it is called Cloud Map. Cloud Map offers service discovery so that our backend services can make network calls to a static DNS address that will reliably point to the correct private IP of the ECS task running the redis container.&lt;/p&gt;

&lt;p&gt;We can define a service discovery namespace (which will essentially be a top level domain, or TLD) that all of our ad hoc environments can share. Let's assume this namespace is called &lt;code&gt;ad-hoc&lt;/code&gt;. Each ad hoc environment can then define a service discovery service in the shared namespace for redis that is called &lt;code&gt;{ad-hoc-env-name}-redis&lt;/code&gt;. This way, we can have a reliable address that we can configure as an environment for our backend that will look like this: &lt;code&gt;redis://{ad-hoc-env-name}-redis.ad-hoc:6379/0&lt;/code&gt;. &lt;code&gt;{ad-hoc-env-name-redis}.ad-hoc&lt;/code&gt; will be the hostname of the redis service, and Route 53 will create records that point to &lt;code&gt;{ad-hoc-env-name}-redis.ad-hoc&lt;/code&gt; to the private IP of the redis Fargate task for each ad hoc environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Balancing
&lt;/h3&gt;

&lt;p&gt;We now have our backend services (gunicorn, celery and celerybeat) running on Fargate spot instances, and these services can communicate with the redis service in our ad hoc environment's ECS cluster using service discovery that we configured with Cloud Map. We still need to think about a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how will we expose our API service to the public (or private) internet&lt;/li&gt;
&lt;li&gt;how will we expose our frontend application to the public (or private) internet&lt;/li&gt;
&lt;li&gt;how will we make sure that requests go the correct ECS services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Application load balancers (ALBs) are a great way to expose web app traffic to the internet. We could either have one application load balancer per ad hoc environment, or one application load balancer shared between all ad hoc environments. ALBs are somewhat slow to create and they also incur a significant monthly cost. They are also highly scalable, so using a shared ALB for all ad hoc environments would work.&lt;/p&gt;

&lt;p&gt;Individual ad hoc environments can then create target groups and listener rules for a shared ALB for each service that needs to serve requests from the internet (the backend and the frontend). In our case this is the backend API server and the frontend server that serves our static frontend site using NGINX.&lt;/p&gt;

&lt;p&gt;ECS services that need to be exposed to the internet can specify the target group, port and container to use for load balancing. A target group is created that defines the health check and other settings, and a load balancer listener rule is created on the shared load balancer that will forward traffic matching certain conditions to the target group for our service.&lt;/p&gt;

&lt;p&gt;For a given ad hoc environment, we need to specify that only traffic with certain paths should be sent to the backend service, and all other traffic should be sent to the frontend service. For example, we may only want to send traffic that starts with the path &lt;code&gt;/api&lt;/code&gt; or &lt;code&gt;/admin&lt;/code&gt; to the backend target group, and all other traffic should be sent to the frontend target group. We can do this by setting conditions on the listener rules that forward traffic do the frontend and backend target groups based on the hostname and path.&lt;/p&gt;

&lt;p&gt;We want our listener rule logic to forward &lt;code&gt;/api&lt;/code&gt;, &lt;code&gt;/admin&lt;/code&gt; and any other backend traffic to the backend target group, and forward all other traffic (&lt;code&gt;/*&lt;/code&gt;) to the frontend target group. In order to do this, we need the backend listener rule to have a higher priority than the frontend listener rule for each ad hoc environment. Since we are using the same load balancer for all ad hoc environments, the priority values for each listener rule need to be unique. If we don't set the priority explicitly, then the priority will be set automatically to the next available value in ascending order. In order to make sure that the backend listener rule has a higher priority than the frontend listener rule for each ad hoc environment, we need to tell Terraform that the frontend module &lt;code&gt;depends_on&lt;/code&gt; the backend module. This way the backend listener rule will have a higher priority (e.g. priority of 1) because it will be created first, and the frontend listener rule will have a lower priority (e.g. priority of 2).&lt;/p&gt;

&lt;h2&gt;
  
  
  More on shared resources vs per-environment resources
&lt;/h2&gt;

&lt;p&gt;Up until now we have discussed infrastructure design decisions at a high level, but we have not yet talked about how to organize our infrastructure as code. At a basic level, components of our ad hoc environment either fall into shared infrastructure or infrastructure that is specific to an individual ad hoc environment. Here's a list of the resources that are shared and the resources that are specific to each ad hoc environment.&lt;/p&gt;

&lt;p&gt;Shared resources include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC&lt;/li&gt;
&lt;li&gt;IAM policies&lt;/li&gt;
&lt;li&gt;Security groups&lt;/li&gt;
&lt;li&gt;RDS instance&lt;/li&gt;
&lt;li&gt;Service Discovery namespace&lt;/li&gt;
&lt;li&gt;Application Load Balancer&lt;/li&gt;
&lt;li&gt;Bastion host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ad hoc environment resources include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ECS Cluster&lt;/li&gt;
&lt;li&gt;ECS Tasks and Services (for backend and frontend applications)&lt;/li&gt;
&lt;li&gt;ECS Tasks for running management commands (such as migrate)&lt;/li&gt;
&lt;li&gt;CloudWatch logging groups for containers defined in ECS Tasks&lt;/li&gt;
&lt;li&gt;ALB Target groups&lt;/li&gt;
&lt;li&gt;ALB listener rules&lt;/li&gt;
&lt;li&gt;Route 53 record that points to the load balancer (e.g. &lt;code&gt;ad-hoc-env-name.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;S3 bucket for static and media assets&lt;/li&gt;
&lt;li&gt;Service Discovery Service for redis service in ECS cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shared resources can be defined in one terraform configuration and deployed once. These resources will be long-lived as long as the application is under active development and the team requires on-demand provisioning of ad hoc environments.&lt;/p&gt;

&lt;p&gt;Ad hoc environment resources can be defined in another terraform configuration that references outputs from the shared resource configuration using &lt;code&gt;terraform_remote_state&lt;/code&gt;. Each ad hoc environment can be defined by a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file that contains the name of the ad hoc environment (such as &lt;code&gt;brian&lt;/code&gt;, &lt;code&gt;brian2&lt;/code&gt;, &lt;code&gt;demo-feature-abc&lt;/code&gt;, etc.). This &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; value will also be the name of the Terraform workspace and will be used to name and tag AWS resources associated with the corresponding ad hoc environment.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file will allow developers to use a simple, standard file interface for defining application specific values, such as the version of the backend and frontend. This brings developers into the concepts and practices of "infrastructure as code" and "configuration as code" and also helps the entire team keep track of how different environments are configured.&lt;/p&gt;

&lt;p&gt;Ad hoc environment &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; files are stored in a directory of a special git repository that also defines the ad hoc environment terraform configuration. Currently, the &lt;code&gt;tfvars&lt;/code&gt; files are stored &lt;a href="https://github.com/briancaffey/django-step-by-step/tree/main/terraform/live/ad-hoc/envs"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now let's look at the two terraform configurations used for defining &lt;strong&gt;shared resources&lt;/strong&gt; and &lt;strong&gt;ad hoc environment resources&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ad Hoc Environment Diagram
&lt;/h2&gt;

&lt;p&gt;Here's an overview of the resources used for the ad hoc environments. The &lt;strong&gt;letters represent shared resources&lt;/strong&gt; and the &lt;strong&gt;numbers represent per-environment resources&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/2qOD4b2hXjxKJI7IMpjh7O-SgWHJKKq12yPYKLMfioo/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3RsNXhn/cW5pdzF3a24yYjU2/MHV2LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/2qOD4b2hXjxKJI7IMpjh7O-SgWHJKKq12yPYKLMfioo/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3RsNXhn/cW5pdzF3a24yYjU2/MHV2LnBuZw" alt="Ad hoc environment diagram" width="880" height="727"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared architecture
&lt;/h3&gt;

&lt;p&gt;A. VPC (created using the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest"&gt;official AWS VPC Module&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;B. Public subnets for bastion host, NAT Gateways and Load Balancer&lt;/p&gt;

&lt;p&gt;C. Private subnets for application workloads and RDS&lt;/p&gt;

&lt;p&gt;D. Application Load Balancer that is shared between all ad hoc environments. A pre-provisioned wildcard ACM certificate is attached to the load balancer that is used to secure traffic for load-balanced ECS services&lt;/p&gt;

&lt;p&gt;E. Service discovery namespace that provides a namespace for application workloads to access the redis service running in ECS&lt;/p&gt;

&lt;p&gt;F. IAM roles needed for ECS tasks to access AWS services&lt;/p&gt;

&lt;p&gt;G. RDS instance using postgres engine that is shared between all ad hoc environments&lt;/p&gt;

&lt;p&gt;H. Bastion host used to access RDS from GitHub Actions (needed for creating per-environment databases)&lt;/p&gt;

&lt;p&gt;I. NAT Gateway used to give traffic in private subnets a route to the public internet&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment-specific architecture
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;ECS Cluster that groups all ECS tasks for a single ad hoc environment&lt;/li&gt;
&lt;li&gt;Listener rules and target groups that direct traffic from the load balancer to the ECS services for an ad hoc environment.&lt;/li&gt;
&lt;li&gt;Redis service running in ECS that provides caching and serves as a task broker for celery&lt;/li&gt;
&lt;li&gt;Route53 records that point to the load balancer&lt;/li&gt;
&lt;li&gt;Frontend service that serves the Vue.js application over NGINX&lt;/li&gt;
&lt;li&gt;API service that serves the backend with Gunicorn&lt;/li&gt;
&lt;li&gt;Celery worker that process jobs in the default queue&lt;/li&gt;
&lt;li&gt;Celery beat that schedules celery tasks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;collectstatic&lt;/code&gt; task&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrate&lt;/code&gt; task&lt;/li&gt;
&lt;li&gt;CloudWatch log groups are created for each ECS task in an ad hoc environment&lt;/li&gt;
&lt;li&gt;Each ad hoc environment gets a database in the shared RDS instance&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Shared resources terraform configuration
&lt;/h2&gt;

&lt;p&gt;Let's have a detailed look at the terraform configuration for shared resources that will support ad hoc environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC
&lt;/h3&gt;

&lt;p&gt;We can use the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest"&gt;AWS VPC module&lt;/a&gt; for creating the shared VPC with Terraform. This module provides a high level interface that will provision lots of the components that are needed for a VPC following best practices, and it is less code for the DevOps team to manage compared to defining each component of a VPC (route tables, subnets, internet gateways, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Map Service Discovery Namespace
&lt;/h3&gt;

&lt;p&gt;Cloud Map is used in order to allow services in our ECS cluster to communicate with each other. The only reason that Cloud Map is needed is so that the backend services (API, celery workers, beat) can communicate with Redis, which will be an important service for our application, providing caching and also serving as a broker for celery. If we were to use Django Channels for websockets, the Redis service would also function as the backend for Django Channels.&lt;/p&gt;

&lt;p&gt;We will only need to specify &lt;code&gt;service_registries&lt;/code&gt; on the redis service in our ECS cluster. What this will do is provide an address that our other services can use to communicate with redis. This address is created in the form of a Route 53 record, and it points to the private IP address of the redis service. If the private IP of the redis service is updated, the Route 53 record record for our redis service will be updated as well.&lt;/p&gt;

&lt;p&gt;In order for service discovery to work in the VPC that we created, we need to add the following options to the terraform AWS VPC module:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# DNS settings
enable_dns_hostnames = true
enable_dns_support   = true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Security Groups
&lt;/h3&gt;

&lt;p&gt;There are two important security groups that we will set up as part of the shared infrastructure layer to be used by each ad hoc environment: one security group for the load balancer, and one security group where all of our ECS services will run.&lt;/p&gt;

&lt;p&gt;The load balancer security group will allow all traffic on port &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt; for &lt;code&gt;HTTP&lt;/code&gt; and &lt;code&gt;HTTPS&lt;/code&gt; traffic. The ECS security group will only allow inbound traffic from the application load balancer security group. It will also allow for traffic from port 6379 for redis traffic.&lt;/p&gt;
&lt;h3&gt;
  
  
  IAM Roles
&lt;/h3&gt;

&lt;p&gt;There are two important IAM roles that we will need for our ECS tasks. We need a task execution role that our ECS tasks will use to interact with other AWS services, such as S3, Secrets Manager, etc.&lt;/p&gt;
&lt;h3&gt;
  
  
  RDS Instance
&lt;/h3&gt;

&lt;p&gt;We will create one RDS instance in one of the private subnets in our VPC. This RDS instance will have one Postgres database per ad hoc environment. This RDS instance has a security group that allows all traffic from our ECS security group.&lt;/p&gt;
&lt;h3&gt;
  
  
  Load Balancer
&lt;/h3&gt;

&lt;p&gt;We will use one load balancer for all ad hoc environments. This load balancer will have a wildcard ACM certificate attached to it (&lt;code&gt;*.dev.example.com&lt;/code&gt;, for example). Each ad hoc environment will create a Route 53 record that will point to this load balancer's public DNS name. For example, &lt;code&gt;brian.dev.example.com&lt;/code&gt; will be the address of my ad hoc environment. Requests to this address will then be routed to either the frontend ECS service or the backend ECS service depending on request header values and request path values that will be set on the listener rules.&lt;/p&gt;

&lt;p&gt;By default, a load balancer supports up to 50 listener rules, so we can create plenty of ad hoc environments before we need to increase the default quota. There will be a discussion at the end of this article about AWS service quotas.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bastion Host
&lt;/h3&gt;

&lt;p&gt;The bastion host will be created in one of the VPC's public subnets. This will primarily be used for connecting to RDS to create new databases for new ad hoc environments, or for manually manipulating data in an ad hoc environment for debugging.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ad hoc environment resources
&lt;/h2&gt;

&lt;p&gt;Now that we have defined a shared set of infrastructure that our ad hoc environments will use, let's have a look at the resources that will be specific to ad hoc environments that will be added on top of the shared resources.&lt;/p&gt;
&lt;h3&gt;
  
  
  ECS Cluster
&lt;/h3&gt;

&lt;p&gt;The ECS Cluster is a simple grouping of ECS tasks and services.&lt;/p&gt;
&lt;h3&gt;
  
  
  ECS Tasks and Services
&lt;/h3&gt;

&lt;p&gt;Each environment will have a set of ECS tasks and services that will be used to run the application.&lt;/p&gt;

&lt;p&gt;There are four important ECS services in our application that are used to run "long-running" ECS tasks. Long-running tasks are tasks that start processes that run indefinitely, rather than running until completion. The long-running tasks in our application include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend web application (gunciron web server)&lt;/li&gt;
&lt;li&gt;backend celery worker&lt;/li&gt;
&lt;li&gt;backend celery beat&lt;/li&gt;
&lt;li&gt;frontend web site (nginx web server)&lt;/li&gt;
&lt;li&gt;redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The infrastructure code also defines some tasks that are not long-running but rather short lived tasks that run until completion and do not start again. These tasks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collectstatic&lt;/li&gt;
&lt;li&gt;database migrations&lt;/li&gt;
&lt;li&gt;any other ad-hoc task that we want to run, usually wrapped in a Django management command&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to setup an ad hoc environment
&lt;/h2&gt;

&lt;p&gt;Now that we have been over the resources that will be created to support our ad hoc environments, let's talk about how we can enable individuals on our team to create and update ad hoc environments.&lt;/p&gt;
&lt;h3&gt;
  
  
  Design decisions
&lt;/h3&gt;

&lt;p&gt;The devops team will decide on the interface that will be used for creating an ad hoc environment. Since we are using Terraform, this interface will be a Terraform configuration. The minimum amount of information that our ad hoc environment configuration needs is image tags for the frontend and backend images to use. Other configurations will be provided by default values set in &lt;code&gt;variables.tf&lt;/code&gt;, and these defaults can easily be overridden by passing values to &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt;. I'm choosing to use &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; as the way to pass configuration values to our ad hoc environments where &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; is the name of the ad hoc environment being created. This will give us the following benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;all ad hoc environments will be visible to the entire team in git since each ad hoc environment will have a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file associated with it&lt;/li&gt;
&lt;li&gt;adding additional customization to an ad hoc environment does not add additional complexity to our automation pipeline since all customization is added through a single file that will be referenced by &lt;code&gt;$WORKSPACE.tfvars&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The downsides of this approach are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating ad hoc environments requires knowledge of git, so non-technical product team members might need help from the engineering team when setting up an ad hoc environment&lt;/li&gt;
&lt;li&gt;there is an additional "manual" step of creating a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file that must be done before running a pipeline to create an ad hoc environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provided that a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file has been created and pushed to the repo, creating or updating an ad hoc environment will be as simple as running a pipeline in GitHub Actions that specifies the &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; of our ad hoc environment. If no such &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file exists, our pipeline will fail.&lt;/p&gt;
&lt;h3&gt;
  
  
  GitHub Action
&lt;/h3&gt;

&lt;p&gt;Creating ad hoc environments will involve manually triggering a GitHub Action that runs on &lt;code&gt;workflow_dispatch&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;on:
  workflow_dispatch:
    inputs:
      workspace:
        description: 'Name of terraform workspace to use'
        required: true
        default: 'dev'
        type: string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We only have to enter the name of the ad hoc environment we want to create or update. The ad hoc environment name is used as the Terraform workspace name. This name is also the name of the &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file that must be created per environment.&lt;/p&gt;

&lt;p&gt;This workflow will do &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; using the &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file. When everything has been created, we will use the AWS CLI to prepare the environment so that it can be used. We will use the &lt;code&gt;aws ecs run-task&lt;/code&gt; command to run database migrations needed so that the application code can make database queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to update code in an existing ad hoc environment
&lt;/h2&gt;

&lt;p&gt;Assuming that we have deployed an ad hoc environment called &lt;code&gt;brian&lt;/code&gt; with version &lt;code&gt;v1.0.0&lt;/code&gt; of the backend application and &lt;code&gt;v2.0.0&lt;/code&gt; of the frontend application, let's think about the process of updating the application to &lt;code&gt;v1.1.0&lt;/code&gt; of the backend and &lt;code&gt;v2.1.0&lt;/code&gt; of the frontend.&lt;/p&gt;

&lt;p&gt;The simplest approach to updating the application would be edit the &lt;code&gt;brian.tfvars&lt;/code&gt; file with the new versions:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# brian.tfvars
be_image_tag = "v1.1.0"
fe_image_tag = "v2.1.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If we run the same pipeline that we initially used to deploy ad hoc environment (with &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt;) against the updated &lt;code&gt;brian.tfvars&lt;/code&gt; file, this will result in a rolling update of the frontend and backend services (&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html"&gt;more on rolling updates here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If there are database migrations included in the new version of the code that is going out, we need to run database migrations after the &lt;code&gt;terraform apply&lt;/code&gt; completes. We use a top level output from the ad hov environment terraform configuration that is a &lt;code&gt;run-task&lt;/code&gt; command with all appropriate arguments that will run database migrations when called from GitHub Actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Order of Operations
&lt;/h3&gt;

&lt;p&gt;For ad hoc environments, it is probably fine to update the services and then run the database migrations. Ad hoc environments may only have a single "user" -- the developer, so &lt;strong&gt;we don't need to worry about any errors that may occur if requests are made against the new version of code before database migrations have been applied&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's consider a simple example to illustrate what can go wrong here. If we add a &lt;code&gt;total_views&lt;/code&gt; to our blog post model to track the total number of page views a post has, we would add a field to the model, generate migration file with &lt;code&gt;makemigrations&lt;/code&gt;, and then update our views and model serializers to make use of this new field. In the time between updating our service and running the database migrations, any requests to endpoints that access the new database field will fail since the table does not yet exist.&lt;/p&gt;

&lt;p&gt;If we first run database migrations &lt;strong&gt;and then&lt;/strong&gt; update application code (ECS services), then we can avoid errors about fields not existing. In our production application, we want to aim for fewer errors, so we should be using this "order of operations": first run new database migrations and then update application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Action for application updates
&lt;/h3&gt;

&lt;p&gt;We need a GitHub Action that can do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch the current container definition JSON files for each backend tasks (&lt;code&gt;aws ecs describe-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;write new container definitions JSON with the new backend image tag (using &lt;code&gt;jq&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;register new task definitions with the new container definition JSON files for each task (&lt;code&gt;aws ecs register-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;call run-task with the newly updated migration ECS task (&lt;code&gt;aws ecs run-task&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;wait for the task to exit and display the logs (&lt;code&gt;aws ecs wait tasks-stopped&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;update the backend services (gunicorn, celery, celery beat) (&lt;code&gt;aws ecs update-service&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;wait for the new backend services to be stable (&lt;code&gt;aws ecs wait services-stable&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a visual representation of the backend update process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/JiqmiPFWgGBDL9cOEdAA03PF8fHmQ-19eVE3LS1Y-4U/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzBkc3Nt/Nmk2bDNqazcwdGxk/MTI2LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/JiqmiPFWgGBDL9cOEdAA03PF8fHmQ-19eVE3LS1Y-4U/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzBkc3Nt/Nmk2bDNqazcwdGxk/MTI2LnBuZw" alt="Backend application update process" width="880" height="1046"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order have the correct arguments for all of the AWS CLI calls used in the above workflow, we can use the AWS CLI to fetch resource names by tag.&lt;/p&gt;

&lt;p&gt;Here is what I'm using for the script. There lots of comments, so please refer to those comments for an explanation of what the script is doing.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c"&gt;# This script will be called to update an ad hoc environment backend&lt;/span&gt;
&lt;span class="c"&gt;# with a new image tag. It will first run pre-update tasks (such as migrations)&lt;/span&gt;
&lt;span class="c"&gt;# and then do a rolling update of the backend services.&lt;/span&gt;

&lt;span class="c"&gt;# It is called from the ad_hock_backend_update.yml GitHub Actions file&lt;/span&gt;

&lt;span class="c"&gt;# Required environment variables that need to be exported before running this script:&lt;/span&gt;

&lt;span class="c"&gt;# WORKSPACE - ad hoc environment workspace&lt;/span&gt;
&lt;span class="c"&gt;# SHARED_RESOURCES_WORKSPACE - shared resources workspace&lt;/span&gt;
&lt;span class="c"&gt;# BACKEND_IMAGE_TAG - backend image tag to update services to (e.g. v1.2.3)&lt;/span&gt;
&lt;span class="c"&gt;# AWS_ACCOUNT_ID - AWS account ID is used for the ECR repository URL&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating backend services..."&lt;/span&gt;

&lt;span class="c"&gt;# first define a variable containing the new image URI&lt;/span&gt;
&lt;span class="nv"&gt;NEW_BACKEND_IMAGE_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AWS_ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;.dkr.ecr.us-east-1.amazonaws.com/backend:&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND_IMAGE_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;


&lt;span class="c"&gt;# register new task definitions&lt;/span&gt;
&lt;span class="c"&gt;# https://docs.aws.amazon.com/cli/latest/reference/ecs/describe-task-definition.html#description&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;TASK &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"migrate"&lt;/span&gt; &lt;span class="s2"&gt;"gunicorn"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="s2"&gt;"beat"&lt;/span&gt;
&lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating &lt;/span&gt;&lt;span class="nv"&gt;$TASK&lt;/span&gt;&lt;span class="s2"&gt; task definition..."&lt;/span&gt;

  &lt;span class="c"&gt;# in Terraform we name our tasks based on the ad hoc environment name&lt;/span&gt;
  &lt;span class="c"&gt;# (also the Terraform workspace name) and the name of the task&lt;/span&gt;
  &lt;span class="c"&gt;# (e.g. migrate, gunicorn, default, beat)&lt;/span&gt;
  &lt;span class="nv"&gt;TASK_FAMILY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;-&lt;span class="nv"&gt;$TASK&lt;/span&gt;

  &lt;span class="c"&gt;# save the task definition JSON to a variable&lt;/span&gt;
  &lt;span class="nv"&gt;TASK_DESCRIPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs describe-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# save container definitions to a file for each task&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.containerDefinitions &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;.json

  &lt;span class="c"&gt;# write new container definition JSON with updated image&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Writing new &lt;/span&gt;&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="s2"&gt; container definitions JSON..."&lt;/span&gt;

  &lt;span class="c"&gt;# replace old image URI with new image URI in a new container definitions JSON&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;.json &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--arg&lt;/span&gt; IMAGE &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_BACKEND_IMAGE_URI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'.[0].image |= $IMAGE'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="nt"&gt;-new&lt;/span&gt;.json

  &lt;span class="c"&gt;# Get the existing configuration for the task definition (memory, cpu, etc.)&lt;/span&gt;
  &lt;span class="c"&gt;# from the variable that we saved the task definition JSON to earlier&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Getting existing configuration for &lt;/span&gt;&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

  &lt;span class="nv"&gt;MEMORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.memory &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;CPU&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.cpu &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;ECS_EXECUTION_ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.executionRoleArn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;ECS_TASK_ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.taskRoleArn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# check the content of the new container definition JSON&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="nt"&gt;-new&lt;/span&gt;.json

  &lt;span class="c"&gt;# register new task definition using the new container definitions&lt;/span&gt;
  &lt;span class="c"&gt;# and the values that we read off of the existing task definitions&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Registering new &lt;/span&gt;&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="s2"&gt; task definition..."&lt;/span&gt;

  aws ecs register-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--family&lt;/span&gt; &lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--container-definitions&lt;/span&gt; file:///tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="nt"&gt;-new&lt;/span&gt;.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--memory&lt;/span&gt; &lt;span class="nv"&gt;$MEMORY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cpu&lt;/span&gt; &lt;span class="nv"&gt;$CPU&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network-mode&lt;/span&gt; awsvpc &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--execution-role-arn&lt;/span&gt; &lt;span class="nv"&gt;$ECS_EXECUTION_ROLE_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-role-arn&lt;/span&gt; &lt;span class="nv"&gt;$ECS_TASK_ROLE_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--requires-compatibilities&lt;/span&gt; &lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;

&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Now we need to run migrate, collectstatic and any other commands that need to be run&lt;/span&gt;
&lt;span class="c"&gt;# before doing a rolling update of the backend services&lt;/span&gt;

&lt;span class="c"&gt;# We will use the new task definitions we just created to run these commands&lt;/span&gt;

&lt;span class="c"&gt;# get the ARN of the most recent revision of the migrate task definition&lt;/span&gt;
&lt;span class="nv"&gt;TASK_DEFINITION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ecs describe-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-migrate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.taskDefinitionArn &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# get private subnets as space separated string from shared resources VPC&lt;/span&gt;
&lt;span class="nv"&gt;SUBNETS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ec2 describe-subnets &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:env,Values=&lt;/span&gt;&lt;span class="nv"&gt;$SHARED_RESOURCES_WORKSPACE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=*private*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Subnets[*].SubnetId'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# replace spaces with commas using tr&lt;/span&gt;
&lt;span class="nv"&gt;SUBNET_IDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SUBNETS&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# https://github.com/aws/aws-cli/issues/5348&lt;/span&gt;
&lt;span class="c"&gt;# get ecs_sg_id - just a single value&lt;/span&gt;
&lt;span class="nv"&gt;ECS_SG_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ec2 describe-security-groups &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=&lt;/span&gt;&lt;span class="nv"&gt;$SHARED_RESOURCES_WORKSPACE&lt;/span&gt;&lt;span class="s2"&gt;-ecs-sg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'SecurityGroups[*].GroupId'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running database migrations..."&lt;/span&gt;

&lt;span class="c"&gt;# timestamp used for log retrieval (milliseconds after Jan 1, 1970 00:00:00 UTC)&lt;/span&gt;
&lt;span class="nv"&gt;START_TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s000&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# run the migration task and capture the taskArn into a variable called TASK_ID&lt;/span&gt;
&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ecs run-task &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DEFINITION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network-configuration&lt;/span&gt; &lt;span class="s2"&gt;"awsvpcConfiguration={subnets=[&lt;/span&gt;&lt;span class="nv"&gt;$SUBNET_IDS&lt;/span&gt;&lt;span class="s2"&gt;],securityGroups=[&lt;/span&gt;&lt;span class="nv"&gt;$ECS_SG_ID&lt;/span&gt;&lt;span class="s2"&gt;],assignPublicIp=ENABLED}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tasks[0].taskArn'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Task ID is &lt;/span&gt;&lt;span class="nv"&gt;$TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# wait for the migrate task to exit&lt;/span&gt;
&lt;span class="c"&gt;# https://docs.aws.amazon.com/cli/latest/reference/ecs/wait/tasks-stopped.html#description&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt; It will poll every 6 seconds until a successful state has been reached.&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt; This will exit with a return code of 255 after 100 failed checks.&lt;/span&gt;
aws ecs &lt;span class="nb"&gt;wait &lt;/span&gt;tasks-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tasks&lt;/span&gt; &lt;span class="nv"&gt;$TASK_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt;

&lt;span class="c"&gt;# timestamp used for log retrieval (milliseconds after Jan 1, 1970 00:00:00 UTC)&lt;/span&gt;
&lt;span class="nv"&gt;END_TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s000&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# print the CloudWatch log events to STDOUT&lt;/span&gt;
aws logs get-log-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-group-name&lt;/span&gt; &lt;span class="s2"&gt;"/ecs/&lt;/span&gt;&lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="s2"&gt;/migrate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-stream-name&lt;/span&gt; &lt;span class="s2"&gt;"migrate/migrate/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="p"&gt;##*/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; &lt;span class="nv"&gt;$START_TIME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; &lt;span class="nv"&gt;$END_TIME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.events[].message'&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Migrations complete. Starting rolling update for backend services..."&lt;/span&gt;

&lt;span class="c"&gt;# update backend services&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;TASK &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"gunicorn"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="s2"&gt;"beat"&lt;/span&gt;
&lt;span class="k"&gt;do&lt;/span&gt;

  &lt;span class="c"&gt;# get taskDefinitionArn for each service to be used in update-service command&lt;/span&gt;
  &lt;span class="c"&gt;# this will get the most recent revision of each task (the one that was just created)&lt;/span&gt;
  &lt;span class="c"&gt;# https://docs.aws.amazon.com/cli/latest/reference/ecs/describe-task-definition.html#description&lt;/span&gt;
  &lt;span class="nv"&gt;TASK_DEFINITION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    aws ecs describe-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;-&lt;span class="nv"&gt;$TASK&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      .taskDefinition.taskDefinitionArn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# update each service with new task definintion&lt;/span&gt;
  aws ecs update-service &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;-&lt;span class="nv"&gt;$TASK&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DEFINITION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-cli-pager&lt;/span&gt;

&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Services updated. Waiting for services to become stable..."&lt;/span&gt;

&lt;span class="c"&gt;# wait for all service to be stable (runningCount == desiredCount for each service)&lt;/span&gt;
aws ecs &lt;span class="nb"&gt;wait &lt;/span&gt;services-stable &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--services&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-gunicorn&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-default&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-beat&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Services are now stable. Backend services are now up to date with &lt;/span&gt;&lt;span class="nv"&gt;$BACKEND_IMAGE_TAG&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backend update is now complete!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this GitHub Actions workflow, a developer can now easily change the version of backend code that is running in their ad hoc environments without needing to involve Terraform. Using the &lt;code&gt;ad_hoc_backend_update.yml&lt;/code&gt; GitHub Actions workflow, a developer only needs to enter the name of the workspace and the version of the backend code they want to use. The workflow will then run the &lt;code&gt;migrate&lt;/code&gt; task and update the backend services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;ignore_changes&lt;/code&gt; in the definitions
&lt;/h3&gt;

&lt;p&gt;There is one more important point to make about Terraform before we conclude this discussion on updating backend code for existing ad hoc environments. Consider the scenario where a developer has launched an ad hoc environment with backend version &lt;code&gt;v1.0.0&lt;/code&gt;. They make a small change to the backend code and push version &lt;code&gt;v1.0.1&lt;/code&gt;. Next, the developer remembers that a backend environment variable needs to be changed. This can be done by updating their &lt;code&gt;*.tfvars&lt;/code&gt; file. If they now re-run the ad hoc environment update pipeline &lt;strong&gt;without also updating the backend version in their &lt;code&gt;*.tfvars&lt;/code&gt; file&lt;/strong&gt;, then their code will be reverted from &lt;code&gt;v1.0.1&lt;/code&gt; to &lt;code&gt;v1.0.0&lt;/code&gt;. We would need to coordinate version changes between updating the backend with the pipelines that use Terraform commands and the pipelines that use the AWS CLI commands.&lt;/p&gt;

&lt;p&gt;There is a setting on the &lt;code&gt;aws_ecs_service&lt;/code&gt; resource in Terraform we can can use to prevent this from happening. This setting is called &lt;a href="https://www.terraform.io/language/meta-arguments/lifecycle"&gt;&lt;code&gt;ignore_changes&lt;/code&gt;&lt;/a&gt; and is defined under the resource's &lt;code&gt;lifecycle&lt;/code&gt; configuration block. With this setting, when we update the &lt;code&gt;*.tfvars&lt;/code&gt; file with our new environment variable value, we will create another recent task definition with the same &lt;code&gt;v1.0.0&lt;/code&gt; image, but the ECS service will not update in response to this change (that's what the &lt;code&gt;ignore_changes&lt;/code&gt; is for). Once we make the &lt;code&gt;*.tfvars&lt;/code&gt; file update and redeploy using the Terraform pipeline, nothing on our ad hoc changes, but we did get a new task definitions defined in our AWS account for each backend service. When we go to make the backend update with the pipeline that uses AWS CLI commands, the most recent task revision is used to create the new task definition, so it will include the environment variable change that we added earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend updates
&lt;/h3&gt;

&lt;p&gt;The process described above is needed for updating the backend application. Updating the frontend application involves a similar process to the backend update. The main difference is that no task (such as the &lt;code&gt;migrate&lt;/code&gt; command on the backend) needs to run before the service is updated. Here's an overview of the frontend update process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch the container definition JSON for the frontend tasks (&lt;code&gt;aws ecs describe-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;write new container definition JSON with the new frontend image tag (using &lt;code&gt;jq&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;register new task definitions with the new container definition JSON file for the frontend task (&lt;code&gt;aws ecs register-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;update the frontend service (&lt;code&gt;aws ecs update-service&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;wait for the new backend services to be stable (&lt;code&gt;aws ecs wait services-stable&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up everything from a new AWS account and GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Here's a quick overview of initial setup steps that are needed in order to use the automation defined in the GitHub Actions for ad hoc environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure AWS credentials locally
&lt;/h3&gt;

&lt;p&gt;There is one Terraform command that we will run on our local machine to setup a remote backend for storing our Terraform state. In order to run this, we need to configure AWS credentials locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run the &lt;code&gt;make tf-bootstrap&lt;/code&gt; command
&lt;/h3&gt;

&lt;p&gt;This command will setup an S3 bucket and DynamoDB table for storing Terraform state. Running this command will also require that a &lt;code&gt;bootstrap.tfvars&lt;/code&gt; file has been created from the template. This will define the AWS region and name to be used for creating resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build and push a backend and frontend image
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;tf-bootstrap&lt;/code&gt; command also creates ECR repositories for the backend and frontend images. We can use the &lt;code&gt;ecr_backend.yml&lt;/code&gt; and &lt;code&gt;ecr_frontend.yml&lt;/code&gt; GitHub Actions workflows to build and push the backend and frontend images to the ECR repositories. These pipelines accept a single parameter which is a git tag that must exist in the repo. This git tag will then be used as the image tag for the backend and frontend images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Purchase a domain name in Route 53
&lt;/h3&gt;

&lt;p&gt;I use Route 53 to manage DNS records, and have purchased a domain name that I use for testing and debugging in this and other projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a wildcard ACM certificate
&lt;/h3&gt;

&lt;p&gt;I chose to create this certificate outside of Terraform and import it via ARN. We need a wildcard certificate so that multiple ad hoc environments can be hosted on subdomains of the same domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a new EC2 key pair
&lt;/h3&gt;

&lt;p&gt;The key pair should be created manually and it needs to be added to GitHub repository secrets so that it can be used in the ad hoc environment pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add secrets to GitHub
&lt;/h3&gt;

&lt;p&gt;The following secrets are needed for GitHub Actions to run. Add these as repository secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ACM_CERTIFICATE_ARN&lt;/code&gt; - ARN of the wildcard ACM certificate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; - AWS access key ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt; - AWS account ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_DEFAULT_REGION&lt;/code&gt; - AWS default region (I use &lt;code&gt;us-east-1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; - AWS secret access key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOMAIN_NAME&lt;/code&gt; - domain name for the ad hoc environment (e.g. example.com)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KEY_NAME&lt;/code&gt; - name of the EC2 key pair&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt; - private key for the EC2 key pair&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TF_BACKEND_BUCKET&lt;/code&gt; - name of the S3 bucket used for storing Terraform state&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TF_BACKEND_DYNAMODB_TABLE&lt;/code&gt; - name of the DynamoDB table used for locking the Terraform state file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TF_BACKEND_REGION&lt;/code&gt; - AWS region for the S3 bucket and DynamoDB table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These secrets are referenced in the GitHub Actions workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create shared resources
&lt;/h3&gt;

&lt;p&gt;Now that we have GitHub secrets configured, we can run the &lt;code&gt;shared_resources_create_update.yml&lt;/code&gt; GitHub Actions workflow. This will create  shared resources environment in which we can build our ad hoc environments. This workflow requires a name (e.g. &lt;code&gt;dev&lt;/code&gt;). This require that we create a &lt;code&gt;dev.tfvars&lt;/code&gt; file in &lt;code&gt;terraform/live/shared-resources&lt;/code&gt; directory. This usually takes between 5 and 7 minutes to complete since it needs to create an RDS instance which takes a few minutes to provision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create an ad hoc environment
&lt;/h3&gt;

&lt;p&gt;We can now create an ad hoc environment. This requires the name of the shared resources environment (e.g. &lt;code&gt;dev&lt;/code&gt;) and the name of the ad hoc environment (e.g. &lt;code&gt;brian-test&lt;/code&gt;). The only thing we need to do before creating the &lt;code&gt;brian-test&lt;/code&gt; ad hoc environment is to create the &lt;code&gt;brian-test.tfvars&lt;/code&gt; file in the &lt;code&gt;terraform/live/ad-hoc/envs&lt;/code&gt; directory. This will define the versions of the application and any other environment configuration that is needed. This pipeline usually takes about 3 minutes to finish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the backend version in an ad hoc environment
&lt;/h3&gt;

&lt;p&gt;Now that the backend is up and running and usable, we can update the backend version used in our ad hoc environment. This can be done by running the &lt;code&gt;ad_hoc_backend_update.yml&lt;/code&gt; GitHub Actions workflow. To run this workflow you must specify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the shared resource workspace (e.g. &lt;code&gt;dev&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the ad hoc environment name (e.g. &lt;code&gt;brian-test&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the new version of the backend to be used (e.g. &lt;code&gt;v1.0.2&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend version should already have been built and pushed to the ECR repository using the &lt;code&gt;ecr_backend.yml&lt;/code&gt; GitHub Actions workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use ECS Exec to access a Django shell in a running container
&lt;/h3&gt;

&lt;p&gt;Instead of SSHing into the container, we can use the &lt;code&gt;ecs-exec&lt;/code&gt; command to access a shell in the container. This is useful for debugging and testing. One of the outputs of the ad hoc environment terraform configuration contains the script needed to run the &lt;code&gt;ecs-exec&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TASK_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs list-tasks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; alpha-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt;  alpha-gunicorn | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.taskArns | .[0]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;
aws ecs execute-command &lt;span class="nt"&gt;--cluster&lt;/span&gt; alpha-cluster &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task&lt;/span&gt; &lt;span class="nv"&gt;$TASK_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--container&lt;/span&gt; gunicorn &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"/bin/bash"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will then have a shell in the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-073d1947fa71c058c
root@ip-10-0-2-167:/code# python manage.py shell
Python 3.9.9 (main, Dec 21 2021, 10:03:34)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Destroy an ad hoc environment
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;ad_hoc_env_destroy.yml&lt;/code&gt; GitHub Actions workflow to destroy an ad hoc environment. To run this workflow you need to specify the shared resource workspace (e.g. &lt;code&gt;dev&lt;/code&gt;) and the ad hoc environment name (e.g. &lt;code&gt;brian-test&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Destroy the shared resources environment
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;shared_resources_destroy.yml&lt;/code&gt; GitHub Actions workflow to destroy the shared resources environment. To run this workflow you need to specify the shared resource workspace (e.g. &lt;code&gt;dev&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This defines the full lifecycle of creating and destroying ad hoc environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future improvements, open questions and next steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enhanced Security
&lt;/h3&gt;

&lt;p&gt;The low hanging fruit here is to use least privilege (for roles used in automation) and to define all roles with IaC. Currently I am using Admin roles for the credentials I store in GitHub which is a shortcut to using IaC and is not a best practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping track of ad hoc environments
&lt;/h3&gt;

&lt;p&gt;We need to think about how we can keep track of our active ad hoc environments. Active environments will incur additional AWS costs, and we do not want developers or the product team to create lots of environments and then leave them running without actively using them.&lt;/p&gt;

&lt;p&gt;We may decide to have some long-lived ad hoc environments, but those would be managed primarily by the DevOps team and respective owners (e.g. QA, product team, etc.).&lt;/p&gt;

&lt;p&gt;One way to check the active ad hoc environments would be to use the AWS CLI. We could list the ECS clusters in our development account, and this would show the number of ad hoc environments running. We could go farther and list the ad hoc environments by when they were last updated. We could then request developers or team members to remove ad hoc environments that are not in use.&lt;/p&gt;

&lt;p&gt;Or we could have a policy that all ad-hoc environments are deleted automatically at the end of each week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform Tooling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Testing Terraform Code&lt;/li&gt;
&lt;li&gt;Testing GitHub Actions&lt;/li&gt;
&lt;li&gt;Using advanced Terraform frameworks like Terragrunt&lt;/li&gt;
&lt;li&gt;Using Terraform Cloud for more advanced Terraform features&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  More secure way of defining RDS username and password
&lt;/h3&gt;

&lt;p&gt;Currently the postgres database does not have a secure password. It is both hardcoded in the module as a default value and it will also be saved in plaintext in the Terraform state file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend application update script
&lt;/h3&gt;

&lt;p&gt;The script used for updating the backend application could be improved or broken up into multiple scripts to better handle errors and failures that happen during the pipeline. The script runs several different commands and could potentially fail at any step, so it would be nice to improve the error messages so that both developers and DevOps teams can more quickly diagnose pipeline failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limiting traffic to ad hoc environments to a VPN
&lt;/h3&gt;

&lt;p&gt;Another good next step would be to show how we can limit traffic to ad hoc environments to a VPN.&lt;/p&gt;

&lt;h3&gt;
  
  
  Figure out how many ad hoc environments we can create with the default quotas
&lt;/h3&gt;

&lt;p&gt;AWS accounts limit the number of resources you can create, but for most of this quota limits you can request an increase per account. I need to figure out how many ad hoc environments I can create with the default quotas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Repo Organization
&lt;/h3&gt;

&lt;p&gt;One minor improvement would be to move the &lt;code&gt;terraform&lt;/code&gt; directory out of the &lt;code&gt;django-step-by-step&lt;/code&gt; monorepo into a dedicated repo. We may also want to move GitHub Actions for creating, updating and destroying environments to this new repo. For early stage development, using a single repository that stores both application code and Terraform configuration works, but it would be better to keep these separate at the repository level as the project grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple AWS Accounts
&lt;/h3&gt;

&lt;p&gt;Everything shown here uses a single AWS account: ECR images, Terraform remote state storage, all shared resource environments and all ad hoc environments. Using one account keeps things simple for a demonstration of this workflow, but in practice it would be beneficial to use multiple AWS accounts for different purposes. This would also involve more carefully planned IAM roles for cross-account resource access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modules for stable environments to be used for long-lived pre-production and production environments
&lt;/h3&gt;

&lt;p&gt;This article looked at how to make tradeoffs between costs, speed of deployment and production parity in ad hoc environments. I'm interested in building a new set of modules that can be used to set up environments that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;are more stable and more long-lived&lt;/li&gt;
&lt;li&gt;have less resource sharing (dedicated RDS and ElastiCache resources)&lt;/li&gt;
&lt;li&gt;implement autoscaling for load-testing (or maybe implement autoscaling for ad hoc environments)&lt;/li&gt;
&lt;li&gt;can be used to perform load testing&lt;/li&gt;
&lt;li&gt;have enhanced observability tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These environments might be used as part of a QA process that does a final sign-off on a new set of features scheduled for deployment to production environments, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This wraps up the tour of my ad hoc environment infrastructure automation. Thank you for having a read through my article. If you have a similar (or different) approach to building ad hoc environments, I would love to hear about it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>github</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
