Skip to content

AWS SSM Parameter Store

Overview

AWS Systems Manager (SSM) Parameter Store provides secure, hierarchical storage for configuration data and secrets management. This document explores the theoretical foundations and implementation patterns for using SSM Parameter Store in production Django applications running on AWS ECS, with LocalStack for local development.

Why SSM Parameter Store?

The Production Secrets Problem

Environment variables work well for local development, but production environments require:

  1. Centralized secrets management - Single source of truth for all environments
  2. Encryption at rest - Secrets stored encrypted, not in plain text
  3. Access control - IAM policies determine who/what can access secrets
  4. Audit logging - CloudTrail records all parameter access
  5. Version history - Parameter changes are tracked and reversible
  6. No filesystem storage - Secrets never touch disk in plain text

Theory: Production secrets should never be stored in environment variables set in container definitions or task definitions. They should be fetched at runtime from a secure store. SSM Parameter Store provides this with native AWS integration.

SSM vs AWS Secrets Manager

AWS offers two secrets management services. Understanding when to use each is important:

SSM Parameter Store: - Free for standard parameters (up to 10,000) - Simple key-value storage - Integrated with AWS Systems Manager - Good for: Configuration data, non-rotating secrets, feature flags - Lower cost for high-volume usage

AWS Secrets Manager: - Costs $0.40/secret/month + $0.05 per 10,000 API calls - Built-in secret rotation - Database credential management - Good for: Database passwords, rotating API keys, OAuth tokens - Better for secrets requiring automatic rotation

Theory: For small teams with Django applications on ECS, SSM Parameter Store is usually sufficient. The application handles secret rotation through deployment, and the cost savings are significant. Use Secrets Manager when you need automatic rotation or RDS integration.

Decision Matrix:

Need Use SSM Parameter Store Use Secrets Manager
Static API keys -
Django SECRET_KEY -
Database password (manual rotation) -
Database password (auto rotation) -
OAuth tokens with refresh -
Feature flags -
High volume access (>100k/day) -

Parameter Naming Conventions

Hierarchical Structure

SSM Parameter Store supports hierarchical naming using forward slashes. This creates a natural tree structure for organizing parameters.

Recommended Structure:

/environment/service/parameter-name

Examples:
/dev/django/DJANGO_SECRET_KEY
/dev/django/DATABASE_PASSWORD
/dev/auth0/CLIENT_SECRET
/prod/django/DJANGO_SECRET_KEY
/prod/django/DATABASE_PASSWORD
/prod/sendgrid/API_KEY

