Deployment cookbook#
End-to-end recipes for deploying the camt053 REST API in
realistic environments, with the surrounding stack you’ll
typically want next to it: a queue, a metric store, and a
dashboard.
The recipes are opinionated minimums: enough to be production-shaped, small enough to read in one sitting. Adapt network policies, secret management, and resource limits to your environment.
Contents#
Common assumptions#
Every recipe assumes:
Python 3.12 (3.10 / 3.11 also supported).
The REST API is
camt053.api.app:app(a FastAPI ASGI app).TLS terminates at a reverse proxy (nginx, Cloud Load Balancer, ingress controller). The API itself runs HTTP on a local socket.
Metrics are exposed at
/metricsvia the OpenTelemetry RED counters that ship with the[telemetry]extra (pip install 'camt053[telemetry]').The audit log is written to a local append-only file unless you mount object storage at the configured path.
1. Local Docker Compose#
The smallest “production-shaped” stack: REST API, Redis (for
rate-limiting + dedupe), Prometheus (scrapes /metrics),
Grafana (dashboards), all wired with a single
docker compose up.
compose.yaml#
services:
api:
image: python:3.12-slim
working_dir: /app
command: >
sh -c "pip install --no-cache-dir 'camt053[telemetry]' uvicorn[standard] &&
uvicorn camt053.api.app:app --host 0.0.0.0 --port 8080"
environment:
CAMT053_AUDIT_LOG_PATH: /var/lib/camt053/audit.log
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_SERVICE_NAME: camt053-api
volumes:
- audit-log:/var/lib/camt053
ports:
- "8080:8080"
depends_on:
- redis
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
grafana:
image: grafana/grafana:latest
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
restart: unless-stopped
volumes:
audit-log:
redis-data:
prometheus-data:
grafana-data:
prometheus.yml#
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: camt053-api
metrics_path: /metrics
static_configs:
- targets: ["api:8080"]
Bring it up#
docker compose up -d
# wait ~10 s for the API health check to flip green
curl -s http://localhost:8080/health
Verify the RED metrics#
curl -s http://localhost:9090/api/v1/query \
--data-urlencode 'query=camt053_requests_total' \
| jq '.data.result[] | {op: .metric.op, status: .metric.status, value: .value[1]}'
You should see counters for parse, validate, and reverse
operations.
A minimal Grafana dashboard#
Add Prometheus as a data source (URL: http://prometheus:9090),
then a panel with rate(camt053_requests_total{status="ok"}[5m])
and a second panel with rate(camt053_requests_total{status="error"}[5m]).
A third panel with the p99 latency from the
camt053_request_duration_seconds_bucket histogram closes the
loop on the RED triad (Rate, Errors, Duration).
2. Single-host systemd#
For deployments that own their own VM. nginx terminates TLS
and proxies to a uvicorn worker pool managed by systemd.
/etc/systemd/system/camt053-api.service#
[Unit]
Description=camt053 REST API
After=network.target
[Service]
Type=notify
User=camt053
Group=camt053
WorkingDirectory=/srv/camt053
Environment=PATH=/srv/camt053/venv/bin
Environment=CAMT053_AUDIT_LOG_PATH=/var/lib/camt053/audit.log
Environment=OTEL_SERVICE_NAME=camt053-api
ExecStart=/srv/camt053/venv/bin/uvicorn camt053.api.app:app \
--uds /run/camt053/api.sock \
--workers 4 \
--loop uvloop \
--http httptools \
--proxy-headers \
--forwarded-allow-ips=127.0.0.1
Restart=on-failure
RestartSec=5
RuntimeDirectory=camt053
RuntimeDirectoryMode=0755
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/camt053
PrivateTmp=true
[Install]
WantedBy=multi-user.target
nginx site#
server {
listen 443 ssl http2;
server_name camt053.example.com;
ssl_certificate /etc/letsencrypt/live/camt053.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/camt053.example.com/privkey.pem;
client_max_body_size 32m; # large camt.053 payloads
client_body_timeout 60s;
location / {
proxy_pass http://unix:/run/camt053/api.sock;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
Bring it up#
sudo useradd --system --home /srv/camt053 --shell /usr/sbin/nologin camt053
sudo install -d -o camt053 -g camt053 /srv/camt053 /var/lib/camt053
sudo -u camt053 python3 -m venv /srv/camt053/venv
sudo -u camt053 /srv/camt053/venv/bin/pip install 'camt053[telemetry]' 'uvicorn[standard]'
sudo systemctl daemon-reload
sudo systemctl enable --now camt053-api.service
sudo systemctl reload nginx
Tail logs with journalctl -u camt053-api.service -f.
3. Kubernetes#
A Deployment + Service + HorizontalPodAutoscaler + a
prometheus-operator ServiceMonitor.
deployment.yaml#
apiVersion: apps/v1
kind: Deployment
metadata:
name: camt053-api
labels: { app: camt053-api }
spec:
replicas: 3
selector:
matchLabels: { app: camt053-api }
template:
metadata:
labels: { app: camt053-api }
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: api
image: ghcr.io/sebastienrousseau/camt053:0.0.6
imagePullPolicy: IfNotPresent
ports:
- { name: http, containerPort: 8080 }
- { name: metrics, containerPort: 8080 }
env:
- name: CAMT053_AUDIT_LOG_PATH
value: /var/lib/camt053/audit.log
- name: OTEL_SERVICE_NAME
value: camt053-api
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector.observability.svc:4317
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }
readinessProbe:
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 30
periodSeconds: 15
volumeMounts:
- { name: audit, mountPath: /var/lib/camt053 }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
volumes:
- name: audit
persistentVolumeClaim: { claimName: camt053-audit }
hpa.yaml#
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: camt053-api }
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: camt053-api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
servicemonitor.yaml#
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: camt053-api
labels: { release: kube-prometheus-stack }
spec:
selector: { matchLabels: { app: camt053-api } }
endpoints:
- port: http
path: /metrics
interval: 30s
Bring it up#
kubectl apply -f deployment.yaml -f hpa.yaml -f servicemonitor.yaml
kubectl rollout status deploy/camt053-api
4. Cloud Run / Lambda one-off#
For workloads that don’t need always-on capacity (audit one-off batches, scheduled reconciliation jobs).
Cloud Run#
gcloud run deploy camt053-api \
--image=ghcr.io/sebastienrousseau/camt053:0.0.6 \
--port=8080 \
--set-env-vars="CAMT053_AUDIT_LOG_PATH=/tmp/audit.log,OTEL_SERVICE_NAME=camt053-api" \
--memory=512Mi --cpu=1 \
--concurrency=20 \
--max-instances=10 \
--allow-unauthenticated=false
The audit log under /tmp is ephemeral; for retention, stream
to a logging sink or mount Cloud Storage via Cloud Run’s
volume mount feature.
AWS Lambda (ASGI via Mangum)#
# handler.py
from mangum import Mangum
from camt053.api.app import app
handler = Mangum(app, lifespan="off")
Package with:
pip install --target package camt053 mangum
cd package && zip -r ../bundle.zip . && cd ..
zip bundle.zip handler.py
aws lambda update-function-code \
--function-name camt053-api \
--zip-file fileb://bundle.zip
Front Lambda with API Gateway HTTP API; configure the audit log
path to /tmp/audit.log (the only writable location).
Recipes you’ll likely want next#
These are intentionally out of scope here, but flagged for your roadmap:
Tier-0 rate-limiting at the reverse proxy (nginx
limit_req_zone, Cloudflare Rules, AWS WAF) — every ISO 20022-shaped POST is a XSD-validation operation; bound it.Body-size hard cap at the reverse proxy. The library is hardened against 1 GiB inputs but you shouldn’t let one in.
X-Forwarded-Forreconciliation if you front the API with multiple proxies; the audit log records the apparent client address.Audit-log shipping to immutable object storage (S3 Object Lock, GCS Bucket Lock, Azure Immutable Blob) for regulatory retention.
Replay sigstore attestation verification on every container pull (
gh attestation verifyorcosign verify-attestation). Every release artefact carries SLSA Build L3 provenance.