SSM Parameter Store: Hierarchical Runtime Config for Coding Agents

0 views

Hardcoding MODEL_ID, TOOL_TIMEOUT_MS, and ALLOWED_REPOS in Lambda environment variables guarantees a redeploy for every tenant tweak. SSM Parameter Store gives you a hierarchical tree — /agents/{env}/{tenant}/tools/* — with IAM path prefixes, standard vs advanced tiers, and SecureString values that can point at Secrets Manager instead of duplicating secrets. AppConfig remains the right hammer for kill switches and progressive flags; Parameter Store is the right hammer for config trees. Do not conflate them.

⚡ TL;DR: Put durable hierarchical config in SSM (GetParametersByPath). Put secrets in Secrets Manager; store only ARNs/names in SecureString or String params. Use IAM ssm:GetParameter* with path conditions per tenant. Use AppConfig for boolean kill switches and timed rollouts. Related: KMS decrypt grants, Verified Permissions.

Design the path taxonomy first

text
/agents
  /prod
    /_shared
      /model_id = anthropic.claude-...
      /max_tool_depth = 8
    /tenant-acme
      /tools
        /run_tests/timeout_ms = 120000
        /run_tests/enabled = true
        /git_push/allowed_repos = ["acme/api","acme/web"]
        /git_push/secret_ref = arn:aws:secretsmanager:...:secret:agents/acme/github
    /tenant-globex
      /tools/...
  /staging/...

Rules of thumb:

  • _shared for env defaults; tenant overrides on read (merge in code).
  • Tool-specific leaves under tools/{tool_name}/….
  • Never put raw GitHub tokens in String params — secret_ref only.
  • Keep kill-switch booleans duplicated in AppConfig if you need timed flags / client-side agents; SSM enabled is fine for slower config.

Read path: GetParametersByPath with merge

typescript
import {
  SSMClient,
  GetParametersByPathCommand,
} from "@aws-sdk/client-ssm";

const ssm = new SSMClient({});

export async function loadAgentConfig(env: string, tenant: string) {
  const prefixes = [
    `/agents/${env}/_shared`,
    `/agents/${env}/tenant-${tenant}`,
  ];
  const map = new Map<string, string>();
  for (const path of prefixes) {
    let token: string | undefined;
    do {
      const page = await ssm.send(
        new GetParametersByPathCommand({
          Path: path,
          Recursive: true,
          WithDecryption: true,
          NextToken: token,
        })
      );
      for (const p of page.Parameters ?? []) {
        const key = p.Name!.split(path)[1] ?? p.Name!;
        map.set(key.replace(/^\//, ""), p.Value ?? "");
      }
      token = page.NextToken;
    } while (token);
  }
  return map; // tenant keys overwrite shared when you apply in order carefully
}
python
# Python merge — shared first, tenant second
import boto3
ssm = boto3.client("ssm")

def params_under(path: str) -> dict[str, str]:
    out, token = {}, None
    while True:
        kw = dict(Path=path, Recursive=True, WithDecryption=True)
        if token:
            kw["NextToken"] = token
        resp = ssm.get_parameters_by_path(**kw)
        for p in resp.get("Parameters", []):
            rel = p["Name"][len(path):].lstrip("/")
            out[rel] = p["Value"]
        token = resp.get("NextToken")
        if not token:
            return out

def load(env: str, tenant: str) -> dict[str, str]:
    cfg = params_under(f"/agents/{env}/_shared")
    cfg.update(params_under(f"/agents/{env}/tenant-{tenant}"))
    return cfg

Cache in-process with a short TTL (30–60s) or use the SSM freeform/extension patterns on Lambda to avoid hot-path throttling. For high QPS interactive agents, consider a thin Redis cache — but start with path reads + TTL.

IAM prefix policies (tenant isolation)

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOwnTenantConfig",
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath"
      ],
      "Resource": [
        "arn:aws:ssm:us-east-1:123456789012:parameter/agents/prod/_shared",
        "arn:aws:ssm:us-east-1:123456789012:parameter/agents/prod/_shared/*",
        "arn:aws:ssm:us-east-1:123456789012:parameter/agents/prod/tenant-acme",
        "arn:aws:ssm:us-east-1:123456789012:parameter/agents/prod/tenant-acme/*"
      ]
    },
    {
      "Sid": "DenyOtherTenants",
      "Effect": "Deny",
      "Action": "ssm:GetParameter*",
      "Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/agents/prod/tenant-*",
      "Condition": {
        "StringNotLike": {
          "ssm:ResourceTag/tenant": "acme"
        }
      }
    }
  ]
}

Tag parameters with tenant=acme when you create them, or rely on path-scoped resources alone. Runtime authorization for tool names still belongs in Verified Permissions / Cedar — SSM tells you timeouts and allowlists; Cedar decides if this principal may call git_push.

SecureString vs Secrets Manager

Store Use for
SSM String Non-secret config (model id, timeouts, JSON allowlists)
SSM SecureString Mildly sensitive config encrypted by KMS (prefer refs)
Secrets Manager Rotating credentials, GitHub PATs, API keys
SSM value = secret ARN Pointer pattern — fetch secret at use time
bash
# Pointer — Parameter Store holds the ARN only
aws ssm put-parameter \
  --name /agents/prod/tenant-acme/tools/git_push/secret_ref \
  --type String \
  --value arn:aws:secretsmanager:us-east-1:123456789012:secret:agents/acme/github

# Decrypt path for any SecureString still uses KMS grants —
# see KMS decrypt grants for agent tools

Follow KMS decrypt grants so tools never ship long-lived plaintext keys in env.

AppConfig kill switches vs SSM config trees

Concern AppConfig SSM Parameter Store
Instant kill switch / flag ✅ designed for it Possible but weaker rollout
Timed/percentage exposure ❌ DIY
Hierarchical tenant config trees Awkward ✅ natural paths
Large JSON documents Limited size / better as flags Standard 4KB / advanced 8KB per param
Agent extension / poll Strong GetParametersByPath

✅ AppConfig: tools.run_tests.enabled=false during an incident (kill switches post).
✅ SSM: tools.run_tests.timeout_ms=180000 and allowlists that change weekly.
❌ Putting your entire tenant config blob only in AppConfig flags, or putting kill switches only in SSM with no cache-bust story.

Checklist: ship hierarchical agent config

  • [ ] Agree path taxonomy /agents/{env}/_shared + tenant-{id}/tools/...
  • [ ] Merge helper with TTL cache in the agent runtime
  • [ ] IAM path-scoped read roles per tenant (or per runtime)
  • [ ] Secrets as ARNs; Secrets Manager owns rotation
  • [ ] AppConfig for kill switches; SSM for config trees — document both
  • [ ] Cedar/IAM still authorize tool invocation
  • [ ] CloudWatch metric on SSM throttle / cache hit rate

Hierarchical SSM config is how multi-tenant coding agents stay tunable without redeploys — as long as you keep secrets, flags, and authorization in the right systems.

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.