A Django application deployed on AWS with high availability, behind an Application Load Balancer, running on an Auto Scaling Group of EC2 instances in private subnets, across two Availability Zones — provisioned entirely with Terraform.
graph TD
User([User / Browser]) -->|HTTPS:443 / HTTP:80| ALB[Application Load Balancer <br> Public Subnets]
subgraph VPC [AWS VPC Boundary]
subgraph PublicSubnets [Public Subnets]
ALB
NAT[NAT Gateways]
end
subgraph PrivateSubnets [Private Subnets]
subgraph ASG [Auto Scaling Group]
EC2_A[EC2 Instance - AZ A]
EC2_B[EC2 Instance - AZ B]
end
RDS[(PostgreSQL Database)]
end
CW[CloudWatch Logs / Agent]
S3[Amazon S3 Bucket <br> Encrypted Media Storage]
end
ALB -->|Forward:8000| EC2_A
ALB -->|Forward:8000| EC2_B
EC2_A -->|Database connection| RDS
EC2_B -->|Database connection| RDS
EC2_A -.->|Read/Write Media| S3
EC2_B -.->|Read/Write Media| S3
EC2_A -.->|Ship logs| CW
EC2_B -.->|Ship logs| CW
EC2_A -->|Outbound traffic| NAT
EC2_B -->|Outbound traffic| NAT
NAT -->|Outbound: Pull Docker Image| DockerHub[DockerHub Registry]
NAT -->|Outbound: Package Updates| Internet[Public Internet]
classDef aws fill:#FF9900,stroke:#333,stroke-width:1px,color:#fff;
classDef database fill:#336699,stroke:#333,stroke-width:1px,color:#fff;
classDef external fill:#777777,stroke:#333,stroke-width:1px,color:#fff;
classDef vpc fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
classDef subnet fill:#efebe9,stroke:#5d4037,stroke-width:1px,stroke-dasharray: 5 5;
class ALB,NAT,EC2_A,EC2_B,S3,CW aws;
class RDS database;
class DockerHub,Internet,User external;
class VPC vpc;
class PublicSubnets,PrivateSubnets,ASG subnet;
├── .github/workflows/terraform.yml # CI: terraform validate, django tests, docker build/push
├── images/IACdeploymentDjango.png # Architecture diagram
├── infra/ # All Terraform (Infrastructure as Code)
│ ├── modules/ # Reusable, standalone modules
│ │ ├── vpc/ # VPC, public/private subnets, IGW, NAT Gateways, routes
│ │ ├── security-groups/ # ALB SG (80/443) and EC2 SG (app port from ALB only)
│ │ ├── alb/ # Application Load Balancer + target group + listeners
│ │ ├── asg/ # Launch template + Auto Scaling Group (min1/desired2/max3)
│ │ └── iam/ # EC2 instance role (SSM + CloudWatch)
│ ├── scripts/
│ │ └── user_data.sh.tpl # Installs Docker, pulls image from DockerHub, runs it
│ └── environments/
│ └── production/ # Wires all modules together into a deployable stack
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── provider.tf
│ ├── backend.tf
│ └── terraform.tfvars.example
├── django_app/ # The Django application itself
│ ├── config/
│ │ ├── settings/
│ │ │ ├── base.py # Shared settings
│ │ │ ├── local.py # Dev settings (sqlite, DEBUG=True)
│ │ │ └── production.py # Prod settings (DATABASE_URL, HSTS, secure cookies)
│ │ ├── urls.py
│ │ ├── wsgi.py / asgi.py
│ ├── apps/
│ │ └── core/ # Health check + home view
│ ├── manage.py
│ ├── requirements.txt / requirements-dev.txt
│ ├── Dockerfile # Multi-stage build, gunicorn on port 8000
│ ├── docker-compose.yml # Local dev: Django + Postgres
│ └── .env.example
└── docs/
| Diagram element | Terraform resource |
|---|---|
| VPC (10.0.0.0/16) | infra/modules/vpc |
| Public Subnets AZ1/AZ2 | aws_subnet.public in vpc module |
| Private Subnets AZ1/AZ2 | aws_subnet.private in vpc module |
| NAT Gateway 1 / 2 | aws_nat_gateway.this (one per public subnet) |
| Application Load Balancer | infra/modules/alb |
| ASG (Min 1, Desired 2, Max 3) | infra/modules/asg → aws_autoscaling_group.app |
| EC2 Instances | Launch template in asg module, AL2023 AMI |
| Port Mapping 8000 → 8000 | container_port / app_port vars, set in user_data.sh.tpl's docker run -p |
| Pull django-app (DockerHub) | docker pull ${docker_image} in user_data.sh.tpl |
| Manages / Manages | ASG manages EC2 instance lifecycle across both private subnets |
-
Push the Django image to DockerHub (CI does this automatically on merge to
main, or manually):cd django_app docker build -t yourdockerhubuser/django-app:latest . docker push yourdockerhubuser/django-app:latest
-
Create remote state resources once (S3 bucket + DynamoDB table), then fill in
infra/environments/production/backend.tf. -
Configure variables:
cd infra/environments/production cp terraform.tfvars.example terraform.tfvars # edit terraform.tfvars: docker_image, region, sizing, etc.
-
Deploy:
terraform init terraform plan -var="django_secret_key=$(openssl rand -hex 32)" -var="database_url=postgres://..." terraform apply -var="django_secret_key=$(openssl rand -hex 32)" -var="database_url=postgres://..."
-
Terraform outputs
alb_dns_name— that's your public URL.
This stack provisions compute/network/load-balancing only. For a managed database, add an RDS module (not included here since it wasn't in the source diagram) and point
database_urlat it.
cd django_app
cp .env.example .env
docker compose up --build
# App: http://localhost:8000 Health check: http://localhost:8000/health/Or without Docker:
cd django_app
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
python manage.py migrate
python manage.py runserver.github/workflows/terraform.yml runs on every push/PR:
terraform fmt -checkandterraform validatefor all infra- Django
manage.py checkandmanage.py test - On
main: builds and pushes the Docker image to DockerHub, ready for the nextterraform apply/ instance refresh to roll it out.
