I needed a repeatable way to deploy web apps on AWS. I developed a baseline structure that works for containerized web apps with a database. Then I modify and tweak it based on specific project requirements.
This setup handles VPC networking, ECS Fargate containers, RDS database, and load balancing. It includes ECS auto-scaling configuration and has separate test and prod environments. For my example project, it hosts API, Migrator, and Worker services.
My Template Structure
infrastructure/
├── cloudformation/
│ ├── parameters/
│ │ ├── test.json
│ │ └── prod.json
│ ├── 01-base-vpc-networking.json
│ ├── 02-base-security-groups.json
│ ├── 03-data-rds.json
│ ├── 04-compute-ecs-cluster.json
│ ├── 05-compute-alb.json
│ └── 06-compute-fargate-scaling.json
├── ecs/
│ ├── api-task-definition.json
│ ├── worker-task-definition.json
│ └── migrator-task-definition.json
├── scripts/
│ ├── deploy-environment.sh
│ ├── deploy-scaling.sh
│ ├── setup-secrets.sh
│ └── resolve-secrets.sh
└── README.md
Some notes on the structure:
- Traffic flow: Internet → ALB (public subnets) → ECS tasks (private subnets) → RDS (private subnets). ECS pulls images from ECR via VPC endpoint. NAT Gateway handles outbound traffic from private subnets.
- Numbering enforces deployment order. CloudFormation stacks have dependencies. You can’t create an ECS service before the VPC exists. The numbered prefix makes deployment sequence explicit.
- parameters/ contains environment-specific values, like CIDR blocks, instance sizes, database credentials, anything that differs between test and prod.
- ecs/ contains task definitions for each service (for my project example: API, Worker, Migrator). These define container specs, CPU, memory, env variables, and secrets.
- scripts/ automates deployment.
deploy-environment.shdeploys stacks 01-05 in sequence.setup-secrets.shcreates secrets in AWS Secrets Manager during initial setup.resolve-secrets.shreplaces secret placeholders with actual ARNs before every deployment.deploy-scaling.shdeploys auto-scaling after services run. - cloudformation/ contains the stack templates:
- 01-base-vpc-networking.json - VPC, public/private subnets, Internet Gateway, NAT Gateway, VPC endpoints. Exports: VPC ID, subnet IDs
- 02-base-security-groups.json - Security groups (ALB, ECS, RDS) and IAM roles. Exports: Security group IDs, IAM role ARNs
- 03-data-rds.json - RDS instance, DB subnet group, Secrets Manager credentials. Exports: Database endpoint
- 04-compute-ecs-cluster.json - ECS cluster, ECR repositories, IAM roles, CloudWatch log groups. Exports: Cluster name, ECR URIs
- 05-compute-alb.json - Application Load Balancer, target groups, SSL certificate, listener rules. Exports: ALB DNS name
- 06-compute-fargate-scaling.json - Auto-scaling policies for ECS services. Deploy after services are already running
Secrets Management
setup-secrets.sh creates secrets in AWS Secrets Manager during initial setup. Run once per environment.
resolve-secrets.sh replaces secret placeholders with actual ARNs before every deployment. AWS adds random suffixes to secret ARNs:
Task definition template:
{
"secrets": [
{
"name": "ConnectionString",
"valueFrom": "myproject/${ENVIRONMENT}/connection-string"
}
]
}
ECS needs the full ARN including random suffix:
{
"secrets": [
{
"name": "ConnectionString",
"valueFrom": "arn:aws:secretsmanager:eu-central-1:123456789:secret:myproject/test/connection-string-AbCdEf"
}
]
}
resolve-secrets.sh queries AWS for actual ARNs and updates task definitions. Runs automatically in CI/CD.
Deploying the Infrastructure
cd infrastructure/scripts
./deploy-environment.sh test myproject-infra
The script:
- Validates all templates
- Deploys stacks 01-05 in order, and waits for each stack to complete
- Uses parameter file from
parameters/test.jsonorparameters/prod.json - Outputs DNS configuration info
After infrastructure deployment:
- Run
setup-secrets.shto create placeholder secrets - Update secrets with actual values via AWS CLI or Console
- Deploy ECS services via GitHub Actions
- Run
deploy-scaling.shto configure auto-scaling
Deploying the Application
Application deployment is separate from infrastructure. Infrastructure changes infrequently. Code changes constantly.
GitHub Actions workflow:
- Build Docker images
- Push to ECR
- Run resolve-secrets.sh on task definitions
- Update ECS task definitions with new image tags
- Deploy to ECS
- Wait for deployment completion
Cleanup
Delete stacks in reverse order (06 → 01):
aws cloudformation delete-stack --stack-name myproject-test-compute-fargate-scaling
aws cloudformation delete-stack --stack-name myproject-test-compute-alb
aws cloudformation delete-stack --stack-name myproject-test-compute-ecs-cluster
aws cloudformation delete-stack --stack-name myproject-test-data-rds
aws cloudformation delete-stack --stack-name myproject-test-base-security-groups
aws cloudformation delete-stack --stack-name myproject-test-base-vpc-networking
Manual cleanup needed: S3 buckets (CloudFormation won’t delete non-empty buckets), ECR images, CloudWatch log groups with never-expire retention, Secrets Manager secrets (deletion protection enabled).