<?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>DEV Community: Nóra Pap</title>
    <description>The latest articles on DEV Community by Nóra Pap (@nra_pap_235c62c0269f8ed3).</description>
    <link>https://dev.to/nra_pap_235c62c0269f8ed3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3596693%2F463dd248-7c00-4b27-9790-a203a49dc836.jpg</url>
      <title>DEV Community: Nóra Pap</title>
      <link>https://dev.to/nra_pap_235c62c0269f8ed3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nra_pap_235c62c0269f8ed3"/>
    <language>en</language>
    <item>
      <title>Deploying Temporal on AWS ECS with Terraform</title>
      <dc:creator>Nóra Pap</dc:creator>
      <pubDate>Wed, 05 Nov 2025 06:37:43 +0000</pubDate>
      <link>https://dev.to/nra_pap_235c62c0269f8ed3/deploying-temporal-on-aws-ecs-with-terraform-592c</link>
      <guid>https://dev.to/nra_pap_235c62c0269f8ed3/deploying-temporal-on-aws-ecs-with-terraform-592c</guid>
      <description>&lt;p&gt;This post was originally published on &lt;a href="https://papnori.github.io/posts/temporal-ecs-terraform/" rel="noopener noreferrer"&gt;my personal blog&lt;/a&gt;. Check it out!&lt;/p&gt;

&lt;h2&gt;
  
  
  👋  Introduction
&lt;/h2&gt;

&lt;p&gt;Most of Temporal’s official &lt;a href="https://docs.temporal.io/production-deployment/worker-deployments/deploy-workers-to-aws-eks" rel="noopener noreferrer"&gt;deployment guides&lt;/a&gt; focus on Kubernetes. While Kubernetes is powerful and battle-tested for large-scale workloads, it also comes with significant overhead — &lt;strong&gt;both financially&lt;/strong&gt; (AWS EKS clusters aren’t cheap 💸) and &lt;strong&gt;operationally&lt;/strong&gt; (node upgrades, cluster maintenance, networking complexity).&lt;/p&gt;

&lt;p&gt;For many teams, especially when just getting started, that’s more than you actually need.&lt;/p&gt;

&lt;p&gt;This guide walks through deploying Temporal workers on AWS ECS with Terraform. Using ECS (especially with Fargate) can &lt;strong&gt;reduce infrastructure costs by ~70%&lt;/strong&gt; while still &lt;strong&gt;providing elasticity, resilience, and production-ready reliability&lt;/strong&gt;. 💪 &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;Amazon Elastic Container Service (ECS) is a fully managed service for running Docker containers on AWS. &lt;/p&gt;

&lt;p&gt;It allows you to deploy, manage, and scale containerized applications without manually handling servers or complex orchestrations.&lt;/p&gt;

&lt;p&gt;ECS gives you two ways to run containers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ECS on EC2&lt;/strong&gt; — you manage the EC2 instances that run your containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS on Fargate&lt;/strong&gt; (The focus of this blog) — AWS manages the infrastructure; you just define your containers and how many to run. This gives you a completely serverless architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ECS handles container scheduling, load balancing, scaling, and networking — so you can focus on your application, not the plumbing.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h4&gt;
  
  
  🏆 By the end, you’ll have:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-scaling Temporal workers on ECS&lt;/strong&gt; (prioritizing Fargate Spot instances when available).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform-managed infrastructure&lt;/strong&gt; with &lt;strong&gt;remote state in S3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Optionally, &lt;strong&gt;CI/CD pipelines&lt;/strong&gt; via GitHub Actions to build, publish, and deploy.&lt;/li&gt;
&lt;li&gt;A working Temporal workflow with one activity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In short&lt;/strong&gt;: a reproducible, extensible setup you can grow into production just by adding your business logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐦 Bird’s-Eye View
&lt;/h2&gt;

&lt;p&gt;Before diving into the setup, let’s zoom out and see how all the pieces fit together.&lt;/p&gt;

&lt;p&gt;You’re running &lt;strong&gt;Temporal Cloud&lt;/strong&gt; or &lt;strong&gt;Self-hosting Temporal Server&lt;/strong&gt; as the orchestration layer, while &lt;strong&gt;your workers&lt;/strong&gt; live inside AWS ECS Fargate, and all of it is wrapped in infrastructure-as-code (IaC) through Terraform.&lt;/p&gt;

&lt;h4&gt;
  
  
  👉 Think of it like this:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Temporal → brain 🧠&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ECS Fargate workers → muscles 💪&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VPC + Terraform → skeleton 🦴 keeping everything in place.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;From a networking perspective:&lt;/u&gt; your workers live in &lt;strong&gt;private subnets&lt;/strong&gt;, they pull Docker images from Amazon ECR (Elastic Container Registry), talk to Temporal Cloud or a Self-hosted Temporal Server, and fetch secrets from Secrets Manager — all securely, with NAT and VPC endpoints handling connectivity.&lt;/p&gt;

&lt;p&gt;Additionally, the Terraform state is safely stored in &lt;strong&gt;S3&lt;/strong&gt;, so multiple developers or CI/CD pipelines can collaborate without fear of stepping-on each others changes. &lt;/p&gt;

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




&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;In this blog, we’re focusing on how to connect Temporal Workers running in &lt;strong&gt;AWS private subnets&lt;/strong&gt; to &lt;strong&gt;external orchestration endpoints&lt;/strong&gt; — either &lt;strong&gt;Temporal Cloud&lt;/strong&gt; or a &lt;strong&gt;self-hosted Temporal Server&lt;/strong&gt; running locally (via Ngrok).&lt;/p&gt;

&lt;p&gt;This design intentionally demonstrates how workers inside a VPC can securely reach &lt;strong&gt;internet-accessible resources&lt;/strong&gt; while remaining isolated from inbound traffic.&lt;/p&gt;

&lt;p&gt;In a production environment, you could alternatively &lt;strong&gt;host Temporal Server inside your AWS VPC&lt;/strong&gt; — allowing internal-only communication between workers and the server.&lt;/p&gt;

&lt;p&gt;We’ve omitted that configuration here for simplicity, since the focus is on showing how private Fargate tasks of Temporal workers connect outward safely and reliably.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🏗️ Architecture Components
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;This section breaks down the core AWS architecture components we'll be using — perfect if you’re new to the cloud or need a refresher. 🍹&lt;/p&gt;

&lt;p&gt;If you already know your way around VPCs, subnets, and gateways etc. feel free to skip ahead. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  🦴 Virtual Private Cloud (VPC) - The Skeleton of Your AWS Neighborhood
&lt;/h4&gt;

&lt;p&gt;A logically isolated virtual network in AWS where you define your own IP ranges, subnets, and routing.&lt;/p&gt;

&lt;p&gt;Think of the VPC as both the &lt;strong&gt;framework&lt;/strong&gt; (the skeleton) of your AWS environment and the private &lt;strong&gt;neighborhood&lt;/strong&gt; where everything lives. 🏡&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You decide the street map (IP ranges). &lt;/li&gt;
&lt;li&gt;You layout the streets inside it (subnets). &lt;/li&gt;
&lt;li&gt;You choose where cars can drive (routing). &lt;/li&gt;
&lt;li&gt;It’s still inside the big city (AWS), but only you control who gets in, who gets out, and how things move around. 🚦&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  🛣️ Subnets — The Streets of Your Neighborhood
&lt;/h4&gt;

&lt;p&gt;A &lt;strong&gt;subnet&lt;/strong&gt; is a slice of your VPC’s IP range used to group resources. &lt;/p&gt;

&lt;p&gt;Subnets can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public&lt;/strong&gt; → A public street connects straight to the highway (&lt;strong&gt;Internet Gateway&lt;/strong&gt;) — anyone can drive in and out. 🚗💨&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private&lt;/strong&gt; → A private street doesn’t connect to the highway. 🚧 
To access external resources, cars must first pass through a special &lt;strong&gt;toll booth (NAT Gateway)&lt;/strong&gt; or utilize &lt;strong&gt;secret tunnels (VPC endpoints)&lt;/strong&gt; to reach specific AWS services. 🛣️✨&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why bother with private subnets?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Private subnets limit exposure of your temporal workers to the internet.&lt;/li&gt;
&lt;li&gt;Ensures all outbound traffic goes through the Internet Gateway for control and monitoring.&lt;/li&gt;
&lt;li&gt;Follows AWS best practices for ECS + Fargate workloads that consume external services.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  🚇 VPC Endpoints — The Secret Tunnels
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;VPC endpoints&lt;/strong&gt; provide private connections to AWS services &lt;em&gt;without sending traffic over the public internet&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Think of them like &lt;strong&gt;secret tunnels&lt;/strong&gt; 🕳️✨ from your neighborhood (VPC) straight to certain AWS stores like &lt;strong&gt;S3&lt;/strong&gt; or &lt;strong&gt;Secrets Manager&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This keeps your data inside AWS’s private network — safe, fast, and never exposed to the open internet.&lt;/p&gt;

&lt;h4&gt;
  
  
  🌐 Internet Gateway (IGW) — The Highway On-Ramp
&lt;/h4&gt;

&lt;p&gt;An &lt;strong&gt;Internet Gateway&lt;/strong&gt; is the VPC’s connection to the public internet. It provides &lt;strong&gt;two-way connectivity&lt;/strong&gt; between your VPC and the outside world.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's directly attached to VPC. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only public subnets&lt;/strong&gt; (with routes to the IGW) can use it directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private subnets&lt;/strong&gt; &lt;em&gt;can't&lt;/em&gt; talk to the Internet Gateway directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We added an Internet Gateway to enable public access and to allow private subnet resources (like Temporal workers) to &lt;strong&gt;reach the internet&lt;/strong&gt; via the NAT Gateway when needed — for example, to connect to &lt;strong&gt;Temporal Cloud&lt;/strong&gt; or a &lt;strong&gt;Self-hosted Temporal Server&lt;/strong&gt; securely.&lt;/p&gt;

&lt;h4&gt;
  
  
  🚧 NAT Gateway — The Toll Booth
&lt;/h4&gt;

