Operations

Environment variables

Env vars are stored in AWS SSM Parameter Store under /cumulus/<app>/<KEY>. The agent fetches them at deploy time and writes a .env file (mode 0600) before starting the service. Secrets are encrypted with your CMK; they never appear in build artifacts.

# Set a plain value
cumulus app env-set api LOG_LEVEL=info

# Set an encrypted secret (uses your CMK)
cumulus app env-set api DATABASE_URL="postgres://user:pass@host/db" --secret
cumulus app env-set api SECRET_KEY="$(openssl rand -hex 32)" --secret

# Pull all vars as KEY=VALUE (e.g. to populate a local .env)
cumulus app env-pull api > .env.production

Reference a secret in cumulus.toml

Use a { secret = "..." } reference to an SSM path instead of inline values:

[app.env]
LOG_LEVEL    = "info"                                # inline
DATABASE_URL = { secret = "/cumulus/api/DATABASE_URL" }  # from SSM
SECRET_KEY   = { secret = "/cumulus/api/SECRET_KEY" }

Secrets declared in [app.env] are read by the agent from SSM at deploy time. For Lambda apps, secrets are read and set as the function's environment variables at deploy.

Rollback

A failed health check rolls the deploy back automatically. To revert on purpose, redeploy a known-good commit. Cumulus keeps the last 5 releases on each server, so you can pick one and deploy its sha.

# List retained releases (newest first, current marked)
cumulus app releases api

# Redeploy a known-good commit to revert
cumulus app deploy api --sha <previous-good-sha>
Automatic rollback: if the health check fails during a deploy, the previous release is restored automatically, with no manual step needed.

Lambda

For Lambda apps the revert is instant: redeploying a previous commit re-points the live alias at that version, with no rebuild.

cumulus app deploy handler --sha <previous-good-sha>

Logs

# Stream live logs from an EC2 app (Ctrl-C to stop)
cumulus app logs api

# List managed servers and their live agent status
cumulus server list
Jun 18 14:22:11 prod-1 api[1234]: INFO listening on 0.0.0.0:8080
Jun 18 14:22:43 prod-1 api[1234]: INFO GET /health 200 1ms
Jun 18 14:23:01 prod-1 api[1234]: INFO POST /api/users 201 45ms

Logs stream directly from journald on the server via the agent, with no CloudWatch setup required.

Release tasks

Tasks are commands that run in your app's release context on the server: database migrations, cache warmup, asset compilation, and so on. Declare them in [app.tasks] and list the ones that should run before cutover in release.

[app.tasks]
migrate    = "python manage.py migrate --noinput"
seed       = "python manage.py seed_data"
check      = "python manage.py check --deploy"

# These run before the new release goes live (expand/contract pattern)
release = ["migrate"]

Run a task on demand

# Run a one-off task on the server (exits with the task's exit code)
cumulus app run api seed
cumulus app run api check
Migration safety: Database migrations run before the new binary starts but while the old binary is still serving traffic. Write backward-compatible migrations (expand/contract pattern). The old binary must continue to work against the new schema during the health check window.

Restart in place

# Restart the service without redeploying (recover a hung process)
cumulus app restart api

Multiple environments

Deploy the same app to separate servers for staging and production. Override the server per-environment in [app.environments.<name>].

[[app]]
name   = "api"
runtime = "rust"
target  = "ec2"
binary  = "api"
server  = "prod-1"         # default (production)

[app.environments.staging]
server = "staging-1"        # override for staging

# [app.health_check]             # optional: probe a live endpoint instead of a fixed wait
# path = "/health"
# port = 8080
# Deploy to staging
cumulus app deploy api --env staging

# Deploy to production (default)
cumulus app deploy api

Custom domains & TLS

Add [app.cdn] (static sites) or [app.tls] (EC2 apps) and cumulus app deploy provisions the custom domain after a successful deploy. Provisioning is non-fatal: the site or app is already live, and a domain hiccup is surfaced in the deploy log and retried on the next deploy (the flow is idempotent). You control DNS via a Route 53 hosted zone in your own account.

Static site (S3 + CloudFront)

Add [app.cdn] to provision a CloudFront distribution with an ACM certificate. First run takes a few minutes while ACM validates the domain via DNS.

[app.cdn]
enabled       = true
custom_domain = "app.example.com"
# hosted_zone_id = "Z1ABC123EXAMPLE"   # optional, auto-discovered from the domain

EC2 app (ALB + ACM)

Add [app.tls] to provision an Application Load Balancer with a regional ACM certificate. The ALB spans two AZs in the server's VPC (discovered for you) and forwards to the app's health-check port.

[app.tls]
enabled = true
domain  = "api.example.com"
# subnets = ["subnet-az-a", "subnet-az-b"]   # optional, two AZs auto-discovered in the server's VPC
# hosted_zone_id = "Z1ABC123EXAMPLE"         # optional, auto-discovered from the domain
Both CDN and TLS provisioning are idempotent. Re-running cumulus app deploy finds and reuses an existing distribution or load balancer rather than creating a new one.