<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chem Patrick Edward</title>
    <description>The latest articles on DEV Community by Chem Patrick Edward (@eddiechem).</description>
    <link>https://dev.to/eddiechem</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3625717%2Feccef460-3ceb-4eae-8e1a-ba455c2ffd09.png</url>
      <title>DEV Community: Chem Patrick Edward</title>
      <link>https://dev.to/eddiechem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eddiechem"/>
    <language>en</language>
    <item>
      <title>DevSecOps on AWS: Deploying a Spring PetClinic App to ECS with Zero Internet Egress</title>
      <dc:creator>Chem Patrick Edward</dc:creator>
      <pubDate>Thu, 05 Nov 2026 08:48:17 +0000</pubDate>
      <link>https://dev.to/eddiechem/devsecops-on-aws-deploying-a-spring-petclinic-app-to-ecs-with-zero-internet-egress-42e0</link>
      <guid>https://dev.to/eddiechem/devsecops-on-aws-deploying-a-spring-petclinic-app-to-ecs-with-zero-internet-egress-42e0</guid>
      <description>&lt;p&gt;&lt;em&gt;Deploying Spring PetClinic to ECS with Terraform, GitHub Actions, Trivy, and SonarCloud, with no internet access from the private subnets.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/EddieByte/devsecops-ecs-pipeline" rel="noopener noreferrer"&gt;https://github.com/EddieByte/devsecops-ecs-pipeline&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time to recreate:&lt;/strong&gt; about 45 minutes · &lt;strong&gt;Cost while running:&lt;/strong&gt; about $60–80/month · &lt;strong&gt;Level:&lt;/strong&gt; intermediate AWS/Terraform&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is, and why I built it
&lt;/h2&gt;

&lt;p&gt;I wanted to see how close I could get to a production-grade AWS deployment without cutting the security corners most tutorials take: no static credentials, no public compute, no manual DB migrations. This is what it took.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem.&lt;/strong&gt; Most "deploy to AWS" tutorials get an app running and stop there. To stay short, they quietly skip the parts that make a deployment safe to run for real: where the credentials live, what the servers can reach, and what gets checked before code ships. Teams that do care about those parts rarely have one end-to-end example they can read, run, and tear down in an afternoon. This project is my attempt to fill that gap.&lt;/p&gt;

&lt;p&gt;Here is what typically goes wrong, and what this deployment does about it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Common problem&lt;/th&gt;
&lt;th&gt;What this deployment does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Long-lived AWS keys stored in CI&lt;/td&gt;
&lt;td&gt;GitHub OIDC gives short-lived credentials, with trust scoped to your org and repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute with a route to the internet (data exfiltration, unvetted downloads)&lt;/td&gt;
&lt;td&gt;Private subnets with no internet route; AWS services reached only through VPC endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerable code or images reaching production&lt;/td&gt;
&lt;td&gt;SonarCloud analysis and a Trivy image scan run before anything deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets in code or config files&lt;/td&gt;
&lt;td&gt;Database credentials live in Secrets Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual database setup that drifts between environments&lt;/td&gt;
&lt;td&gt;Flyway runs versioned migrations at startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed deployments that hang or retry forever&lt;/td&gt;
&lt;td&gt;ECS deployment circuit breaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-built infrastructure and forgotten bills&lt;/td&gt;
&lt;td&gt;Everything is in Terraform, with a teardown script&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you've built ECS deployments before and want a hardened baseline, or you're learning DevSecOps and want something concrete to run, this is for you.&lt;/p&gt;

