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.
- Store a JSON secret, for example
prod/myappwith keysSECRET_KEY,DATABASE_URL,STRIPE_API_KEY. - ECS: reference keys in the task definition
secrets(valueFrom: arn:...:secret:prod/myapp:SECRET_KEY::); the execution role needssecretsmanager:GetSecretValue. - EKS: sync with External Secrets Operator or mount with the Secrets Store CSI driver.
- EC2 or elsewhere: fetch at startup with boto3 and cache in memory; do not call the API per request.
- Grant least privilege: one secret ARN per role, plus
kms:Decryptif you use a customer-managed key. - 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.
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.
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
.envfile with fake values, not production secrets.
Next steps
- Compare approaches in Manage Django Secrets in Production.
- Rotate the Django key without logouts using Rotate Django SECRET_KEY Safely.
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.