Treating infrastructure as code (IaC) means your AWS environment lives in a version-controlled template instead of a sequence of console clicks. For a Django app that usually involves a VPC, an EC2 fleet, a managed database, security groups, and IAM roles — exactly the kind of setup you do not want to recreate by hand for staging, production, and disaster recovery.
AWS CloudFormation is Amazon's native IaC service. You describe the resources you want in a declarative YAML (or JSON) template, hand it to CloudFormation, and it provisions and tracks them as a single unit called a stack. Change the template and CloudFormation works out the delta, then applies only what changed.
This guide walks through a realistic, production-shaped CloudFormation template for Django on AWS — an Application Load Balancer in front of an Auto Scaling Group of EC2 instances, backed by RDS PostgreSQL — plus how to deploy it with the AWS CLI v2, keep secrets out of the template, and run migrations on release. If you would rather hand provisioning to a managed platform, see our companion guide on deploying Django on Elastic Beanstalk; we contrast the two approaches below.
CloudFormation in one minute: templates and stacks
A CloudFormation template is a declarative document that lists the AWS resources you want and how they relate. You submit it and CloudFormation creates a stack — a single managed collection of those resources. The stack becomes the unit you create, update, and delete as one.
Two ideas make this powerful:
- Declarative, not imperative. You describe the desired end state; CloudFormation figures out the order to create things (it builds a dependency graph from your references) and rolls everything back automatically if any resource fails.
- Idempotent updates. Re-applying the same template is a no-op. Changing it produces a precise diff you can preview with a change set before it touches production.
Because the template is just text, it belongs in your Git repo next to the Django code, gets code-reviewed, and is reproducible across regions and accounts.
Anatomy of a CloudFormation template
Every template is organised into a handful of top-level sections. Only Resources is mandatory:
Parameters— typed inputs supplied at deploy time (instance type, VPC id, environment name). They keep one template reusable across environments.Mappings— static lookup tables, e.g. a region-to-AMI map (largely replaced today by SSM public parameters — see below).Conditions— boolean expressions that toggle resources or properties (for example, only attach a larger instance type inprod).Resources— the actual AWS resources to provision. This is the only required section.Outputs— values to surface after the stack is built, such as the load balancer DNS name or the database endpoint.
You wire sections together with intrinsic functions: !Ref returns a parameter value or a resource's primary id, !GetAtt reads a resource attribute (like an instance profile ARN), and !Sub interpolates values into strings.
A realistic Django stack
The template below provisions a small but production-shaped Django environment:
- An Application Load Balancer (ALB) that terminates public traffic.
- An Auto Scaling Group (ASG) of EC2 instances running Gunicorn, registered to the ALB's target group, so the fleet scales and self-heals. Our deep dive on auto scaling groups behind a load balancer covers this pattern in detail.
- An RDS PostgreSQL instance for the database, with a
Snapshotdeletion policy so you do not lose data on a stack teardown. - Security groups that only let the web tier receive traffic from the ALB.
- An IAM role and instance profile so instances can read secrets and be managed by SSM — no SSH keys required. See AWS IAM roles and policies for how least-privilege roles are structured.
To keep the example focused it deploys into an existing VPC and subnets (your account's default VPC works fine to start). The AMI is resolved at deploy time from a public SSM parameter, so you always get the latest patched Amazon Linux 2023 image instead of a hardcoded id.
AWSTemplateFormatVersion: "2010-09-09"
Description: Django stack - ALB + Auto Scaling Group + RDS PostgreSQL (IaC)
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC to deploy into (your default VPC works to start)
Subnets:
Type: List<AWS::EC2::Subnet::Id>
Description: Two or more subnets across Availability Zones
InstanceType:
Type: String
Default: t3.small
LatestAmiId:
# AMI resolved at deploy time from a public SSM parameter (always patched)
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Resources:
AlbSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Public HTTP to the ALB
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: App traffic from the ALB only
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8000
ToPort: 8000
SourceSecurityGroupId: !Ref AlbSecurityGroup
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: ec2.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles: [!Ref InstanceRole]
Database:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
Properties:
Engine: postgres
DBInstanceClass: db.t3.micro
AllocatedStorage: "20"
DBName: appdb
MasterUsername: appuser
# Pull the password from Secrets Manager at deploy time - never hardcode it
MasterUserPassword: "{{resolve:secretsmanager:django/prod/db:SecretString:password}}"
VPCSecurityGroups: [!Ref WebSecurityGroup]
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
ImageId: !Ref LatestAmiId
InstanceType: !Ref InstanceType
IamInstanceProfile: { Arn: !GetAtt InstanceProfile.Arn }
SecurityGroupIds: [!Ref WebSecurityGroup]
UserData:
Fn::Base64: !Sub |
#!/bin/bash
dnf install -y python3.12 git
# clone code, create a venv, install requirements, then:
export DB_HOST=${Database.Endpoint.Address}
python3.12 manage.py migrate --noinput
python3.12 manage.py collectstatic --noinput
gunicorn config.wsgi -b 0.0.0.0:8000
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref VpcId
Port: 8000
Protocol: HTTP
HealthCheckPath: /healthz/
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets: !Ref Subnets
SecurityGroups: [!Ref AlbSecurityGroup]
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
MinSize: "2"
MaxSize: "4"
DesiredCapacity: "2"
VPCZoneIdentifier: !Ref Subnets
TargetGroupARNs: [!Ref TargetGroup]
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
Outputs:
SiteUrl:
Description: Public URL of the Django app
Value: !Sub "http://${LoadBalancer.DNSName}"
Deploying the stack with the AWS CLI v2
The modern way to ship a template is aws cloudformation deploy. It creates the stack if it does not exist and updates it if it does — one idempotent command that fits cleanly into CI/CD. Because the template creates IAM resources, you pass --capabilities CAPABILITY_IAM to acknowledge that.
Before letting an update touch production, generate a change set: a preview of exactly what CloudFormation will add, modify, or replace. Reviewing change sets is the single most effective habit for avoiding surprise replacements, because some property changes force a resource to be recreated (which can mean downtime or data loss).
# Validate the template before anything else
aws cloudformation validate-template --template-body file://django-stack.yaml
# Create OR update the stack idempotently (AWS CLI v2)
aws cloudformation deploy \
--stack-name django-prod \
--template-file django-stack.yaml \
--capabilities CAPABILITY_IAM \
--parameter-overrides VpcId=vpc-0abc123 Subnets=subnet-0a,subnet-0b
# Preview changes safely with a change set before they apply
aws cloudformation create-change-set \
--stack-name django-prod --change-set-name release-2026-06 \
--template-body file://django-stack.yaml \
--capabilities CAPABILITY_IAM
aws cloudformation describe-change-set \
--stack-name django-prod --change-set-name release-2026-06
# Detect manual ("out of band") changes that drifted from the template
aws cloudformation detect-stack-drift --stack-name django-prod
Keep secrets out of the template
Never hardcode database passwords, Django's SECRET_KEY, or API keys in a template — templates land in Git and in the CloudFormation console as plain text. Instead, store them in AWS Secrets Manager or SSM Parameter Store and pull them in with a dynamic reference.
In the template above the RDS password uses {{resolve:secretsmanager:django/prod/db:SecretString:password}}, which CloudFormation resolves at deploy time without ever printing the value. For inputs you do pass as parameters, mark them NoEcho: true. Create the secret once, out of band:
# Create the secret once; rotate it later via Secrets Manager
aws secretsmanager create-secret \
--name django/prod/db \
--secret-string '{"password":"REPLACE_WITH_A_STRONG_PASSWORD"}'
# At runtime, instances read it with boto3 using their IAM role -
# nothing sensitive is ever baked into the AMI or the template:
# import boto3, json
# sm = boto3.client("secretsmanager")
# secret = json.loads(sm.get_secret_value(SecretId="django/prod/db")["SecretString"])
Running migrations on deploy
Provisioning infrastructure and releasing application code are two different jobs, and migrate should run exactly once per release — not on every instance boot. A few patterns, from simplest to most robust:
- UserData (simple). The launch template's UserData pulls the code, installs dependencies, runs
python3 manage.py migrate --noinputandcollectstatic, then starts Gunicorn. Fine for a single instance or low-traffic app, but it runs on every instance that launches — Django migrations are idempotent, but you still risk races. - A one-off migration task (recommended). Run migrations from a single place — a CodeDeploy lifecycle hook, a one-off ECS task, or a CI step — before traffic shifts to the new version. This avoids race conditions when several instances boot at once.
- CodeDeploy integrates with the Auto Scaling Group for blue/green or rolling releases and gives you a
BeforeAllowTraffichook — the right place to run migrations and smoke tests.
Whatever you choose, keep migrations backward-compatible so old and new code can run side by side during a rolling deploy.
CloudFormation vs CDK vs Terraform vs Elastic Beanstalk
CloudFormation is not the only way to provision a Django stack. Here is how the main options compare:
| CloudFormation | AWS CDK | Terraform | Elastic Beanstalk | |
|---|---|---|---|---|
| Approach | Declarative IaC | Imperative code that synthesises CloudFormation | Declarative IaC (third party) | Managed PaaS |
| Authoring | YAML / JSON | Python, JavaScript, Go, Java | HCL | Minimal config (.ebextensions) |
| AWS coverage | Full, native, same-day | Full (compiles to CloudFormation) | Full, plus other clouds | AWS-managed subset |
| State | Managed by AWS | Managed by AWS (via CFN) | You manage state files | Managed by AWS |
| Best for | AWS-only teams wanting native IaC | Django teams who prefer Python over YAML | Multi-cloud or existing Terraform shops | Shipping fast with little ops |
Two takeaways for Django teams. First, the AWS CDK lets you author the very same CloudFormation in Python — appealing if your team already lives in Python and wants loops, conditionals, and unit tests around infrastructure. Second, if you want AWS to provision and manage the platform for you rather than describing every resource yourself, Elastic Beanstalk is the managed-PaaS alternative — see our guide to deploying Django on Elastic Beanstalk. CloudFormation sits in the middle: full declarative control of raw resources, without a third-party tool or state file to manage.
CloudFormation best practices
- Never hardcode secrets. Use Secrets Manager or SSM Parameter Store with dynamic references; mark sensitive parameters
NoEcho: true. - Always preview with change sets before updating a production stack.
- Resolve AMIs from SSM public parameters instead of pinning ids in
Mappings, so you inherit the latest patched image. - Use
!Refand!GetAttto wire resources together rather than hardcoding ARNs or ids — it keeps the dependency graph (and rollback order) correct. - Run drift detection regularly to catch manual console changes that diverge from the template.
- Set a sensible
DeletionPolicy(e.g.Snapshoton RDS) so a stack delete cannot wipe data. - Split large templates into nested stacks (networking, data, app) and parameterise per environment instead of copy-pasting templates.
- Let rollback do its job — CloudFormation reverts a failed update to the last good state automatically; keep that behaviour on for production.
Need a hand with Django on AWS?
For 12+ years and across 50+ delivered projects, MicroPyramid has shipped Django applications on AWS with infrastructure as code, repeatable environments, and least-privilege IAM baked in from day one. We help teams stand up CloudFormation (or CDK) stacks, harden them, and wire them into CI/CD — and we have helped clients cut cloud spend by around 30% along the way.
Explore our AWS consulting services, cloud migration services, and Django development services — or read AWS tips and tricks to optimise cost and performance for more ways to get better ROI from your stack.
Frequently Asked Questions
Should I use CloudFormation or Elastic Beanstalk for Django?
Use Elastic Beanstalk when you want AWS to provision and manage the platform for you with minimal ops effort, and CloudFormation when you need explicit, declarative control over every resource (VPC, ALB, ASG, RDS, IAM) as version-controlled infrastructure as code. They are not mutually exclusive — Elastic Beanstalk itself uses CloudFormation under the hood, and you can even describe Beanstalk environments inside a CloudFormation template. For full detail on the managed-PaaS route, see our companion guide on deploying Django on Elastic Beanstalk.
CloudFormation or Terraform, which should a Django team pick?
Pick CloudFormation if you are AWS-only and want a native, AWS-managed service with no separate state file to store and lock. Pick Terraform if you run multiple clouds, already have Terraform expertise, or want its larger ecosystem of providers. Both are declarative and production-grade; the deciding factors are usually existing team skills and whether you are committed to a single cloud.
How do I handle Django secrets in a CloudFormation template?
Never put passwords or the Django SECRET_KEY in the template. Store them in AWS Secrets Manager or SSM Parameter Store and pull them in with a dynamic reference such as the secretsmanager resolve syntax, which CloudFormation substitutes at deploy time without exposing the value. At runtime, instances fetch secrets with boto3 using an IAM role, so nothing sensitive is ever baked into the template or the AMI.
How do I run Django migrations during a CloudFormation deploy?
Run migrations exactly once per release, ideally from a single place such as a CodeDeploy BeforeAllowTraffic hook, a one-off ECS task, or a CI step, before traffic shifts to the new version. Running migrate in EC2 UserData works for a single instance, but in an Auto Scaling Group several instances can boot at once and race, so a dedicated one-off task is safer. Keep every migration backward-compatible so old and new code can run together during a rolling deploy.
Should I write CloudFormation templates in JSON or YAML?
Prefer YAML. It is far more readable, supports comments, and has concise short forms for intrinsic functions like !Ref and !GetAtt. JSON is still valid and is sometimes generated by tooling, but for hand-authored templates YAML is the modern default. If you would rather write infrastructure in Python, the AWS CDK synthesises CloudFormation for you.
What is a CloudFormation change set?
A change set is a preview of exactly what CloudFormation will do to a stack before it does it — which resources will be added, modified, or replaced. Because some property changes force a resource to be recreated (which can mean downtime or data loss), reviewing a change set before updating a production stack is the most important safety habit in CloudFormation. Create one with create-change-set, inspect it with describe-change-set, then execute or discard it.