{"componentChunkName":"component---src-templates-post-tsx","path":"/reactapigw/","webpackCompilationHash":"39ef227213c852f81d08","result":{"data":{"logo":{"childImageSharp":{"fixed":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAC4jAAAuIwF4pT92AAACYklEQVQ4y32UzWtTQRTF73vJm5e2ltLGGivWWqx9SY2t/aIIlahYBJGCS8GN5D2tiILfacSPooJbF4K4UxHpyoUL9/oniO5q+weIWEUXVYm/20zSRy0ZOMy7N3POPZM7M2KyoSMMPxvtAZdNEHVrbLKR5mSj4dc5YcYPonOszVsNp7ogiAzJBTBjf3BNEDYSVE5VNAhPgiWMtcYW4C4If4Iejac6h5O3vIRT9jxn1hh31vNW15WNEWLngSsy3ZFLas7LntkE7zuYjAuO4O4HyG7kCOFVsY1GS//pDLxlBA/FBYe0Sioo5jS+YVLTCJxA6ADzeF3YmARup5j3lYxfnBdpk6GbHXCXES3UBfm/ekn+SeRmdmlcMuYVWxtBMA0+IrBT84i94Lto1zy+m5CzUqk4GFmBPyprDYhKVJjX+LWIE3PUBZYQ7StrAWMWEU3Wfr/Y1ObZxjwBD9FKqjuPj+cI3qstnK2SL1iRT6Cb3CnwwTp1EasXhlsGL9HxbSLsp8IvGb2ffibSDvELOGhdLuJwK3Oe3ApzpiZUEXFl4Hwb3K9gYq0pQTSoTZG913rfi7RCXIB4DOwGn8El4IJH4A3ou+6nBp+KbJHhO+1wv4FCXHBUjw1HIND4it/Sg6sCxGa210WBI3ynrONxMHnVb95ut9upZpgPx4/NBEk9h/3SYOhB/+/WBGEnPD3YU3GHeRK/QZ/GY9sKydtJV2+KNsCx2xULdy4hzkD30YR1mIb3l+buX3c/o3e+7bRecr/h4xBVH4/qfZ8Db/Xur3s5oiZwnHhHjdRI0M6bwVjtldEC/wD9ZqZKu9EgUgAAAABJRU5ErkJggg==","width":400,"height":301,"src":"/static/7740e24d3a6d05bf053373472324910d/647de/ghost-logo.png","srcSet":"/static/7740e24d3a6d05bf053373472324910d/647de/ghost-logo.png 1x,\n/static/7740e24d3a6d05bf053373472324910d/59139/ghost-logo.png 1.5x,\n/static/7740e24d3a6d05bf053373472324910d/af144/ghost-logo.png 2x"}}},"markdownRemark":{"html":"<p>Modern cloud architectures often require keeping application workloads private while still making them accessible to end users over the internet. AWS provides a powerful combination of services to achieve this — running containers privately on ECS Fargate, fronting them with an internal Application Load Balancer, and exposing them securely through API Gateway using a VPC Link.</p>\n<p>In this blog, we will walk through deploying a Next.js React application as a private ECS Fargate task and exposing it to the internet via an AWS API Gateway HTTP API. All infrastructure is provisioned using Terraform, making it repeatable and easy to adapt to your own projects.</p>\n<p>The whole code for this solution is available on Github <a href=\"https://github.com/amlana21/react-apigw-publish\" target=\"_blank\">Here</a>. If you want to follow along, this can be used to stand up your own infrastructure.</p>\n<p>By the end of this guide, you will have a fully working setup where a containerized React app runs in a private subnet with no public IP, yet is accessible via a clean API Gateway URL. Let’s get started!</p>\n<h2>Pre Requisites</h2>\n<p>Before I start the walkthrough, there are some pre-requisites which are good to have if you want to follow along or want to try this on your own:</p>\n<ul>\n<li>Basic AWS knowledge</li>\n<li>An AWS account</li>\n<li>AWS CLI installed and configured with appropriate permissions</li>\n<li>Terraform installed (>= 1.14.0)</li>\n<li>Docker installed locally</li>\n<li>Basic knowledge of React / Next.js</li>\n<li>Familiarity with ECS and API Gateway concepts</li>\n</ul>\n<p>With that out of the way, lets dive into the details.</p>\n<h2>What is API Gateway and how does VPC Link work?</h2>\n<p>AWS API Gateway is a fully managed service that allows you to create, publish, and manage APIs at any scale. It acts as a front door for applications to access backend services — whether those are Lambda functions, HTTP endpoints, or private VPC resources.</p>\n<p>Key features of API Gateway:</p>\n<ul>\n<li><strong>HTTP APIs and REST APIs</strong> — HTTP APIs are lighter weight and lower cost, ideal for proxying to backend services</li>\n<li><strong>VPC Link support</strong> — enables private integration with resources inside a VPC without exposing them to the internet</li>\n<li><strong>Auto-deploy stages</strong> — changes can be automatically deployed to a stage without manual promotion</li>\n<li><strong>Built-in throttling and security</strong> — rate limiting, API keys, and IAM authorization options out of the box</li>\n</ul>\n<p>A key feature we leverage in this post is <strong>VPC Link</strong>. By default, API Gateway can only reach publicly accessible endpoints. A VPC Link establishes a private connection between API Gateway and resources inside your VPC, so your ALB and ECS tasks never need to be exposed to the internet.</p>\n<p>With a VPC Link:</p>\n<ul>\n<li>Your ALB and ECS tasks can stay in <strong>private subnets</strong> with no internet exposure</li>\n<li>Traffic from API Gateway flows privately through AWS’s internal network</li>\n<li>No need to open inbound internet access on your security groups for the application tier</li>\n</ul>\n<p>This is the component that makes the “private ECS + public API Gateway” pattern possible.</p>\n<h2>What is ECS Fargate?</h2>\n<p>Amazon ECS (Elastic Container Service) with the Fargate launch type lets you run containers without managing the underlying EC2 instances. You define the CPU, memory, and container image — AWS handles the rest.</p>\n<p>Key benefits for this use case:</p>\n<ul>\n<li><strong>Serverless containers</strong> — no EC2 fleet to patch or scale</li>\n<li><strong>Private networking</strong> — tasks can run in private subnets with no public IP</li>\n<li><strong>Pay per use</strong> — billed only for the vCPU and memory consumed while the task is running</li>\n<li><strong>Easy integration with ALB</strong> — task IPs register directly as ALB targets</li>\n</ul>\n<h2>Architecture Overview</h2>\n<p>Now that we have covered the key services, let me walk through the high-level architecture of what we are building.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/43fcec4b437ad7eac2a7365d74272f1d/6924e/architecture.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 51.716961498439126%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsSAAALEgHS3X78AAABkUlEQVQoz3WRu24VMRCGz1PxQLQIiY4qb4AURAWipEgqGlJEoJSQBnGIuIiAQs7RLru+e3ftsdd3vDlE4SJGo9HvX/ONxvaqXEfKOaTrrLqUm+NVxlzib2aMYVWxfAUbH4n2FDw1iQrG8CWFcJMm4kagDati54SU/oGVZaV0x/vk4S0G8zKomtqzWM7vPf1y5wl1+f9w7XOl/Xq2efMcK4u1JxBqZXO6OHl/8WpdxbId+L9hrKpbR8zEAJsdlrSmUIJOvFO8jxolwGZARmJQIeY/YO4inyMBN4+sDCQL5ig33Co5YcvBYmMRsZJ6Scy0wCklykRMgY/24EG3PgVZzOlRc7TfJiFtu+GHd+XH485OU4eAEKTZ2ae+5WPMeWXnedv8AGOoNI/uf3h9wsdiXx6eP9v77Cmfvn/bPr7N370gXrPLVjaoV/TtetvwYYFzrh+Yd2s3UmHtCPhh4nogSrJR8L6nlAtsB6Z7rlEVzUQQjL/uvAsfkwnJ+AQuahe039VgQqxPC4tO1awCaq3dOf8EQnExAooA/AUAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Architecture Diagram\"\n        title=\"Architecture Diagram\"\n        src=\"/static/43fcec4b437ad7eac2a7365d74272f1d/913fc/architecture.png\"\n        srcset=\"/static/43fcec4b437ad7eac2a7365d74272f1d/bc34b/architecture.png 293w,\n/static/43fcec4b437ad7eac2a7365d74272f1d/da9f0/architecture.png 585w,\n/static/43fcec4b437ad7eac2a7365d74272f1d/913fc/architecture.png 1170w,\n/static/43fcec4b437ad7eac2a7365d74272f1d/efb0c/architecture.png 1755w,\n/static/43fcec4b437ad7eac2a7365d74272f1d/6924e/architecture.png 1922w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span> </br>  </p>\n<p>The architecture is built around a single VPC that is split into two tiers — a <strong>public tier</strong> and a <strong>private tier</strong> — each serving a distinct role.</p>\n<h3>Public Tier</h3>\n<p>The public subnets contain the components that need outbound internet access. A <strong>NAT Gateway</strong> sits here, allowing resources in the private subnets to make outbound calls (for example, pulling container images or reaching AWS service endpoints) without being directly reachable from the internet. An <strong>Internet Gateway</strong> is attached to the VPC to facilitate this outbound traffic.</p>\n<p>Importantly, there is no application workload in the public tier. Nothing serving the React app is exposed here.</p>\n<h3>Private Tier</h3>\n<p>The private subnets are where all the application workload lives. Two key resources sit here:</p>\n<ul>\n<li><strong>Internal Application Load Balancer (ALB)</strong> — this ALB has no public-facing DNS. It is only reachable from within the VPC or via an API Gateway VPC Link. It listens for incoming HTTP traffic and forwards it to the ECS tasks registered in its target group.</li>\n<li><strong>ECS Fargate Tasks</strong> — the containerized React app runs here. Each task gets a private IP address only, with no public IP assigned. The container listens on port 3000 and registers itself with the ALB target group automatically when the service starts.</li>\n</ul>\n<h3>API Gateway and VPC Link</h3>\n<p>Sitting outside the VPC entirely, the <strong>API Gateway HTTP API</strong> is the only public entry point. It exposes a URL to the internet and is configured with two routes — <code class=\"language-text\">ANY /</code> and <code class=\"language-text\">ANY /{proxy+}</code> — so all paths are forwarded to the backend.</p>\n<p>The API Gateway is connected to the internal ALB through a <strong>VPC Link</strong>. The VPC Link is what bridges the gap between API Gateway (which lives outside the VPC) and the internal ALB (which lives inside). AWS handles the private network plumbing behind the scenes — from the user’s perspective, they hit the API Gateway URL and the request lands on the React app.</p>\n<h3>Container Registry</h3>\n<p>Before the ECS task can run, the Docker image for the React app needs to be stored somewhere AWS can pull it from. An <strong>Amazon ECR (Elastic Container Registry)</strong> repository holds the built image. When ECS starts a new task, it pulls the image from ECR over the private network using the NAT Gateway for the initial pull — no public image registry required.</p>\n<h3>Logging</h3>\n<p>All container output from the ECS tasks is streamed to <strong>Amazon CloudWatch Logs</strong>. This gives a central place to debug the application without needing to SSH into any servers (there are none with Fargate).</p>\n<h3>Request Flow</h3>\n<p>Putting it all together, here is the end-to-end flow when a user opens the React app:</p>\n<ol>\n<li>User sends a request to the <strong>API Gateway</strong> endpoint URL</li>\n<li>API Gateway matches the route and forwards the request via the <strong>VPC Link</strong></li>\n<li>The VPC Link delivers the request to the <strong>internal ALB</strong> inside the private subnet</li>\n<li>The ALB selects a healthy <strong>ECS Fargate task</strong> from its target group and forwards the request</li>\n<li>The Next.js app running in the container handles the request and sends back a response</li>\n<li>The response travels back through ALB → VPC Link → API Gateway → user</li>\n</ol>\n<p>At no point does any internet traffic touch the ECS task directly. The only publicly addressable component is the API Gateway endpoint.</p>\n<h2>Infrastructure Walkthrough</h2>\n<p>Now let’s walk through the Terraform code that provisions all of this. The infrastructure is organized into three modules: <strong>networking</strong>, <strong>compute</strong>, and <strong>security</strong>. All of this lives under <code class=\"language-text\">infrastructure/</code>, with a <code class=\"language-text\">dev</code> environment wiring the modules together in <code class=\"language-text\">environments/dev/main.tf</code>.</p>\n<h3>Networking Module</h3>\n<p>This is the foundation of the whole setup. It provisions the VPC, public and private subnets across availability zones, an Internet Gateway, a NAT Gateway (so private resources can reach the internet for things like pulling images without being publicly reachable), and the route tables to wire everything together.</p>\n<p>Two security groups are defined here:</p>\n<ul>\n<li><code class=\"language-text\">web_sg</code> — attached to the ALB, allows inbound HTTP/HTTPS from the internet</li>\n<li><code class=\"language-text\">app_sg</code> — attached to ECS tasks, allows inbound traffic only from <code class=\"language-text\">web_sg</code> on port 3000</li>\n</ul>\n<p>This enforces a clean two-tier boundary — no internet traffic can reach the ECS tasks directly.</p>\n<p>The most important resource in this module is the <strong>internal ALB</strong>. Setting <code class=\"language-text\">internal = true</code> is what keeps it off the public internet — it has no public DNS and is only reachable from within the VPC or via the API Gateway VPC Link.</p>\n<div class=\"gatsby-highlight\" data-language=\"hcl\"><pre class=\"language-hcl\"><code class=\"language-hcl\"><span class=\"token keyword\">resource <span class=\"token type variable\">\"aws_lb\"</span></span> <span class=\"token string\">\"app_svc_lb\"</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">name</span>               <span class=\"token punctuation\">=</span> <span class=\"token string\">\"<span class=\"token interpolation\"><span class=\"token punctuation\">$</span><span class=\"token punctuation\">{</span><span class=\"token keyword\">var</span><span class=\"token punctuation\">.</span><span class=\"token type variable\">environment_val</span><span class=\"token punctuation\">}</span></span>-app-svc-lb\"</span>\n  <span class=\"token property\">internal</span>           <span class=\"token punctuation\">=</span> <span class=\"token boolean\">true</span>\n  <span class=\"token property\">load_balancer_type</span> <span class=\"token punctuation\">=</span> <span class=\"token string\">\"application\"</span>\n  <span class=\"token property\">security_groups</span>    <span class=\"token punctuation\">=</span> <span class=\"token punctuation\">[</span>aws_security_group.web_sg.id<span class=\"token punctuation\">]</span>\n  <span class=\"token property\">subnets</span>            <span class=\"token punctuation\">=</span> aws_subnet.app_private<span class=\"token punctuation\">[</span>*<span class=\"token punctuation\">]</span>.id\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The ALB target group uses <code class=\"language-text\">target_type = &quot;ip&quot;</code>, which is required for Fargate — there are no EC2 instances to register, so Fargate registers each task’s private IP directly.</p>\n<h3>Compute Module</h3>\n<p>This module provisions everything needed to run the app and expose it. It covers four things:</p>\n<p><strong>ECR</strong> — a private container registry to store the Docker image. ECS pulls from here when starting new tasks.</p>\n<p><strong>ECS cluster and task definition</strong> — the task definition is where the container is configured: the ECR image URL, CPU (512) and memory (1024 MB), port 3000, and CloudWatch log shipping. The cluster itself is just a logical grouping.</p>\n<p><strong>ECS service</strong> — runs the Fargate tasks in the private subnets. The key setting here is <code class=\"language-text\">assign_public_ip = false</code>, which ensures tasks never get a public IP. The service also registers tasks with the ALB target group automatically as they start up.</p>\n<p><strong>API Gateway + VPC Link</strong> — this is the piece that makes the private app accessible from the internet. A VPC Link bridges API Gateway (which lives outside the VPC) to the internal ALB. An HTTP API is created with an <code class=\"language-text\">HTTP_PROXY</code> integration pointing at the ALB listener, and two routes (<code class=\"language-text\">ANY /</code> and <code class=\"language-text\">ANY /{proxy+}</code>) forward all traffic through.</p>\n<div class=\"gatsby-highlight\" data-language=\"hcl\"><pre class=\"language-hcl\"><code class=\"language-hcl\"><span class=\"token keyword\">resource <span class=\"token type variable\">\"aws_apigatewayv2_integration\"</span></span> <span class=\"token string\">\"app_alb_integration\"</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">api_id</span>             <span class=\"token punctuation\">=</span> aws_apigatewayv<span class=\"token number\">2</span>_api.app_api.id\n  <span class=\"token property\">integration_type</span>   <span class=\"token punctuation\">=</span> <span class=\"token string\">\"HTTP_PROXY\"</span>\n  <span class=\"token property\">integration_method</span> <span class=\"token punctuation\">=</span> <span class=\"token string\">\"ANY\"</span>\n  <span class=\"token property\">integration_uri</span>    <span class=\"token punctuation\">=</span> var.app_svc_alb_listener_arn\n  <span class=\"token property\">connection_type</span>    <span class=\"token punctuation\">=</span> <span class=\"token string\">\"VPC_LINK\"</span>\n  <span class=\"token property\">connection_id</span>      <span class=\"token punctuation\">=</span> aws_apigatewayv<span class=\"token number\">2</span>_vpc_link.app_vpc_link.id\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The stage is set to <code class=\"language-text\">$default</code> with <code class=\"language-text\">auto_deploy = true</code>, so changes go live immediately without a manual deploy step.</p>\n<h3>Security Module</h3>\n<p>This module defines the IAM role used by ECS as both the execution role and the task role. The execution role lets ECS pull images from ECR and write logs to CloudWatch. The task role is what the running container itself uses to interact with AWS services.</p>\n<p>The core permission is the <code class=\"language-text\">AmazonECSTaskExecutionRolePolicy</code> managed policy. Additional policies for CloudWatch Agent and X-Ray are also attached to support observability.</p>\n<h2>Application Walkthrough</h2>\n<p>The application is a standard Next.js app. The main page lives in <code class=\"language-text\">app/page.tsx</code> — for this post it is a simple scaffold, but the infrastructure pattern works the same regardless of what the app actually does. The focus here is on how it gets packaged and run inside a container.</p>\n<h3>Dockerfile</h3>\n<p>The Dockerfile uses a <strong>multi-stage build</strong> to keep the final image small. The three stages are:</p>\n<ul>\n<li><strong>deps</strong> — installs dependencies using <code class=\"language-text\">npm ci</code> for a clean, reproducible install from <code class=\"language-text\">package-lock.json</code></li>\n<li><strong>builder</strong> — copies the source and runs <code class=\"language-text\">npm run build</code> to produce the Next.js production output</li>\n<li><strong>runner</strong> — the final image. Only the compiled output and necessary files are copied over from the builder, leaving the build toolchain behind</li>\n</ul>\n<p>A few things in the runner stage are worth calling out. A non-root <code class=\"language-text\">nextjs</code> system user is created and used to run the app — a standard security practice for containerized workloads. And <code class=\"language-text\">HOSTNAME</code> is explicitly set to <code class=\"language-text\">0.0.0.0</code>:</p>\n<div class=\"gatsby-highlight\" data-language=\"dockerfile\"><pre class=\"language-dockerfile\"><code class=\"language-dockerfile\"><span class=\"token keyword\">ENV</span> PORT=3000\n<span class=\"token keyword\">ENV</span> HOSTNAME=0.0.0.0\n<span class=\"token keyword\">CMD</span> <span class=\"token punctuation\">[</span><span class=\"token string\">\"npm\"</span><span class=\"token punctuation\">,</span> <span class=\"token string\">\"start\"</span><span class=\"token punctuation\">]</span></code></pre></div>\n<p>This is important. By default Next.js binds only to <code class=\"language-text\">localhost</code>, which means ALB health checks — which arrive on the container’s network interface — would fail. Setting <code class=\"language-text\">HOSTNAME=0.0.0.0</code> tells Next.js to listen on all interfaces, making the container reachable from the ALB.</p>\n<p>One other thing to keep in mind when building locally on Apple Silicon: the image needs to be built for <code class=\"language-text\">linux/amd64</code> since that is what ECS Fargate runs on. The Makefile handles this with the <code class=\"language-text\">--platform linux/amd64</code> flag.</p>\n<h2>Deploying the Solution</h2>\n<p>With the code in place, deploying the full solution involves two phases: provisioning the infrastructure and then building and pushing the container image. A <code class=\"language-text\">Makefile</code> wraps both phases into numbered targets.</p>\n<h3>Folder Structure</h3>\n<p>Before deploying, it helps to understand how the repo is laid out:</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">.\n├── Makefile                          # top-level commands for deploying infra and building the app\n├── infrastructure/\n│   ├── environments/\n│   │   └── dev/                      # the dev environment — Terraform is run from here\n│   │       ├── locals.tf             # defines environment name, region, and naming conventions\n│   │       ├── main.tf               # wires the three modules together and passes variables between them\n│   │       └── provider.tf           # AWS provider configuration\n│   └── modules/\n│       ├── networking/               # VPC, public/private subnets, NAT Gateway, internal ALB, security groups\n│       ├── compute/                  # ECR repository, ECS cluster, task definition, ECS service, API Gateway, VPC Link\n│       └── security/                 # IAM task execution role and the policies attached to it\n└── src/\n    └── api-app/                      # the Next.js application\n        ├── app/                      # Next.js app directory — pages and layouts live here\n        │   ├── layout.tsx            # root layout wrapping all pages\n        │   └── page.tsx              # home page\n        ├── Dockerfile                # multi-stage build for the production container image\n        └── package.json              # app dependencies and build scripts</code></pre></div>\n<p>The <code class=\"language-text\">infrastructure/environments/dev</code> directory is where Terraform commands are run from. The modules under <code class=\"language-text\">infrastructure/modules</code> are reusable and environment-agnostic — the <code class=\"language-text\">dev</code> environment passes in the values via variables.</p>\n<h3>Lets Deploy!!</h3>\n<p><strong>Step 1 — Plan the infrastructure</strong></p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">make</span> <span class=\"token number\">1</span>-plan-infra</code></pre></div>\n<p>This runs <code class=\"language-text\">terraform init</code> and <code class=\"language-text\">terraform plan</code> inside <code class=\"language-text\">infrastructure/environments/dev</code>. Terraform will print out every resource it intends to create — VPC, subnets, NAT Gateway, ALB, security groups, ECR repo, ECS cluster, API Gateway, and the VPC Link. It is worth taking a minute to review the plan output before applying, especially the first time, to make sure nothing unexpected is included.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/47fbb274f42f247928322bb638726abf/6dbbe/tf-plan.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 39.70013037809648%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABY0lEQVQoz2WReU/CUBDE+ULiUa5CgQIt9KAnbYFIMAKi4BX9/sm482pJjH9Mtsd7s7+drXXNBGF+wizYwYufME+PomelMDup6sUHuNFe/feTI5xwh/44R9dMYdprNHtzXDds3DSnqNW1CQZWJlqiZQSY+g/KhBeDxYu61DZC6INYlKBnLqAPE7T7EToXxbhtOcq0xoNjZ4P+JBcVGForZT60y0oC0tDIkGqMspKOpmz0Wy+Emu7CS/ZwZGTb38r4Z2T3X0hXHyg23xLFoxqR43JsPlveFo2uL1Qz3LWdC50y5AdSElvruNI5RW9EmkzRsHuzO0dD90S+nClrZfTPUB/GiIqzInGjA6L8FcnyHXHxhsX6E76QkcoN94qwohxIPGxGgD8jcynGeCG5FGj1ApWZLYupxKUwJ5WjkPOdJiSua9ZFNFNLYfAcl4doyPAZOLdHE00uXt1NwMalSoOKqFL1/gPShiLMv3RfcAAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Terraform plan output\"\n        title=\"Terraform plan output\"\n        src=\"/static/47fbb274f42f247928322bb638726abf/913fc/tf-plan.png\"\n        srcset=\"/static/47fbb274f42f247928322bb638726abf/bc34b/tf-plan.png 293w,\n/static/47fbb274f42f247928322bb638726abf/da9f0/tf-plan.png 585w,\n/static/47fbb274f42f247928322bb638726abf/913fc/tf-plan.png 1170w,\n/static/47fbb274f42f247928322bb638726abf/6dbbe/tf-plan.png 1534w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>Step 2 — Apply the infrastructure</strong></p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">make</span> <span class=\"token number\">2</span>-deploy-infra</code></pre></div>\n<p>This runs <code class=\"language-text\">terraform apply -auto-approve</code>, which provisions all the resources in the correct dependency order. Terraform handles the sequencing — for example, it knows the ECS service depends on the ALB target group, which depends on the VPC, so it wires all of that up automatically.</p>\n<p>Once the apply completes, note down the API Gateway endpoint URL from the Terraform outputs — you will need it for testing. You can also find it in the AWS Console under <strong>API Gateway → your API → Stages</strong>.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/5dcd4b788b34454069cbfd987d421436/3d35d/tf-apply.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 10.575296108291033%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsSAAALEgHS3X78AAAAe0lEQVQI1z2Oyw6CMAAE+SMgtCogEVRo01olKkRvPk6G/z+PtIkeJjubvWzU2B6PKAyJVMRiT7pQgVi0pEtNlhtEbmcM2cqE/ktZ2LB5T2RHpN0bd/5wGib08YVyT/pxojs8Qvded3eq7RAo6wtlc/3npr1R7UbWs/sTXzzFSBok6MONAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Terraform apply output and API Gateway URL\"\n        title=\"Terraform apply output and API Gateway URL\"\n        src=\"/static/5dcd4b788b34454069cbfd987d421436/913fc/tf-apply.png\"\n        srcset=\"/static/5dcd4b788b34454069cbfd987d421436/bc34b/tf-apply.png 293w,\n/static/5dcd4b788b34454069cbfd987d421436/da9f0/tf-apply.png 585w,\n/static/5dcd4b788b34454069cbfd987d421436/913fc/tf-apply.png 1170w,\n/static/5dcd4b788b34454069cbfd987d421436/3d35d/tf-apply.png 1182w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>Step 3 — Build and push the container image</strong></p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">make</span> <span class=\"token number\">2</span>-1-build-app</code></pre></div>\n<p>Before ECS can run the app, the Docker image needs to be in ECR. This Makefile target takes care of the full flow in one command:</p>\n<ol>\n<li>Authenticates your local Docker client with ECR using <code class=\"language-text\">aws ecr get-login-password</code></li>\n<li>Builds the image targeting <code class=\"language-text\">linux/amd64</code> (required for Fargate, important if you are on Apple Silicon)</li>\n<li>Tags and pushes the image to the ECR repository that was created in Step 2</li>\n</ol>\n<p>You can verify the image landed in ECR by checking the repository in the AWS Console under <strong>Elastic Container Registry</strong>.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/ccbedf83ba9b3c289c5558586acd6ce0/18787/ecr-image.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 40.10025062656642%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABbElEQVQoz1VR2W6DMBDkF5oDCJeDARuwARGakiaVoirqSx/6/38z3eXo8TAa78LOzthO+fwBod8h6wGZOSOUBgdpERxLRLJGmFaIM4PCDEh1B1+U8I8VDgRfVHP9B468fuFoL4j0AF+ToOohmivS6oS0sHCjDNtDOiOQE3bMVDPvw+wfnH644v74RNWN9JOEnxRo+xEt9SMzwk00pGrIbUluS4jcIBAKsmwphcbGT2hOYOvPcKr+Bt2/wSO7bqywoy0cNaDoriLXqZkGfUFIFA7EHnEwRdbYRzmlyH/YyZsRZnwgyhsSow+EqrvAnm4QRQuPlnjk0o1JiJHopVYT9lGBHWE9O/3LHbZ/nQa5wVt44FgNEKqbzvwAsuwRpPWPONe8OMosJTKIcwtlz3AScvb7o5qscyQvtbMLWsI9HlojchIWEKqdOCZRnuGzk9Fr1uQwJFF2yC+1ocvdeMnMy2U/LfV6+Zult2KtvwF0Xd7UgKRdiwAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"ECR repository with pushed image\"\n        title=\"ECR repository with pushed image\"\n        src=\"/static/ccbedf83ba9b3c289c5558586acd6ce0/913fc/ecr-image.png\"\n        srcset=\"/static/ccbedf83ba9b3c289c5558586acd6ce0/bc34b/ecr-image.png 293w,\n/static/ccbedf83ba9b3c289c5558586acd6ce0/da9f0/ecr-image.png 585w,\n/static/ccbedf83ba9b3c289c5558586acd6ce0/913fc/ecr-image.png 1170w,\n/static/ccbedf83ba9b3c289c5558586acd6ce0/18787/ecr-image.png 1197w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>Step 4 — Start the ECS service</strong></p>\n<p>The ECS service is provisioned with <code class=\"language-text\">desired_count = 0</code> so no tasks run before the image is available. Once the image is in ECR, bring the service up by setting the desired count to 1:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">aws ecs update-service <span class=\"token punctuation\">\\</span>\n  --cluster dev-app-cluster <span class=\"token punctuation\">\\</span>\n  --service dev-app-svc <span class=\"token punctuation\">\\</span>\n  --desired-count <span class=\"token number\">1</span></code></pre></div>\n<p>ECS will schedule a Fargate task in the private subnet, pull the image from ECR, and start the container. As the task comes up, it registers its private IP with the ALB target group. Once the ALB health check on port 3000 passes, the task is marked healthy and traffic will start flowing through.</p>\n<p>You can watch the task come up in the AWS Console under <strong>ECS → Clusters → dev-app-cluster → Tasks</strong>.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1149px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 52.21932114882506%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAAB90lEQVQoz21SSZLbMAzUAyaumrEsWztJSRS1WJIX2bFnvFQmueSQ//+m02SqckkOXSAAEmg06NWH78jrT4j2CEXEqsM6N9gQoewQyRahaKCaPar+hJh+kGoEWU37Lzx5/QWxvSDtr9gYPtB7ZO2MnIiaM9ayx6YYsC5GBGqLSPBhUmIVF/+FN+0vuH/+RL+7uMA6rVB3e+zmD1TdAaukgh8K+JHEiliGEm8b4awfKxf3I+XeLnnPk8MVojkyWLhRlrR6nFFPZxTtAXE5sKh24/gsnhQ9mukCZXZY2TFFRzCXlqxzgGeT43x3F4O8YacSYVkjqgxHI7v4TyPLfJ1ppEWHrOydtfqmzYzEdGxsKNcOXkkW28O7Y2P1sgWjvIbUA0TZIaSmYUNtySJRDQozIswqLsc4PVW9RaZaxickkkvJqwGynpxWQWoZlZBMVv1MRvXfjTotuQybd+e44sjasYxY0Mas75nhjH7/7hyrkU3EwkBzMSULK00GRcvuDW1PlhyTfsaRU57t8o5fH45xwpiX8lKuR6Tl1nULkgJvxRFL88CreWKhH1hUNyzUCW/tBV/EjMDcENQ3vGRHvOo7/P4HXsTZ+Y7h4fINeju7D2zXn9tPPn5A7R5Q+yfk9EDenZGPJ2TdCYr/1iKjtmK8Qe6eEPzH1v8NSh0cs4QG/+0AAAAASUVORK5CYII='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"ECS task running in private subnet\"\n        title=\"ECS task running in private subnet\"\n        src=\"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png\"\n        srcset=\"/static/7f4019d536c56f5519a22cdb95e98de9/bc34b/ecs-task.png 293w,\n/static/7f4019d536c56f5519a22cdb95e98de9/da9f0/ecs-task.png 585w,\n/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png 1149w\"\n        sizes=\"(max-width: 1149px) 100vw, 1149px\"\n      />\n  </a>\n    </span></p>\n<h2>AWS Resources</h2>\n<p>Once everything is deployed, it is worth taking a look at the resources in the AWS Console to get a feel for what was provisioned and how it all connects. Here are the key ones to check.</p>\n<p><strong>VPC and Subnets</strong></p>\n<p>The VPC shows the full network layout — public and private subnets spread across availability zones, the route tables, and the attached Internet Gateway. You can confirm the private subnets have no direct route to the internet, only through the NAT Gateway.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/20108e4f55f1c061c54d575da74c597e/3389c/vpc-subnets.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 62.12198872373141%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsSAAALEgHS3X78AAACAElEQVQoz4WS2XLaQBBF9e44AYQQWtC+gRYkgTFbjOPkIf//Qze3R6iCq1KVh66Znp4+c7t7tLj7BS/vEeYNnGiNxSrB0kvv/gZWUMAJ1zzLYLiJiovJ3lil3Kd/fZpW7i5Iqz2TCsysANOlz0CMen9BVu8RFS3iTQebj82dWMVkXQjcjWA4d18eYkyLyxcEGdWEOSamNwAZrA/vcJMauh0pJVHVYu4KMFWA8uUdcffGM/q8U/RXJN13aNXuCi+tYQcZJoQJcO5EaI4fyNqzKtWkVd0ZablTLRBg1hyQVBSTb1U82vTwsi2B/VkpVEDTx4xAUeXlLVZJhaU/AKVsSR57JWZS+djbpZ/DZtu0sjvxYvNJoU4F4fYCJ21g8aIV5CpBEkW99FHnqtuhelwUy37KGWib9giX0xx7OAzGU+Wl9QEO+7ggTBTmvGvHpfJDDmv7ekOw7mDS97Mau8tPaM3+ilgOvUSpG4EW5TvxRikTVVGzR3f6IKDnxEtEXF9vv5FvT2p4Mofm8Db00E8rjj96APr4MjOVPetLPE0XCKqeCTf4RacABr+MFWRq1e0A3wwbX+cWtJr/MKJCCTwCJTiagAXYHn8gECDVCOBpagyP3h8WUyWvpE//AfojkKWKwsnC/XRnNK3sTwSW/wQ+j0CWHNa7oYfFCHSUokeY+H8AbQNLR8M450UAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"VPC and subnet layout in the AWS Console\"\n        title=\"VPC and subnet layout in the AWS Console\"\n        src=\"/static/20108e4f55f1c061c54d575da74c597e/913fc/vpc-subnets.png\"\n        srcset=\"/static/20108e4f55f1c061c54d575da74c597e/bc34b/vpc-subnets.png 293w,\n/static/20108e4f55f1c061c54d575da74c597e/da9f0/vpc-subnets.png 585w,\n/static/20108e4f55f1c061c54d575da74c597e/913fc/vpc-subnets.png 1170w,\n/static/20108e4f55f1c061c54d575da74c597e/efb0c/vpc-subnets.png 1755w,\n/static/20108e4f55f1c061c54d575da74c597e/3389c/vpc-subnets.png 1951w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>Internal Application Load Balancer</strong></p>\n<p>The ALB should show as <code class=\"language-text\">internal</code> with no public DNS name. Under the target group, you can see the registered ECS task IPs and their health check status. A healthy target here means traffic can flow from the VPC Link all the way to the container.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/06b9f74120eabbeaf6e905642841f181/1070c/alb-targets.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 53.92156862745098%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAAB30lEQVQoz0VT2Y7UQAzMJ7CZ3EfnPieZDUyG2V2BVgIESLzsP/D/31CUnYR9sOy2u+2qcmIl1Yh2XhFkHU5RAScucQoLxqXGh0ntqDtxpXU7zNVc0yMc73DzEVbRP+L28g1R1sJLarhxjbSb4Oetxi5zXtogqc4IywkuY5sDBUA1fkTeXTSWvAyy5GLXXpAx4e1JQdxM170ZzbRo7r9Q3H8jGT+jIoi0ntBdbij7BVEx6F1tKNDVeJDkZlvz46wIn99gfv5F9fwH69MrTDMrUt9sdzcwJayQ3ZtpRVKOCj2mj+gDogrzXs1nXN1+wCxs1F9Rj582CXhPatJMvCKUCc2uRU1fkEI83JB1j8jbGdWwIOVjictuRkFv6jNp9moiiS5QWHJBVjZc0V+/UpMzUQ6I6xlhs+CUNLrB/5unt/dNiz0EGT74Rs1l3TUdbMpjFet3jK9vyJszYt2kUJZmmTZ8f/h+VtuHhXmHiEC8eoGT9bAiJgrS1aXs4qYUXLSV7TqcKjLkNG/XSzYsWruUS+6dhEHawt40bDUpD32KLPG8fkHNRQVciMfvs6C2ctZB+YCkI5OaTFg3ZKWIyUK8dVA46Mg0eSj0fT5wkkpzMlAYKBo5cxkBzTD/sP8xYv8A1vUvlFeFwUgAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Internal ALB and target group health\"\n        title=\"Internal ALB and target group health\"\n        src=\"/static/06b9f74120eabbeaf6e905642841f181/913fc/alb-targets.png\"\n        srcset=\"/static/06b9f74120eabbeaf6e905642841f181/bc34b/alb-targets.png 293w,\n/static/06b9f74120eabbeaf6e905642841f181/da9f0/alb-targets.png 585w,\n/static/06b9f74120eabbeaf6e905642841f181/913fc/alb-targets.png 1170w,\n/static/06b9f74120eabbeaf6e905642841f181/1070c/alb-targets.png 1734w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>ECS Cluster and Running Task</strong></p>\n<p>The ECS cluster view shows the service and the running task. Clicking into the task shows the private IP it was assigned, the container status, and a link to its CloudWatch logs. Notice that the task has no public IP — the only entry point is through the ALB.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1149px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 52.21932114882506%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAAB90lEQVQoz21SSZLbMAzUAyaumrEsWztJSRS1WJIX2bFnvFQmueSQ//+m02SqckkOXSAAEmg06NWH78jrT4j2CEXEqsM6N9gQoewQyRahaKCaPar+hJh+kGoEWU37Lzx5/QWxvSDtr9gYPtB7ZO2MnIiaM9ayx6YYsC5GBGqLSPBhUmIVF/+FN+0vuH/+RL+7uMA6rVB3e+zmD1TdAaukgh8K+JHEiliGEm8b4awfKxf3I+XeLnnPk8MVojkyWLhRlrR6nFFPZxTtAXE5sKh24/gsnhQ9mukCZXZY2TFFRzCXlqxzgGeT43x3F4O8YacSYVkjqgxHI7v4TyPLfJ1ppEWHrOydtfqmzYzEdGxsKNcOXkkW28O7Y2P1sgWjvIbUA0TZIaSmYUNtySJRDQozIswqLsc4PVW9RaZaxickkkvJqwGynpxWQWoZlZBMVv1MRvXfjTotuQybd+e44sjasYxY0Mas75nhjH7/7hyrkU3EwkBzMSULK00GRcvuDW1PlhyTfsaRU57t8o5fH45xwpiX8lKuR6Tl1nULkgJvxRFL88CreWKhH1hUNyzUCW/tBV/EjMDcENQ3vGRHvOo7/P4HXsTZ+Y7h4fINeju7D2zXn9tPPn5A7R5Q+yfk9EDenZGPJ2TdCYr/1iKjtmK8Qe6eEPzH1v8NSh0cs4QG/+0AAAAASUVORK5CYII='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"ECS cluster with running Fargate task\"\n        title=\"ECS cluster with running Fargate task\"\n        src=\"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png\"\n        srcset=\"/static/7f4019d536c56f5519a22cdb95e98de9/bc34b/ecs-task.png 293w,\n/static/7f4019d536c56f5519a22cdb95e98de9/da9f0/ecs-task.png 585w,\n/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png 1149w\"\n        sizes=\"(max-width: 1149px) 100vw, 1149px\"\n      />\n  </a>\n    </span></p>\n<p><strong>API Gateway</strong></p>\n<p>The API Gateway view shows the HTTP API, the two configured routes (<code class=\"language-text\">ANY /</code> and <code class=\"language-text\">ANY /{proxy+}</code>), and the VPC Link integration. The endpoint URL shown on the Stages page is the public URL that users hit to reach the app.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/ea7d6248f0e21f26c55d6d24a021ab74/0df9d/apigw-routes.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 42.721016410799365%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsSAAALEgHS3X78AAABY0lEQVQoz3VR2XLCMBDzF5QCuchl5z4INFBaeCud6f//kqp1Gko7w4NGtrPRrrTKdGfo7oiwGhGXA+K8Q5S1SMheUsJPK3JFrn/QINDN3f0vVFrt4LHIiSuswxyrjbEC+fAGz/RYRQXvBZxo+rYMNJ791LLcf5FZVkV3QMiJVht9K3Yp2J6/UJ4+EdUHpOWWU3dWNOHZNHs66e19HWa3Zg7/U3lLq8X21klEpUAXPbQImQZeTOssDnSN4+WKw/kD1fYVWfvCBoO1Wg8n+LqFCrPeWpSRRUwg1hNOtn+/Im2OWEaMI25sNDnzFoE5V2GXDTfMVc4qMB1c5iSj21wEFHTMDsXugqAY8RS2WESs0z0KZrsxLSJalhgEYl8W5VBHzR1mwWnCjDanJi7Z57bnjU9WRysSUkwg4rILyVbJ2P8FbcDsJmLC95DcTL239iTruS7vRvuuZNR7wXkxj7DwEotH7993wPRPumoOaQAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"API Gateway routes and VPC Link integration\"\n        title=\"API Gateway routes and VPC Link integration\"\n        src=\"/static/ea7d6248f0e21f26c55d6d24a021ab74/913fc/apigw-routes.png\"\n        srcset=\"/static/ea7d6248f0e21f26c55d6d24a021ab74/bc34b/apigw-routes.png 293w,\n/static/ea7d6248f0e21f26c55d6d24a021ab74/da9f0/apigw-routes.png 585w,\n/static/ea7d6248f0e21f26c55d6d24a021ab74/913fc/apigw-routes.png 1170w,\n/static/ea7d6248f0e21f26c55d6d24a021ab74/efb0c/apigw-routes.png 1755w,\n/static/ea7d6248f0e21f26c55d6d24a021ab74/0df9d/apigw-routes.png 1889w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>CloudWatch Logs</strong></p>\n<p>Container output from the ECS task is streamed to CloudWatch under the log group <code class=\"language-text\">/dev-app-svc-logs</code>. This is where you would look to debug the app or confirm it is receiving requests.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/00127d567e9b895759fe1d214b7a0186/598e4/cloudwatch-logs.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 50%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAABvElEQVQoz22S6Y7TQBCE/Qiw6zuxx9eM7fGVxE6Cg8KChMQv3v9xippxAmjhxyfP1d3V1Xaa9ScS+YayPyHvzohyjajoEJc99kVrKZoJerogUyP3GnHWIkjVf3HU+gPd9Q1B3iOsZ0TVgFgd4XLv7SuEokbIhz7XJiDKap6XeAkzvEYGsRHnpIBTtRO6wycIyQS7Aqqfcbp+QUlVORFqQFJqpLxPqw57roXskFCtL48UsVg80cBlUkcfVshutkp8K1uyxQPK9sBEHXZ5Y78GcxeKrTU/kfASBS99wA5cCnJssD4hpm/errSHUdYgzjefXJ7F9FH1CwoWEfUEL9N45Tv38d59rmmFM53vuN6/o7t8RVAMtlLF4IY2qPHCZBo7qmumKxL6a9VQmW87+hdnuX3DkUPJWDkoRz6WqGiBSSCHhVNvmVBDDWcU7ESyWCIHBktr0XucYb6hpRI3LFm1sQlr7vv5s1X5VKiPKwc02mT7qv+TxCj9CycfVqTzHeE82ikbP8yAIg7D/CLWQ6pUw+ZhYqZNBL0PqpFelg8vNxzVLdDLDVGvrX8v/Lc+BOlvPobC8lybe8vj/D2/AHR3GO8dMChkAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"CloudWatch log group for the ECS task\"\n        title=\"CloudWatch log group for the ECS task\"\n        src=\"/static/00127d567e9b895759fe1d214b7a0186/913fc/cloudwatch-logs.png\"\n        srcset=\"/static/00127d567e9b895759fe1d214b7a0186/bc34b/cloudwatch-logs.png 293w,\n/static/00127d567e9b895759fe1d214b7a0186/da9f0/cloudwatch-logs.png 585w,\n/static/00127d567e9b895759fe1d214b7a0186/913fc/cloudwatch-logs.png 1170w,\n/static/00127d567e9b895759fe1d214b7a0186/598e4/cloudwatch-logs.png 1230w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<h2>Testing the Setup</h2>\n<p>With everything deployed and the ECS task healthy, it is time to verify the end-to-end flow works as expected. There are a few things worth testing here — not just that the app loads, but that the private networking is actually working the way it should.</p>\n<p><strong>Step 1 — Get the API Gateway URL</strong></p>\n<p>The public entry point is the API Gateway endpoint URL. Grab it from the Terraform output after <code class=\"language-text\">make 2-deploy-infra</code>, or find it in the AWS Console under <strong>API Gateway → your API → Stages → $default</strong>.</p>\n<p>It will look like:</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">https://&lt;api-id&gt;.execute-api.us-east-1.amazonaws.com</code></pre></div>\n<p><strong>Step 2 — Hit the endpoint</strong></p>\n<p>Open the URL in a browser — you should see the Next.js app load. You can also test it from the terminal:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">curl</span> -I https://<span class=\"token operator\">&lt;</span>api-id<span class=\"token operator\">></span>.execute-api.us-east-1.amazonaws.com</code></pre></div>\n<p>A <code class=\"language-text\">200 OK</code> response confirms the full request path is working — API Gateway received the request, forwarded it through the VPC Link to the internal ALB, and the ALB routed it to the ECS Fargate task.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/943e41ac515b2060a15aad6066887f2f/0400b/app-browser.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 67.62103071317023%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsSAAALEgHS3X78AAAAsklEQVQ4y9WRQQqDMBBFM4lialq1VQw0Liw0uYq5/4W+JuCiiNXixgYeWQzz+MxnBbOYuTGHUrzxuHa4qw5VblBenpBUQ/LmE2qgRI1K9HFvdrDpYY1cSTBan69AWMJA05+mKYj4hmCx+3OCLf5ZqLWGMQZt2yLLsmNCIppkHfr+haIooZSCEOJYwiANTYd0Ukpwzk92w5AwcN6WnXOw1sbb7bzfd6H3HsMwxGKSJNklHAG2aR+l4RGrywAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"React app loading via the API Gateway URL\"\n        title=\"React app loading via the API Gateway URL\"\n        src=\"/static/943e41ac515b2060a15aad6066887f2f/913fc/app-browser.png\"\n        srcset=\"/static/943e41ac515b2060a15aad6066887f2f/bc34b/app-browser.png 293w,\n/static/943e41ac515b2060a15aad6066887f2f/da9f0/app-browser.png 585w,\n/static/943e41ac515b2060a15aad6066887f2f/913fc/app-browser.png 1170w,\n/static/943e41ac515b2060a15aad6066887f2f/efb0c/app-browser.png 1755w,\n/static/943e41ac515b2060a15aad6066887f2f/0400b/app-browser.png 1921w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>Step 3 — Verify the private networking</strong></p>\n<p>This is an important one. Navigate to the running ECS task in the Console under <strong>ECS → Clusters → dev-app-cluster → Tasks</strong>. Click into the task and confirm:</p>\n<ul>\n<li><strong>No public IP is assigned</strong> — the task only has a private IP in the VPC</li>\n<li>The only way to reach the app is through the API Gateway URL</li>\n</ul>\n<p>This confirms the architecture is working as intended — the container is completely off the public internet.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/756eb708cd650ef9411e229f23b62b22/1c71c/ecs-no-public-ip.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 38.779731127197515%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVQoz2VRW3KDMAzkBm1pwIQANg9DgMBAU5J02knvf6qtZDAh0w+N7JVX3pWcor/h2H2iGa/I6gFBoiFiDTdQcPfP8SoSvAXyH/7ix6bGZydrJ/hRQY1KhOkRoToi1icEsoJHONe8JWTZPfC4WLJG3oxU6yGohzNMP0h0B0WAqnpEWQORlhBSrwQbUd4aksU5871ozyjJJdedZvwyzXaH3AQ/ClX9RPKiOc91veKcd5TdMMM7BdedqrvgkD4aiEXJ1pq/KuXm+YwtlkPC96TSuiCFtw1Zm18kjYCtb1VyA9VesNcjPCazYoqIuCk5tG+drB6NXPsDn9vpF+XwPS/EzpDO1fmOQ3OFL+t1WX5S4TTdEZMrY1k209PwGUyrAWn9AZF1C9HOTa8f2M1vl8b3P/jX2wCqfFsQAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"ECS task with private IP only and no public IP\"\n        title=\"ECS task with private IP only and no public IP\"\n        src=\"/static/756eb708cd650ef9411e229f23b62b22/913fc/ecs-no-public-ip.png\"\n        srcset=\"/static/756eb708cd650ef9411e229f23b62b22/bc34b/ecs-no-public-ip.png 293w,\n/static/756eb708cd650ef9411e229f23b62b22/da9f0/ecs-no-public-ip.png 585w,\n/static/756eb708cd650ef9411e229f23b62b22/913fc/ecs-no-public-ip.png 1170w,\n/static/756eb708cd650ef9411e229f23b62b22/efb0c/ecs-no-public-ip.png 1755w,\n/static/756eb708cd650ef9411e229f23b62b22/1c71c/ecs-no-public-ip.png 1934w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<p><strong>Step 4 — Check the ALB target health</strong></p>\n<p>Navigate to <strong>EC2 → Load Balancers → dev-app-svc-lb → Target Groups → dev-app-svc-lb-tg</strong>. The registered target should show as <strong>healthy</strong>. If it shows unhealthy, the most common cause is the <code class=\"language-text\">HOSTNAME=0.0.0.0</code> environment variable missing from the container — Next.js will only be listening on localhost and the health check on the container’s network interface will fail.</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/53702940881daaa410af7f540dfcab32/2889c/alb-healthy-target.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 46.16724738675958%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsSAAALEgHS3X78AAABdElEQVQoz01S2ZKCQAzkG0QRnOFmuBGPdS2t2qr9/5/qTQd0fehKZiZ0Jx28MKkxnB+oxyvCtEZgK2xNKSiwkzyIK803UQb/kGtOMPejVJApQndGUE7wtrZEd3rAFD3iaoQtByWfLg8c8g5JNWEvonkz6xtFiDBtYbobwvqKQPIdhUTE2wmhG9hdo4W+PFh3RDk/tVsliJ2Sv2oCOVNkn/WKQHLe8c1j+5/KppYPswa+la6KDmYlMR+EJu+XXKw5ZK2KvQnphesvqKcbGsF4eaKScywitEE7EYRrXOA+8H+3EJoKtv9G3s5I3Cg+kqgT1VY74DI2Ub5C8oMshNas0AUR1umi1MP5/ov59oNq+FLzi/Yk8QQrHXIcs0aOF9ciLA0Q8XDXuqy7wo7iedotI5fdWUkS2TJjWh9R9Cf1NnHTO/JeR+eShJyCsUyVytvLY49j8P/jgV5EUsilBPnyD9JsRtoQSc3yj5ZaS1v2uXv7x/H/ALyO+OyQQKPqAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"ALB target group showing healthy ECS task\"\n        title=\"ALB target group showing healthy ECS task\"\n        src=\"/static/53702940881daaa410af7f540dfcab32/913fc/alb-healthy-target.png\"\n        srcset=\"/static/53702940881daaa410af7f540dfcab32/bc34b/alb-healthy-target.png 293w,\n/static/53702940881daaa410af7f540dfcab32/da9f0/alb-healthy-target.png 585w,\n/static/53702940881daaa410af7f540dfcab32/913fc/alb-healthy-target.png 1170w,\n/static/53702940881daaa410af7f540dfcab32/2889c/alb-healthy-target.png 1722w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </a>\n    </span></p>\n<h2>Cleanup</h2>\n<p>Once you are done testing, make sure to tear down the infrastructure to avoid unnecessary AWS charges. Resources like the NAT Gateway, ALB, and Fargate tasks accrue costs while running even if there is no traffic.</p>\n<p><strong>Step 1 — Destroy the infrastructure</strong></p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">make</span> <span class=\"token number\">3</span>-destroy-infra</code></pre></div>\n<p>This runs <code class=\"language-text\">terraform destroy -auto-approve</code> inside <code class=\"language-text\">infrastructure/environments/dev</code>. Terraform will remove all provisioned resources in the correct order — ECS service, API Gateway, VPC Link, ALB, NAT Gateway, subnets, and VPC. The ECR repository is also deleted since it was created with <code class=\"language-text\">force_delete = true</code>, which means Terraform can remove it even if it still contains images.</p>\n<p>Once complete, verify in the AWS Console that the resources are gone — particularly the NAT Gateway and ALB, as these are the most common sources of unexpected charges if a destroy is incomplete.</p>\n<p><strong>Step 2 — Clean up local Docker images</strong></p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">make</span> <span class=\"token number\">4</span>-clean</code></pre></div>\n<p>This removes the locally built Docker images that were created during the build step. It is a good habit to run this after a project teardown to keep your local Docker environment tidy, especially since the <code class=\"language-text\">linux/amd64</code> image can be a few hundred MB.</p>\n<h2>Conclusion</h2>\n<p>In this post, we walked through deploying a Next.js React app as a private ECS Fargate workload and exposing it to the internet through AWS API Gateway — without ever putting the application itself on the public internet. The key pieces that make this pattern work are the internal ALB sitting in the private subnet, and the VPC Link that bridges API Gateway into the VPC to reach it.</p>\n<p>Everything was provisioned with Terraform, split across three focused modules — networking, compute, and security — making it straightforward to understand, modify, and extend. The Makefile ties the deployment workflow together into a handful of commands.</p>\n<p>This pattern is a solid foundation for any containerized web application that needs to stay private while remaining publicly accessible. It is easy to build on — you can swap the Next.js app for any other containerized service, add a custom domain with Route 53 and ACM, or layer on API Gateway features like throttling and authorization as your requirements grow.</p>\n<p>The full code is available on Github <a href=\"https://github.com/amlana21/react-apigw-publish\" target=\"_blank\">Here</a>.</p>","htmlAst":{"type":"root","children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Modern cloud architectures often require keeping application workloads private while still making them accessible to end users over the internet. AWS provides a powerful combination of services to achieve this — running containers privately on ECS Fargate, fronting them with an internal Application Load Balancer, and exposing them securely through API Gateway using a VPC Link."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"In this blog, we will walk through deploying a Next.js React application as a private ECS Fargate task and exposing it to the internet via an AWS API Gateway HTTP API. All infrastructure is provisioned using Terraform, making it repeatable and easy to adapt to your own projects."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The whole code for this solution is available on Github "},{"type":"element","tagName":"a","properties":{"href":"https://github.com/amlana21/react-apigw-publish","target":"_blank"},"children":[{"type":"text","value":"Here"}]},{"type":"text","value":". If you want to follow along, this can be used to stand up your own infrastructure."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"By the end of this guide, you will have a fully working setup where a containerized React app runs in a private subnet with no public IP, yet is accessible via a clean API Gateway URL. Let’s get started!"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Pre Requisites"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Before I start the walkthrough, there are some pre-requisites which are good to have if you want to follow along or want to try this on your own:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Basic AWS knowledge"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"An AWS account"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"AWS CLI installed and configured with appropriate permissions"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Terraform installed (>= 1.14.0)"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Docker installed locally"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Basic knowledge of React / Next.js"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Familiarity with ECS and API Gateway concepts"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"With that out of the way, lets dive into the details."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"What is API Gateway and how does VPC Link work?"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"AWS API Gateway is a fully managed service that allows you to create, publish, and manage APIs at any scale. It acts as a front door for applications to access backend services — whether those are Lambda functions, HTTP endpoints, or private VPC resources."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Key features of API Gateway:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"HTTP APIs and REST APIs"}]},{"type":"text","value":" — HTTP APIs are lighter weight and lower cost, ideal for proxying to backend services"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"VPC Link support"}]},{"type":"text","value":" — enables private integration with resources inside a VPC without exposing them to the internet"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Auto-deploy stages"}]},{"type":"text","value":" — changes can be automatically deployed to a stage without manual promotion"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Built-in throttling and security"}]},{"type":"text","value":" — rate limiting, API keys, and IAM authorization options out of the box"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"A key feature we leverage in this post is "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"VPC Link"}]},{"type":"text","value":". By default, API Gateway can only reach publicly accessible endpoints. A VPC Link establishes a private connection between API Gateway and resources inside your VPC, so your ALB and ECS tasks never need to be exposed to the internet."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"With a VPC Link:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Your ALB and ECS tasks can stay in "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"private subnets"}]},{"type":"text","value":" with no internet exposure"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Traffic from API Gateway flows privately through AWS’s internal network"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"No need to open inbound internet access on your security groups for the application tier"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This is the component that makes the “private ECS + public API Gateway” pattern possible."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"What is ECS Fargate?"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Amazon ECS (Elastic Container Service) with the Fargate launch type lets you run containers without managing the underlying EC2 instances. You define the CPU, memory, and container image — AWS handles the rest."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Key benefits for this use case:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Serverless containers"}]},{"type":"text","value":" — no EC2 fleet to patch or scale"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Private networking"}]},{"type":"text","value":" — tasks can run in private subnets with no public IP"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Pay per use"}]},{"type":"text","value":" — billed only for the vCPU and memory consumed while the task is running"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Easy integration with ALB"}]},{"type":"text","value":" — task IPs register directly as ALB targets"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Architecture Overview"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Now that we have covered the key services, let me walk through the high-level architecture of what we are building."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/43fcec4b437ad7eac2a7365d74272f1d/6924e/architecture.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 51.716961498439126%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsSAAALEgHS3X78AAABkUlEQVQoz3WRu24VMRCGz1PxQLQIiY4qb4AURAWipEgqGlJEoJSQBnGIuIiAQs7RLru+e3ftsdd3vDlE4SJGo9HvX/ONxvaqXEfKOaTrrLqUm+NVxlzib2aMYVWxfAUbH4n2FDw1iQrG8CWFcJMm4kagDati54SU/oGVZaV0x/vk4S0G8zKomtqzWM7vPf1y5wl1+f9w7XOl/Xq2efMcK4u1JxBqZXO6OHl/8WpdxbId+L9hrKpbR8zEAJsdlrSmUIJOvFO8jxolwGZARmJQIeY/YO4inyMBN4+sDCQL5ig33Co5YcvBYmMRsZJ6Scy0wCklykRMgY/24EG3PgVZzOlRc7TfJiFtu+GHd+XH485OU4eAEKTZ2ae+5WPMeWXnedv8AGOoNI/uf3h9wsdiXx6eP9v77Cmfvn/bPr7N370gXrPLVjaoV/TtetvwYYFzrh+Yd2s3UmHtCPhh4nogSrJR8L6nlAtsB6Z7rlEVzUQQjL/uvAsfkwnJ+AQuahe039VgQqxPC4tO1awCaq3dOf8EQnExAooA/AUAAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"Architecture Diagram","title":"Architecture Diagram","src":"/static/43fcec4b437ad7eac2a7365d74272f1d/913fc/architecture.png","srcSet":["/static/43fcec4b437ad7eac2a7365d74272f1d/bc34b/architecture.png 293w","/static/43fcec4b437ad7eac2a7365d74272f1d/da9f0/architecture.png 585w","/static/43fcec4b437ad7eac2a7365d74272f1d/913fc/architecture.png 1170w","/static/43fcec4b437ad7eac2a7365d74272f1d/efb0c/architecture.png 1755w","/static/43fcec4b437ad7eac2a7365d74272f1d/6924e/architecture.png 1922w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]},{"type":"text","value":" "},{"type":"element","tagName":"br","properties":{},"children":[]},{"type":"text","value":"  "}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The architecture is built around a single VPC that is split into two tiers — a "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"public tier"}]},{"type":"text","value":" and a "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"private tier"}]},{"type":"text","value":" — each serving a distinct role."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Public Tier"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The public subnets contain the components that need outbound internet access. A "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"NAT Gateway"}]},{"type":"text","value":" sits here, allowing resources in the private subnets to make outbound calls (for example, pulling container images or reaching AWS service endpoints) without being directly reachable from the internet. An "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Internet Gateway"}]},{"type":"text","value":" is attached to the VPC to facilitate this outbound traffic."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Importantly, there is no application workload in the public tier. Nothing serving the React app is exposed here."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Private Tier"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The private subnets are where all the application workload lives. Two key resources sit here:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Internal Application Load Balancer (ALB)"}]},{"type":"text","value":" — this ALB has no public-facing DNS. It is only reachable from within the VPC or via an API Gateway VPC Link. It listens for incoming HTTP traffic and forwards it to the ECS tasks registered in its target group."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECS Fargate Tasks"}]},{"type":"text","value":" — the containerized React app runs here. Each task gets a private IP address only, with no public IP assigned. The container listens on port 3000 and registers itself with the ALB target group automatically when the service starts."}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"API Gateway and VPC Link"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Sitting outside the VPC entirely, the "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"API Gateway HTTP API"}]},{"type":"text","value":" is the only public entry point. It exposes a URL to the internet and is configured with two routes — "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"ANY /"}]},{"type":"text","value":" and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"ANY /{proxy+}"}]},{"type":"text","value":" — so all paths are forwarded to the backend."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The API Gateway is connected to the internal ALB through a "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"VPC Link"}]},{"type":"text","value":". The VPC Link is what bridges the gap between API Gateway (which lives outside the VPC) and the internal ALB (which lives inside). AWS handles the private network plumbing behind the scenes — from the user’s perspective, they hit the API Gateway URL and the request lands on the React app."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Container Registry"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Before the ECS task can run, the Docker image for the React app needs to be stored somewhere AWS can pull it from. An "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Amazon ECR (Elastic Container Registry)"}]},{"type":"text","value":" repository holds the built image. When ECS starts a new task, it pulls the image from ECR over the private network using the NAT Gateway for the initial pull — no public image registry required."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Logging"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"All container output from the ECS tasks is streamed to "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Amazon CloudWatch Logs"}]},{"type":"text","value":". This gives a central place to debug the application without needing to SSH into any servers (there are none with Fargate)."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Request Flow"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Putting it all together, here is the end-to-end flow when a user opens the React app:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ol","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"User sends a request to the "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"API Gateway"}]},{"type":"text","value":" endpoint URL"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"API Gateway matches the route and forwards the request via the "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"VPC Link"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"The VPC Link delivers the request to the "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"internal ALB"}]},{"type":"text","value":" inside the private subnet"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"The ALB selects a healthy "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECS Fargate task"}]},{"type":"text","value":" from its target group and forwards the request"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"The Next.js app running in the container handles the request and sends back a response"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"The response travels back through ALB → VPC Link → API Gateway → user"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"At no point does any internet traffic touch the ECS task directly. The only publicly addressable component is the API Gateway endpoint."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Infrastructure Walkthrough"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Now let’s walk through the Terraform code that provisions all of this. The infrastructure is organized into three modules: "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"networking"}]},{"type":"text","value":", "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"compute"}]},{"type":"text","value":", and "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"security"}]},{"type":"text","value":". All of this lives under "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"infrastructure/"}]},{"type":"text","value":", with a "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"dev"}]},{"type":"text","value":" environment wiring the modules together in "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"environments/dev/main.tf"}]},{"type":"text","value":"."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Networking Module"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This is the foundation of the whole setup. It provisions the VPC, public and private subnets across availability zones, an Internet Gateway, a NAT Gateway (so private resources can reach the internet for things like pulling images without being publicly reachable), and the route tables to wire everything together."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Two security groups are defined here:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"web_sg"}]},{"type":"text","value":" — attached to the ALB, allows inbound HTTP/HTTPS from the internet"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"app_sg"}]},{"type":"text","value":" — attached to ECS tasks, allows inbound traffic only from "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"web_sg"}]},{"type":"text","value":" on port 3000"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This enforces a clean two-tier boundary — no internet traffic can reach the ECS tasks directly."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The most important resource in this module is the "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"internal ALB"}]},{"type":"text","value":". Setting "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"internal = true"}]},{"type":"text","value":" is what keeps it off the public internet — it has no public DNS and is only reachable from within the VPC or via the API Gateway VPC Link."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"hcl"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-hcl"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-hcl"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","keyword"]},"children":[{"type":"text","value":"resource "},{"type":"element","tagName":"span","properties":{"className":["token","type","variable"]},"children":[{"type":"text","value":"\"aws_lb\""}]}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"app_svc_lb\""}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"{"}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"name"}]},{"type":"text","value":"               "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\""},{"type":"element","tagName":"span","properties":{"className":["token","interpolation"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"$"}]},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"{"}]},{"type":"element","tagName":"span","properties":{"className":["token","keyword"]},"children":[{"type":"text","value":"var"}]},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"."}]},{"type":"element","tagName":"span","properties":{"className":["token","type","variable"]},"children":[{"type":"text","value":"environment_val"}]},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"}"}]}]},{"type":"text","value":"-app-svc-lb\""}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"internal"}]},{"type":"text","value":"           "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","boolean"]},"children":[{"type":"text","value":"true"}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"load_balancer_type"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"application\""}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"security_groups"}]},{"type":"text","value":"    "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"["}]},{"type":"text","value":"aws_security_group.web_sg.id"},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"]"}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"subnets"}]},{"type":"text","value":"            "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" aws_subnet.app_private"},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"["}]},{"type":"text","value":"*"},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"]"}]},{"type":"text","value":".id\n"},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"}"}]}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The ALB target group uses "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"target_type = \"ip\""}]},{"type":"text","value":", which is required for Fargate — there are no EC2 instances to register, so Fargate registers each task’s private IP directly."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Compute Module"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This module provisions everything needed to run the app and expose it. It covers four things:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECR"}]},{"type":"text","value":" — a private container registry to store the Docker image. ECS pulls from here when starting new tasks."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECS cluster and task definition"}]},{"type":"text","value":" — the task definition is where the container is configured: the ECR image URL, CPU (512) and memory (1024 MB), port 3000, and CloudWatch log shipping. The cluster itself is just a logical grouping."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECS service"}]},{"type":"text","value":" — runs the Fargate tasks in the private subnets. The key setting here is "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"assign_public_ip = false"}]},{"type":"text","value":", which ensures tasks never get a public IP. The service also registers tasks with the ALB target group automatically as they start up."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"API Gateway + VPC Link"}]},{"type":"text","value":" — this is the piece that makes the private app accessible from the internet. A VPC Link bridges API Gateway (which lives outside the VPC) to the internal ALB. An HTTP API is created with an "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"HTTP_PROXY"}]},{"type":"text","value":" integration pointing at the ALB listener, and two routes ("},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"ANY /"}]},{"type":"text","value":" and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"ANY /{proxy+}"}]},{"type":"text","value":") forward all traffic through."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"hcl"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-hcl"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-hcl"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","keyword"]},"children":[{"type":"text","value":"resource "},{"type":"element","tagName":"span","properties":{"className":["token","type","variable"]},"children":[{"type":"text","value":"\"aws_apigatewayv2_integration\""}]}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"app_alb_integration\""}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"{"}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"api_id"}]},{"type":"text","value":"             "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" aws_apigatewayv"},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"2"}]},{"type":"text","value":"_api.app_api.id\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"integration_type"}]},{"type":"text","value":"   "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"HTTP_PROXY\""}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"integration_method"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"ANY\""}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"integration_uri"}]},{"type":"text","value":"    "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" var.app_svc_alb_listener_arn\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"connection_type"}]},{"type":"text","value":"    "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"VPC_LINK\""}]},{"type":"text","value":"\n  "},{"type":"element","tagName":"span","properties":{"className":["token","property"]},"children":[{"type":"text","value":"connection_id"}]},{"type":"text","value":"      "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"="}]},{"type":"text","value":" aws_apigatewayv"},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"2"}]},{"type":"text","value":"_vpc_link.app_vpc_link.id\n"},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"}"}]}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The stage is set to "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"$default"}]},{"type":"text","value":" with "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"auto_deploy = true"}]},{"type":"text","value":", so changes go live immediately without a manual deploy step."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Security Module"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This module defines the IAM role used by ECS as both the execution role and the task role. The execution role lets ECS pull images from ECR and write logs to CloudWatch. The task role is what the running container itself uses to interact with AWS services."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The core permission is the "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"AmazonECSTaskExecutionRolePolicy"}]},{"type":"text","value":" managed policy. Additional policies for CloudWatch Agent and X-Ray are also attached to support observability."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Application Walkthrough"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The application is a standard Next.js app. The main page lives in "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"app/page.tsx"}]},{"type":"text","value":" — for this post it is a simple scaffold, but the infrastructure pattern works the same regardless of what the app actually does. The focus here is on how it gets packaged and run inside a container."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Dockerfile"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The Dockerfile uses a "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"multi-stage build"}]},{"type":"text","value":" to keep the final image small. The three stages are:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"deps"}]},{"type":"text","value":" — installs dependencies using "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"npm ci"}]},{"type":"text","value":" for a clean, reproducible install from "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"package-lock.json"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"builder"}]},{"type":"text","value":" — copies the source and runs "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"npm run build"}]},{"type":"text","value":" to produce the Next.js production output"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"runner"}]},{"type":"text","value":" — the final image. Only the compiled output and necessary files are copied over from the builder, leaving the build toolchain behind"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"A few things in the runner stage are worth calling out. A non-root "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"nextjs"}]},{"type":"text","value":" system user is created and used to run the app — a standard security practice for containerized workloads. And "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"HOSTNAME"}]},{"type":"text","value":" is explicitly set to "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"0.0.0.0"}]},{"type":"text","value":":"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"dockerfile"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-dockerfile"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-dockerfile"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","keyword"]},"children":[{"type":"text","value":"ENV"}]},{"type":"text","value":" PORT=3000\n"},{"type":"element","tagName":"span","properties":{"className":["token","keyword"]},"children":[{"type":"text","value":"ENV"}]},{"type":"text","value":" HOSTNAME=0.0.0.0\n"},{"type":"element","tagName":"span","properties":{"className":["token","keyword"]},"children":[{"type":"text","value":"CMD"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"["}]},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"npm\""}]},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":","}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","string"]},"children":[{"type":"text","value":"\"start\""}]},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"]"}]}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This is important. By default Next.js binds only to "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"localhost"}]},{"type":"text","value":", which means ALB health checks — which arrive on the container’s network interface — would fail. Setting "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"HOSTNAME=0.0.0.0"}]},{"type":"text","value":" tells Next.js to listen on all interfaces, making the container reachable from the ALB."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"One other thing to keep in mind when building locally on Apple Silicon: the image needs to be built for "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"linux/amd64"}]},{"type":"text","value":" since that is what ECS Fargate runs on. The Makefile handles this with the "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"--platform linux/amd64"}]},{"type":"text","value":" flag."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Deploying the Solution"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"With the code in place, deploying the full solution involves two phases: provisioning the infrastructure and then building and pushing the container image. A "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Makefile"}]},{"type":"text","value":" wraps both phases into numbered targets."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Folder Structure"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Before deploying, it helps to understand how the repo is laid out:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"text"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-text"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":".\n├── Makefile                          # top-level commands for deploying infra and building the app\n├── infrastructure/\n│   ├── environments/\n│   │   └── dev/                      # the dev environment — Terraform is run from here\n│   │       ├── locals.tf             # defines environment name, region, and naming conventions\n│   │       ├── main.tf               # wires the three modules together and passes variables between them\n│   │       └── provider.tf           # AWS provider configuration\n│   └── modules/\n│       ├── networking/               # VPC, public/private subnets, NAT Gateway, internal ALB, security groups\n│       ├── compute/                  # ECR repository, ECS cluster, task definition, ECS service, API Gateway, VPC Link\n│       └── security/                 # IAM task execution role and the policies attached to it\n└── src/\n    └── api-app/                      # the Next.js application\n        ├── app/                      # Next.js app directory — pages and layouts live here\n        │   ├── layout.tsx            # root layout wrapping all pages\n        │   └── page.tsx              # home page\n        ├── Dockerfile                # multi-stage build for the production container image\n        └── package.json              # app dependencies and build scripts"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"infrastructure/environments/dev"}]},{"type":"text","value":" directory is where Terraform commands are run from. The modules under "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"infrastructure/modules"}]},{"type":"text","value":" are reusable and environment-agnostic — the "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"dev"}]},{"type":"text","value":" environment passes in the values via variables."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Lets Deploy!!"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 1 — Plan the infrastructure"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"bash"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","function"]},"children":[{"type":"text","value":"make"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"1"}]},{"type":"text","value":"-plan-infra"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This runs "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"terraform init"}]},{"type":"text","value":" and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"terraform plan"}]},{"type":"text","value":" inside "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"infrastructure/environments/dev"}]},{"type":"text","value":". Terraform will print out every resource it intends to create — VPC, subnets, NAT Gateway, ALB, security groups, ECR repo, ECS cluster, API Gateway, and the VPC Link. It is worth taking a minute to review the plan output before applying, especially the first time, to make sure nothing unexpected is included."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/47fbb274f42f247928322bb638726abf/6dbbe/tf-plan.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 39.70013037809648%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABY0lEQVQoz2WReU/CUBDE+ULiUa5CgQIt9KAnbYFIMAKi4BX9/sm482pJjH9Mtsd7s7+drXXNBGF+wizYwYufME+PomelMDup6sUHuNFe/feTI5xwh/44R9dMYdprNHtzXDds3DSnqNW1CQZWJlqiZQSY+g/KhBeDxYu61DZC6INYlKBnLqAPE7T7EToXxbhtOcq0xoNjZ4P+JBcVGForZT60y0oC0tDIkGqMspKOpmz0Wy+Emu7CS/ZwZGTb38r4Z2T3X0hXHyg23xLFoxqR43JsPlveFo2uL1Qz3LWdC50y5AdSElvruNI5RW9EmkzRsHuzO0dD90S+nClrZfTPUB/GiIqzInGjA6L8FcnyHXHxhsX6E76QkcoN94qwohxIPGxGgD8jcynGeCG5FGj1ApWZLYupxKUwJ5WjkPOdJiSua9ZFNFNLYfAcl4doyPAZOLdHE00uXt1NwMalSoOKqFL1/gPShiLMv3RfcAAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"Terraform plan output","title":"Terraform plan output","src":"/static/47fbb274f42f247928322bb638726abf/913fc/tf-plan.png","srcSet":["/static/47fbb274f42f247928322bb638726abf/bc34b/tf-plan.png 293w","/static/47fbb274f42f247928322bb638726abf/da9f0/tf-plan.png 585w","/static/47fbb274f42f247928322bb638726abf/913fc/tf-plan.png 1170w","/static/47fbb274f42f247928322bb638726abf/6dbbe/tf-plan.png 1534w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 2 — Apply the infrastructure"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"bash"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","function"]},"children":[{"type":"text","value":"make"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"2"}]},{"type":"text","value":"-deploy-infra"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This runs "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"terraform apply -auto-approve"}]},{"type":"text","value":", which provisions all the resources in the correct dependency order. Terraform handles the sequencing — for example, it knows the ECS service depends on the ALB target group, which depends on the VPC, so it wires all of that up automatically."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Once the apply completes, note down the API Gateway endpoint URL from the Terraform outputs — you will need it for testing. You can also find it in the AWS Console under "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"API Gateway → your API → Stages"}]},{"type":"text","value":"."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/5dcd4b788b34454069cbfd987d421436/3d35d/tf-apply.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 10.575296108291033%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsSAAALEgHS3X78AAAAe0lEQVQI1z2Oyw6CMAAE+SMgtCogEVRo01olKkRvPk6G/z+PtIkeJjubvWzU2B6PKAyJVMRiT7pQgVi0pEtNlhtEbmcM2cqE/ktZ2LB5T2RHpN0bd/5wGib08YVyT/pxojs8Qvded3eq7RAo6wtlc/3npr1R7UbWs/sTXzzFSBok6MONAAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"Terraform apply output and API Gateway URL","title":"Terraform apply output and API Gateway URL","src":"/static/5dcd4b788b34454069cbfd987d421436/913fc/tf-apply.png","srcSet":["/static/5dcd4b788b34454069cbfd987d421436/bc34b/tf-apply.png 293w","/static/5dcd4b788b34454069cbfd987d421436/da9f0/tf-apply.png 585w","/static/5dcd4b788b34454069cbfd987d421436/913fc/tf-apply.png 1170w","/static/5dcd4b788b34454069cbfd987d421436/3d35d/tf-apply.png 1182w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 3 — Build and push the container image"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"bash"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","function"]},"children":[{"type":"text","value":"make"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"2"}]},{"type":"text","value":"-1-build-app"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Before ECS can run the app, the Docker image needs to be in ECR. This Makefile target takes care of the full flow in one command:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ol","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Authenticates your local Docker client with ECR using "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"aws ecr get-login-password"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Builds the image targeting "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"linux/amd64"}]},{"type":"text","value":" (required for Fargate, important if you are on Apple Silicon)"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"Tags and pushes the image to the ECR repository that was created in Step 2"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"You can verify the image landed in ECR by checking the repository in the AWS Console under "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Elastic Container Registry"}]},{"type":"text","value":"."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/ccbedf83ba9b3c289c5558586acd6ce0/18787/ecr-image.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 40.10025062656642%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABbElEQVQoz1VR2W6DMBDkF5oDCJeDARuwARGakiaVoirqSx/6/38z3eXo8TAa78LOzthO+fwBod8h6wGZOSOUBgdpERxLRLJGmFaIM4PCDEh1B1+U8I8VDgRfVHP9B468fuFoL4j0AF+ToOohmivS6oS0sHCjDNtDOiOQE3bMVDPvw+wfnH644v74RNWN9JOEnxRo+xEt9SMzwk00pGrIbUluS4jcIBAKsmwphcbGT2hOYOvPcKr+Bt2/wSO7bqywoy0cNaDoriLXqZkGfUFIFA7EHnEwRdbYRzmlyH/YyZsRZnwgyhsSow+EqrvAnm4QRQuPlnjk0o1JiJHopVYT9lGBHWE9O/3LHbZ/nQa5wVt44FgNEKqbzvwAsuwRpPWPONe8OMosJTKIcwtlz3AScvb7o5qscyQvtbMLWsI9HlojchIWEKqdOCZRnuGzk9Fr1uQwJFF2yC+1ocvdeMnMy2U/LfV6+Zult2KtvwF0Xd7UgKRdiwAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"ECR repository with pushed image","title":"ECR repository with pushed image","src":"/static/ccbedf83ba9b3c289c5558586acd6ce0/913fc/ecr-image.png","srcSet":["/static/ccbedf83ba9b3c289c5558586acd6ce0/bc34b/ecr-image.png 293w","/static/ccbedf83ba9b3c289c5558586acd6ce0/da9f0/ecr-image.png 585w","/static/ccbedf83ba9b3c289c5558586acd6ce0/913fc/ecr-image.png 1170w","/static/ccbedf83ba9b3c289c5558586acd6ce0/18787/ecr-image.png 1197w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 4 — Start the ECS service"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The ECS service is provisioned with "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"desired_count = 0"}]},{"type":"text","value":" so no tasks run before the image is available. Once the image is in ECR, bring the service up by setting the desired count to 1:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"bash"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-bash"]},"children":[{"type":"text","value":"aws ecs update-service "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"\\"}]},{"type":"text","value":"\n  --cluster dev-app-cluster "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"\\"}]},{"type":"text","value":"\n  --service dev-app-svc "},{"type":"element","tagName":"span","properties":{"className":["token","punctuation"]},"children":[{"type":"text","value":"\\"}]},{"type":"text","value":"\n  --desired-count "},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"1"}]}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"ECS will schedule a Fargate task in the private subnet, pull the image from ECR, and start the container. As the task comes up, it registers its private IP with the ALB target group. Once the ALB health check on port 3000 passes, the task is marked healthy and traffic will start flowing through."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"You can watch the task come up in the AWS Console under "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECS → Clusters → dev-app-cluster → Tasks"}]},{"type":"text","value":"."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1149px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 52.21932114882506%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAAB90lEQVQoz21SSZLbMAzUAyaumrEsWztJSRS1WJIX2bFnvFQmueSQ//+m02SqckkOXSAAEmg06NWH78jrT4j2CEXEqsM6N9gQoewQyRahaKCaPar+hJh+kGoEWU37Lzx5/QWxvSDtr9gYPtB7ZO2MnIiaM9ayx6YYsC5GBGqLSPBhUmIVF/+FN+0vuH/+RL+7uMA6rVB3e+zmD1TdAaukgh8K+JHEiliGEm8b4awfKxf3I+XeLnnPk8MVojkyWLhRlrR6nFFPZxTtAXE5sKh24/gsnhQ9mukCZXZY2TFFRzCXlqxzgGeT43x3F4O8YacSYVkjqgxHI7v4TyPLfJ1ppEWHrOydtfqmzYzEdGxsKNcOXkkW28O7Y2P1sgWjvIbUA0TZIaSmYUNtySJRDQozIswqLsc4PVW9RaZaxickkkvJqwGynpxWQWoZlZBMVv1MRvXfjTotuQybd+e44sjasYxY0Mas75nhjH7/7hyrkU3EwkBzMSULK00GRcvuDW1PlhyTfsaRU57t8o5fH45xwpiX8lKuR6Tl1nULkgJvxRFL88CreWKhH1hUNyzUCW/tBV/EjMDcENQ3vGRHvOo7/P4HXsTZ+Y7h4fINeju7D2zXn9tPPn5A7R5Q+yfk9EDenZGPJ2TdCYr/1iKjtmK8Qe6eEPzH1v8NSh0cs4QG/+0AAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"ECS task running in private subnet","title":"ECS task running in private subnet","src":"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png","srcSet":["/static/7f4019d536c56f5519a22cdb95e98de9/bc34b/ecs-task.png 293w","/static/7f4019d536c56f5519a22cdb95e98de9/da9f0/ecs-task.png 585w","/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png 1149w"],"sizes":["(max-width:","1149px)","100vw,","1149px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"AWS Resources"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Once everything is deployed, it is worth taking a look at the resources in the AWS Console to get a feel for what was provisioned and how it all connects. Here are the key ones to check."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"VPC and Subnets"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The VPC shows the full network layout — public and private subnets spread across availability zones, the route tables, and the attached Internet Gateway. You can confirm the private subnets have no direct route to the internet, only through the NAT Gateway."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/20108e4f55f1c061c54d575da74c597e/3389c/vpc-subnets.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 62.12198872373141%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsSAAALEgHS3X78AAACAElEQVQoz4WS2XLaQBBF9e44AYQQWtC+gRYkgTFbjOPkIf//Qze3R6iCq1KVh66Znp4+c7t7tLj7BS/vEeYNnGiNxSrB0kvv/gZWUMAJ1zzLYLiJiovJ3lil3Kd/fZpW7i5Iqz2TCsysANOlz0CMen9BVu8RFS3iTQebj82dWMVkXQjcjWA4d18eYkyLyxcEGdWEOSamNwAZrA/vcJMauh0pJVHVYu4KMFWA8uUdcffGM/q8U/RXJN13aNXuCi+tYQcZJoQJcO5EaI4fyNqzKtWkVd0ZablTLRBg1hyQVBSTb1U82vTwsi2B/VkpVEDTx4xAUeXlLVZJhaU/AKVsSR57JWZS+djbpZ/DZtu0sjvxYvNJoU4F4fYCJ21g8aIV5CpBEkW99FHnqtuhelwUy37KGWib9giX0xx7OAzGU+Wl9QEO+7ggTBTmvGvHpfJDDmv7ekOw7mDS97Mau8tPaM3+ilgOvUSpG4EW5TvxRikTVVGzR3f6IKDnxEtEXF9vv5FvT2p4Mofm8Db00E8rjj96APr4MjOVPetLPE0XCKqeCTf4RacABr+MFWRq1e0A3wwbX+cWtJr/MKJCCTwCJTiagAXYHn8gECDVCOBpagyP3h8WUyWvpE//AfojkKWKwsnC/XRnNK3sTwSW/wQ+j0CWHNa7oYfFCHSUokeY+H8AbQNLR8M450UAAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"VPC and subnet layout in the AWS Console","title":"VPC and subnet layout in the AWS Console","src":"/static/20108e4f55f1c061c54d575da74c597e/913fc/vpc-subnets.png","srcSet":["/static/20108e4f55f1c061c54d575da74c597e/bc34b/vpc-subnets.png 293w","/static/20108e4f55f1c061c54d575da74c597e/da9f0/vpc-subnets.png 585w","/static/20108e4f55f1c061c54d575da74c597e/913fc/vpc-subnets.png 1170w","/static/20108e4f55f1c061c54d575da74c597e/efb0c/vpc-subnets.png 1755w","/static/20108e4f55f1c061c54d575da74c597e/3389c/vpc-subnets.png 1951w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Internal Application Load Balancer"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The ALB should show as "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"internal"}]},{"type":"text","value":" with no public DNS name. Under the target group, you can see the registered ECS task IPs and their health check status. A healthy target here means traffic can flow from the VPC Link all the way to the container."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/06b9f74120eabbeaf6e905642841f181/1070c/alb-targets.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 53.92156862745098%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAAB30lEQVQoz0VT2Y7UQAzMJ7CZ3EfnPieZDUyG2V2BVgIESLzsP/D/31CUnYR9sOy2u+2qcmIl1Yh2XhFkHU5RAScucQoLxqXGh0ntqDtxpXU7zNVc0yMc73DzEVbRP+L28g1R1sJLarhxjbSb4Oetxi5zXtogqc4IywkuY5sDBUA1fkTeXTSWvAyy5GLXXpAx4e1JQdxM170ZzbRo7r9Q3H8jGT+jIoi0ntBdbij7BVEx6F1tKNDVeJDkZlvz46wIn99gfv5F9fwH69MrTDMrUt9sdzcwJayQ3ZtpRVKOCj2mj+gDogrzXs1nXN1+wCxs1F9Rj582CXhPatJMvCKUCc2uRU1fkEI83JB1j8jbGdWwIOVjictuRkFv6jNp9moiiS5QWHJBVjZc0V+/UpMzUQ6I6xlhs+CUNLrB/5unt/dNiz0EGT74Rs1l3TUdbMpjFet3jK9vyJszYt2kUJZmmTZ8f/h+VtuHhXmHiEC8eoGT9bAiJgrS1aXs4qYUXLSV7TqcKjLkNG/XSzYsWruUS+6dhEHawt40bDUpD32KLPG8fkHNRQVciMfvs6C2ctZB+YCkI5OaTFg3ZKWIyUK8dVA46Mg0eSj0fT5wkkpzMlAYKBo5cxkBzTD/sP8xYv8A1vUvlFeFwUgAAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"Internal ALB and target group health","title":"Internal ALB and target group health","src":"/static/06b9f74120eabbeaf6e905642841f181/913fc/alb-targets.png","srcSet":["/static/06b9f74120eabbeaf6e905642841f181/bc34b/alb-targets.png 293w","/static/06b9f74120eabbeaf6e905642841f181/da9f0/alb-targets.png 585w","/static/06b9f74120eabbeaf6e905642841f181/913fc/alb-targets.png 1170w","/static/06b9f74120eabbeaf6e905642841f181/1070c/alb-targets.png 1734w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECS Cluster and Running Task"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The ECS cluster view shows the service and the running task. Clicking into the task shows the private IP it was assigned, the container status, and a link to its CloudWatch logs. Notice that the task has no public IP — the only entry point is through the ALB."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1149px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 52.21932114882506%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAAB90lEQVQoz21SSZLbMAzUAyaumrEsWztJSRS1WJIX2bFnvFQmueSQ//+m02SqckkOXSAAEmg06NWH78jrT4j2CEXEqsM6N9gQoewQyRahaKCaPar+hJh+kGoEWU37Lzx5/QWxvSDtr9gYPtB7ZO2MnIiaM9ayx6YYsC5GBGqLSPBhUmIVF/+FN+0vuH/+RL+7uMA6rVB3e+zmD1TdAaukgh8K+JHEiliGEm8b4awfKxf3I+XeLnnPk8MVojkyWLhRlrR6nFFPZxTtAXE5sKh24/gsnhQ9mukCZXZY2TFFRzCXlqxzgGeT43x3F4O8YacSYVkjqgxHI7v4TyPLfJ1ppEWHrOydtfqmzYzEdGxsKNcOXkkW28O7Y2P1sgWjvIbUA0TZIaSmYUNtySJRDQozIswqLsc4PVW9RaZaxickkkvJqwGynpxWQWoZlZBMVv1MRvXfjTotuQybd+e44sjasYxY0Mas75nhjH7/7hyrkU3EwkBzMSULK00GRcvuDW1PlhyTfsaRU57t8o5fH45xwpiX8lKuR6Tl1nULkgJvxRFL88CreWKhH1hUNyzUCW/tBV/EjMDcENQ3vGRHvOo7/P4HXsTZ+Y7h4fINeju7D2zXn9tPPn5A7R5Q+yfk9EDenZGPJ2TdCYr/1iKjtmK8Qe6eEPzH1v8NSh0cs4QG/+0AAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"ECS cluster with running Fargate task","title":"ECS cluster with running Fargate task","src":"/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png","srcSet":["/static/7f4019d536c56f5519a22cdb95e98de9/bc34b/ecs-task.png 293w","/static/7f4019d536c56f5519a22cdb95e98de9/da9f0/ecs-task.png 585w","/static/7f4019d536c56f5519a22cdb95e98de9/24e38/ecs-task.png 1149w"],"sizes":["(max-width:","1149px)","100vw,","1149px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"API Gateway"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The API Gateway view shows the HTTP API, the two configured routes ("},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"ANY /"}]},{"type":"text","value":" and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"ANY /{proxy+}"}]},{"type":"text","value":"), and the VPC Link integration. The endpoint URL shown on the Stages page is the public URL that users hit to reach the app."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/ea7d6248f0e21f26c55d6d24a021ab74/0df9d/apigw-routes.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 42.721016410799365%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsSAAALEgHS3X78AAABY0lEQVQoz3VR2XLCMBDzF5QCuchl5z4INFBaeCud6f//kqp1Gko7w4NGtrPRrrTKdGfo7oiwGhGXA+K8Q5S1SMheUsJPK3JFrn/QINDN3f0vVFrt4LHIiSuswxyrjbEC+fAGz/RYRQXvBZxo+rYMNJ791LLcf5FZVkV3QMiJVht9K3Yp2J6/UJ4+EdUHpOWWU3dWNOHZNHs66e19HWa3Zg7/U3lLq8X21klEpUAXPbQImQZeTOssDnSN4+WKw/kD1fYVWfvCBoO1Wg8n+LqFCrPeWpSRRUwg1hNOtn+/Im2OWEaMI25sNDnzFoE5V2GXDTfMVc4qMB1c5iSj21wEFHTMDsXugqAY8RS2WESs0z0KZrsxLSJalhgEYl8W5VBHzR1mwWnCjDanJi7Z57bnjU9WRysSUkwg4rILyVbJ2P8FbcDsJmLC95DcTL239iTruS7vRvuuZNR7wXkxj7DwEotH7993wPRPumoOaQAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"API Gateway routes and VPC Link integration","title":"API Gateway routes and VPC Link integration","src":"/static/ea7d6248f0e21f26c55d6d24a021ab74/913fc/apigw-routes.png","srcSet":["/static/ea7d6248f0e21f26c55d6d24a021ab74/bc34b/apigw-routes.png 293w","/static/ea7d6248f0e21f26c55d6d24a021ab74/da9f0/apigw-routes.png 585w","/static/ea7d6248f0e21f26c55d6d24a021ab74/913fc/apigw-routes.png 1170w","/static/ea7d6248f0e21f26c55d6d24a021ab74/efb0c/apigw-routes.png 1755w","/static/ea7d6248f0e21f26c55d6d24a021ab74/0df9d/apigw-routes.png 1889w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"CloudWatch Logs"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Container output from the ECS task is streamed to CloudWatch under the log group "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"/dev-app-svc-logs"}]},{"type":"text","value":". This is where you would look to debug the app or confirm it is receiving requests."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/00127d567e9b895759fe1d214b7a0186/598e4/cloudwatch-logs.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 50%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAABvElEQVQoz22S6Y7TQBCE/Qiw6zuxx9eM7fGVxE6Cg8KChMQv3v9xippxAmjhxyfP1d3V1Xaa9ScS+YayPyHvzohyjajoEJc99kVrKZoJerogUyP3GnHWIkjVf3HU+gPd9Q1B3iOsZ0TVgFgd4XLv7SuEokbIhz7XJiDKap6XeAkzvEYGsRHnpIBTtRO6wycIyQS7Aqqfcbp+QUlVORFqQFJqpLxPqw57roXskFCtL48UsVg80cBlUkcfVshutkp8K1uyxQPK9sBEHXZ5Y78GcxeKrTU/kfASBS99wA5cCnJssD4hpm/errSHUdYgzjefXJ7F9FH1CwoWEfUEL9N45Tv38d59rmmFM53vuN6/o7t8RVAMtlLF4IY2qPHCZBo7qmumKxL6a9VQmW87+hdnuX3DkUPJWDkoRz6WqGiBSSCHhVNvmVBDDWcU7ESyWCIHBktr0XucYb6hpRI3LFm1sQlr7vv5s1X5VKiPKwc02mT7qv+TxCj9CycfVqTzHeE82ikbP8yAIg7D/CLWQ6pUw+ZhYqZNBL0PqpFelg8vNxzVLdDLDVGvrX8v/Lc+BOlvPobC8lybe8vj/D2/AHR3GO8dMChkAAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"CloudWatch log group for the ECS task","title":"CloudWatch log group for the ECS task","src":"/static/00127d567e9b895759fe1d214b7a0186/913fc/cloudwatch-logs.png","srcSet":["/static/00127d567e9b895759fe1d214b7a0186/bc34b/cloudwatch-logs.png 293w","/static/00127d567e9b895759fe1d214b7a0186/da9f0/cloudwatch-logs.png 585w","/static/00127d567e9b895759fe1d214b7a0186/913fc/cloudwatch-logs.png 1170w","/static/00127d567e9b895759fe1d214b7a0186/598e4/cloudwatch-logs.png 1230w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Testing the Setup"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"With everything deployed and the ECS task healthy, it is time to verify the end-to-end flow works as expected. There are a few things worth testing here — not just that the app loads, but that the private networking is actually working the way it should."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 1 — Get the API Gateway URL"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The public entry point is the API Gateway endpoint URL. Grab it from the Terraform output after "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"make 2-deploy-infra"}]},{"type":"text","value":", or find it in the AWS Console under "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"API Gateway → your API → Stages → $default"}]},{"type":"text","value":"."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"It will look like:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"text"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-text"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"https://<api-id>.execute-api.us-east-1.amazonaws.com"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 2 — Hit the endpoint"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Open the URL in a browser — you should see the Next.js app load. You can also test it from the terminal:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"bash"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","function"]},"children":[{"type":"text","value":"curl"}]},{"type":"text","value":" -I https://"},{"type":"element","tagName":"span","properties":{"className":["token","operator"]},"children":[{"type":"text","value":"<"}]},{"type":"text","value":"api-id"},{"type":"element","tagName":"span","properties":{"className":["token","operator"]},"children":[{"type":"text","value":">"}]},{"type":"text","value":".execute-api.us-east-1.amazonaws.com"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"A "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"200 OK"}]},{"type":"text","value":" response confirms the full request path is working — API Gateway received the request, forwarded it through the VPC Link to the internal ALB, and the ALB routed it to the ECS Fargate task."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/943e41ac515b2060a15aad6066887f2f/0400b/app-browser.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 67.62103071317023%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsSAAALEgHS3X78AAAAsklEQVQ4y9WRQQqDMBBFM4lialq1VQw0Liw0uYq5/4W+JuCiiNXixgYeWQzz+MxnBbOYuTGHUrzxuHa4qw5VblBenpBUQ/LmE2qgRI1K9HFvdrDpYY1cSTBan69AWMJA05+mKYj4hmCx+3OCLf5ZqLWGMQZt2yLLsmNCIppkHfr+haIooZSCEOJYwiANTYd0Ukpwzk92w5AwcN6WnXOw1sbb7bzfd6H3HsMwxGKSJNklHAG2aR+l4RGrywAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"React app loading via the API Gateway URL","title":"React app loading via the API Gateway URL","src":"/static/943e41ac515b2060a15aad6066887f2f/913fc/app-browser.png","srcSet":["/static/943e41ac515b2060a15aad6066887f2f/bc34b/app-browser.png 293w","/static/943e41ac515b2060a15aad6066887f2f/da9f0/app-browser.png 585w","/static/943e41ac515b2060a15aad6066887f2f/913fc/app-browser.png 1170w","/static/943e41ac515b2060a15aad6066887f2f/efb0c/app-browser.png 1755w","/static/943e41ac515b2060a15aad6066887f2f/0400b/app-browser.png 1921w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 3 — Verify the private networking"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This is an important one. Navigate to the running ECS task in the Console under "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"ECS → Clusters → dev-app-cluster → Tasks"}]},{"type":"text","value":". Click into the task and confirm:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"ul","properties":{},"children":[{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"No public IP is assigned"}]},{"type":"text","value":" — the task only has a private IP in the VPC"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"li","properties":{},"children":[{"type":"text","value":"The only way to reach the app is through the API Gateway URL"}]},{"type":"text","value":"\n"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This confirms the architecture is working as intended — the container is completely off the public internet."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/756eb708cd650ef9411e229f23b62b22/1c71c/ecs-no-public-ip.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 38.779731127197515%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVQoz2VRW3KDMAzkBm1pwIQANg9DgMBAU5J02knvf6qtZDAh0w+N7JVX3pWcor/h2H2iGa/I6gFBoiFiDTdQcPfP8SoSvAXyH/7ix6bGZydrJ/hRQY1KhOkRoToi1icEsoJHONe8JWTZPfC4WLJG3oxU6yGohzNMP0h0B0WAqnpEWQORlhBSrwQbUd4aksU5871ozyjJJdedZvwyzXaH3AQ/ClX9RPKiOc91veKcd5TdMMM7BdedqrvgkD4aiEXJ1pq/KuXm+YwtlkPC96TSuiCFtw1Zm18kjYCtb1VyA9VesNcjPCazYoqIuCk5tG+drB6NXPsDn9vpF+XwPS/EzpDO1fmOQ3OFL+t1WX5S4TTdEZMrY1k209PwGUyrAWn9AZF1C9HOTa8f2M1vl8b3P/jX2wCqfFsQAAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"ECS task with private IP only and no public IP","title":"ECS task with private IP only and no public IP","src":"/static/756eb708cd650ef9411e229f23b62b22/913fc/ecs-no-public-ip.png","srcSet":["/static/756eb708cd650ef9411e229f23b62b22/bc34b/ecs-no-public-ip.png 293w","/static/756eb708cd650ef9411e229f23b62b22/da9f0/ecs-no-public-ip.png 585w","/static/756eb708cd650ef9411e229f23b62b22/913fc/ecs-no-public-ip.png 1170w","/static/756eb708cd650ef9411e229f23b62b22/efb0c/ecs-no-public-ip.png 1755w","/static/756eb708cd650ef9411e229f23b62b22/1c71c/ecs-no-public-ip.png 1934w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 4 — Check the ALB target health"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Navigate to "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"EC2 → Load Balancers → dev-app-svc-lb → Target Groups → dev-app-svc-lb-tg"}]},{"type":"text","value":". The registered target should show as "},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"healthy"}]},{"type":"text","value":". If it shows unhealthy, the most common cause is the "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"HOSTNAME=0.0.0.0"}]},{"type":"text","value":" environment variable missing from the container — Next.js will only be listening on localhost and the health check on the container’s network interface will fail."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n      "},{"type":"element","tagName":"a","properties":{"className":["gatsby-resp-image-link"],"href":"/static/53702940881daaa410af7f540dfcab32/2889c/alb-healthy-target.png","style":"display: block","target":"_blank","rel":["noopener"]},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 46.16724738675958%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsSAAALEgHS3X78AAABdElEQVQoz01S2ZKCQAzkG0QRnOFmuBGPdS2t2qr9/5/qTQd0fehKZiZ0Jx28MKkxnB+oxyvCtEZgK2xNKSiwkzyIK803UQb/kGtOMPejVJApQndGUE7wtrZEd3rAFD3iaoQtByWfLg8c8g5JNWEvonkz6xtFiDBtYbobwvqKQPIdhUTE2wmhG9hdo4W+PFh3RDk/tVsliJ2Sv2oCOVNkn/WKQHLe8c1j+5/KppYPswa+la6KDmYlMR+EJu+XXKw5ZK2KvQnphesvqKcbGsF4eaKScywitEE7EYRrXOA+8H+3EJoKtv9G3s5I3Cg+kqgT1VY74DI2Ub5C8oMshNas0AUR1umi1MP5/ov59oNq+FLzi/Yk8QQrHXIcs0aOF9ciLA0Q8XDXuqy7wo7iedotI5fdWUkS2TJjWh9R9Cf1NnHTO/JeR+eShJyCsUyVytvLY49j8P/jgV5EUsilBPnyD9JsRtoQSc3yj5ZaS1v2uXv7x/H/ALyO+OyQQKPqAAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n  "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"alt":"ALB target group showing healthy ECS task","title":"ALB target group showing healthy ECS task","src":"/static/53702940881daaa410af7f540dfcab32/913fc/alb-healthy-target.png","srcSet":["/static/53702940881daaa410af7f540dfcab32/bc34b/alb-healthy-target.png 293w","/static/53702940881daaa410af7f540dfcab32/da9f0/alb-healthy-target.png 585w","/static/53702940881daaa410af7f540dfcab32/913fc/alb-healthy-target.png 1170w","/static/53702940881daaa410af7f540dfcab32/2889c/alb-healthy-target.png 1722w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":"\n    "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Cleanup"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Once you are done testing, make sure to tear down the infrastructure to avoid unnecessary AWS charges. Resources like the NAT Gateway, ALB, and Fargate tasks accrue costs while running even if there is no traffic."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 1 — Destroy the infrastructure"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"bash"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","function"]},"children":[{"type":"text","value":"make"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"3"}]},{"type":"text","value":"-destroy-infra"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This runs "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"terraform destroy -auto-approve"}]},{"type":"text","value":" inside "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"infrastructure/environments/dev"}]},{"type":"text","value":". Terraform will remove all provisioned resources in the correct order — ECS service, API Gateway, VPC Link, ALB, NAT Gateway, subnets, and VPC. The ECR repository is also deleted since it was created with "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"force_delete = true"}]},{"type":"text","value":", which means Terraform can remove it even if it still contains images."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Once complete, verify in the AWS Console that the resources are gone — particularly the NAT Gateway and ALB, as these are the most common sources of unexpected charges if a destroy is incomplete."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"Step 2 — Clean up local Docker images"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"bash"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-bash"]},"children":[{"type":"element","tagName":"span","properties":{"className":["token","function"]},"children":[{"type":"text","value":"make"}]},{"type":"text","value":" "},{"type":"element","tagName":"span","properties":{"className":["token","number"]},"children":[{"type":"text","value":"4"}]},{"type":"text","value":"-clean"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This removes the locally built Docker images that were created during the build step. It is a good habit to run this after a project teardown to keep your local Docker environment tidy, especially since the "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"linux/amd64"}]},{"type":"text","value":" image can be a few hundred MB."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Conclusion"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"In this post, we walked through deploying a Next.js React app as a private ECS Fargate workload and exposing it to the internet through AWS API Gateway — without ever putting the application itself on the public internet. The key pieces that make this pattern work are the internal ALB sitting in the private subnet, and the VPC Link that bridges API Gateway into the VPC to reach it."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Everything was provisioned with Terraform, split across three focused modules — networking, compute, and security — making it straightforward to understand, modify, and extend. The Makefile ties the deployment workflow together into a handful of commands."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"This pattern is a solid foundation for any containerized web application that needs to stay private while remaining publicly accessible. It is easy to build on — you can swap the Next.js app for any other containerized service, add a custom domain with Route 53 and ACM, or layer on API Gateway features like throttling and authorization as your requirements grow."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The full code is available on Github "},{"type":"element","tagName":"a","properties":{"href":"https://github.com/amlana21/react-apigw-publish","target":"_blank"},"children":[{"type":"text","value":"Here"}]},{"type":"text","value":"."}]}],"data":{"quirksMode":false}},"excerpt":"Modern cloud architectures often require keeping application workloads private while still making them accessible to end users over the…","timeToRead":17,"frontmatter":{"title":"How to expose a private React App running on ECS Fargate via AWS API Gateway","userDate":"12 May 2026","date":"2026-05-12T00:00:00.000Z","tags":["AWS","React","Terraform"],"image":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAAC+ElEQVQozzWTy1OadxSG2bS1laiAAn5guH3Ix9XwBRBElAqUiyiQMF6DgSQYJeqYxphJG5s0tRPtaCfJNNPLpjNtk24y3XXZmUynmy46/TO66B/x9CczXZw5u+e87znv0fQagnRLPyZ6GK3xIn1Sgj7zuKg4/aYJdMNTGKxpBkeymGx5LLY5bPYKHvcSodE1Yu4Wac8mee8umv+B2iGVwfMpTJ5ZLPFrmEZnMcl5BiyTXaBemhbQGYwCaHVWcboWUDOfkJo/IRnokA5skVO20LynC3DOqCL55hgO1rCnN3CV9rCl1rGG6jgjV9BZptAJoElKY5VreNQm4QvXCUzcJpU9YCp1n1LlGbPRjwRQH2LYnccsgM7iLr6Vx7jn9wldf0qw8RyLv4Ys5xjQRTE6y8jv7+IOrOL31rF4lpG9wnLugPKNl+SmHpxZvoCklLBGV7DPbDDWOSVy9xuC7WOhdhvJexlX7CpKSww4fUPst39Rbn1Ltb7Ph/tP2dk9Ya35OWF1g4i6iabfHGVIKLCMr6JcukvkzguSX7zudjm7iXvuEaHjv1CO/kZ5/CfK1/8QfPQL5UKDdvs+ly7v0F5soiZvEi0conlXH+gCpbGFLjTUOUEVMH/rMxy5PaIPf8dTfYLBUcYcbuBI72Fx1YnEipRiKqXxGPl4htAH95hs/YimR+/H5EgKa1Xsk22U7A2cM20ciSt4117gK9xDksbRmRPojRMY7GX8tUOubR+xvfMpW1sP6dw5Jb/6nLg4juadAT89WpkRscPkolhu5SbpfJNM9Rbpxa9wuWv0mmLobRkG3fOonR+I7rxionyb0voJpfYpBXHEg+Zrjlu/onlbAN/q8+Gu7KNefUa4dkS+8T3Z4pcslb8jHloXYRchH5lGLxeRZz/m4sZPqO2fKR68ofHgD5ZXXrG99JKl6cMzhT7OqkdcW2tJMuQuM6Iscd5RwSAC3WuI0DsYQSu+5pwpIR4gQb8lg+RfRhqtI48uEFAWkZ0FEf40/wGAiYdRBfZDOgAAAABJRU5ErkJggg==","aspectRatio":1.743083003952569,"src":"/static/6112a0d34f45823362f23cb579ccee1e/923f4/main.png","srcSet":"/static/6112a0d34f45823362f23cb579ccee1e/4c9af/main.png 930w,\n/static/6112a0d34f45823362f23cb579ccee1e/923f4/main.png 1323w","sizes":"(max-width: 1323px) 100vw, 1323px"}}},"author":{"id":"Amlan","bio":"Cloud architect and DevOps engineer. Love to code and develop new stuff. A nerd by nature.","avatar":{"children":[{"__typename":"ImageSharp","fixed":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAC4jAAAuIwF4pT92AAAC7ElEQVQ4y5VVSU8UURCumZ7uBhkNMIjMwAyb9DQuuICDAVwuHkyMyIFoCDEQutG4HUiMzIRETIxe3KMXL1w8mxiX6M2TCYle1JMnl8QfgAwmxAS/el0NT1DRTr7UvHpVX1dVV9WQ7XoRwmNlvXW2648CLp8hlf53D2zVnZ31HfgPWK5XKnp29EgIXgB3A0OPz3/iYyKy5B5294DHi4SB0svgUMRFixhFaZUntIFsBtg3s5SC63cgqm9AE58zDX2RccuO5C2LCqapbPIiC9DhjtpT+6Lim4bfDALKaTXxd0NZxKUiTDb1L9YPpBEmEbIVkZuuXwffWUTYrYefA1mxJDvSHOrgnAaS2jkqkZp4SePZ0vK1fI47Qw3wnUVQnRqh184p40IRjlvWQxCMAb3AjUVS02wD4WUQusDzq1HqoZ6pRElQ/136V9sKwu+06XStEN4GUaLA6ZrmV8CRKF+B8KDYXJqM0Rl6uhBDZvMgbNNTHkJ004mNA7/0HpzjIPsEoiZgJ/ARiK3oS9d/iV4cDdNNAu+g7FOKjitRiYbTOgL5GaRJyCHgTUgyYRpMZEjfHkaWb/GlU2GEg1BOj8RrIvJlb8L5PmQceI/fGeA48CEk7K1sVbYt9YeY+DU4jukpbwHhHOWuJe6gDUEyD+caSfsLzimcHeAHUKecthVogShKraeqVQ8vq2GOe4m2T6SmiNaDoJgParaZf0MOShkeAc/wEuO8XercImqkHZPV0od62wSNXeYMNwRf0B6E0ziQlta5DlSiljHIi9Cfu2CVqEY23BO1QtilE3ZxYwP19J8PerBO+Wb9PXpjd8voqQj7KrIxnmOeX56QPCP4zTD4bu+GznCWefS4sfcvq6E/iws1KWuc4cjJsipFEBLll+aZMHbKRlomI9umU1+YFVDMAQfkBcY/rC9DZD8wgyzjpG9myDHgCZBefWP7suX9etg9AI4G6WOT88a2l7ZvFVIvl9rQX/4CQhmHj62/5CfkMNSMmRyxmgAAAABJRU5ErkJggg==","width":400,"height":400,"src":"/static/06e2c36866e770f223b96269c0afc0fd/4e842/ghost.png","srcSet":"/static/06e2c36866e770f223b96269c0afc0fd/4e842/ghost.png 1x"}}]}}}},"relatedPosts":{"totalCount":47,"edges":[{"node":{"id":"8f0f6c6f-b720-519f-a28d-efefa48b3472","timeToRead":3,"excerpt":"This will be a short post about one of my side projects which I recently completed. This post is a description and workings of an utility…","frontmatter":{"title":"Project: Automate migration of Amazon Connect Contact flows from one Instance to another"},"fields":{"slug":"/connectmigrate/"}}},{"node":{"id":"92fb5ecb-3ed5-5777-8697-e8ae886b589e","timeToRead":4,"excerpt":"DockerFiles to quickly get build environments for personal projects This is short and simple list of various Docker Files using which…","frontmatter":{"title":"DockerFiles to quickly get build environments for personal projects"},"fields":{"slug":"/dockerbuilds/"}}},{"node":{"id":"222d3aa3-ed2c-520b-bf84-62d92dc47f7a","timeToRead":11,"excerpt":"Integrate a CRM Application with Alexa using AWS Lambda-Part-2 This post is in continuation to the earlier post where I described the basic…","frontmatter":{"title":"Integrate a CRM Application with Alexa using AWS Lambda-Part-2"},"fields":{"slug":"/alexa-crm-integration-2/"}}}]}},"pageContext":{"isCreatedByStatefulCreatePages":false,"slug":"/reactapigw/","prev":{"excerpt":"As cloud environments grow more complex, real-time observability becomes crucial. Developers and DevOps teams rely heavily on timely metrics…","timeToRead":21,"frontmatter":{"title":"How to Develop and Deploy an AWS metrics Slack bot using Bedrock Agents and Terraform","tags":["AWS","slack","Bedrock"],"date":"2025-09-13T00:00:47.149Z","draft":false,"image":{"childImageSharp":{"fluid":{"aspectRatio":1.1428571428571428,"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAAsTAAALEwEAmpwYAAACT0lEQVQ4y2NgGJlAVd+eQUXfDsxWNnJgUDawh4rbgTEIxO56CKYTDr1gSDzymSl6cwtT1KY6RpBY1KYGZMPsGFT0bBmBmAnIZgRiGA0Us2MCYsb///+D1SYefsOI7pioTTVQQxsZojY3EO+LpGMfGCEufC4CdKFn9OZm26hN9UpQw+CGgrzKDsTxQJwIdJU1ELsAsTkQOwPF8oDYGaTYrnslE4hO3fPIMunQx5Uxm9vuxG2qqQUHx6Z6Zp9NbRCbdYxsxFX1bROBXpsONKQUiKcCDekG8jOA4uVahjZh7/7/B7sg/MLP+uhTn19FnfszNWhz2xbvTW3LfDe3TQjY1MoKkvcFGSqi5mStrGfXBNRYBcQR6ga2xUDcoGds7Qg00AYo387A0MUG0rAwa1L5lLol66eWLq3YvCEue8/mqO2rNyVOZdgECWMgm4Hh21QmETNzS3UGhv/Q0PoP9NpzsPckNBw1zjbyhf2fzyAO4t9Qdi+8I+c0+bKCz6kXiz2nvt7qfvX9RrcpzzZ7gdW/3+QG1D6LQe7/Iob4//MYQv7PZjD6v4BBCYhVgGyL/0sZgv/PZWj8PYPBDBY5J6zjnE9rR9x7sNJn7YOdPl4gsSdAA59t8kLE4OMedp4/MxiC/s1gaATi/r8zGDr/zWTI/z6VyeBuFyc70FBmmNrzZqF8p5XCNR5v8tYHGiQH1r8ZYthjkKFAjYzEJBtQUjylEY0h/mS7F6Z+oKEgzATFjFAMY4PlYeCcaSjDGbUwpscbvBmfbIEY9mSzF8MooB4AACPb6bUgnnalAAAAAElFTkSuQmCC","sizes":"(max-width: 800px) 100vw, 800px","src":"/static/0dd46ac9eb9e32555e827f20e5f0d6da/af144/main.png","srcSet":"/static/0dd46ac9eb9e32555e827f20e5f0d6da/af144/main.png 800w"}}},"author":{"id":"Amlan","bio":"Cloud architect and DevOps engineer. Love to code and develop new stuff. A nerd by nature.","avatar":{"children":[{"fixed":{"src":"/static/06e2c36866e770f223b96269c0afc0fd/4e842/ghost.png"}}]}}},"fields":{"layout":"post","slug":"/slackagent/"}},"next":null,"primaryTag":"AWS"}}}