&lt;p&gt;A &lt;strong&gt;NAT Gateway&lt;/strong&gt; (Network Address Translation Gateway) is a managed AWS service that allows private subnets to &lt;em&gt;reach the internet&lt;/em&gt; &lt;em&gt;without being exposed to it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It &lt;strong&gt;sits inside a public subnet&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Private subnets &lt;strong&gt;route their outbound internet traffic through the NAT Gateway&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;NAT then forwards it through the Internet Gateway&lt;/strong&gt;.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, &lt;strong&gt;private instances can reach the internet&lt;/strong&gt;, but nothing can come in directly 🎉&lt;/p&gt;

&lt;p&gt;Think of the NAT Gateway as a &lt;strong&gt;toll booth&lt;/strong&gt; 🚧💳 at the edge of your private street. Cars (private subnet resources) &lt;em&gt;can’t&lt;/em&gt; drive straight onto the highway 🛣️. Instead, they go through the toll booth (NAT Gateway), which gives them a special pass. 🎟️ &lt;/p&gt;

&lt;p&gt;This way, private cars can go out to the highway (e.g. download updates, reach Temporal Cloud) but no one from the highway can drive straight into your private neighborhood.&lt;/p&gt;

&lt;p&gt;This again a crucial piece in the puzzle so our &lt;strong&gt;private Temporal workers&lt;/strong&gt; can securely reach &lt;strong&gt;Temporal Cloud&lt;/strong&gt;, a &lt;strong&gt;Self-hosted Temporal Server&lt;/strong&gt; or any other external services.&lt;/p&gt;

&lt;h4&gt;
  
  
  🧠 Temporal — The Brain
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Temporal&lt;/strong&gt; acts as the control center for your workflows. It tracks workflow state, retries, timers, and execution history — ensuring everything runs in order and nothing slips through the cracks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It manages task scheduling and queues.&lt;/li&gt;
&lt;li&gt;Workers connect over the internet to Temporal Cloud’s or Self-hosted Temporal Server's endpoint to poll, fetch and execute tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  🚢 Amazon ECR — The Container Registry
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Elastic Container Registry (ECR)&lt;/strong&gt; stores your worker Docker images.
Every time you update your code, a &lt;strong&gt;GitHub Actions&lt;/strong&gt; workflow (&lt;code&gt;.github/workflows/build-and-publish-ecr-dev.yaml&lt;/code&gt;) automatically builds and pushes a new image to ECR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ECR act as the "warehouse" where your latest worker code packaged and stored, ready for ECS to pull and run.&lt;/p&gt;

&lt;h4&gt;
  
  
  🔐 AWS Secrets Manager — The Vault
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; securely stores sensitive connection details such as your &lt;strong&gt;Temporal endpoint, port, namespace, and API key&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At runtime, these secrets are injected directly into ECS tasks — no hardcoding, no plaintext configs. Your workers get exactly what they need to authenticate and connect, safely.&lt;/p&gt;

&lt;h4&gt;
  
  
  🏘️ ECS Cluster — The Control Room
&lt;/h4&gt;

&lt;p&gt;The &lt;strong&gt;ECS cluster&lt;/strong&gt; is where your Temporal workers live and operate, running as &lt;strong&gt;Fargate tasks&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Autoscaling is configured via &lt;strong&gt;CloudWatch alarms on CPU usage&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Scale-out policy &lt;em&gt;adds workers&lt;/em&gt; when CPU &amp;gt; 30%. &lt;/li&gt;
&lt;li&gt;Scale-in policy &lt;em&gt;removes idle workers&lt;/em&gt; when CPU &amp;lt; 20%.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;This setup delivers a &lt;strong&gt;secure, cost-efficient, self-managing and self-healing&lt;/strong&gt; worker fleet — no need for Kubernetes overhead and maybe even enough savings 💰 to keep you stocked with more instant ramen. 🍜 😉&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  💪 ECS Fargate — The Muscle
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;AWS Fargate&lt;/strong&gt; provides the compute power behind your ECS tasks — a &lt;strong&gt;serverless, container-based infrastructure&lt;/strong&gt; that eliminates server management entirely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Containerized Workers&lt;/strong&gt; → Temporal workers are packaged into Docker images and deployed as ECS services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Delivery&lt;/strong&gt; → Images are built and published to Amazon ECR (can be manually or automatically via GitHub Actions), then pulled by ECS at deploy time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking&lt;/strong&gt; → Tasks run inside &lt;strong&gt;private subnets&lt;/strong&gt;, pull images from ECR, and connect outbound to Temporal Cloud or a Self-hosted Temporal Server through the &lt;strong&gt;NAT Gateway&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity Providers&lt;/strong&gt; → ECS can use both &lt;strong&gt;On-Demand&lt;/strong&gt; and &lt;strong&gt;Spot&lt;/strong&gt; capacity.

&lt;ul&gt;
&lt;li&gt;In this setup, Spot is prioritized to reduce costs.&lt;/li&gt;
&lt;li&gt;On-Demand acts as a fallback for reliability.&lt;/li&gt;
&lt;li&gt;If a Spot instance is interrupted (2-minute warning), Temporal automatically retries tasks on other workers.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Pay-as-you-go&lt;/strong&gt; → Billing is only for CPU and memory actually used.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Secrets &amp;amp; Config&lt;/strong&gt; → Environment variables (namespace, API key, endpoint, etc.) are securely injected from AWS Secrets Manager at runtime, allowing workers to register and run workflows seamlessly.&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  🔍 &lt;strong&gt;Amazon CloudWatch&lt;/strong&gt; — The Observability Dashboard
&lt;/h4&gt;

&lt;p&gt;Observability is handled entirely through &lt;strong&gt;Amazon CloudWatch&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Logs&lt;/strong&gt; → all worker/container logs ship here and are centralized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Alarms&lt;/strong&gt; → trigger autoscaling actions when thresholds are crossed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Metrics&lt;/strong&gt; → Track CPU, memory usage, scaling history to spot trends or issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudWatch ties it all together providing, monitoring, alerting, and scaling decisions in one place.&lt;/p&gt;

&lt;h4&gt;
  
  
  📈 Autoscaling — The Reflexes
&lt;/h4&gt;

&lt;p&gt;Autoscaling keeps your worker fleet responsive and efficient.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Alarms&lt;/strong&gt; monitor CPU utilization in real time.&lt;/li&gt;
&lt;li&gt;When usage is high, ECS automatically scales-out more workers.&lt;/li&gt;
&lt;li&gt;When usage is consistently low, ECS scales-in to save costs automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s the system’s built-in reflex — expanding under load and contracting when idle.&lt;/p&gt;




&lt;h3&gt;
  
  
  😎 Data flow
&lt;/h3&gt;

&lt;p&gt;Now that we’ve covered the components, let’s quickly walk through how data actually moves through the system, as defined in the above diagram — step by step.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client Trigger&lt;/strong&gt; → A client app (👩‍💻) calls the Temporal Cloud or Self-hosted Temporal Server API to trigger a workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task Scheduling&lt;/strong&gt; → &lt;strong&gt;Temporal Cloud&lt;/strong&gt; or &lt;strong&gt;Self-hosted Temporal Server&lt;/strong&gt; adds the workflow’s tasks to its internal queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker Polling&lt;/strong&gt; → &lt;strong&gt;ECS Fargate workers&lt;/strong&gt; continuously poll the Temporal queue and pick up available tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution&lt;/strong&gt; → Workers run the assigned activities — this is your business logic 👔 (in the &lt;a href="https://github.com/papnori/temporal-ecs-terraform" rel="noopener noreferrer"&gt;linked demo example&lt;/a&gt;, we just simply save a text file to S3 bucket).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results &amp;amp; Updates&lt;/strong&gt; → Workers send results and progress updates back to &lt;strong&gt;Temporal Cloud&lt;/strong&gt; or a &lt;strong&gt;Self-hosted Temporal Server&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring &amp;amp; Scaling&lt;/strong&gt; → &lt;strong&gt;CloudWatch&lt;/strong&gt; tracks performance metrics and automatically scales the number of workers based on load.&lt;/li&gt;
&lt;/ol&gt;

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




&lt;h3&gt;
  
  
  ✨ Why this architecture works well:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resilient&lt;/strong&gt;: Workflows survive worker crashes. 
Temporal guarantees retries &amp;amp; state persistence, which is super useful, as we use mainly Spot instances. &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;Spot instances are &lt;em&gt;ephemeral and can be terminated&lt;/em&gt; with a 2-minute warning. If the worker simply "dies", the task will fail with a &lt;code&gt;TimeOutError&lt;/code&gt;, and Temporal will reschedule the task to be executed by another worker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secure&lt;/strong&gt;: Workers live in private subnets, and secrets are injected securely at runtime - never hardcoded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost-efficient&lt;/strong&gt;: Fargate Spot + Auto-scaling means you only pay for what you use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalable&lt;/strong&gt;: Worker capacity can multiply within minutes when workflow volume spikes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Maintainable&lt;/strong&gt;: Infrastructure is managed via Terraform, with state stored securely in S3.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📝 Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you begin, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporal cloud account&lt;/strong&gt;, if you're using Temporal Cloud&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://temporal.io/get-cloud" rel="noopener noreferrer"&gt;Sign up at Temporal Cloud&lt;/a&gt; and create a &lt;code&gt;namespace&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Temporal Server locally&lt;/strong&gt; (&lt;a href="https://github.com/papnori/temporal-ecs-terraform/tree/main/local-temporal-server" rel="noopener noreferrer"&gt;see GitHub repo on how&lt;/a&gt;) &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;An &lt;strong&gt;AWS account with&lt;/strong&gt; &lt;code&gt;AdministratorAccess&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’ll need permissions to create VPCs, ECS clusters, Secrets, IAM roles, and CloudWatch alarms.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Terraform&lt;/strong&gt; &amp;gt;= 1.13.3&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub repo to store your code and Terraform configurations.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Some nice champagne to celebrate in 30 minutes, when we are done 🥂 (optional but recommended).&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;👉 Full code is available here: &lt;a href="https://github.com/papnori/temporal-ecs-terraform" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  💻 How to set it up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. 🤫 Store Temporal secrets in AWS Secrets Manager
&lt;/h3&gt;

