Skip to content

Backend Deployment - AWS Systems Manager Parameter Store

Overview

AWS Systems Manager Parameter Store (SSM) provides a central, secure, and versioned way to store configuration variables and secrets for your applications.

Benefits:

  • Centralized configuration across environments
  • Native encryption for secrets using AWS KMS
  • Easy integration with EC2, ECS, Lambda, and CI/CD
  • Supports tagging, versioning, and auditing

We use Parameter Store to replace hard-coded .env values in production deployments, allowing us to manage secrets safely and uniformly.


Parameter Types

Type Encrypted Typical Use
String Non-secret values (DB name, region)
SecureString ✅ (KMS) Secrets (passwords, keys, tokens)

Naming Convention

Parameters are stored in a hierarchical path format:

/<project>/<component>/<service>/<environment>/<KEY>

Example:

/mow/backend/postgres/stage/POSTGRES_DB
/mow/backend/django/prod/SECRET_KEY
  • Use lowercase project names with underscores.
  • Separate logical layers clearly (project → component → service → environment → key).
  • Secrets always use SecureString type.

Setup Prerequisites

You’ll need:

  • AWS CLI v2
  • jq (for JSON handling)
  • Logged in via SSO:
aws sso login --profile admin-cli-sso

Recommended directory layout:

aws/
  scripts/
    ssm-put-param.sh
    ssm-put-secure-param.sh
    ssm-put-and-tag.sh
    ssm-put-secure-and-tag.sh
    ssm-list-param-keys.sh
    ssm-seed-from-env.sh
    ssm-export-env.sh
    ssm-delete-param.sh
    ssm-delete-by-path.sh
    tag-ssm-resources-by-id.sh
  tags/
    backend/
      base.json
      postgres.json
      stage.json

Make scripts executable after cloning:

chmod +x aws/scripts/ssm-*.sh

AWS SSM Parameter Store Toolkit

All scripts default to DRY_RUN=1 → preview only. Set DRY_RUN=0 to apply changes.

✅ If --profile is omitted and AWS_PROFILE is set, that profile will be used automatically. ✅ All scripts accept flags (--profile, --region, --name, etc.) rather than positional arguments.


1. Create / Update Parameters

a) Plain Text Parameter (String)

Script: ssm-put-param.sh

# Dry run (default)
./aws/scripts/ssm-put-param.sh \
  --region us-east-1 \
  --name /mow/backend/postgres/stage/POSTGRES_DB \
  --value ok_db
# Apply for real
DRY_RUN=0 ./aws/scripts/ssm-put-param.sh \
  --region us-east-1 \
  --name /mow/backend/postgres/stage/POSTGRES_DB \
  --value ok_db

b) Encrypted Parameter (SecureString)

Script: ssm-put-secure-param.sh

# Dry run
./aws/scripts/ssm-put-secure-param.sh \
  --region us-east-1 \
  --name /mow/backend/django/stage/SECRET_KEY \
  --value REDACTED
# Apply with default key (alias/aws/ssm)
DRY_RUN=0 ./aws/scripts/ssm-put-secure-param.sh \
  --region us-east-1 \
  --name /mow/backend/django/stage/SECRET_KEY \
  --value "super-secret"
# Apply with custom KMS key
DRY_RUN=0 ./aws/scripts/ssm-put-secure-param.sh \
  --region us-east-1 \
  --name /mow/backend/django/stage/SECRET_KEY \
  --value "super-secret" \
  --key-id alias/myapp/ssm

2. Create + Tag in One Step

Tag files follow a docker-compose layering model (-f base -f service -f env).

a) Plain Text + Tagging

Script: ssm-put-and-tag.sh

# Dry run
./aws/scripts/ssm-put-and-tag.sh \
  --region us-east-1 \
  --name /mow/backend/postgres/stage/POSTGRES_USER \
  --value mow \
  -f aws/tags/backend/base.json \
  -f aws/tags/backend/postgres.json \
  -f aws/tags/backend/stage.json \
  --name-tag "mow-backend-postgres-stage" \
  --print-keys
# Apply
DRY_RUN=0 ./aws/scripts/ssm-put-and-tag.sh \
  --region us-east-1 \
  --name /mow/backend/postgres/stage/POSTGRES_USER \
  --value mow \
  -f aws/tags/backend/base.json \
  -f aws/tags/backend/postgres.json \
  -f aws/tags/backend/stage.json \
  --name-tag "mow-backend-postgres-stage" \
  --print-keys

