AWS SSM Parameter Store¶
What it does¶
- Reads parameters via
boto3.client("ssm").get_parameter(...)for single fetches andget_parameters(...)for batches (capped at 10 names per call by SSM;vaultlychunks larger requests automatically). - Decrypts
SecureStringparameters by passingWithDecryption=True(default; passwith_decryption=Falseto disable). - Maps boto3 / botocore exceptions to vaultly's error hierarchy.
Default config¶
AWSSSMBackend(region_name="...") constructs a boto3 SSM client with a
sensible default botocore.Config:
DEFAULT_CONFIG = Config(
retries={"mode": "adaptive", "max_attempts": 3},
connect_timeout=2.0,
read_timeout=5.0,
)
This protects you from indefinite hangs on a network blip. Override:
from botocore.config import Config
backend = AWSSSMBackend(
region_name="eu-west-1",
config=Config(read_timeout=10.0),
)
Or pass a fully-configured client:
import boto3
client = boto3.client("ssm", config=my_org_config)
backend = AWSSSMBackend(client=client)
Versioning¶
SSM stores every change as a new version. Pin a specific one:
class App(SecretModel):
pinned: str = Secret("/db/password", version=2)
latest: str = Secret("/db/password")
vaultly translates version=2 to SSM's Name=/db/password:2 syntax.
IAM¶
The IAM identity running your app needs ssm:GetParameter for single
reads and ssm:GetParameters for batched reads. If your secrets are
SecureString (recommended), also grant kms:Decrypt on the relevant
KMS key.
A minimal least-privilege policy for prod:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParameters"],
"Resource": "arn:aws:ssm:eu-west-1:123:parameter/prod/*"
},
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:eu-west-1:123:key/<key-id>"
}
]
}
Error mapping¶
| boto3 / SSM error | vaultly maps to |
|---|---|
ParameterNotFound |
SecretNotFoundError |
AccessDeniedException, UnauthorizedAccessException |
AuthError |
ThrottlingException, RequestLimitExceeded, ServiceUnavailable, InternalServerError |
TransientError |
BotoCoreError (network-level) |
TransientError |
Anything else (unknown ClientError codes) |
TransientError — let RetryingBackend decide |
Combining with retries¶
from vaultly import RetryingBackend
from vaultly.backends.aws_ssm import AWSSSMBackend
backend = RetryingBackend(
AWSSSMBackend(region_name="eu-west-1"),
max_attempts=3,
total_timeout=10.0,
)
Note that boto3 already retries at the transport layer (configured via
Config.retries). RetryingBackend is a semantic layer on top — it
retries vaultly's TransientError, which only fires after boto3 has
already given up. Don't increase both budgets to "many" or you'll
multiply.
Recipes¶
Hierarchical, multi-stage app¶
class DbConfig(SecretModel):
password: str = Secret("/{stage}/db/password")
pool_size: int = Secret("/{stage}/db/pool")
class App(SecretModel, validate="fetch"):
stage: str
db: DbConfig
api_key: str = Secret("/{stage}/api/key")
flags: dict = Secret("/{stage}/feature_flags")
config = App(stage="prod", db={}, backend=AWSSSMBackend(region_name="eu-west-1"))
validate="fetch" prefetches everything via one batched GetParameters
call (or two if you have more than 10 secrets), so a missing or
mis-permissioned parameter fails the deploy at startup rather than at
3 a.m. when someone reads it.
Local + prod with the same model¶
import os
from vaultly import EnvBackend
from vaultly.backends.aws_ssm import AWSSSMBackend
def make_backend():
if os.getenv("ENV") == "local":
return EnvBackend(prefix="MYAPP")
return AWSSSMBackend(region_name=os.environ["AWS_REGION"])
config = App(stage=os.environ["STAGE"], backend=make_backend())