&lt;p&gt;First, you’ll need to store your Temporal Cloud or a Self-hosted Temporal Server credentials in &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; so that ECS tasks can fetch them securely at runtime.&lt;/p&gt;

&lt;p&gt;These secrets allow your workers to authenticate and connect to Temporal without exposing credentials in your codebase.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Warning&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Never commit secrets to version control!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Create a new secret (for example, &lt;code&gt;dev/sample-config&lt;/code&gt;) in the &lt;code&gt;us-east-1&lt;/code&gt; region &lt;strong&gt;— the same region where your ECS cluster will run&lt;/strong&gt; — with the following key–value pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TEMPORAL_NAMESPACE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TEMPORAL_SERVER_ENDPOINT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"temporal.cluster-xxxx.us-east-1.aws.cloud.temporal.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Ngrok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;you're&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self-hosting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;locally&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TEMPORAL_SERVER_PORT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7233"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;needed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tunneling&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;through&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Ngrok ⁠&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TEMPORAL_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ey..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;needed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;self-hosting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;locally&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;Replace the values above with the ones from your Temporal Cloud account. The API key should come from the Temporal Cloud console.&lt;/p&gt;

&lt;p&gt;If you’re running a &lt;strong&gt;self-hosted Temporal Server locally&lt;/strong&gt;, replace &lt;code&gt;TEMPORAL_SERVER_ENDPOINT&lt;/code&gt; with your &lt;strong&gt;Ngrok URL&lt;/strong&gt;, and omit &lt;code&gt;TEMPORAL_API_KEY&lt;/code&gt; , it’s only needed for Temporal Cloud.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can skip configuring rotation on AWS Secrets Manager for this demo.&lt;/p&gt;

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

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

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




&lt;h3&gt;
  
  
  2. 🦄 Terraform Infrastructure Setup
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;❗ Important&lt;/p&gt;

&lt;p&gt;This section assumes you have a basic understanding of Terraform — how it works, and how a Terraform project is typically structured.&lt;/p&gt;

&lt;p&gt;The layout we’ll use here is a &lt;strong&gt;hybrid&lt;/strong&gt; of the &lt;a href="https://developer.hashicorp.com/terraform/language/style#repository-structure" rel="noopener noreferrer"&gt;official HashiCorp repository structure&lt;/a&gt; and the approach outlined in Yevgeniy Brikman’s excellent book, &lt;a href="https://www.terraformupandrunning.com/" rel="noopener noreferrer"&gt;&lt;em&gt;Terraform: Up &amp;amp; Running&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll use &lt;strong&gt;four main folders&lt;/strong&gt; in our Terraform repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform/bootstrap/&lt;/code&gt; - one-time setup to create the S3 bucket for remote state storage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform/global/&lt;/code&gt; -  sources shared across &lt;em&gt;all environments&lt;/em&gt; or needed &lt;em&gt;before&lt;/em&gt; environment deployment (for example, ECR, IAM roles, or Route53 zones)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;terraform/live/&lt;/code&gt; - environment-specific configurations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; For this demo, we’ll create only a &lt;code&gt;dev&lt;/code&gt; environment, but you can easily extend to &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;, or others later.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;terraform/modules/&lt;/code&gt; - reusable building blocks such networking, ECS cluster and service definitions that are referenced by the live environment configurations.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;If you’re new to Terraform, you might wonder: &lt;em&gt;why do we need a separate setup just for the state files?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When you run Terraform, it generates a &lt;strong&gt;state file&lt;/strong&gt; — basically a snapshot of everything Terraform has created and manages. That state file has to live somewhere safe. &lt;/p&gt;

&lt;p&gt;Storing it only on your laptop is risky (imagine spilling coffee on it and suddenly losing all records of your infrastructure 🙃 — not exactly the ideal way to setup your new startup ☕💻🔥).&lt;/p&gt;

&lt;p&gt;That’s why we store it in &lt;strong&gt;Amazon S3&lt;/strong&gt; as a remote backend: durable, centralized, and accessible to your whole team.&lt;/p&gt;

&lt;p&gt;But here’s the catch: Terraform needs an S3 bucket to store state, but the bucket doesn’t exist until Terraform makes it — this is what’s often called IaC's &lt;strong&gt;classic chicken-and-egg problem&lt;/strong&gt;. 🐥🥚&lt;/p&gt;

&lt;p&gt;So the solution is to &lt;strong&gt;bootstrap&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;1) Run a small, one-time Terraform setup that creates the S3 bucket and state locking.&lt;/p&gt;

&lt;p&gt;2) Update your main Terraform project to use that remote S3 backend for storing state.&lt;/p&gt;

&lt;p&gt;This way, the chicken (Terraform) has a safe place to lay its egg (the state file). 🐣🥚&lt;/p&gt;

&lt;p&gt;An additional benefit: because Terraform state is stored in plain-text by default and may include secrets or config values, placing it in an encrypted S3 bucket (with proper access control) helps keep those values safe.&lt;/p&gt;

&lt;p&gt;Ready? Let's do it! 🏁&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  i. 👯‍♂️ Clone the Repository
&lt;/h4&gt;

&lt;p&gt;After setting up your AWS Secrets, clone or fork the repository so you can deploy the demo and build on top of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/papnori/temporal-ecs-terraform.git
&lt;span class="nb"&gt;cd &lt;/span&gt;temporal-ecs-terraform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  ii. 👢 Bootstrap the Amazon S3 bucket for storing Terraform State
&lt;/h4&gt;

&lt;p&gt;Next, we’ll create the &lt;strong&gt;S3 bucket&lt;/strong&gt; that will hold the remote Terraform state for our infrastructure.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Change directory&lt;/strong&gt; into &lt;code&gt;terraform/bootstrap/&lt;/code&gt; folder:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cd &lt;/span&gt;terraform/bootstrap/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open&lt;/strong&gt; &lt;code&gt;terraform/bootstrap/providers.tf&lt;/code&gt; and &lt;strong&gt;temporarily comment out&lt;/strong&gt; the &lt;code&gt;backend "s3" {}&lt;/code&gt; block (line 8-14).&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;This code block responsible for using the state file from S3 bucket.&lt;/p&gt;

&lt;p&gt;While commented out, Terraform fallsback to using a local state, as the S3 bucket does not exist yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;   &lt;span class="c1"&gt;#   backend "s3" {&lt;/span&gt;
   &lt;span class="c1"&gt;#     bucket       = "my-little-sample-terraform-state" # name of the bucket - globally unique&lt;/span&gt;
   &lt;span class="c1"&gt;#     key          = "bootstrap/terraform.tfstate"      # path to the state file in the bucket&lt;/span&gt;
   &lt;span class="c1"&gt;#     region       = "us-east-1"                        # region of the bucket&lt;/span&gt;
   &lt;span class="c1"&gt;#     use_lockfile = true                               # instead of dynamodb&lt;/span&gt;
   &lt;span class="c1"&gt;#     encrypt = true # encrypt the state file&lt;/span&gt;
   &lt;span class="c1"&gt;#   }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Warning&lt;/p&gt;

&lt;p&gt;Change the bucket name in &lt;code&gt;terraform/bootstrap/main.tf&lt;/code&gt; (line 2 in the file) and &lt;code&gt;terraform/bootstrap/providers.tf&lt;/code&gt; (line 9) to your &lt;em&gt;own&lt;/em&gt;, &lt;em&gt;unique bucket name&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;⚠️ Warning&lt;/p&gt;

&lt;p&gt;&lt;u&gt;&lt;strong&gt;Important&lt;/strong&gt;:&lt;/u&gt; &lt;strong&gt;S3 bucket names need to be *globally unique&lt;/strong&gt;* i.e. your bucket name should be unique across all AWS accounts and regions. You need to change the bucket name above to be compliant of this requirement otherwise bucket creation will fail. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialize&lt;/strong&gt; the Terraform backend and download the AWS Provider:
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;p&gt;The output may look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nv"&gt;$ &lt;/span&gt;terraform init
   Initializing the backend...
   Initializing provider plugins...
   - Finding hashicorp/aws versions matching &lt;span class="s2"&gt;"~&amp;gt; 6.0"&lt;/span&gt;...
   - Installing hashicorp/aws v6.15.0...
   - Installed hashicorp/aws v6.15.0 &lt;span class="o"&gt;(&lt;/span&gt;signed by HashiCorp&lt;span class="o"&gt;)&lt;/span&gt;
   ...
   Terraform has been successfully initialized!
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preview changes&lt;/strong&gt; to make sure correct infrastructure will be provisioned.
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;p&gt;Review the output, it should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nv"&gt;$ &lt;/span&gt;terraform plan

   Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
     + create

   Terraform will perform the following actions:

     &lt;span class="c"&gt;# aws_s3_bucket.terraform_state will be created&lt;/span&gt;
     + resource &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
         + acceleration_status         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
         + acl                         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
         + arn                         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
         ...
      &lt;span class="c"&gt;# aws_s3_bucket_public_access_block.block will be created&lt;/span&gt;
     + resource &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"block"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
         + block_public_acls       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
         ...
   Plan: 4 to add, 0 to change, 0 to destroy.

   Changes to Outputs:
     + s3_bucket_arn  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
     + s3_bucket_name &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-little-sample-terraform-state"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Finally, after a quick review we can &lt;strong&gt;apply&lt;/strong&gt; to create the infrastructure.
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;p&gt;It will again output the results of &lt;code&gt;terraform plan&lt;/code&gt; and ask for a confirmation. Reply with a &lt;code&gt;yes&lt;/code&gt; and the result should look like this:&lt;br&gt;
&lt;/p&gt;

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

   Plan: 4 to add, 0 to change, 0 to destroy.
   ...
   Do you want to perform these actions?
     Terraform will perform the actions described above.
     Only &lt;span class="s1"&gt;'yes'&lt;/span&gt; will be accepted to approve.

     Enter a value: &lt;span class="nb"&gt;yes

   &lt;/span&gt;aws_s3_bucket.terraform_state: Creating...
   aws_s3_bucket.terraform_state: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 7s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-little-sample-terraform-state]
   aws_s3_bucket_public_access_block.block: Creating...
   aws_s3_bucket_versioning.enabled: Creating...
   aws_s3_bucket_server_side_encryption_configuration.default: Creating...
   aws_s3_bucket_public_access_block.block: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 1s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-little-sample-terraform-state]
   aws_s3_bucket_server_side_encryption_configuration.default: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 1s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-little-sample-terraform-state]
   aws_s3_bucket_versioning.enabled: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 2s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-little-sample-terraform-state]

   Apply &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; Resources: 4 added, 0 changed, 0 destroyed.                                                                                                               
   Outputs:                                                                                                                     

   s3_bucket_arn &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:s3:::my-little-sample-terraform-state"&lt;/span&gt;               s3_bucket_name &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-little-sample-terraform-state"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;✅ Result:&lt;/strong&gt;&lt;br&gt;
