Configuration & secrets

Loading model

All configuration is read from os.environ — by config.py (the Flask config object) and, at import time, by pyb_flask_utils. Values arrive from three places, highest precedence first:

  1. Lambda environment variables, set by template.yaml from its Env mapping, keyed by the EnvName parameter (the only value samconfig.toml passes) — non-secret values only (URLs, log level, ENV_NAME, the secret’s name).

  2. actionstools/<env> in Secrets Manager, copied into os.environ by secrets_loader.py at cold start with setdefault, so a Lambda environment variable with the same name wins.

  3. common/crm, the shared CRM secret, read by pyb_flask_utils.auth into its own dictionary (it is not copied to os.environ): the instance-id → token pairs for Authorization: Token … and a fallback AUTH_JWT_RSA_PUBLIC_KEY.

secret_id = os.environ.get("APP_SECRET_ID")
if not secret_id or os.environ.get("AWS_SECRETS_MANAGER") == "False":
    return

secrets_loader must be imported before anything else reads the environment. It is the first import of config.py and of actionstools_app/init.py; keep it there (# noqa: F401). A module-level os.environ[…​] in new code is fine as long as that module is imported after config.

The service secret

One JSON secret per account, holding only secrets:

{
  "SLACK_TOKEN": "xoxb-…",
  "SENTRY_DSN": "https://<key>@<org>.ingest.sentry.io/<project>",
  "MESSAGING_API_KEY": "<messaging app token for instance 8001>",
  "EVENT_LOGGER_API_KEY": "<event logger token for instance 8001>",
  "AUTH_JWT_RSA_PUBLIC_KEY": "ssh-rsa AAAA… (CRM JWT public key; required by pyb_flask_utils)"
}
Env Secret Account

prod

actionstools/prod

625663093496

stage

actionstools/stage

605668041275

The functions' IAM roles allow GetSecretValue on exactly this secret and common/crm (AWSSecretsManagerGetSecretValuePolicy). Every key is also documented in Settings & secrets reference.

Changing a value

A secret. Update the secret, then force new execution environments — secrets are read once per cold start:

aws secretsmanager put-secret-value --secret-id actionstools/stage \
  --secret-string file://actionstools-stage.json --profile dev
# any configuration change recycles the environments, e.g. re-deploy:
AWS_PROFILE=dev make deploy stage=stage

put-secret-value replaces the whole JSON; fetch it first with get-secret-value, edit, and write it back.

A non-secret value. Edit the environment’s column of the Env mapping in template.yaml (for a new value, add the key to both prod and stage and read it with !FindInMap [Env, !Ref EnvName, <Key>]), commit, deploy. Do not add it to parameter_overrides.

A new secret key. Add it to both secrets, read it in config.py with an assert if it is required, add a dummy value to tests/init.py and a placeholder to .env.example, and document it in Settings & secrets reference.

Environments

prod stage

Stack

actionstools-prod

actionstools-stage

ENV_NAME

production

staging

BASE_PREFIX

/actionstools

(empty)

Tables

em_actions_*, em_actions_notes2

em_actions_*_staging

VPC

subnet-adecc0c8, subnet-4f415d16, sg-6c6f230a

subnet-0f2f29c7510986dd0, subnet-0b027bfee9b60750e, sg-05fdec837243c8245

Counters schedule

on

off

Swagger

at the base URL

at the base URL

Gotchas

  • Secrets are cached for the life of an execution environment. After a rotation, old environments keep the old value until they are recycled.

  • AWS_SECRETS_MANAGER=False disables both secrets, including common/crm.

  • ENV_NAME selects table names, not just labels: anything other than production uses the _staging tables.

  • The secret’s name, not its ARN. APP_SECRET_ID is the name; Secrets Manager ARNs end in a random suffix, which is why IAM policies use <name>-*.