Load pydantic settings from AWS Parameter Store

pydantic-settings reads environment variables and .env files out of the box, but not AWS Parameter Store. The clean integration is a custom settings source, not a pile of boto3 calls scattered through your app. This page builds it, extending Settings from AWS Parameter Store.

Problem 1: fetching parameters one by one

# ANTI-PATTERN: N API calls, no validation, scattered everywhere
db = boto3.client("ssm").get_parameter(Name="/myapp/database_url")["Parameter"]["Value"]
key = boto3.client("ssm").get_parameter(Name="/myapp/api_key")["Parameter"]["Value"]

Per-parameter calls are slow, untyped, and bypass the settings model entirely.

Problem 2: forgetting decryption

# ANTI-PATTERN: SecureString returned still encrypted
ssm.get_parameters_by_path(Path="/myapp/")   # WithDecryption defaults to False

Without WithDecryption=True, SecureString parameters come back as ciphertext.

Secure implementation

# config/ssm_source.py
import boto3
from pydantic import SecretStr
from pydantic_settings import (
    BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict,
)

class SSMSource(PydanticBaseSettingsSource):
    def __call__(self) -> dict[str, object]:
        ssm = boto3.client("ssm")
        values: dict[str, object] = {}
        for page in ssm.get_paginator("get_parameters_by_path").paginate(
            Path="/myapp/", Recursive=True, WithDecryption=True,   # decrypt SecureStrings
        ):
            for p in page["Parameters"]:
                values[p["Name"].rsplit("/", 1)[-1].lower()] = p["Value"]
        return values

    def get_field_value(self, field, field_name):
        return None, field_name, False

class Settings(BaseSettings):
    model_config = SettingsConfigDict(extra="forbid")
    database_url: str
    api_key: SecretStr

    @classmethod
    def settings_customise_sources(cls, settings_cls, init_settings,
                                   env_settings, dotenv_settings, file_secret_settings):
        return (init_settings, env_settings, SSMSource(settings_cls))  # env still wins

One paginated call fetches the whole prefix, decryption is explicit, and the model validates the result. Environment variables remain above SSM so local overrides still work.

Gotchas & version-specific behaviour

  • settings_customise_sources returns sources highest-priority first — put env_settings before the SSM source.
  • get_parameters_by_path is paginated; use the paginator or you will miss parameters past the first page.
  • WithDecryption=True requires kms:Decrypt on the parameter’s KMS key.
  • Strip the path prefix to map /myapp/database_url to the field database_url.

Production parity checklist

  • A single custom source, not scattered get_parameter calls.
  • WithDecryption=True with kms:Decrypt granted.
  • IAM scoped to the exact path prefix.
  • Secret fields typed SecretStr; extra="forbid" set.
  • Environment variables outrank SSM for local overrides.

Conclusion

A PydanticBaseSettingsSource subclass makes Parameter Store just another validated source under the environment. To keep it fast at scale, add caching — see Cache Parameter Store Values to Reduce API Calls.