You now have a remote S3 bucket ( in this demo: &lt;code&gt;my-little-sample-terraform-state&lt;/code&gt;) ready to hold your Terraform state.&lt;/p&gt;

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

&lt;p&gt;The Terraform state file from this bootstrap run is still &lt;strong&gt;local&lt;/strong&gt; (&lt;code&gt;terraform/bootstrap/terraform.tfstate&lt;/code&gt;) — so don’t spill your coffee yet ☕💻 — but the bucket now exists for future remote state storage.&lt;/p&gt;


&lt;h4&gt;
  
  
  iii. 📤 Update the setup to use the new S3 bucket for remote state.
&lt;/h4&gt;

&lt;p&gt;Time to make it &lt;strong&gt;coffee-proof.&lt;/strong&gt; ☕️&lt;/p&gt;

&lt;p&gt;Now that the S3 bucket exists, we can switch Terraform to use it as the &lt;strong&gt;remote backend&lt;/strong&gt; — so your state is stored safely in the cloud instead of locally.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Uncomment&lt;/strong&gt; the &lt;code&gt;backend "s3" {}&lt;/code&gt; block (line 8-14) in &lt;code&gt;terraform/bootstrap/providers.tf&lt;/code&gt; so it looks like this, but with &lt;em&gt;your own unique bucket name&lt;/em&gt;:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;   &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-little-sample-terraform-state"&lt;/span&gt; &lt;span class="c1"&gt;# globally unique bucket name&lt;/span&gt;
     &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bootstrap/terraform.tfstate"&lt;/span&gt;      &lt;span class="c1"&gt;# path to the state file in the bucket&lt;/span&gt;
     &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;                        &lt;span class="c1"&gt;# region of the bucket&lt;/span&gt;
     &lt;span class="nx"&gt;use_lockfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;                               &lt;span class="c1"&gt;# replaces DynamoDB for this demo&lt;/span&gt;
     &lt;span class="nx"&gt;encrypt&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;                               &lt;span class="c1"&gt;# encrypt the state file&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Re-initialize Terraform&lt;/strong&gt; to reconfigure the backend:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   terraform init &lt;span class="nt"&gt;-reconfigure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Terraform will detect your previous local state and prompt to migrate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   Do you want to copy existing state to the new backend?
     Pre-existing state was found &lt;span class="k"&gt;while &lt;/span&gt;migrating the previous &lt;span class="s2"&gt;"local"&lt;/span&gt; backend to the
     newly configured &lt;span class="s2"&gt;"s3"&lt;/span&gt; backend. No existing state was found &lt;span class="k"&gt;in &lt;/span&gt;the newly
     configured &lt;span class="s2"&gt;"s3"&lt;/span&gt; backend. Do you want to copy this state to the new &lt;span class="s2"&gt;"s3"&lt;/span&gt;
     backend? Enter &lt;span class="s2"&gt;"yes"&lt;/span&gt; to copy and &lt;span class="s2"&gt;"no"&lt;/span&gt; to start with an empty state.

     Enter a value: &lt;span class="nb"&gt;yes

     &lt;/span&gt;Successfully configured the backend &lt;span class="s2"&gt;"s3"&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; Terraform will automatically
     use this backend unless the backend configuration changes
     ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Type &lt;code&gt;yes&lt;/code&gt; to confirm.&lt;/strong&gt; Terraform will automatically &lt;strong&gt;move your local bootstrap state into the S3 bucket&lt;/strong&gt; at: &lt;code&gt;terraform/bootstrap/terraform.tfstate&lt;/code&gt;. 🙌&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;✅ &lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
Your Terraform state is now safely stored in Amazon S3 at &lt;code&gt;terraform/bootstrap/terraform.tfstate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No more local coffee hazards — your infrastructure state is &lt;em&gt;centralized, versioned, and team-ready&lt;/em&gt;. ☕💪&lt;/p&gt;

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


&lt;h4&gt;
  
  
  iv. 🚢 Create ECR Repository
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Warning&lt;/p&gt;

&lt;p&gt;Before you run anything below make sure to replace the S3 remote backend bucket name in &lt;code&gt;terraform/global/ecr/providers.tf&lt;/code&gt; (line 5) to the one you created above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With our remote backend ready, the next step is to set up a &lt;strong&gt;container registry&lt;/strong&gt; where we can store and version Docker images. This is where your &lt;strong&gt;Temporal Worker&lt;/strong&gt; image will live — built either manually or automatically later via GitHub Actions. 🤖&lt;/p&gt;

&lt;p&gt;We’ll use &lt;strong&gt;Amazon Elastic Container Registry (ECR)&lt;/strong&gt; for this purpose.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; create the repository manually in the AWS Console, but let’s go the &lt;strong&gt;Terraform route&lt;/strong&gt; — it’s reproducible, version-controlled, and integrates seamlessly with CI/CD pipelines. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Change directory&lt;/strong&gt; to the ECR Terraform module.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cd &lt;/span&gt;terraform/global/ecr/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialize Terraform&lt;/strong&gt; to set up providers and backend.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preview the plan&lt;/strong&gt; to verify what will be created.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt; the configuration.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Confirm with &lt;code&gt;yes&lt;/code&gt; when prompted.&lt;/p&gt;

&lt;p&gt;You should see output similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   Changes to Outputs:
     + temporal_worker_dev_repository_url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;

   Do you want to perform these actions?
     Terraform will perform the actions described above.
     Only &lt;span class="s1"&gt;'yes'&lt;/span&gt; will be accepted to approve.

     Enter a value: &lt;span class="nb"&gt;yes

   &lt;/span&gt;aws_ecr_repository.temporal_worker_dev: Creating...
   aws_ecr_repository.temporal_worker_dev: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 2s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;temporal-worker-dev]
   aws_ecr_lifecycle_policy.temporal_worker_dev: Creating...
   aws_ecr_lifecycle_policy.temporal_worker_dev: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 0s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;temporal-worker-dev]

   Apply &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; Resources: 2 added, 0 changed, 0 destroyed.

   Outputs:

   temporal_worker_dev_repository_url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"012345678912.dkr.ecr.us-east-1.amazonaws.com/temporal-worker-dev"&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a &lt;strong&gt;new state file&lt;/strong&gt; in your S3 remote state bucket specifically for managing the ECR repository.&lt;br&gt;
&lt;em&gt;(This modular pattern can be extended to other independent resources — for example, an S3 bucket or IAM role that you want managed separately from your main infrastructure.)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an &lt;strong&gt;ECR repository&lt;/strong&gt; named &lt;code&gt;temporal-worker-dev&lt;/code&gt; in the &lt;code&gt;us-east-1&lt;/code&gt; region.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Attach a &lt;strong&gt;lifecycle policy&lt;/strong&gt; that automatically deletes old images, keeping only the most recent five (again helping us save some money for ramen 🍜 😉 )&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Output the &lt;strong&gt;repository URL&lt;/strong&gt; — which you’ll use to push Docker images or reference in CI/CD pipelines.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
 Your ECR repository is ready:&lt;/p&gt;

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

&lt;p&gt;State file for managing ECR repository is in S3:&lt;/p&gt;

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

&lt;p&gt;You can now push images to this registry manually, and later let your GitHub Actions workflow (&lt;code&gt;build-and-publish-ecr-dev.yaml&lt;/code&gt;) handle it automatically whenever you push new code. 🤖&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;This setup bridges the gap between &lt;strong&gt;manual provisioning&lt;/strong&gt; and &lt;strong&gt;automated pipelines&lt;/strong&gt; — Terraform creates the infrastructure once, and CI/CD keeps it up to date.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  v. 🫸 Pushing a Container to ECR
&lt;/h4&gt;

&lt;p&gt;Before deploying our Temporal Worker, we need its Docker image available in a registry that ECS can pull from. In this step, we’ll &lt;strong&gt;build, tag, authenticate, and push&lt;/strong&gt; our Temporal Worker image into &lt;strong&gt;Amazon Elastic Container Registry (ECR)&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;br&gt;
The ECR repository URL was output by Terraform in the previous step.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;temporal_worker_dev_repository_url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/temporal-worker-dev"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;For reference, here’s the official AWS guide on this process: &lt;a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html" rel="noopener noreferrer"&gt;AWS Documentation — Push a Docker Image to Amazon ECR&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can adapt these commands for your own &lt;strong&gt;AWS account ID&lt;/strong&gt;, &lt;strong&gt;region&lt;/strong&gt;, or &lt;strong&gt;repository name&lt;/strong&gt;.&lt;br&gt;
(Replace &lt;code&gt;&amp;lt;AWS_ACCOUNT_ID&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;REGION&amp;gt;&lt;/code&gt; with your actual values — for region we've been using &lt;code&gt;us-east-1&lt;/code&gt;.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;Heads up! You can copy all the commands for steps 2 and beyond, below, from the &lt;code&gt;Push Commands&lt;/code&gt; in AWS Console:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Navigate to your project root&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   cd temporal-ecs-terraform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build the Docker image locally&lt;/strong&gt; (don't forget the &lt;code&gt;.&lt;/code&gt; at the end of the statement).
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   docker build -t temporal-worker-dev:latest .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This builds a local image named &lt;code&gt;temporal-worker-dev:latest&lt;/code&gt;, where &lt;code&gt;temporal-worker-dev&lt;/code&gt; is the name of them Docker image and &lt;code&gt;latest&lt;/code&gt; is the tag.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;In case you are building the image on an ARM machine like M-series Mac, use the following command to build for AMD64 platform:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker buildx build --platform linux/amd64 -t temporal-worker-dev .
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate Docker with your ECR registry&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use the AWS CLI to obtain a temporary login token and pass it to Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   aws ecr get-login-password --region &amp;lt;REGION&amp;gt; | \
     docker login --username AWS --password-stdin &amp;lt;AWS_ACCOUNT_ID&amp;gt;.dkr.ecr.&amp;lt;REGION&amp;gt;.amazonaws.com/temporal-worker-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Use your correct &lt;strong&gt;AWS account ID&lt;/strong&gt;, &lt;strong&gt;region&lt;/strong&gt;, and &lt;strong&gt;repository URI&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The username should always be &lt;code&gt;AWS&lt;/code&gt; when authenticating via &lt;code&gt;get-login-password&lt;/code&gt;.
  &lt;a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html" rel="noopener noreferrer"&gt;AWS Documentation — Registry Authentication&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The login token is valid for &lt;strong&gt;12 hours&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tag the image for your ECR repository&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   docker tag temporal-worker-dev:latest &amp;lt;AWS_ACCOUNT_ID&amp;gt;.dkr.ecr.&amp;lt;REGION&amp;gt;.amazonaws.com/temporal-worker-dev:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Docker which remote repository and tag to associate with your local image.&lt;br&gt;
   Example: &lt;code&gt;123456789012.dkr.ecr.us-east-1.amazonaws.com/temporal-worker-dev:latest&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Push the image to ECR&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   docker push &amp;lt;AWS_ACCOUNT_ID&amp;gt;.dkr.ecr.&amp;lt;REGION&amp;gt;.amazonaws.com/temporal-worker-dev:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Docker will upload image layers to ECR.&lt;/p&gt;