&lt;p&gt;The goal was a pipeline where code goes from a git tag to a running container &lt;strong&gt;without any workload ever touching the public internet&lt;/strong&gt;, and where security checks are part of the pipeline, not an afterthought.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on "air-gapped":&lt;/strong&gt; here it means the workloads have no route to the internet. They still reach AWS services privately through PrivateLink, so it isn't fully disconnected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By the end of this deployment, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub Actions pipeline: &lt;strong&gt;test → scan → build → deploy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Code quality checks (SonarCloud) and image vulnerability scanning (Trivy)&lt;/li&gt;
&lt;li&gt;ECS on EC2 in private subnets, behind an ALB, with RDS for the database&lt;/li&gt;
&lt;li&gt;Deployments authenticated with &lt;strong&gt;OIDC&lt;/strong&gt;, so there are no AWS access keys in GitHub&lt;/li&gt;
&lt;li&gt;Everything created and destroyed with Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph LR
  A["git tag dev/v*"] --&amp;gt; B["TEST: mvn + SonarCloud"]
  B --&amp;gt; C["BUILD &amp;amp; PUBLISH: Docker + Trivy + ECR"]
  C --&amp;gt; D["DEPLOY: render task def + ECS rolling update"]
  D --&amp;gt; E["ALB"] --&amp;gt; F["ECS on EC2 (private subnets)"] --&amp;gt; G[("RDS")]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gs08zg6oetpk26m8czx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gs08zg6oetpk26m8czx.webp" alt="Architecture diagram: a GitHub Actions pipeline tests, scans, and deploys PetClinic to ECS in private AWS subnets with no internet route" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The full picture: pipeline on top, private AWS environment below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three decisions shape everything else:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No NAT gateway, so VPC endpoints.&lt;/strong&gt; The instances have no route to the internet. They reach ECR, S3, Secrets Manager, CloudWatch, and SSM through PrivateLink endpoints (9 interface endpoints in total). The workloads can only talk to AWS services you've explicitly allowed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9odevednvftzkrxd0t06.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9odevednvftzkrxd0t06.webp" alt="VPC console endpoints list showing the endpoints used instead of a NAT gateway" width="799" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The endpoints that replace the NAT gateway.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. OIDC instead of access keys.&lt;/strong&gt; GitHub Actions gets short-lived credentials by assuming an IAM role. The trust policy is scoped to your specific org and repo, so a leaked workflow file is useless anywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Security checks inside the pipeline.&lt;/strong&gt; SonarCloud runs with the tests, and Trivy scans the image before it's pushed to ECR. If either fails, nothing deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://developer.hashicorp.com/terraform/install" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;= 1.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;= 2.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.docker.com/desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;= 24.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://adoptium.net" rel="noopener noreferrer"&gt;JDK 17 + Maven 3.9&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;17 / 3.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git, PowerShell 7+&lt;/td&gt;
&lt;td&gt;any / &amp;gt;= 7.x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You'll also need an AWS account with admin access (or a scoped policy for EC2, ECS, ECR, RDS, VPC, IAM, Secrets Manager, S3, KMS, CloudWatch, SSM, Auto Scaling) and a free &lt;a href="https://sonarcloud.io" rel="noopener noreferrer"&gt;SonarCloud&lt;/a&gt; account with a project, token, and organisation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it costs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2 (x2)&lt;/td&gt;
&lt;td&gt;t3.medium&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RDS&lt;/td&gt;
&lt;td&gt;db.t3.micro, 20 GiB&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC Endpoints (x9)&lt;/td&gt;
&lt;td&gt;Interface, 2 AZs&lt;/td&gt;
&lt;td&gt;$14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECR + CloudWatch&lt;/td&gt;
&lt;td&gt;small&lt;/td&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$60–80&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Run the teardown at the end of this post. Leaving it up for a month is the expensive mistake.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Build it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Clone and authenticate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/EddieByte/devsecops-ecs-pipeline.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;devsecops-ecs-pipeline&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;configure&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="c"&gt;# or: aws sso login --profile &amp;lt;profile&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get-caller-identity&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c"&gt;# confirm you're in the right account&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Bootstrap the Terraform state bucket
&lt;/h3&gt;

