Use Google Secret Manager With Django

Load Django secrets from Google Cloud Secret Manager: mount secrets as environment variables or files on Cloud Run, read them with the Python client on Compute Engine and GKE, grant least-privilege access to service accounts, and rotate secrets with versions.

Problem statement

Your Django app runs on Google Cloud, and secrets such as SECRET_KEY, the database password, and API keys should not sit in plain environment variables in deployment files or in a .env baked into an image. Google Secret Manager stores secrets with versions and IAM access control. Cloud Run can expose them directly as environment variables or files, and other platforms can read them with the client library at startup.

Secret Manager for DjangoSecretversioned valueIAMservice account accessDeliveryenv, file, or APIDjango settingsread at startupSecrets are versioned in GCP and handed to Django through the platform or the API.
  1. Create secrets: printf %s "$VALUE" | gcloud secrets create django-secret-key --data-file=-.
  2. Grant the runtime service account roles/secretmanager.secretAccessor on each secret.
  3. Cloud Run: --set-secrets=SECRET_KEY=django-secret-key:latest exposes it as an environment variable.
  4. Compute Engine or GKE: read secrets with google-cloud-secret-manager in settings, or sync with External Secrets Operator.
  5. Pin versions for predictable deploys, and rotate by adding a new version and redeploying.

Step-by-step solution

1. Create secrets

python -c "import secrets; print(secrets.token_urlsafe(50), end='')" | \
  gcloud secrets create django-secret-key --replication-policy=automatic --data-file=-
printf %s "postgres://shop:...@/shop?host=/cloudsql/acme:europe-west1:shop-db" | \
  gcloud secrets create django-database-url --data-file=-

Use printf %s or end='' so no trailing newline ends up in the value, a common cause of authentication failures.

2. Grant access

gcloud secrets add-iam-policy-binding django-secret-key \
  --member=serviceAccount:shop-run@acme.iam.gserviceaccount.com \
  --role=roles/secretmanager.secretAccessor

Grant per secret rather than project-wide, so the web service cannot read secrets meant for other services.

platform and methodMethodIdentityCloud Run--set-secrets env or fileservice accountCompute Engineclient library at startVM service accountGKECSI driver or ESOworkload identityCloud Build / CIavailableSecretsbuild service accountEvery method authenticates with a service account, never a key filedjango-deployment.com
Grant secretAccessor on each secret to exactly the identity that needs it.

3. Cloud Run

gcloud run deploy shop --image=europe-west1-docker.pkg.dev/acme/app/shop:3f2a1c9 \
  --service-account=shop-run@acme.iam.gserviceaccount.com \
  --set-secrets=SECRET_KEY=django-secret-key:latest,DATABASE_URL=django-database-url:latest

Django reads os.environ["SECRET_KEY"] as usual. With :latest, the value is resolved when an instance starts, so a new secret version reaches new instances without a redeploy, but running instances keep the old value. Pinning a version number (:3) makes rollouts explicit. Mounting as a file (--set-secrets=/secrets/key=django-secret-key:latest) lets the file update in running instances.

4. Compute Engine and GKE

Read secrets in settings at startup:

from google.cloud import secretmanager

def gcp_secret(name, version="latest", project=os.environ.get("GOOGLE_CLOUD_PROJECT")):
    client = secretmanager.SecretManagerServiceClient()
    path = f"projects/{project}/secrets/{name}/versions/{version}"
    return client.access_secret_version(name=path).payload.data.decode()

SECRET_KEY = os.environ.get("SECRET_KEY") or gcp_secret("django-secret-key")

Credentials come from the VM's service account or GKE workload identity. Fetching at import time adds a network call to every process start; cache values for the process lifetime. On GKE, the Secret Manager CSI driver or External Secrets Operator avoids code changes by providing files or Kubernetes Secrets.

5. Rotate secrets

Add a new version, deploy or restart to pick it up, then disable the old version:

printf %s "$NEW_VALUE" | gcloud secrets versions add django-database-url --data-file=-
gcloud secrets versions disable 2 --secret=django-database-url

For SECRET_KEY, keep the previous value in SECRET_KEY_FALLBACKS during the transition so sessions and password reset links stay valid.

version-based rotation1Add new secret versiongcloud versions add2Keep old key as fallbackSECRET_KEY_FALLBACKS3Deploy or restartinstances read new4Disable old versionafter it is unused5Destroy when safeaudit logs checkedVersions make rotation and rollback explicitdjango-deployment.com
Disabled versions can be re-enabled if something still needs them.

Explanation

Secret Manager stores each secret as a series of immutable versions, protected by IAM and logged in Cloud Audit Logs. Platforms either resolve versions for you (Cloud Run, Cloud Build) or let your code fetch them with the identity attached to the workload. Django needs no special integration because the values end up as ordinary environment variables or strings in settings.

Edge cases / notes

  • Trailing newlines. Values created with echo include a newline; always use printf %s.
  • Local development. Use a local .env or gcloud auth application-default login with a dev project's secrets, never production ones.
  • Costs. Access operations are billed per 10,000 calls; fetching once per process start is negligible.

Next steps

FAQ

How does Cloud Run pass secrets to Django?

As environment variables or mounted files, configured with --set-secrets.

Do I need a service account key file?

No. Use the service account attached to Cloud Run, the VM, or the GKE workload.

Does update running instances?

Not for environment variables. New instances get the new value; mounted files can update in place.

Should I fetch secrets on every request?

No. Read them once at startup and keep them for the process lifetime.

Keep going