&lt;p&gt;You should see output similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   The push refers to repository &lt;span class="o"&gt;[&lt;/span&gt;012345678912.dkr.ecr.us-east-1.amazonaws.com/temporal-worker-dev]
   4112fcf261vf: Pushed 
   f58ce7v94305: Pushed 
   f64v6a759ca2: Pushed 
   2ec442f83634: Pushed 
   be33cd6b7vb5: Pushed 
   d34v2bcfd62c: Pushed 
   59d2ev8aa0a3: Pushed 
   9ec25079v1a1: Pushed 
   ffbd160b7v21: Pushed 
   a16e5v119267: Pushed 
   0v31e520412d: Pushed 
   e9bcv8fab0a2: Pushed 
   06d872a0v231: Pushed 
   latest: digest: sha256:1b43...a006e75b6ff3 size: 856

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ &lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once complete, you’ll see your image listed under the &lt;em&gt;Images&lt;/em&gt; tab of your ECR repository in the AWS Console.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built&lt;/strong&gt; the container with your Temporal Worker code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticated&lt;/strong&gt; your Docker client to ECR using AWS IAM credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tagged&lt;/strong&gt; the image so ECS knows which image to pull.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pushed&lt;/strong&gt; it to ECR, uploading all layers.&lt;/li&gt;
&lt;li&gt;ECS will later &lt;strong&gt;pull&lt;/strong&gt; this image automatically when deploying your Fargate tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional Best Practices&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;versioned tags&lt;/strong&gt; (e.g. &lt;code&gt;:v1.0.0&lt;/code&gt;, &lt;code&gt;:commit-sha&lt;/code&gt;) instead of &lt;code&gt;:latest&lt;/code&gt; in production to ensure reproducible deployments. 
For this demo, the current &lt;code&gt;:latest&lt;/code&gt; tag will suffice.&lt;/li&gt;
&lt;li&gt;If your build process runs in CI/CD, these steps will be automated — GitHub Actions will handle authentication, tagging, and pushing for each new commit.&lt;/li&gt;
&lt;li&gt;Confirm that your IAM user or role includes ECR permissions:
&lt;code&gt;ecr:GetAuthorizationToken&lt;/code&gt;, &lt;code&gt;ecr:BatchCheckLayerAvailability&lt;/code&gt;, &lt;code&gt;ecr:PutImage&lt;/code&gt;, and &lt;code&gt;ecr:InitiateLayerUpload&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h4&gt;
  
  
  vi. 🌍 Main Terraform setup
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Warning&lt;/p&gt;