b) SecureString + Tagging

Script: ssm-put-secure-and-tag.sh

# Dry run
./aws/scripts/ssm-put-secure-and-tag.sh \
  --region us-east-1 \
  --name /mow/backend/postgres/stage/POSTGRES_PASSWORD \
  --value REDACTED \
  -f aws/tags/backend/base.json \
  -f aws/tags/backend/postgres.json \
  -f aws/tags/backend/stage.json \
  --name-tag "mow-backend-postgres-stage" \
  --print-keys
# Apply (example: custom key)
DRY_RUN=0 ./aws/scripts/ssm-put-secure-and-tag.sh \
  --region us-east-1 \
  --name /mow/backend/postgres/stage/POSTGRES_PASSWORD \
  --value "s3cr3t!" \
  --key-id alias/myapp/ssm \
  -f aws/tags/backend/base.json \
  -f aws/tags/backend/postgres.json \
  -f aws/tags/backend/stage.json \
  --name-tag "mow-backend-postgres-stage" \
  --print-keys

3. List Parameters

Script: ssm-list-param-keys.sh

# List all parameters
./aws/scripts/ssm-list-param-keys.sh --region us-east-1
# Filter by prefix
./aws/scripts/ssm-list-param-keys.sh \
  --region us-east-1 \
  --prefix /mow/backend/
# Filter by substring
./aws/scripts/ssm-list-param-keys.sh \
  --region us-east-1 \
  --contains POSTGRES \
  --raw

4. Seed from .env

Script: ssm-seed-from-env.sh