&lt;p&gt;Terraform can't create the bucket that stores its own state, so a small script does it once per account. The bucket is versioned, encrypted, and blocks public access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\scripts\bootstrap_s3.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bucket name comes from your account ID (&lt;code&gt;petclinic-tfstate-&amp;lt;ACCOUNT_ID&amp;gt;&lt;/code&gt;), and the script checks that &lt;code&gt;backend.tf&lt;/code&gt; matches it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure your variables
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;terraform/terraform.tfvars&lt;/code&gt; (it's git-ignored, so don't commit it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;github_org&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your-github-user-or-org&amp;gt;"&lt;/span&gt;
&lt;span class="nx"&gt;github_repo&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"devsecops-ecs-pipeline"&lt;/span&gt;
&lt;span class="nx"&gt;github_org_id&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;numeric owner ID&amp;gt;"&lt;/span&gt;
&lt;span class="nx"&gt;github_repo_id&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;numeric repo ID&amp;gt;"&lt;/span&gt;
&lt;span class="nx"&gt;db_password&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;strong password, min 8 chars&amp;gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Optional&lt;/span&gt;
&lt;span class="c1"&gt;# aws_region        = "us-east-1"&lt;/span&gt;
&lt;span class="c1"&gt;# ec2_instance_type = "t3.medium"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why numeric IDs?&lt;/strong&gt; The OIDC trust policy matches on your org and repo &lt;em&gt;IDs&lt;/em&gt;, not just their names. If they're wrong, the pipeline fails later with an &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; error (see "What broke" below). Here is how to get them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: GitHub CLI&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repos/&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--jq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'.id'&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c"&gt;# github_repo_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repos/&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--jq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'.owner.id'&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# github_org_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: No GitHub CLI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;https://api.github.com/repos/&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&lt;/code&gt; in your browser. The repo's &lt;code&gt;"id"&lt;/code&gt; is &lt;code&gt;github_repo_id&lt;/code&gt;, and &lt;code&gt;"owner": { "id": ... }&lt;/code&gt; is &lt;code&gt;github_org_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Provision the infrastructure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c"&gt;# expect about 57 resources&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes 8–12 minutes, and RDS accounts for about 7 of them. Keep the outputs handy: &lt;code&gt;alb_dns_name&lt;/code&gt;, &lt;code&gt;ecr_repository_url&lt;/code&gt;, and &lt;code&gt;github_actions_role_arn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7mr57tbtdf5yy1mibgmd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7mr57tbtdf5yy1mibgmd.webp" alt="Terminal output of terraform apply showing resources added and the outputs" width="800" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A clean apply, with the outputs you'll need next.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Create the database secret (manual, don't skip)
&lt;/h3&gt;

&lt;p&gt;The app reads its DB credentials from Secrets Manager. Use the same password you put in &lt;code&gt;terraform.tfvars&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secretsmanager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create-secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev/app/rds"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--secret-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{""RDS_USER"":""admin"",""RDS_PASS"":""&amp;lt;your-password&amp;gt;""}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Add the GitHub secrets
&lt;/h3&gt;

&lt;p&gt;Go to your repo → &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt; and add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_ROLE_TO_ASSUME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;output &lt;code&gt;github_actions_role_arn&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_REGION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;us-east-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REGISTRY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;ACCOUNT_ID&amp;gt;.dkr.ecr.us-east-1.amazonaws.com&lt;/code&gt; (the ECR URL without &lt;code&gt;/petclinic&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SONAR_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SonarCloud → My Account → Security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SONAR_PROJECT_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SonarCloud → Project Information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SONAR_ORGANIZATION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SonarCloud → Organisation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 7: Trigger the pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev/v1.0.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev/v1.0.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to the &lt;strong&gt;Actions&lt;/strong&gt; tab. Three jobs run in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TEST:&lt;/strong&gt; &lt;code&gt;mvn clean test&lt;/code&gt; plus SonarCloud analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BUILD_AND_PUBLISH:&lt;/strong&gt; Docker build, Trivy scan, push to ECR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEPLOY:&lt;/strong&gt; three steps. It downloads the live task definition, uses &lt;code&gt;amazon-ecs-render-task-definition&lt;/code&gt; to swap in the new image tag, then deploys the result as a rolling update. Rendering into the live definition means you only change the image and keep everything else Terraform set up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjd2livx7ardra7lzyivx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjd2livx7ardra7lzyivx.webp" alt="GitHub Actions run with the test, build and publish, and deploy jobs all passing" width="800" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TEST, BUILD_AND_PUBLISH, and DEPLOY, all passing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6eqa3ea7yzbk91uxg2en.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6eqa3ea7yzbk91uxg2en.webp" alt="Trivy scan output in the build and publish job log" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Trivy scans the image before it reaches ECR.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fot8gfxy0ktsh86k0gfig.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fot8gfxy0ktsh86k0gfig.webp" alt="SonarCloud dashboard showing the quality gate result" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The SonarCloud quality gate for the same commit.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Prove it works
&lt;/h2&gt;

&lt;p&gt;Don't just trust the green checkmarks. Check each layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 1: Targets are healthy behind the ALB
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;describe-target-health&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--target-group-arn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;elbv2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;describe-target-groups&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--names&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;petclinic-tg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TargetGroups[0].TargetGroupArn"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TargetHealthDescriptions[].{Target:Target.Id,State:TargetHealth.State}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlzzk9m3lcoadqpyj66l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlzzk9m3lcoadqpyj66l.webp" alt="ALB target group showing both targets healthy" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Both ECS instances are healthy behind the ALB.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 2: The app is up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$alb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-raw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;alb_dns_name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://&lt;/span&gt;&lt;span class="nv"&gt;$alb&lt;/span&gt;&lt;span class="s2"&gt;/actuator/health"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExpandProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Content&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# {"status":"UP"}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://&amp;lt;alb_dns_name&amp;gt;/&lt;/code&gt; and you should see the PetClinic UI with owners, pets, and vets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyn8bo4i1a5jngkyp6he.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyn8bo4i1a5jngkyp6he.webp" alt="Spring PetClinic page loaded from the ALB URL (1 of 3)" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xfm7p0gnws5gw2unjel.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xfm7p0gnws5gw2unjel.webp" alt="Spring PetClinic page loaded from the ALB URL (2 of 3)" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fji3ap0jh5zqhl3nple25.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fji3ap0jh5zqhl3nple25.webp" alt="Spring PetClinic page loaded from the ALB URL (3 of 3)" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The app running behind the ALB.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 3: Flyway ran the migrations
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;describe-log-streams&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--log-group-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/ecs/petclinic-cluster"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--order-by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;LastEventTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--descending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logStreams[0].logStreamName"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get-log-events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--log-group-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/ecs/petclinic-cluster"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--log-stream-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"events[*].message"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flyway|migration"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-CaseSensitive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;Successfully applied 2 migrations to schema 'petclinic'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhuhmh1zal2amrxa8z4w7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhuhmh1zal2amrxa8z4w7.webp" alt="CloudWatch log lines showing Flyway applying two migrations" width="791" height="51"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Flyway applying both migrations on startup.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 4: The air-gap is real
&lt;/h3&gt;

&lt;p&gt;This is the check that matters most. Connect through SSM (no SSH keys or open ports):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$instance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;describe-instances&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--filters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name=tag:aws:autoscaling:groupName,Values=petclinic-ecs-asg"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name=instance-state-name,Values=running"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reservations[0].Instances[0].InstanceId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;start-session&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$instance&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run these two commands inside the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 https://google.com
&lt;span class="c"&gt;# Expected: Connection timed out. No internet route.&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://&amp;lt;ACCOUNT_ID&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/v2/ &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt;
&lt;span class="c"&gt;# Expected: 401. ECR is reachable over PrivateLink, just not authenticated.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6mm6h2ihq929a5lmq0d.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6mm6h2ihq929a5lmq0d.webp" alt="Test 1: curl to google.com times out after 5000 milliseconds, proving there is no internet route" width="800" height="27"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnts0ctxkwpbt4dswll8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnts0ctxkwpbt4dswll8.webp" alt="Test 2: the ECR endpoint returns 401, so it is reachable over PrivateLink" width="800" height="34"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Test 1 times out, Test 2 returns 401: no route to the internet, but ECR is reachable over PrivateLink.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A timeout on Google and a &lt;code&gt;401&lt;/code&gt; from ECR show that the instance can't reach the internet but can reach the AWS services it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What broke (and how I fixed it)
&lt;/h2&gt;

&lt;p&gt;This is the part I'd have wanted before starting. Each of these cost me time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Tasks stuck with &lt;code&gt;RESOURCE:MEMORY&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; I started with &lt;code&gt;t3.small&lt;/code&gt;. The task needs 2048 MiB, and a t3.small can't fit it alongside the ECS agent and OS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Use &lt;code&gt;t3.medium&lt;/code&gt; or larger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;i/o timeout&lt;/code&gt; pulling images from ECR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; ECR stores image layers in S3, so pulling an image also needs S3 access. The ECS security group was missing an egress rule for the S3 prefix list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Allow egress to the S3 prefix list from the ECS security group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Note:&lt;/strong&gt; This one is easy to miss in an air-gapped setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;Not authorized to perform sts:AssumeRoleWithWebIdentity&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; The OIDC trust policy didn't match the token GitHub issued, because the org/repo IDs were wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Set the correct &lt;code&gt;github_org_id&lt;/code&gt; and &lt;code&gt;github_repo_id&lt;/code&gt; in &lt;code&gt;terraform.tfvars&lt;/code&gt; and re-apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;Unknown database 'petclinic'&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; RDS was created with &lt;code&gt;db_name = "accounts"&lt;/code&gt;, so the app's database didn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Add &lt;code&gt;createDatabaseIfNotExist=true&lt;/code&gt; to the JDBC URL so Flyway can run against a fresh database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Pipeline hanging for 5+ minutes on deploy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; A bad task kept retrying forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Enable the deployment circuit breaker (&lt;code&gt;deployment_circuit_breaker { enable = true, rollback = false }&lt;/code&gt;) so ECS gives up on a failing deployment instead of hanging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why &lt;code&gt;rollback = false&lt;/code&gt;:&lt;/strong&gt; On a first deploy there's no previous task definition to revert to, and the old tasks (if any) keep running. Once you have a stable version, &lt;code&gt;rollback = true&lt;/code&gt; lets ECS revert automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. &lt;code&gt;ResourceNotFoundException&lt;/code&gt; on secrets&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; I forgot to create &lt;code&gt;dev/app/rds&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Create it in Step 5. It's manual on purpose, so the password never ends up in Terraform state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More detail is in &lt;a href="https://github.com/EddieByte/devsecops-ecs-pipeline/blob/main/docs/troubleshooting.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/troubleshooting.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tear it down
&lt;/h2&gt;

&lt;p&gt;Run the drain script &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;terraform destroy&lt;/code&gt;. ECS managed termination protection blocks instances from terminating while tasks are running, and skipping the script costs you a 20-minute timeout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\scripts\teardown.ps1&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c"&gt;# scales service and ASG to 0, removes scale-in protection&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;destroy&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="c"&gt;# RDS deletion is the slow part&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secretsmanager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete-secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--secret-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev/app/rds"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--force-delete-without-recovery&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3LeiOHq0bUou38UYOCNEqARdDP0%2FScre.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3LeiOHq0bUou38UYOCNEqARdDP0%2FScre.webp" title="Terraform Destroy" alt="Terraform Destroy deprovisions all resources successfully" width="800" height="53"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional: Delete the state bucket
&lt;/h3&gt;

&lt;p&gt;Only do this if you're done with the project entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"petclinic-tfstate-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get-caller-identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Delete all object versions, then delete markers (versioning is on)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s3api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list-object-versions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Versions[*].{Key:Key,VersionId:VersionId}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ConvertFrom-Json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s3api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete-object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;VersionId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s3api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list-object-versions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DeleteMarkers[*].{Key:Key,VersionId:VersionId}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ConvertFrom-Json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s3api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete-object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;VersionId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s3api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete-bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;us-east-1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I took away from this deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Air-gapped isn't free.&lt;/strong&gt; Every AWS service the workload touches needs an endpoint &lt;em&gt;and&lt;/em&gt; the right security group rules. Plan the list first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OIDC is worth the setup.&lt;/strong&gt; No keys to rotate or leak, but the trust policy has to be exact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify, don't assume.&lt;/strong&gt; The timeout-on-Google test is a two-minute check that proves the whole design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teardown is part of the design.&lt;/strong&gt; Write the destroy path while you build, not after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next on the list: HTTPS on the ALB with ACM and Route 53, and splitting dev and prod into separate AWS accounts. The pipeline already supports &lt;code&gt;prod/v*&lt;/code&gt; tags. It just needs somewhere to deploy to.&lt;/p&gt;

&lt;p&gt;If you recreate this and get stuck, open an issue on the repo. If you find a better way to do any of it, I'd like to hear that too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/EddieByte/devsecops-ecs-pipeline" rel="noopener noreferrer"&gt;https://github.com/EddieByte/devsecops-ecs-pipeline&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>Upward and Onward…</title>
      <dc:creator>Chem Patrick Edward</dc:creator>
      <pubDate>Tue, 09 Dec 2025 14:46:36 +0000</pubDate>
      <link>https://dev.to/eddiechem/testing-123-3ooo</link>
      <guid>https://dev.to/eddiechem/testing-123-3ooo</guid>
      <description>&lt;p&gt;Hi Dev Community 👋&lt;/p&gt;

&lt;p&gt;It’s a real pleasure to be here and an even greater privilege to learn from and share with such a brilliant community of minds. This is officially my first post, and I thought I’d start with something simple—yet meaningful.&lt;/p&gt;

&lt;p&gt;A reminder.&lt;/p&gt;

&lt;p&gt;If you’re reading this, I want you to know something:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You’ve got this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, times are changing. Yes, things are getting harder. But don’t let the difficulty of the present moment convince you that it defines who you are or where you’re headed. It doesn’t.&lt;/p&gt;

&lt;p&gt;You have more to offer than you may realize, and no temporary setback has the authority to write the story of your future.&lt;/p&gt;

&lt;p&gt;The right moment is always &lt;strong&gt;&lt;em&gt;now.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take ownership of today. Dust yourself off from whatever you’re carrying, and keep moving forward—valiantly. That decision to continue, even when things feel heavy, often becomes one of the most meaningful choices we ever make.&lt;/p&gt;

&lt;p&gt;Ignore the naysayers. They don’t get a vote.&lt;br&gt;
Hold your &lt;strong&gt;&lt;em&gt;why&lt;/em&gt;&lt;/strong&gt; close and honor it, because when you finally look back, let it be without regret.&lt;/p&gt;

&lt;p&gt;Don’t allow the weight of the world to pull you into despair. Guard your mind. Protect it. Grow it. Train yourself to notice the good, even while pushing forward with the focus and resolve of someone who refuses to quit.&lt;/p&gt;

&lt;p&gt;You can control the narrative.&lt;br&gt;
You have what it takes.&lt;br&gt;
And you deserve the rewards that come from persistence and growth.&lt;/p&gt;

&lt;p&gt;So please—don’t give up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Dust yourself off and keep going.&lt;br&gt;
Your future self is counting on you&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;— CPE&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>learning</category>
      <category>community</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