&lt;p&gt;Before you run anything below make sure to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Replace the S3 remote backend bucket name in &lt;code&gt;terraform/live/dev/providers.tf&lt;/code&gt; (line 5) to the one you created above.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Uncomment and replace the variable &lt;code&gt;worker_container_image&lt;/code&gt;  in &lt;code&gt;terraform/live/dev/terraform.tfvars&lt;/code&gt; (line 29) to the ECR image URI you pushed, above. It should look something like this:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;br&gt;
    ```hcl&lt;br&gt;
   worker_container_image = "012345678912.dkr.ecr.us-east-1.amazonaws.com/temporal-worker-dev:latest"&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


   🤫 Copy URI from your ECR repository in the AWS Console:

   ![Copy URI from ECR](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dw9zlm38nt530x6oh1eq.png)

3. Uncomment and replace the variable `s3_data_bucket_name`  in `terraform/live/dev/terraform.tfvars` (line 37) to your desired globally unique AWS S3 bucket name for storing your business logic (in our demo, saving messages).
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that the container registry, secrets, and remote state backend are ready, this is where everything comes together.&lt;/p&gt;

&lt;p&gt;We’ll scaffold the &lt;strong&gt;core infrastructure&lt;/strong&gt; that hosts our worker fleet — networking, ECS cluster, auto-scaling, task definitions, S3 storage, and everything in between.&lt;/p&gt;

&lt;p&gt;Before running Terraform, let’s quickly walk through how the code is structured.&lt;/p&gt;




&lt;p&gt;The infrastructure is organized using a modular approach with the following structure:&lt;/p&gt;

&lt;h6&gt;
  
  
  🧩 Modules (&lt;code&gt;terraform/modules&lt;/code&gt;)
&lt;/h6&gt;

&lt;p&gt;Reusable components that encapsulate infrastructure patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;network/&lt;/code&gt;&lt;/strong&gt; – wraps the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/6.4.0" rel="noopener noreferrer"&gt;Community VPC Module&lt;/a&gt; to provision:

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;VPC with public and private subnets&lt;/strong&gt; across multiple Availability Zones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single NAT Gateway for cost optimization&lt;/strong&gt; &lt;em&gt;(suitable for dev/staging; production should use one NAT per AZ)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;S3 &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/6.4.0/submodules/vpc-endpoints" rel="noopener noreferrer"&gt;VPC Gateway Endpoint&lt;/a&gt;&lt;/strong&gt; so private subnet resources can access S3 &lt;em&gt;without&lt;/em&gt; using the Internet Gateway.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;&lt;code&gt;ecs_cluster/&lt;/code&gt;&lt;/strong&gt; – wraps the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/ecs/aws/6.6.1/submodules/cluster" rel="noopener noreferrer"&gt;Community ECS Cluster Module&lt;/a&gt; to create an efficient ECS cluster that can: 

&lt;ul&gt;
&lt;li&gt;Stream logs to CloudWatch with configurable retention.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;FARGATE_SPOT&lt;/code&gt; or &lt;code&gt;FARGATE&lt;/code&gt; (on-demand) capacity for flexible compute.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;&lt;code&gt;ecs_service/&lt;/code&gt;&lt;/strong&gt; – wraps the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/ecs/aws/6.6.1/submodules/service" rel="noopener noreferrer"&gt;Community ECS Service&lt;/a&gt; to provision:

&lt;ul&gt;
&lt;li&gt;Task definitions with container configuration.&lt;/li&gt;
&lt;li&gt;Security groups for network access control&lt;/li&gt;
&lt;li&gt;Auto-scaling policies.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h5&gt;
  
  
  🔴 &lt;strong&gt;Live Environment&lt;/strong&gt; (&lt;code&gt;terraform/live/dev/&lt;/code&gt;)
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;Brings the modules together for a specific environment:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/strong&gt; – the orchestration file, organized into logical sections:&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. **Local Variables (lines 5-22)** – define reusable values and decode secrets from AWS Secrets Manager.

   - Network configuration flags
   - Protocol constants for security group rules
   - Temporal server connection details pulled from Secrets Manager and marked as sensitive

2. **Secrets Manager Data Sources (lines 28-37)** – pulls Temporal credentials securely at runtime from Secrets Manger as we defined step-1.

3. **VPC Module (lines 43-63)** – creates isolated network environment.

   - Public subnets for internet facing resources such as NAT Gateway
   - Private subnets for ECS tasks (no direct internet access)
   - **Gateway VPC endpoint for S3** to *avoid* NAT Gateway data transfer costs

4. **ECS Cluster Module (lines 71-80)** – provisions the container management cluster 

   That defines the logical grouping where all ECS tasks (i.e Temporal worker containers) will be housed along with the time period for ECS log retention (default 7 days).

5. **Worker Service Module (lines 87-147)** – defines the Temporal Worker tasks.

   - Container definitions (lines 96-140) with environment variables injected from Secrets Manager (environment variables that end up as `null` will not be passed to the container).
   - Port mappings for Temporal server communication (port `7233`).
   - Security group rules allowing outbound internet access for Temporal Cloud or a Self-hosted Temporal Server connectivity
   - Deployed in private subnets with internet access via NAT Gateway.

6. **Auto-Scaling Configuration (lines 173-257)** – CloudWatch alarms and scaling policies:

   - Scale out ≥ 30% CPU for 2 minutes.
   - Scale in ≤ 20% CPU for 10 minutes.
   - Step-scaling used for fine-grained control.

7. **S3 Bucket (lines 237-245)** – dev data bucket (`force_destroy = true` for convenience).

8. **IAM Permissions (lines 279-302)** – grants ECS tasks scoped S3 access.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/strong&gt; – declares inputs for environment configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;terraform.tfvars&lt;/code&gt;&lt;/strong&gt; – holds concrete values (region, CIDRs, image URI, etc.).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;providers.tf&lt;/code&gt;&lt;/strong&gt; – sets AWS provider and S3 backend for remote state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation follows Terraform best practices — modules define "how" infrastructure is built, while the live environment defines "what" gets built with environment-specific values. ✨&lt;/p&gt;

&lt;p&gt;You can easily replicate this for &lt;code&gt;staging&lt;/code&gt; or &lt;code&gt;production&lt;/code&gt; by creating new directories under &lt;code&gt;terraform/live/&lt;/code&gt; with different variable values.&lt;/p&gt;




&lt;h5&gt;
  
  
  🚀 Deploying the Infrastructure
&lt;/h5&gt;

&lt;p&gt;Make sure the previous steps are complete — secrets stored, image pushed to ECR, and Temporal Cloud or your self-hosted Temporal Server reachable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;code&gt;live/dev&lt;/code&gt; directory.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cd &lt;/span&gt;terraform/live/dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialize Terraform&lt;/strong&gt; to set up providers and backend.
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Preview &lt;code&gt;plan&lt;/code&gt; to see what exactly will be deployed.
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Finally, hit &lt;code&gt;apply&lt;/code&gt; and deploy the infrastructure.
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;p&gt;✅&lt;strong&gt;Result:&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;After these steps, you now have a &lt;strong&gt;production-ready foundation&lt;/strong&gt;: private network, container registry, worker fleet with auto-scaling, and code deployed from your ECR image.&lt;/p&gt;

&lt;p&gt;You can view the deployed infrastructure in AWS by navigating to the &lt;strong&gt;ECS console&lt;/strong&gt; → &lt;strong&gt;Clusters&lt;/strong&gt; → &lt;code&gt;sample-dev-cluster&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;There, you'll see one worker container &lt;code&gt;sample-temporal-worker&lt;/code&gt; running. &lt;/p&gt;

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

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

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




&lt;h3&gt;
  
  
  🎯 Test it out
&lt;/h3&gt;

&lt;p&gt;To check everything is working, you can start a sample workflow execution. &lt;/p&gt;

&lt;p&gt;Open Temporal Cloud, and trigger a new workflow with the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task Queue&lt;/strong&gt;: &lt;code&gt;test-queue&lt;/code&gt; - this is the queue our worker is listening to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow Type&lt;/strong&gt;: &lt;code&gt;MessageWorkflow&lt;/code&gt; - this is the workflow defined in &lt;code&gt;workflows/sample_workflow.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input/Data/JSON&lt;/strong&gt;: &lt;code&gt;{"message": " 🐢 Hello, World!"}&lt;/code&gt; - this is the message the workflow will save in your S3 bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, click "Start Workflow".&lt;/p&gt;

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

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

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

&lt;p&gt;Additionally there is a &lt;a href="https://github.com/papnori/temporal-ecs-terraform/blob/main/run_workflow.py" rel="noopener noreferrer"&gt;&lt;code&gt;run_workflow.py&lt;/code&gt; file provided in the GitHub repo&lt;/a&gt;. You can use that to trigger two new parallel workflows and accomplish the same thing as above from the terminal.&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;# Navigate to the project root directory&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;temporal-ecs-terraform

&lt;span class="c"&gt;# Run the workflow file&lt;/span&gt;
python run_workflow.py 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal output will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python run_workflow.py

...
Connecting to Temporal server at temporal.clusterX.aws.cloud.temporal.io:7233 ...
Connecting to Temporal server at temporal.clusterX.aws.cloud.temporal.io:7233 ...
Starting my workflow...
Starting my workflow...
Workflow completed: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'printed_message'&lt;/span&gt;: &lt;span class="s1"&gt;'🦄 Hello, World!'&lt;/span&gt;, &lt;span class="s1"&gt;'status'&lt;/span&gt;: &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
Workflow completed: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'printed_message'&lt;/span&gt;: &lt;span class="s1"&gt;'🐰 Bye, World!'&lt;/span&gt;, &lt;span class="s1"&gt;'status'&lt;/span&gt;: &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h3&gt;
  
  
  3. 🐙 Optional: Configure GitHub Actions for Continuous Delivery
&lt;/h3&gt;

&lt;p&gt;At this point, you can take it one step further by setting up a continuous delivery pipeline in GitHub.&lt;br&gt;
This will automate the build and deployment of your Temporal Workers whenever you push new code.&lt;/p&gt;
&lt;h4&gt;
  
  
  i. 👯‍♂️🫸 Clone the Repo and Push to Your Own GitHub
&lt;/h4&gt;

&lt;p&gt;After setting up AWS and secrets, it’s best to work from your own repository — this allows you to safely make changes, track versions, and run CI/CD workflows.&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;Clone&lt;/strong&gt; the starter repo locally (if not done already):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/papnori/temporal-ecs-terraform.git
&lt;span class="nb"&gt;cd &lt;/span&gt;temporal-ecs-terraform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) &lt;strong&gt;Create a new repository on GitHub&lt;/strong&gt; (e.g. &lt;code&gt;temporal-ecs-terraform&lt;/code&gt;) under your account or organization.&lt;br&gt;
3) &lt;strong&gt;Push&lt;/strong&gt; the code to your repository (make sure you're on the &lt;code&gt;main&lt;/code&gt; branch before pushing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote rename origin upstream
git remote add origin git@github.com:&amp;lt;your-username&amp;gt;/temporal-ecs-terraform.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;Using your own repo allows GitHub Actions workflows to run with your secrets and makes it easier to customize the setup for your team or environment.&lt;/p&gt;

&lt;p&gt;❗ Important&lt;/p&gt;

&lt;p&gt;At this point all the GitHub actions will fail, and its alright we'll fix them in the next steps.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h4&gt;
  
  
  ii. 🔐 Setup AWS OIDC
&lt;/h4&gt;

&lt;p&gt;GitHub uses &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt; to allow workflows to assume IAM roles &lt;em&gt;without&lt;/em&gt; long-lived credentials.&lt;br&gt;
Let’s configure this in AWS so your CI/CD can deploy infrastructure and push Docker images securely.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an Identity provider&lt;/strong&gt; (IAM → Identity providers → Add provider)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Select Provider Type: &lt;code&gt;OpenID Connect&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;URL: &lt;code&gt;https://token.actions.githubusercontent.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Audience: &lt;code&gt;sts.amazonaws.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an IAM Role for GitHub Actions&lt;/strong&gt; (IAM → Roles → Create role)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Trusted entity: Web identity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Provider: &lt;code&gt;token.actions.githubusercontent.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Audience: &lt;code&gt;sts.amazonaws.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GitHub org/repo: &lt;code&gt;&amp;lt;your-org&amp;gt;/&amp;lt;your-repo&amp;gt;&lt;/code&gt; (e.g., &lt;code&gt;papnori/temporal-ecs-terraform&lt;/code&gt;)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;No need to add permissions in the first setup.&lt;/p&gt;

&lt;p&gt;Just skip to &lt;code&gt;Step 3 (Name, review, and create)&lt;/code&gt; and hit the &lt;code&gt;Create Role&lt;/code&gt; button. We'll edit and add premissions later&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8ff5bwgqggoxrqjd66o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8ff5bwgqggoxrqjd66o.png" alt="IAM Role" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Give your new role a name and a helpful description to identify later. You may use mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Role name&lt;/code&gt;: &lt;code&gt;temporal-ecs-terraform-github-action-role&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Description&lt;/code&gt;: &lt;code&gt;Role used in temporal-ecs-terraform demo project https://github.com/papnori/temporal-ecs-terraform&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt; (IAM → Roles → [Role you created in the previous step] → Add permissions → Create inline policy → JSON)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your GitHub Actions workflows need access to manage ECS, ECR, S3, CloudWatch, and Terraform state.&lt;/p&gt;

&lt;p&gt;You can start with this broad policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TerraformCorePermissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetPolicyVersion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:ListRolePolicies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:ListAttachedRolePolicies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:ListRoleTags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:CreateRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:DeleteRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:UpdateRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:TagRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:UntagRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:PutRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:DeleteRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:AttachRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:DetachRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"iam:PassRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:CreateBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:DeleteBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListAllMyBuckets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketVersioning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketTagging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketAcl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketCORS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketWebsite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketLogging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketRequestPayment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketPublicAccessBlock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetReplicationConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetLifecycleConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetAccelerateConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetEncryptionConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketObjectLockConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutBucketTagging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutBucketVersioning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutBucketPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:DeleteBucketPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecs:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"elasticloadbalancing:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"application-autoscaling:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"cloudwatch:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"logs:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:ListCertificates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:DescribeCertificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:RequestCertificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:DeleteCertificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:GetCertificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:AddTagsToCertificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:RemoveTagsFromCertificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"acm:ListTagsForCertificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:GetResourcePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:DescribeSecret"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TerraformStateAndECRAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:CreateRepository"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:DeleteRepository"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:DescribeRepositories"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:ListImages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:DescribeImages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:BatchDeleteImage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:GetDownloadUrlForLayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:BatchCheckLayerAvailability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:PutImage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:InitiateLayerUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:UploadLayerPart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:CompleteLayerUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:GetLifecyclePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:PutLifecyclePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:PutImageTagMutability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:TagResource"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:UntagResource"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecr:ListTagsForResource"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::&amp;lt;S3_REMOTE_BACKEND_BUCKET_NAME&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::&amp;lt;S3_REMOTE_BACKEND_BUCKET_NAME&amp;gt;/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ecr:us-east-1:012345678912:repository/temporal-worker-dev"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Warning&lt;/p&gt;

&lt;p&gt;Make sure to replace the &lt;code&gt;&amp;lt;S3_REMOTE_BACKEND_BUCKET_NAME&amp;gt;&lt;/code&gt; placeholder, at the bottom, with the name of the remote S3 backend bucket you created during the bootstrap process. &lt;/p&gt;

&lt;p&gt;⚠️ Warning&lt;/p&gt;

&lt;p&gt;Later adjust it for your own use case and follow the principle of least privilege for production use.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;We can now finally, create a policy by giving it a name (e.g. &lt;code&gt;temporal-ecs-terraform-github-action-policy&lt;/code&gt;)&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;After creating the role, copy its ARN as we will store it soon as a GitHub repository secret (e.g., &lt;code&gt;AWS_GITHUB_ACTIONS_ROLE_ARN&lt;/code&gt;).&lt;/p&gt;

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

&lt;h4&gt;
  
  
  iii. 🛠️ GitHub Secrets Setup and Variables
&lt;/h4&gt;




&lt;p&gt;Now that GitHub is connected to AWS via OIDC, you'll configure the repository’s &lt;strong&gt;secrets and variables&lt;/strong&gt; so your workflows can deploy Temporal Workers automatically.&lt;/p&gt;

&lt;p&gt;This step ensures your GitHub Actions pipeline can authenticate with AWS, fetch Docker images from ECR, and pass environment-specific parameters during deployment.&lt;/p&gt;

&lt;p&gt;🤫 &lt;strong&gt;Repository Secrets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;br&gt;
  &lt;strong&gt;Settings → Secrets and variables → Actions → Secrets&lt;/strong&gt; → “&lt;strong&gt;New repository secret&lt;/strong&gt;” &lt;/p&gt;

&lt;p&gt;Add the following secret: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AWS_GITHUB_ACTIONS_ROLE_ARN&lt;/code&gt; - The ARN of the IAM you recently created in the OIDC setup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;Keep the secret name exactly as written — it will be referenced by your GitHub Actions workflow later for assuming the AWS IAM role.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your GitHub repository is now fully configured with the secrets required for CI/CD.&lt;/p&gt;

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

&lt;p&gt;🪄 &lt;strong&gt;Repository variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;br&gt;
&lt;strong&gt;Settings → Secrets and variables → Actions → Variables → New repository variable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt; - Your AWS account ID (you'll use this to fetch the latest image from ECR before deploying)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; (environment: &lt;code&gt;dev&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As an additional layer of customization, let’s define a few &lt;strong&gt;GitHub environment variables&lt;/strong&gt; for our &lt;code&gt;dev&lt;/code&gt; setup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;Environment variables are scoped per environment. This helps later when you extend to multiple environments (e.g., &lt;code&gt;staging&lt;/code&gt; or &lt;code&gt;production&lt;/code&gt;) using the same workflow file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For now, let’s create variables for the name of your S3 bucket and ECR repository under the &lt;code&gt;dev&lt;/code&gt; environment:&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;br&gt;
   &lt;strong&gt;Settings → Environments → dev&lt;/strong&gt; (create new environment) &lt;strong&gt;→ Environment variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WORKFLOW_ECR_REPO&lt;/code&gt; – The name of your ECR repository ( &lt;code&gt;temporal-worker-dev&lt;/code&gt; this is the same as you created in the ECR setup)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;S3_DATA_BUCKET_NAME&lt;/code&gt; — The name of your S3 bucket (for example &lt;code&gt;sample-temporal-data-dev&lt;/code&gt;).
 You can later assign this value to the Terraform variable &lt;code&gt;s3_data_bucket_name&lt;/code&gt; in &lt;code&gt;live/dev/variables.tf&lt;/code&gt; by prefixing an Action-level environment variable with &lt;code&gt;TF_VAR_&lt;/code&gt; which is a &lt;a href="https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_var_name" rel="noopener noreferrer"&gt;Terraform mechanism to assign values to variables&lt;/a&gt; — great for CI/CD automations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your GitHub repository is now fully configured with the secrets and variables required for CI/CD.&lt;/p&gt;

&lt;p&gt;In the next step, we’ll activate the &lt;strong&gt;GitHub Actions workflows&lt;/strong&gt; defined in the &lt;a href="https://github.com/papnori/temporal-ecs-terraform" rel="noopener noreferrer"&gt;repository&lt;/a&gt; —&lt;br&gt;
&lt;code&gt;build-and-publish-ecr-dev.yaml&lt;/code&gt; and &lt;code&gt;terraform-live-dev-deploy.yaml&lt;/code&gt; — which tie everything together by automating Docker builds, ECR pushes, and Terraform deployments. 🚀&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;You can even store environment variables and application secrets in GitHub and skip the Step 1, above, where we use AWS Secrets Manager to store Temporal secrets 😉.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h4&gt;
  
  
  iv. 🫸🚀 Push &amp;amp; Trigger
&lt;/h4&gt;

&lt;p&gt;Finally, with everything in place, push the updated code base to your repository.&lt;/p&gt;

&lt;p&gt;Comment out the &lt;code&gt;worker_container_image&lt;/code&gt; and &lt;code&gt;s3_data_bucket_name&lt;/code&gt; variables in &lt;code&gt;terraform/live/dev/terraform.tfvars&lt;/code&gt; (lines 29 and 38). These values are now provided by the GitHub Actions workflows (either dynamically discovered or passed as environment-scoped variables).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Commented out container image and s3 bucket name"&lt;/span&gt;

&lt;span class="c"&gt;# Finally push&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should trigger the &lt;code&gt;Deploy to Dev 🚀&lt;/code&gt; workflow, it will use the &lt;code&gt;latest&lt;/code&gt; image in the ECR repository.&lt;/p&gt;

&lt;p&gt;You can also build an updated image by triggering the &lt;code&gt;Build &amp;amp; Push to ECR (Dev) 🗄️🚀&lt;/code&gt; workflow which will not only build and push an updated image tagged with the latest commit hash but also then trigger the &lt;code&gt;Deploy to Dev 🚀&lt;/code&gt; workflow on completion. Try it out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0eyobl54qnmswsc9p5wq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0eyobl54qnmswsc9p5wq.png" alt="Github Build &amp;amp; Push to ECR Trigger Workflow Manually" width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  🧩 How the two workflows fit together
&lt;/h5&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Key Steps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Build &amp;amp; Push to ECR (Dev)&lt;/strong&gt;🗄️🚀&lt;/td&gt;
&lt;td&gt;Builds your Temporal Worker image and pushes it to ECR&lt;/td&gt;
&lt;td&gt;On push to &lt;code&gt;main&lt;/code&gt; or manual run&lt;/td&gt;
&lt;td&gt;Checkout → OIDC auth → Docker build → Push → Output image URI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Deploy to Dev&lt;/strong&gt;🚀&lt;/td&gt;
&lt;td&gt;Applies Terraform changes with the new image&lt;/td&gt;
&lt;td&gt;Triggered after successful build or on Terraform file changes&lt;/td&gt;
&lt;td&gt;Terraform init → Plan → Apply → ECS service stability check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;📓 Note&lt;/p&gt;

&lt;p&gt;You can view the &lt;strong&gt;full, production-ready workflow files&lt;/strong&gt; in the GitHub repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/papnori/temporal-ecs-terraform/blob/main/.github/workflows/build-and-publish-ecr-dev.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;build-and-publish-ecr-dev.yaml&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/papnori/temporal-ecs-terraform/blob/main/.github/workflows/terraform-live-dev-deploy.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;terraform-live-dev-deploy.yaml&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;✅ &lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
Your CI/CD pipeline is now fully automated — pushing code to &lt;code&gt;main&lt;/code&gt; builds and deploys new workers to ECS.&lt;br&gt;
In other words, you now have a continuous delivery setup for your Temporal Worker running on AWS Fargate. 🚀&lt;/p&gt;


&lt;h2&gt;
  
  
  📊 Scaling
&lt;/h2&gt;

&lt;p&gt;Before we wrap up, let’s quickly chat about &lt;strong&gt;scaling&lt;/strong&gt; — the magic that keeps your Temporal workers responsive without wasting compute.&lt;/p&gt;

&lt;p&gt;Scaling works through &lt;strong&gt;three AWS services working together&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Alarms&lt;/strong&gt; – continuously monitor ECS service metrics (CPU utilization in this case).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Auto Scaling Policies&lt;/strong&gt; – define how to react (scale out/in, by how much, with what cooldowns).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS Service&lt;/strong&gt; – adjusts the number of running worker tasks based on those signals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When CPU goes above or below the thresholds you’ve set, &lt;strong&gt;CloudWatch&lt;/strong&gt; fires an alarm → the corresponding &lt;strong&gt;Auto Scaling Policy&lt;/strong&gt; kicks in → &lt;strong&gt;ECS&lt;/strong&gt; changes the desired task count.&lt;/p&gt;
&lt;h3&gt;
  
  
  📈 Scale-Out (Add Capacity)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;terraform/live/dev/main.tf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;threshold &lt;span class="o"&gt;=&lt;/span&gt; 30 &lt;span class="c"&gt;# Scale out when CPU &amp;gt; 30%&lt;/span&gt;
evaluation_periods &lt;span class="o"&gt;=&lt;/span&gt; 2 &lt;span class="c"&gt;# Require 2 sustained readings of high CPU before scaling out&lt;/span&gt;
cooldown  &lt;span class="o"&gt;=&lt;/span&gt; 60 &lt;span class="c"&gt;# Wait 60s before allowing another scale-out&lt;/span&gt;

&lt;span class="c"&gt;# First scaling adjustment: 30% &amp;lt;= CPU utilization &amp;lt; 40%&lt;/span&gt;
scaling_adjustment &lt;span class="o"&gt;=&lt;/span&gt; 1 &lt;span class="c"&gt;# Add 1 worker tasks each time alarm fires&lt;/span&gt;

&lt;span class="c"&gt;# Second scaling adjustment:  40% &amp;lt; CPU utilization &lt;/span&gt;
scaling_adjustment &lt;span class="o"&gt;=&lt;/span&gt; 2 &lt;span class="c"&gt;# Add 2 worker tasks each time alarm fires&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;➡️ How it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudWatch checks the average CPU utilization across all running tasks.&lt;/li&gt;
&lt;li&gt;If it stays between &lt;strong&gt;30% - 40%&lt;/strong&gt; for 2 sustained readings, the &lt;em&gt;high CPU&lt;/em&gt; alarm triggers.&lt;/li&gt;
&lt;li&gt;The scale-out policy adds &lt;strong&gt;1 new worker task&lt;/strong&gt; if we are over the cooldown period.&lt;/li&gt;
&lt;li&gt;The second, more aggressive, scale-out alarm triggers when average CPU is above &lt;strong&gt;40%&lt;/strong&gt;, adding &lt;strong&gt;2 tasks&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Cooldown (60s) prevents AWS from adding more tasks too quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tuning knobs:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;threshold&lt;/code&gt; → Lowering (e.g. 20%) makes workers scale up earlier.&lt;br&gt;
&lt;code&gt;evaluation_periods&lt;/code&gt; → Longer = wait more time before scaling out (avoids flapping).&lt;br&gt;
&lt;code&gt;cooldown&lt;/code&gt; → Shorter = faster scaling, but risk overshooting.&lt;br&gt;
&lt;code&gt;scaling_adjustment&lt;/code&gt; → Higher = more tasks added per alarm.&lt;/p&gt;
&lt;h3&gt;
  
  
  📉 Scale-In (Remove Capacity)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;threshold &lt;span class="o"&gt;=&lt;/span&gt; 20 &lt;span class="c"&gt;# Scale in when CPU &amp;lt; 20%&lt;/span&gt;
evaluation_periods &lt;span class="o"&gt;=&lt;/span&gt; 10 &lt;span class="c"&gt;# Require 10 sustained readings of low CPU before scaling in&lt;/span&gt;
cooldown  &lt;span class="o"&gt;=&lt;/span&gt; 300 &lt;span class="c"&gt;# Wait 5 minutes before another scale-in&lt;/span&gt;

&lt;span class="c"&gt;# Only one scaling adjustment set for scale-in: CPU utilization &amp;lt;= 20% &lt;/span&gt;
scaling_adjustment &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="c"&gt;# Remove 1 worker each time alarm fires&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;➡️ How it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudWatch checks if the CPU stays below &lt;strong&gt;20%&lt;/strong&gt; for 10 sustained readings.&lt;/li&gt;
&lt;li&gt;If true, the &lt;em&gt;low CPU alarm&lt;/em&gt; triggers.&lt;/li&gt;
&lt;li&gt;The scale-in policy removes &lt;strong&gt;1 worker task&lt;/strong&gt; if we are over the cooldown period.&lt;/li&gt;
&lt;li&gt;A 5-minute cooldown ensures ECS doesn’t scale in too aggressively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tuning knobs:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;threshold&lt;/code&gt; → Higher (e.g., 25%) = scales in faster, lower = safer.&lt;br&gt;
&lt;code&gt;evaluation_periods&lt;/code&gt; → Longer = wait more time before scaling in (avoids flapping).&lt;br&gt;
&lt;code&gt;cooldown&lt;/code&gt; → Longer = fewer oscillations between scale-in/out.&lt;br&gt;
&lt;code&gt;scaling_adjustment&lt;/code&gt; → Can remove more tasks at once (e.g., 2).&lt;/p&gt;

&lt;p&gt;👉 In summary, this setup ensures &lt;strong&gt;fast scale-out&lt;/strong&gt; when workers are under pressure, but conservative scale-in, so you don’t accidentally kill capacity during brief idle periods.&lt;/p&gt;

&lt;p&gt;🔮 In the future, you can also monitor the Temporal Cloud pipeline and scale based on workflow queue depth or task backlog.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip&lt;/p&gt;

&lt;p&gt;The type of scaling policy we’ve used here is a &lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-step-scaling-policies.html" rel="noopener noreferrer"&gt;Step Scaling Policy&lt;/a&gt;&lt;/strong&gt; — where scaling happens in &lt;em&gt;defined steps&lt;/em&gt; based on metric thresholds.&lt;/p&gt;

&lt;p&gt;AWS also offers other policy types worth exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-target-tracking.html" rel="noopener noreferrer"&gt;Target Tracking Scaling Policy&lt;/a&gt;&lt;/strong&gt; — automatically adjusts task count to maintain a target metric (like 50% CPU).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-predictive-scaling.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Predictive Scaling Policy&lt;/strong&gt;&lt;/a&gt; — uses historical data to forecast demand and scale ahead of time.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  💥☢️ Destroy the infrastructure
&lt;/h2&gt;

&lt;p&gt;Alright — we’ve built, deployed, scaled, and automated like champs. But unless you &lt;em&gt;really&lt;/em&gt; want to sponsor Jeff Bezos’ next rocket launch, it’s time to &lt;strong&gt;tear it all down&lt;/strong&gt; 😅.&lt;/p&gt;

&lt;p&gt;Let’s go step-by-step — in reverse order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Destroy the live &lt;code&gt;dev&lt;/code&gt; environment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;terraform/live/dev
terraform destroy
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Destroy the ECR repo:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cd &lt;/span&gt;terraform/global/ecr
   terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Destroy the S3 remote backend:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one’s a bit trickier — Terraform protects it by default so you don’t accidentally delete your state&lt;/p&gt;

&lt;p&gt;First, set the &lt;code&gt;prevent_destroy&lt;/code&gt; lifecycle policy to  &lt;code&gt;false&lt;/code&gt; , on line 11 in &lt;code&gt;terraform/bootstrap/main.tf&lt;/code&gt;, otherwise Terraform will not let us destroy. This is a safety mechanism to prevent accidents. Then add  new variable ⁠ force_destory = true &lt;/p&gt;

&lt;p&gt;It should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   force_destroy &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true
   &lt;/span&gt;lifecycle &lt;span class="o"&gt;{&lt;/span&gt;
      prevent_destroy &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;strong&gt;re-apply&lt;/strong&gt; the change so Terraform acknowledges the update.&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="nb"&gt;cd &lt;/span&gt;terraform/bootstrap
   terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Comment out&lt;/strong&gt; the &lt;code&gt;backend "s3" {}&lt;/code&gt; block (line 8-14) again in &lt;code&gt;terraform/bootstrap/providers.tf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;   &lt;span class="c1"&gt;#   backend "s3" {&lt;/span&gt;
   &lt;span class="c1"&gt;#     bucket       = "my-little-sample-terraform-state" # name of the bucket - globally unique&lt;/span&gt;
   &lt;span class="c1"&gt;#     key          = "bootstrap/terraform.tfstate"      # path to the state file in the bucket&lt;/span&gt;
   &lt;span class="c1"&gt;#     region       = "us-east-1"                        # region of the bucket&lt;/span&gt;
   &lt;span class="c1"&gt;#     use_lockfile = true                               # instead of dynamodb&lt;/span&gt;
   &lt;span class="c1"&gt;#     encrypt = true # encrypt the state file&lt;/span&gt;
   &lt;span class="c1"&gt;#   }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Migrate Terraform state&lt;/strong&gt; to fall back to local state.&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="nb"&gt;cd &lt;/span&gt;terraform/bootstrap
   terraform init &lt;span class="nt"&gt;-migrate-state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output may look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nv"&gt;$ &lt;/span&gt;terraform init &lt;span class="nt"&gt;-migrate-state&lt;/span&gt;
   Initializing the backend...
   Terraform has detected you&lt;span class="s1"&gt;'re unconfiguring your previously set "s3" backend.
   Do you want to copy existing state to the new backend?
     Pre-existing state was found while migrating the previous "s3" backend to the
     newly configured "local" backend. No existing state was found in the newly
     configured "local" backend. Do you want to copy this state to the new "local"
     backend? Enter "yes" to copy and "no" to start with an empty state.

     Enter a value: yes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;strong&gt;re-apply&lt;/strong&gt; the change so Terraform acknowledges the update.&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="nb"&gt;cd &lt;/span&gt;terraform/bootstrap
   terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can destroy.&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="nb"&gt;cd &lt;/span&gt;terraform/bootstrap
   terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your AWS account is squeaky-clean again 🧼.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎬 Wrapping up
&lt;/h2&gt;

&lt;p&gt;And that’s a wrap! &lt;/p&gt;

&lt;p&gt;If you’ve made it all the way here — congratulations, you’ve officially deployed, scaled, automated, &lt;em&gt;and destroyed&lt;/em&gt; Temporal workers like a cloud wizard. 🧙‍♂️⚡ Time for that cold champagne! 🥂&lt;/p&gt;

&lt;p&gt;I tried to leave no stone unturned (and no Terraform state file un-backend-ed 😅).&lt;br&gt;
Hopefully this guide made your journey from &lt;em&gt;“where do I even start?”&lt;/em&gt; to &lt;em&gt;“oh wow, I just built production-ready infra”&lt;/em&gt; a little easier — and maybe even fun.&lt;/p&gt;

&lt;p&gt;This article was never meant to exist — until we started building &lt;a href="https://skinsight.me/" rel="noopener noreferrer"&gt;&lt;strong&gt;Skinsight.me&lt;/strong&gt;&lt;/a&gt; 💜  Somewhere between spinning up servers and obsessing over skin types, we stumbled onto a few too many &lt;em&gt;“wait, this is actually cool”&lt;/em&gt; moments. So here we are — turning late-night debugging and skincare debates into something worth sharing 💜 All while not breaking the bank 💰💵.&lt;/p&gt;

&lt;p&gt;Because yes — you can optimize cloud costs &lt;em&gt;and&lt;/em&gt; your skincare routine at the same time 😅💜&lt;/p&gt;

&lt;p&gt;If you're curious what else we can spin up for nearly nothing, follow us on &lt;a href="//skinsight.me/blog/brain/deploying-temporal-on-aws-ecs-with-terraform"&gt;🧠 Skinsight&lt;/a&gt; — where beauty meets brains (and budget). 💅💻💜&lt;/p&gt;




&lt;h2&gt;
  
  
  🙏 Acknowledgments
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Built with ❤️ for teams seeking cost-effective Temporal deployments&lt;/li&gt;
&lt;li&gt;Inspired by the need for simpler alternatives to Kubernetes&lt;/li&gt;
&lt;li&gt;Special thanks to the Temporal and AWS communities&lt;/li&gt;
&lt;li&gt;But most and foremost to &lt;a href="https://www.linkedin.com/in/rafay-khan-02939b145/" rel="noopener noreferrer"&gt;Rafay&lt;/a&gt; - whose relentless drive, guidance, and hands-on contributions not only shaped this project but continue to inspire everyone around him 💪✨ None of this would’ve come together without him 😊💜&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📧 Contact
&lt;/h2&gt;

&lt;p&gt;If you found this useful or just want to geek out over Temporal, ECS, or Terraform setups, feel free to reach out on LinkedIn &lt;a href="https://www.linkedin.com/in/norapap753/" rel="noopener noreferrer"&gt;@norapap753&lt;/a&gt; 💌&lt;/p&gt;




&lt;p&gt;Made with ☕️ and ✨ by Nora&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