Automatically:

  • Detects service sections (# service=<name>)
  • Detects one-shot secret markers (# secret=true)
  • Creates String vs SecureString
  • Layers tag files (base → service → environment → extra)
  • Supports dry run (DRY_RUN=1, default)
  • Optionally prints keys processed (--print-keys)

Example .env

# service=postgres
POSTGRES_USER=mow
# secret=true
POSTGRES_PASSWORD=super-secret
POSTGRES_DB=ok_db

# service=django
DEBUG=False
# secret=true
SECRET_KEY=foo

Preview run

% ./aws/scripts/ssm-seed-from-env.sh \
  --region us-east-1 \
  --project mow \
  --environment prod \
  --env-file env.prod \
  --print-keys

Apply + custom dynamic tags

Sometimes you want to pass dynamic or project-specific tags at seed time— for example:

  • build metadata (version)
  • organizational metadata (owner, cost center)
  • repository location (repo)
  • runtime or deployment annotations (managed-by, etc.)

These fit naturally into --extra-tag-file, which provides an additional JSON-based tag layer after the built-in layers:

base.json → service.json → environment.json → extra-tag-file(s)

Create an ephemeral file that contains dynamic tags:

(replace with the real GitHub org)

EXTRA_TAGS="$(mktemp)"
cat > "$EXTRA_TAGS" <<'JSON'
[
  {"Key":"mow:project","Value":"mow"},
  {"Key":"mow:owner","Value":"james"},
  {"Key":"mow:managed-by","Value":"manual"},
  {"Key":"mow:repo","Value":"github.com/<your-org>/mow-backend"},
  {"Key":"mow:version","Value":"backend@1.0.0"}
]
JSON

Then pass it to the seeder:

DRY_RUN=0 ./aws/scripts/ssm-seed-from-env.sh \
  --region us-east-1 \
  --project mow \
  --environment prod \
  --env-file env.prod \
  --print-keys \
  --extra-tag-file "$EXTRA_TAGS"

Multiple --extra-tag-file entries may be supplied. Each must be a JSON array of {"Key","Value"} tag objects.


Tag layering

Tag order (lowest → highest precedence):

  1. aws/tags/backend/base.json
  2. aws/tags/backend/<service>.json (if present)
  3. aws/tags/backend/<environment>.json (if present)
  4. --extra-tag-file … (optional, repeatable)

--extra-tag-file is therefore the right place to supply:

✅ Dynamic info (version, git SHA, build/date) ✅ Per-instance metadata (repo URL, owner) ✅ CI/CD injected values

It does not require modifying the repo, though you may check in shared layers such as base/service/environment.


5. Export Parameters → .env

Script: ssm-export-env.sh

# Instance role
sudo ./aws/scripts/ssm-export-env.sh \
  --region us-east-1 \
  --project mow \
  --env stage \
  /opt/app/.env
# Named profile
./aws/scripts/ssm-export-env.sh \
  --profile admin-cli-sso \
  --region us-east-1 \
  --project mow \
  --env prod \
  ./out.env

Output example:

# generated 2025-10-15T12:34:56Z
# project=mow env=stage region=us-east-1

# service=django
SECRET_KEY='…'
DEBUG=False

# service=postgres
POSTGRES_DB=ok_db
POSTGRES_USER=mow
POSTGRES_PASSWORD='…'

Options

  • --service <name> export only one service
  • --no-decrypt
  • Output mode 0600

6. Tag Parameters After Creation

Script: tag-ssm-resources-by-id.sh

Same layering as all tag tools. Accepts multiple names after --.

# Preview
./aws/scripts/tag-ssm-resources-by-id.sh \
  --region us-east-1 \
  -f aws/tags/backend/base.json \
  -f aws/tags/backend/postgres.json \
  -f aws/tags/backend/stage.json \
  --name-tag "mow-backend-postgres-stage" \
  -- \
  /mow/backend/postgres/stage/POSTGRES_DB
# Apply
DRY_RUN=0 ./aws/scripts/tag-ssm-resources-by-id.sh \
  --region us-east-1 \
  -f aws/tags/backend/base.json \
  -f aws/tags/backend/postgres.json \
  -f aws/tags/backend/stage.json \
  --name-tag "mow-backend-postgres-stage" \
  -- \
  /mow/backend/postgres/stage/POSTGRES_DB

Supports:

  • --tag '{"Key":"k","Value":"v"}'
  • --resource-type Parameter (default)
  • Layered files override by key (last-write-wins)
  • --print-keys

7. Delete a Single Parameter

Script: ssm-delete-param.sh

# Preview
./aws/scripts/ssm-delete-param.sh \
  --region us-east-1 \
  --name /mow/backend/postgres/stage/POSTGRES_DB \
  --print-tags 1
# Delete for real
DRY_RUN=0 ./aws/scripts/ssm-delete-param.sh \
  --region us-east-1 \
  --name /mow/backend/django/stage/SECRET_KEY

Prints:

  • Type
  • Version
  • ARN
  • Last modified
  • Tags (opt)

8. Delete by Path (Recursive)

Script: ssm-delete-by-path.sh

# Preview
./aws/scripts/ssm-delete-by-path.sh \
  --region us-east-1 \
  --path-prefix /mow/backend/ \
  --print-names
# Delete all under path
DRY_RUN=0 ./aws/scripts/ssm-delete-by-path.sh \
  --region us-east-1 \
  --path-prefix /mow/backend/
# Delete subset containing text
DRY_RUN=0 ./aws/scripts/ssm-delete-by-path.sh \
  --region us-east-1 \
  --path-prefix /mow/backend/ \
  --contains POSTGRES

Notes:

  • Recurses via get-parameters-by-path
  • Batches of 10 via delete-parameters
  • Optional filter --contains
  • Optional preview list --print-names

Best Practices

✅ Always dry-run first ✅ Use layered tagging (-f base -f service -f env) ✅ Export .env before deletion if you need backup ✅ Use full SSM paths starting with / ✅ Use --contains when narrowing deletes


9. Permissions Required

The following IAM actions are needed for these scripts:

  • ssm:PutParameter
  • ssm:AddTagsToResource
  • ssm:ListTagsForResource
  • ssm:GetParametersByPath
  • ssm:GetParameter
  • kms:Encrypt (for SecureString)
  • kms:Decrypt (for export with decryption)

10. Tips

  • Keep tag files reusable across EC2 and SSM (arrays of {Key,Value}).
  • Tag all secrets and configs with environment and service.
  • Use --print-keys in CI logs for visibility without exposing values.
  • Rotate secrets by updating the same parameter path (new version is created automatically).

Summary

Parameter Store is now your central source of configuration truth:

  • Configuration lives in AWS, not .env files.
  • Secrets are encrypted by KMS.
  • Tags tie parameters to owners, projects, and billing.
  • Scripts handle creation, tagging, seeding, and export with consistent DRY_RUN safety.

< Backend Deployment - AWS Tagging Guide

Next: Backend Deployment - AWS EC2 Manual Deploy >