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 /metrics via 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-For reconciliation 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 verify or cosign verify-attestation). Every release artefact carries SLSA Build L3 provenance.