Case Study
ECS container platform cover image

Container Platform on AWS — Use Case

A production-style container platform on the AWS Free Tier: ECS on EC2 behind an ALB, images in ECR, deploys from GitHub Actions via OIDC, defined in Terraform, and secured with HTTPS (ACM).

ECS (EC2) Application Load Balancer ECR Terraform GitHub Actions OIDC CloudWatch ACM + Route 53 Free Tier

Why I built This

I wanted a real-world container platform that felt like production (CI/CD, load balancing, observability, least-privileged IAM) but without burning morning while I iterate.

Solution

A Node/Express demo app that runs on ECS (EC2 launch type) behind an ALB. Pushing to main triggers a GitHub Actions workflow that assumes an AWS role via OIDC, builds and pushes an image to ECR, registers a new task definition, and updates the ECS service. HTTPS is handled by ACM + ALB. Everything is defined in Terraform.

Public URL: app.gabrielejiro.tech (ALB → ECS task on port 3000)

Architecture

flowchart TD Dev["Developer pushes to main"] --> CI["GitHub Actions"] CI --> IAM["IAM role demo-gh-deploy via OIDC"] CI --> ECR["ECR repository demo-web"] CI --> TD["Register task definition"] TD --> ECSService["ECS service demo-web-svc"] subgraph AWS ALB["Application Load Balancer ports 80 and 443"] ECSCluster["ECS cluster EC2"] ASG["Auto Scaling Group 1 x t3 micro"] EC2["EC2 host with ECS agent"] Task["Container Node Express on port 3000"] CW["CloudWatch logs and metrics"] end ECR --> ECSService ECSService --> ECSCluster ECSCluster --> ASG ASG --> EC2 EC2 --> Task Task --> ALB Task --> CW User["User"] --> ALB

CI/CD Without Secrets

GitHub Actions uses OIDC to assume a deploy role in AWS with short-lived credentials (no static access keys). The pipeline builds, pushes to ECR, registers a task definition with the new image SHA, and updates the ECS service. A force-new-deployment can roll tasks instantly.

What actually went wrong (and how I fixed it)

  • Terraform single-line blocks: converted to multiline blocks to satisfy HCL parser.
  • OIDC provider already exists: switched to a data source referencing the existing provider instead of trying to recreate it.
  • Git push rejected (685 MB binary): added proper .gitignore and used git filter-repo to purge .terraform/ from history.
  • ALB 503 with healthy targets: pinned containerPort:3000 to hostPort:3000 to avoid random host ports blocked by security groups.
  • HTTPS not reachable: attached the ACM cert to the 443 listener and opened port 443 on the ALB security group, with :80 → 301 to HTTPS.

Monitoring

CloudWatch Dashboard shows ECS CPU utilization, ALB request count, and target response time. Logs stream to /ecs/demo-web and I use Logs Insights to spot-check the last lines. Quick test load:

for i in {1..50}; do curl -s https://app.gabrielejiro.tech/health > /dev/null; done

Then refresh the dashboard to see metrics move.

What I shipped

  • ECS cluster (EC2) with one t3.micro host
  • ALB with HTTP→HTTPS redirect and ACM certificate
  • ECR repository and CI/CD pipeline from GitHub Actions (OIDC)
  • Terraform for VPC, ALB, ECS, ECR, IAM, CloudWatch
  • CloudWatch dashboard + log insights

Lessons learned

  • OIDC is the clean way to deploy from GitHub without long-lived AWS keys.
  • ALB + ECS port mappings require matching security groups to avoid 503s.
  • Keep providers/state out of Git, or GitHub will block large pushes.
  • HTTPS on ALB demands the cert in the same region and SNI on the custom domain.

Outcome

A production-style container platform that’s automated, secure, and affordable to run on the Free Tier.