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.

Django infra with Pulumi__main__.pyresources in Pythonpulumi previewdiff before changepulumi upcreate or updateStacksstaging, productionInfrastructure becomes reviewed Python code, with one stack per environment.
  1. pip install pulumi pulumi-aws and pulumi new aws-python in an infra/ directory.
  2. Define the network, database, container service, and storage in __main__.py.
  3. Store secrets with pulumi config set --secret, read them with config.require_secret.
  4. Create one stack per environment: pulumi stack init staging, pulumi stack init production.
  5. Run pulumi preview in pull requests and pulumi up on merge.
  6. 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.

same code, per-stack configstagingproductionDB instance classdb.t4g.smalldb.t4g.mediumDeletion protectionoffโœ“onFinal snapshotskippedโœ“takenSecretsown encrypted configown encrypted configDifferences live in config and conditionals, not in copied codedjango-deployment.com
Stacks share the program and keep their own state and secrets.

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.

change infrastructure like code1Edit __main__.py in a branchPython, tests optional2pulumi preview in CIdiff posted to the PR3Review and mergesame as app code4pulumi up on mainstaging, then production5Outputs feed app deploydb host, bucket namesEvery infrastructure change is reviewed before it touches a cloud accountdjango-deployment.com
Run staging first so production applies a change that has already worked once.

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=True on databases.
  • Importing existing resources is possible with pulumi import, useful when adopting IaC for a console-built setup.
  • Drift. pulumi refresh updates state from the cloud; run it before big changes.
  • Terraform providers can be bridged, so most cloud services are available.

Next steps

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.

Keep going