Theory: Hierarchical naming enables: 1. Path-based permissions - Grant access to all /dev/* or specific /prod/django/* 2. Bulk operations - Retrieve all parameters under a path 3. Clear organization - Immediately see environment and service ownership 4. Namespace isolation - Different services can use same parameter names

Naming Convention Patterns

Environment Prefix Pattern:

/dev/PARAMETER_NAME
/staging/PARAMETER_NAME
/prod/PARAMETER_NAME

Pros: Simple, flat structure within environment Cons: All parameters for all services in one namespace

Service-Scoped Pattern:

/environment/service/PARAMETER_NAME

/dev/poseidon/DJANGO_SECRET_KEY
/dev/poseidon/AUTH0_CLIENT_SECRET
/dev/celery/BROKER_URL
/prod/poseidon/DJANGO_SECRET_KEY

Pros: Clear service ownership, can scope IAM per service Cons: More verbose, requires consistent service naming

Hybrid Pattern (Recommended):

/environment/PARAMETER_NAME              # Shared parameters
/environment/service/PARAMETER_NAME      # Service-specific parameters

/dev/DATABASE_PASSWORD                   # Shared database
/dev/poseidon/DJANGO_SECRET_KEY         # Poseidon-specific
/dev/poseidon/AUTH0_CLIENT_SECRET       # Poseidon-specific
/dev/celery/BROKER_URL                  # Celery-specific

Theory: Start with environment-scoped parameters for shared resources. Add service namespacing when multiple services need different values for the same parameter name. This balance keeps the structure simple while preventing conflicts.

Naming Best Practices

  1. Use all caps for parameter names - Matches environment variable convention
  2. Use underscores, not hyphens - Consistent with Python naming
  3. Be specific - AUTH0_CLIENT_SECRET not CLIENT_SECRET
  4. Avoid redundancy - /prod/django/DJANGO_SECRET_KEY/prod/django/SECRET_KEY
  5. Document exceptions - Some parameters may need different conventions
# Good naming
/prod/poseidon/SECRET_KEY
/prod/poseidon/AUTH0_CLIENT_ID
/prod/poseidon/SENDGRID_API_KEY

# Inconsistent naming
/prod/poseidon/secret-key           # Lowercase, hyphens
/prod/poseidon/auth0ClientId        # camelCase
/prod/poseidon/APIKey_SendGrid      # Mixed conventions

Encryption at Rest (SecureString)

Parameter Types

SSM Parameter Store supports three parameter types:

  1. String: Plain text, no encryption
  2. StringList: Comma-separated values, no encryption
  3. SecureString: Encrypted at rest using AWS KMS

Theory: Use SecureString for anything sensitive. Use String for non-sensitive configuration. Never use String for secrets "because it's easier" - the encryption overhead is negligible.

SecureString Implementation

Creating SecureString Parameters:

# Using default AWS-managed KMS key
aws ssm put-parameter \
  --name "/prod/django/SECRET_KEY" \
  --value "your-secret-value" \
  --type "SecureString" \
  --description "Django SECRET_KEY for production"

# Using custom KMS key
aws ssm put-parameter \
  --name "/prod/django/SECRET_KEY" \
  --value "your-secret-value" \
  --type "SecureString" \
  --key-id "alias/my-custom-key" \
  --description "Django SECRET_KEY for production"

Theory: AWS encrypts the parameter value before storing it. When you retrieve the parameter with WithDecryption=True, AWS decrypts it using KMS. The decrypted value never touches disk - it's encrypted in S3, encrypted in transit via TLS, and only decrypted in memory.

KMS Key Management

Default AWS-Managed Key: - Key alias: alias/aws/ssm - No additional cost - AWS manages rotation - Suitable for most use cases

Custom Customer-Managed Key (CMK): - You control rotation schedule - More granular access control - Additional cost ($1/month + API usage) - Required for cross-account access

Theory: Use the default AWS-managed key unless you have specific compliance requirements or need cross-account parameter sharing. The default key provides strong encryption (AES-256) with automatic rotation and no management overhead.

Encryption and Access Flow

sequenceDiagram
    participant App as Django Application
    participant SSM as SSM Parameter Store
    participant KMS as AWS KMS
    participant S3 as S3 (Storage)

    App->>SSM: GetParameter(WithDecryption=true)
    SSM->>S3: Retrieve encrypted value
    S3-->>SSM: Return encrypted blob
    SSM->>KMS: Decrypt(encrypted_blob)
    KMS->>KMS: Verify IAM permissions
    KMS-->>SSM: Return decrypted value
    SSM-->>App: Return plain text parameter

    Note over App,S3: TLS encrypts all network traffic
    Note over S3: Values stored encrypted at rest
    Note over App: Decrypted value only in memory

Theory: Encryption at rest protects against storage compromise. Even if an attacker gained access to the underlying S3 bucket, they couldn't decrypt parameters without KMS access. The application must have both SSM and KMS permissions.

IAM Policies for Access

Principle of Least Privilege

IAM policies should grant the minimum permissions required for the application to function.

ECS Task Role Pattern:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath"
      ],
      "Resource": [
        "arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt"
      ],
      "Resource": [
        "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
      ]
    }
  ]
}

Theory: This policy grants: 1. Read-only access to parameters under /prod/poseidon/* 2. Decrypt permission for the KMS key encrypting those parameters 3. No write access - tasks can't modify parameters 4. No access to other environments - /dev/* or /staging/* are blocked

Scope-Based Permissions

Path-Based Scoping:

{
  "Effect": "Allow",
  "Action": ["ssm:GetParameter*"],
  "Resource": [
    "arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/*",
    "arn:aws:ssm:us-east-1:123456789012:parameter/prod/shared/*"
  ]
}

This grants access to: - Service-specific parameters: /prod/poseidon/* - Shared parameters: /prod/shared/*

Parameter-Specific Scoping:

{
  "Effect": "Allow",
  "Action": ["ssm:GetParameter"],
  "Resource": [
    "arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/SECRET_KEY",
    "arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/DATABASE_PASSWORD"
  ]
}

This grants access to specific parameters only - maximum security, maximum maintenance.

Theory: Path-based scoping strikes the best balance for small teams. It's secure (can't access other environments), flexible (new parameters don't need IAM updates), and maintainable (one policy per service/environment combination).

Development vs Production IAM

Development Environment (more permissive): - Access to /dev/* parameters - Possibly /staging/* for testing - Never /prod/* from development

Production Environment (most restrictive): - Access only to /prod/service/* - Read-only access - Specific KMS key permissions

// Production task role - minimal permissions
{
  "Effect": "Allow",
  "Action": ["ssm:GetParameter", "ssm:GetParametersByPath"],
  "Resource": "arn:aws:ssm:*:*:parameter/prod/poseidon/*"
}

// Development task role - broader for debugging
{
  "Effect": "Allow",
  "Action": ["ssm:GetParameter*", "ssm:DescribeParameters"],
  "Resource": "arn:aws:ssm:*:*:parameter/dev/*"
}

Theory: Production should have the absolute minimum permissions to run. Development can have broader permissions for debugging and experimentation. Never give production tasks access to development parameters or write access to any parameters.

Local Development with LocalStack

LocalStack SSM Simulation

LocalStack provides a local AWS cloud stack for development and testing. The SSM service runs at http://localhost:4566.

Theory: LocalStack enables developers to work with SSM Parameter Store locally without AWS credentials, costs, or network dependencies. It simulates the SSM API, allowing the same code to run locally and in production.

Configuration for LocalStack

Environment Variables:

# .env for local development
AWS_ENDPOINT_URL=http://localstack:4566
AWS_DEFAULT_REGION=us-east-1
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test

Django Settings Integration:

# settings/development.py
import os
import boto3

# Configure boto3 for LocalStack
ssm_endpoint = os.environ.get('AWS_ENDPOINT_URL')
if ssm_endpoint:
    # LocalStack configuration
    ssm_client = boto3.client(
        'ssm',
        endpoint_url=ssm_endpoint,
        region_name='us-east-1',
        aws_access_key_id='test',
        aws_secret_access_key='test'
    )
else:
    # Production configuration (uses IAM role)
    ssm_client = boto3.client('ssm', region_name='us-east-1')

Theory: The application detects LocalStack via AWS_ENDPOINT_URL. When set, it uses LocalStack. When unset (production), it uses real AWS. Same application code, different runtime environment.

Initializing LocalStack Parameters

Real-world production applications need a mechanism to populate LocalStack with development parameters. This is typically done with an initialization script.

Script Pattern (based on zenith project):

#!/usr/bin/env python3
"""Initialize LocalStack SSM with development parameters."""

import boto3

def init_localstack_ssm():
    ssm = boto3.client(
        'ssm',
        endpoint_url='http://localhost:4566',
        region_name='us-east-1',
        aws_access_key_id='test',
        aws_secret_access_key='test'
    )

    # Parameters to create
    parameters = {
        '/dev/django/SECRET_KEY': 'dev-secret-key-not-for-production',
        '/dev/django/DATABASE_PASSWORD': 'postgres',
        '/dev/auth0/CLIENT_SECRET': 'dev-auth0-secret',
    }

    for name, value in parameters.items():
        ssm.put_parameter(
            Name=name,
            Value=value,
            Type='SecureString',
            Overwrite=True
        )
        print(f"Created parameter: {name}")

if __name__ == '__main__':
    init_localstack_ssm()

Theory: The initialization script creates development-safe values for all parameters the application needs. These values work for local development but are clearly not production secrets. Run this script after starting LocalStack to prepare the environment.

Production Parameter Export Pattern

A more sophisticated approach exports production parameter names (not values) and creates development equivalents:

Two-Phase Process:

  1. Export Production Structure (read-only, safe):
  2. List all production parameter names
  3. Capture metadata (type, description)
  4. Save to local file for review

  5. Initialize LocalStack (uses exported structure):

  6. Read parameter names from export
  7. Create development values (not production values)
  8. Optionally redact sensitive parameters

Implementation:

# Phase 1: Export production structure (safe, read-only)
def export_production_parameters():
    """Fetch production parameter names and metadata."""
    prod_ssm = boto3.client('ssm', region_name='us-east-1')

    # Get all parameter names
    paginator = prod_ssm.get_paginator('describe_parameters')
    parameters = []

    for page in paginator.paginate():
        for param in page['Parameters']:
            parameters.append({
                'Name': param['Name'],
                'Type': param['Type'],
                'Description': param.get('Description', ''),
            })

    # Save to file
    with open('production-parameters.json', 'w') as f:
        json.dump(parameters, f, indent=2)

    return parameters

# Phase 2: Initialize LocalStack from export
def init_localstack_from_export():
    """Create LocalStack parameters from production structure."""
    local_ssm = boto3.client(
        'ssm',
        endpoint_url='http://localhost:4566',
        aws_access_key_id='test',
        aws_secret_access_key='test'
    )

    with open('production-parameters.json') as f:
        parameters = json.load(f)

    for param in parameters:
        # Use development-safe values
        dev_value = f"[DEV-VALUE-{param['Name']}]"

        local_ssm.put_parameter(
            Name=param['Name'],
            Value=dev_value,
            Type=param['Type'],
            Description=f"Development: {param['Description']}"
        )

Theory: This two-phase approach ensures: 1. Production parameters are never exposed during export (names only, not values) 2. Development environment matches production structure 3. Missing parameters are caught early (not at runtime) 4. New parameters in production automatically sync to development

Docker Compose Integration

docker-compose.yml:

services:
  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      - SERVICES=ssm,kms,s3
      - DEBUG=1
    volumes:
      - localstack-data:/var/lib/localstack

  django:
    build: .
    environment:
      - AWS_ENDPOINT_URL=http://localstack:4566
      - AWS_DEFAULT_REGION=us-east-1
      - AWS_ACCESS_KEY_ID=test
      - AWS_SECRET_ACCESS_KEY=test
    depends_on:
      - localstack

Theory: Docker Compose orchestrates LocalStack alongside the application. The depends_on ensures LocalStack starts before Django. The shared network allows Django to reach LocalStack at http://localstack:4566.

Startup Scripts

Initialize LocalStack parameters automatically when containers start:

#!/bin/bash
# scripts/wait-for-localstack.sh

echo "Waiting for LocalStack to be ready..."
until curl -s http://localstack:4566/_localstack/health | grep -q "\"ssm\": \"available\""; do
  sleep 1
done

echo "LocalStack ready, initializing SSM parameters..."
python scripts/init-localstack-ssm.py

Theory: LocalStack takes a few seconds to start. The health check endpoint reports when services are available. Only after SSM is ready should you create parameters. The startup script ensures proper ordering.

Parameter Versioning

Automatic Version Tracking

SSM Parameter Store automatically versions parameters when you update them:

# Create initial parameter (version 1)
aws ssm put-parameter \
  --name "/prod/django/SECRET_KEY" \
  --value "initial-secret" \
  --type "SecureString"

# Update parameter (version 2)
aws ssm put-parameter \
  --name "/prod/django/SECRET_KEY" \
  --value "updated-secret" \
  --type "SecureString" \
  --overwrite

# Get current version (version 2)
aws ssm get-parameter \
  --name "/prod/django/SECRET_KEY" \
  --with-decryption

# Get specific version
aws ssm get-parameter \
  --name "/prod/django/SECRET_KEY:1" \
  --with-decryption

Theory: Versioning provides an audit trail and rollback capability. If a parameter update breaks production, you can immediately retrieve the previous version. Every update increments the version number automatically.

Version Labels

You can also apply labels to specific versions:

# Label version 3 as "stable"
aws ssm label-parameter-version \
  --name "/prod/django/SECRET_KEY" \
  --parameter-version 3 \
  --labels "stable"

# Get parameter by label
aws ssm get-parameter \
  --name "/prod/django/SECRET_KEY:stable" \
  --with-decryption

Theory: Labels provide semantic meaning to versions. You might label the current production version as stable, pre-deployment versions as staging, or known-good versions as verified. This is useful for staged rollouts or blue-green deployments.

Version Retention

SSM retains up to 100 versions of a parameter. Older versions are automatically purged.

Theory: 100 versions is sufficient for most use cases. If you update a parameter daily, that's over 3 months of history. For critical parameters, document the current version in your runbooks or deployment scripts.

Parameter Retrieval Patterns

Python Implementation

Basic Retrieval:

import boto3

ssm = boto3.client('ssm', region_name='us-east-1')

def get_parameter(name: str) -> str:
    """Retrieve a single parameter value."""
    response = ssm.get_parameter(
        Name=name,
        WithDecryption=True
    )
    return response['Parameter']['Value']

# Usage
secret_key = get_parameter('/prod/django/SECRET_KEY')

Theory: WithDecryption=True is essential for SecureString parameters. Without it, you get the encrypted blob, not the plain text value.

Batch Retrieval

Retrieving parameters individually is slow. Batch retrieval is more efficient:

def get_parameters(names: list[str]) -> dict[str, str]:
    """Retrieve multiple parameters in one call."""
    response = ssm.get_parameters(
        Names=names,
        WithDecryption=True
    )

    # Convert to dict
    parameters = {
        param['Name']: param['Value']
        for param in response['Parameters']
    }

    # Check for invalid parameters
    invalid = response.get('InvalidParameters', [])
    if invalid:
        raise ValueError(f"Invalid parameters: {invalid}")

    return parameters

# Usage
params = get_parameters([
    '/prod/django/SECRET_KEY',
    '/prod/django/DATABASE_PASSWORD',
    '/prod/auth0/CLIENT_SECRET',
])

Theory: get_parameters() retrieves up to 10 parameters in a single API call. This reduces latency (one round trip vs multiple) and cost (fewer API calls).

Path-Based Retrieval

Retrieve all parameters under a path:

def get_parameters_by_path(path: str) -> dict[str, str]:
    """Retrieve all parameters under a path."""
    paginator = ssm.get_paginator('get_parameters_by_path')

    parameters = {}
    for page in paginator.paginate(
        Path=path,
        Recursive=True,
        WithDecryption=True
    ):
        for param in page['Parameters']:
            # Strip path prefix from name
            key = param['Name'].replace(f"{path}/", "")
            parameters[key] = param['Value']

    return parameters

# Usage
params = get_parameters_by_path('/prod/django')
# Returns: {'SECRET_KEY': '...', 'DATABASE_PASSWORD': '...'}

Theory: Path-based retrieval is convenient for loading all parameters for a service. The Recursive=True option traverses nested paths. Be careful with broad paths - they can retrieve many parameters and increase startup time.

Caching Strategy

Problem: Fetching parameters on every request is slow and expensive.

Solution: Cache parameters in application memory at startup.

class ParameterCache:
    """Cache SSM parameters in memory."""

    def __init__(self):
        self._cache = {}
        self._ssm = boto3.client('ssm', region_name='us-east-1')

    def load_parameters(self, names: list[str]) -> None:
        """Load parameters into cache at startup."""
        response = self._ssm.get_parameters(
            Names=names,
            WithDecryption=True
        )

        for param in response['Parameters']:
            self._cache[param['Name']] = param['Value']

    def get(self, name: str) -> str:
        """Get parameter from cache."""
        if name not in self._cache:
            raise KeyError(f"Parameter {name} not in cache")
        return self._cache[name]

# Initialize at application startup
param_cache = ParameterCache()
param_cache.load_parameters([
    '/prod/django/SECRET_KEY',
    '/prod/django/DATABASE_PASSWORD',
])

# Use throughout application
SECRET_KEY = param_cache.get('/prod/django/SECRET_KEY')

Theory: Load parameters once at startup, then serve from memory. This provides: 1. Fast access - No network latency after initial load 2. Lower cost - Fewer SSM API calls 3. Fail-fast - Missing parameters crash at startup, not during requests 4. Consistent values - Parameters don't change mid-request

Trade-off: Parameter updates require application restart. This is acceptable for most Django deployments where configuration changes trigger redeployment anyway.

Production Implementation Pattern

Real-world implementation from the zenith project:

# poseidon/commons/config/ps_config.py
import boto3
from botocore.exceptions import ClientError
import logging

logger = logging.getLogger(__name__)

class SSMParameterManager:
    """Manages SSM parameter retrieval with caching."""

    def __init__(self, ssm_client=None):
        self.ssm = ssm_client or boto3.client('ssm', region_name='us-east-1')

    def get_parameter(self, parameter_name: str) -> str:
        """Retrieve parameter from SSM."""
        try:
            response = self.ssm.get_parameter(
                Name=parameter_name,
                WithDecryption=True
            )
            return response['Parameter']['Value']
        except ClientError as e:
            if e.response['Error']['Code'] == 'ParameterNotFound':
                raise ValueError(f"Parameter '{parameter_name}' not found in SSM.")
            else:
                logger.exception(f"Failed to retrieve parameter '{parameter_name}'")
                raise

class PlanstoneConfig:
    """Configuration manager for production settings."""

    # Whitelist of allowed parameters
    _params = [
        'DJANGO_SECRET_KEY',
        'DATABASE_PASSWORD',
        'AUTH0_CLIENT_SECRET',
        'SENDGRID_API_KEY',
        # ... more parameters
    ]

    def __init__(self, ssm_client=None):
        self.ssm_manager = SSMParameterManager(ssm_client=ssm_client)

    def get_param(self, param_name: str) -> str:
        """Get parameter with validation."""
        if param_name not in self._params:
            raise ValueError(f"Unknown parameter: {param_name}")

        return self.ssm_manager.get_parameter(param_name)

# Global instance
pconfig = PlanstoneConfig()

Usage in Settings:

# settings/production.py
from poseidon.commons.config.ps_config import pconfig

SECRET_KEY = pconfig.get_param('DJANGO_SECRET_KEY')
DATABASE_PASSWORD = pconfig.get_param('DATABASE_PASSWORD')

Theory: This pattern provides: 1. Parameter validation - Only whitelisted parameters can be retrieved 2. Error handling - Clear messages for missing parameters 3. Testability - Can inject mock SSM client 4. Centralization - All SSM access goes through one class 5. Logging - Failed retrievals are logged for debugging

Mermaid Diagrams

SSM Parameter Store Architecture

graph TB
    subgraph "Development (LocalStack)"
        D1[Docker Compose] --> D2[LocalStack Container]
        D2 --> D3[SSM Mock Service]
        D4[Django App] --> D3
        D5[Init Script] --> D3
    end

    subgraph "Production (AWS)"
        P1[ECS Task] --> P2[IAM Task Role]
        P2 --> P3[SSM Parameter Store]
        P3 --> P4[KMS Encryption]
        P5[S3 Storage] --> P4
    end

    style D1 fill:#E3F2FD
    style P1 fill:#FFEBEE

Parameter Access Flow

sequenceDiagram
    participant App as Django Application
    participant Cache as Parameter Cache
    participant SSM as SSM Parameter Store
    participant IAM as IAM Service

    Note over App,SSM: Application Startup

    App->>Cache: Initialize parameter cache
    Cache->>IAM: Verify task role permissions
    IAM-->>Cache: Permissions validated
    Cache->>SSM: GetParameters(batch)
    SSM-->>Cache: Return decrypted values
    Cache->>Cache: Store in memory

    Note over App,SSM: Request Processing

    App->>Cache: Get parameter
    Cache-->>App: Return from memory (fast)

    Note over App: No SSM calls during requests

Environment-Based Configuration Decision

graph TD
    A[Application Starts] --> B{Environment?}
    B -->|Development| C[Check AWS_ENDPOINT_URL]
    B -->|Production| D[Use IAM Role]

    C --> E{LocalStack URL set?}
    E -->|Yes| F[Connect to LocalStack]
    E -->|No| G[Fall back to .env]

    F --> H[Load from LocalStack SSM]
    G --> I[Load from environment vars]
    D --> J[Load from AWS SSM]

    H --> K[Application Ready]
    I --> K
    J --> K

    style K fill:#90EE90

Next Steps

  1. Set up LocalStack in your Docker Compose configuration
  2. Create an initialization script to populate LocalStack SSM parameters
  3. Implement a parameter cache class for production
  4. Configure IAM policies for your ECS task roles
  5. Document your parameter naming convention
  6. Create a runbook for parameter rotation procedures

SSM Best Practices

  • Use hierarchical naming with environment prefixes
  • Always use SecureString for secrets
  • Cache parameters at startup, not per-request
  • Use path-based IAM policies for flexibility
  • Initialize LocalStack automatically in development

Common Pitfalls

  • Fetching parameters on every request (slow, expensive)
  • Using String type for secrets (no encryption)
  • Forgetting WithDecryption=True (returns encrypted blob)
  • Over-scoping IAM permissions (granting access to all parameters)
  • Not initializing LocalStack in development (runtime failures)

Production Considerations

  • Never log parameter values, even in debug mode
  • Rotate secrets regularly and track rotation in parameter descriptions
  • Monitor SSM API costs - batch retrievals are more efficient
  • Use parameter versioning for rollback capability
  • Ensure ECS tasks have both SSM and KMS permissions