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
SecureStringtype.
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
--profileis omitted andAWS_PROFILEis 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
StringvsSecureString - 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
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):
aws/tags/backend/base.jsonaws/tags/backend/<service>.json(if present)aws/tags/backend/<environment>.json(if present)--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:PutParameterssm:AddTagsToResourcessm:ListTagsForResourcessm:GetParametersByPathssm:GetParameterkms: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-keysin 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
.envfiles. - 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.