Provision Django Infrastructure With Pulumi
Provision Django infrastructure with Pulumi in Python: define a VPC, database, container service, and secrets as code, manage stacks for staging and production, keep secrets encrypted in config, and preview changes before applying.
Problem statement
Infrastructure clicked together in a cloud console drifts, cannot be reviewed, and is hard to reproduce for staging. Terraform is the common fix, but Python teams sometimes prefer to write infrastructure in the same language as their Django app. Pulumi lets you define cloud resources in Python with real loops, functions, and tests, keeps state for you, encrypts secrets in configuration, and shows a preview of every change before it is applied.
pip install pulumi pulumi-awsandpulumi new aws-pythonin aninfra/directory.- Define the network, database, container service, and storage in
__main__.py. - Store secrets with
pulumi config set --secret, read them withconfig.require_secret. - Create one stack per environment:
pulumi stack init staging,pulumi stack init production. - Run
pulumi previewin pull requests andpulumi upon merge. - Export outputs (URLs, database host) for the app's deploy pipeline.
Step-by-step solution
1. Start a project
mkdir infra && cd infra
pulumi new aws-python --name myapp-infra --stack staging
pulumi config set aws:region eu-west-1
State is stored in Pulumi Cloud by default; use pulumi login s3://my-pulumi-state to keep it in your own bucket instead.
2. Define the core resources
# infra/__main__.py
import pulumi
import pulumi_aws as aws
cfg = pulumi.Config()
env = pulumi.get_stack()
db_password = cfg.require_secret("dbPassword")
vpc = aws.ec2.Vpc("vpc", cidr_block="10.20.0.0/16", enable_dns_hostnames=True)
private = [
aws.ec2.Subnet(f"private-{i}", vpc_id=vpc.id, cidr_block=f"10.20.{i}.0/24",
availability_zone=az)
for i, az in enumerate(["eu-west-1a", "eu-west-1b"])
]
db_subnets = aws.rds.SubnetGroup("db-subnets", subnet_ids=[s.id for s in private])
db = aws.rds.Instance(
"postgres",
engine="postgres", engine_version="16",
instance_class="db.t4g.small" if env == "staging" else "db.t4g.medium",
allocated_storage=20, db_name="app", username="app", password=db_password,
db_subnet_group_name=db_subnets.name,
backup_retention_period=7, deletion_protection=(env == "production"),
skip_final_snapshot=(env != "production"),
)
media = aws.s3.BucketV2("media")
pulumi.export("db_host", db.address)
pulumi.export("media_bucket", media.bucket)
Ordinary Python makes environment differences explicit and reviewable. Add an ECS cluster, task definition, ALB, and IAM roles the same way, or use higher-level components such as pulumi_awsx that bundle a VPC or a load-balanced Fargate service.
3. Handle secrets
pulumi config set --secret dbPassword "$(openssl rand -base64 32)"
pulumi config set --secret djangoSecretKey "$(python -c 'import secrets;print(secrets.token_urlsafe(50))')"
Values are encrypted in Pulumi.<stack>.yaml, safe to commit. Pass them into AWS Secrets Manager so the app reads them at runtime rather than baking them into task definitions:
secret = aws.secretsmanager.Secret("django-env")
aws.secretsmanager.SecretVersion("django-env-v", secret_id=secret.id,
secret_string=pulumi.Output.json_dumps({
"SECRET_KEY": cfg.require_secret("djangoSecretKey"),
"DATABASE_URL": pulumi.Output.concat("postgres://app:", db_password, "@", db.address, "/app"),
}))
4. Preview in pull requests, apply on merge
# .github/workflows/infra.yml (excerpt)
- uses: pulumi/actions@v6
with:
command: ${{ github.event_name == 'pull_request' && 'preview' || 'up' }}
stack-name: production
work-dir: infra
Reviewers see the exact resources that will change. Use OIDC to give the workflow AWS credentials instead of long-lived keys.
5. Connect to the app pipeline
The app deploy reads outputs instead of hard-coding hostnames:
DB_HOST=$(pulumi stack output db_host --stack production)
Keep infrastructure and application deploys separate: infrastructure changes rarely, app images change on every merge.
Verification check: pulumi preview on an unchanged stack shows no changes (no drift), pulumi stack output lists the expected values, and the Django app connects to the database and bucket created by the stack.
Explanation
Pulumi programs declare desired resources; the engine compares them with the stack's recorded state and the cloud provider, and computes create, update, and delete operations, much like Terraform. Writing the program in Python brings functions, loops, packages, and unit tests to infrastructure, which suits teams that already think in Python. Outputs are lazily resolved values, so resources can reference each other (the database address in a secret) before they exist, with Pulumi ordering creation accordingly.
Edge cases / notes
- Destructive changes. Some property changes replace resources (for example renaming an RDS instance). Read previews carefully and use
protect=Trueon databases. - Importing existing resources is possible with
pulumi import, useful when adopting IaC for a console-built setup. - Drift.
pulumi refreshupdates state from the cloud; run it before big changes. - Terraform providers can be bridged, so most cloud services are available.
Next steps
- The Terraform equivalent: Terraform a Django Server Baseline.
- An AWS-native alternative: Deploy Django on AWS With the CDK.
FAQ
Pulumi or Terraform for Django infrastructure?
Both work. Pulumi lets you write Python; Terraform has the larger ecosystem and HCL.
Where does Pulumi store state?
In Pulumi Cloud by default, or in your own backend such as S3 with pulumi login.
Are Pulumi secrets safe to commit?
Values set with --secret are encrypted in the stack config file and can be committed.
Should infrastructure and app deploys be one pipeline?
Keep them separate: infrastructure changes rarely and needs review; app images deploy often.