Load Django Secrets From AWS Secrets Manager

Load Django secrets such as SECRET_KEY and database passwords from AWS Secrets Manager: inject them into ECS and EKS as environment variables, fetch them at startup with boto3 and caching, grant least-privilege IAM, and rotate RDS credentials.

Problem statement

Django needs secrets: SECRET_KEY, database credentials, API keys for email and payments. On AWS, keeping them in plain environment variables in task definitions, user data scripts, or a committed .env spreads them across places that are hard to audit and rotate. AWS Secrets Manager stores secrets encrypted with KMS, controls access with IAM, logs reads in CloudTrail, and can rotate RDS passwords automatically. Django can consume secrets from it either through platform injection (ECS, EKS) or by fetching them at startup.

Secrets from Secrets ManagerSecret storedKMS encryptedIAM roleread only its secretsInject or fetchECS env or boto3Django settingsenv valuesSecrets live in one audited place; the compute role is the only thing allowed to read them.
  1. Store a JSON secret, for example prod/myapp with keys SECRET_KEY, DATABASE_URL, STRIPE_API_KEY.
  2. ECS: reference keys in the task definition secrets (valueFrom: arn:...:secret:prod/myapp:SECRET_KEY::); the execution role needs secretsmanager:GetSecretValue.
  3. EKS: sync with External Secrets Operator or mount with the Secrets Store CSI driver.
  4. EC2 or elsewhere: fetch at startup with boto3 and cache in memory; do not call the API per request.
  5. Grant least privilege: one secret ARN per role, plus kms:Decrypt if you use a customer-managed key.
  6. Enable rotation for RDS credentials and make Django reconnect cleanly after rotation.

Step-by-step solution

1. Create the secret

aws secretsmanager create-secret --name prod/myapp --secret-string "$(jq -n \
  --arg sk "$(python -c 'import secrets;print(secrets.token_urlsafe(50))')" \
  --arg db "postgres://app:REPLACE@mydb.abc.eu-west-1.rds.amazonaws.com:5432/app?sslmode=require" \
  '{SECRET_KEY:$sk, DATABASE_URL:$db}')"

Group values used by one app in one JSON secret to reduce API calls and cost; split by environment (staging/myapp, prod/myapp).

2. Inject into ECS tasks

"secrets": [
  { "name": "SECRET_KEY",   "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/myapp-AbCdEf:SECRET_KEY::" },
  { "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/myapp-AbCdEf:DATABASE_URL::" }
]

ECS fetches the values when a task starts and exposes them as environment variables, so Django settings read them with os.environ or django-environ unchanged. The task execution role needs read access. Values are fixed for the task's lifetime; updated secrets take effect on the next deployment.

delivery method by runtimeMechanismRefreshECS / Fargate✓task definition secretson task startEKSExternal Secrets / CSIsync intervalEC2 / VMboto3 at startup, cachedon restart or TTLLambdaextension or boto3cached with TTLPrefer platform injection; fetch in code only where none existsdjango-deployment.com
Never call Secrets Manager on every request; cache values in the process.

3. Fetch at startup on EC2

# config/settings/aws_secrets.py
import json, functools
import boto3

@functools.lru_cache(maxsize=None)
def load_secret(name: str, region: str = "eu-west-1") -> dict:
    client = boto3.client("secretsmanager", region_name=region)
    return json.loads(client.get_secret_value(SecretId=name)["SecretString"])
# config/settings/production.py
from .aws_secrets import load_secret
_secrets = load_secret(os.environ.get("APP_SECRET_ID", "prod/myapp"))
SECRET_KEY = _secrets["SECRET_KEY"]
DATABASES = {"default": dj_database_url.parse(_secrets["DATABASE_URL"], conn_max_age=60)}

Settings load once per process, so each Gunicorn worker makes one call at startup. With preloading (preload_app = True), the master makes it once. The instance profile provides credentials.

4. Least-privilege IAM

{
  "Effect": "Allow",
  "Action": "secretsmanager:GetSecretValue",
  "Resource": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/myapp-*"
}

Staging roles get only staging/*. Add kms:Decrypt on the key if the secret uses a customer-managed KMS key.

5. Rotation

Secrets Manager can rotate RDS master or app user passwords on a schedule with managed rotation. During rotation there is a short window where the old password stops working; Django processes holding open connections continue, and new connections must use the new password. With task-definition injection, roll the service after rotation; with startup fetching, restart workers, or use RDS IAM authentication to avoid passwords entirely.

Secrets Manager for DjangoOne JSON secret per app and environmentprod/myapp, staging/myappPlatform injection where availableECS secrets, External SecretsCached fetch at startup otherwisenever per requestIAM scoped to exact secret ARNsplus kms:Decrypt if neededRotation tested end to endapp reconnects or redeploysCentralised secrets help only if access is scoped and rotation worksdjango-deployment.com
CloudTrail shows every GetSecretValue call; alert on unexpected callers.

Verification check: the app starts with no secrets in its task definition plain environment or in the image (docker history and the task definition show none), python manage.py check --deploy passes, and CloudTrail shows GetSecretValue calls only from the expected roles.

Explanation

Secrets Manager stores each secret version encrypted with KMS and returns plaintext only to principals whose IAM policies allow GetSecretValue. Integrations such as ECS task definitions fetch secrets on the caller's behalf at container start, so application code sees ordinary environment variables while the secret never appears in task definitions, images, or repositories. Fetching in application code achieves the same on platforms without integration, at the cost of an API call per process start and a dependency on AWS during startup.

Edge cases / notes

  • SSM Parameter Store SecureString parameters are a cheaper alternative for simple secrets without managed rotation; ECS can inject them the same way.
  • Startup dependency. If Secrets Manager is unreachable (VPC without a NAT or endpoint), startup fails; add a VPC interface endpoint for Secrets Manager.
  • Cost. Each secret has a monthly price plus API calls; grouping values and caching keeps it low.
  • Local development should use a .env file with fake values, not production secrets.

Next steps

FAQ

How do I get AWS Secrets Manager values into Django?

On ECS, reference them in the task definition secrets; elsewhere, fetch them once at startup with boto3.

Does Django call Secrets Manager on every request?

It should not. Load secrets when settings import and keep them in memory.

Which IAM role needs access on ECS?

The task execution role for injected secrets; the task role if your code fetches secrets itself.

Can Secrets Manager rotate my database password?

Yes, for RDS with managed rotation. Make sure Django picks up the new password on reconnect or redeploy.

Keep going