author-image

Andrew James Okpainmo

Published: May 6, 2026Last Updated: May 6, 2026

SwiftDeploy: Infrastructure That Writes Itself, Then Refuses Unsafe Deploys

swiftdeploydeploymentcloud-and-devopsbackend-developmentdocker-composenginxopen-policy-agentopacanary-deploymentplatform-engineering

post banner

SwiftDeploy started as a task given to help me get a grasp of how composing and shipping infrastructure without the usual boilerplate and operational overhead would look like.

No hand-written docker-compose.yml. No manually tuned Nginx config copied between projects. No hidden operational checklist living in somebody's head. Just one source of truth:

yaml
1services:
2  image: "sd-demo-api-1:latest"
3  port: 5000
4  mode: stable
5  version: 1.0.0
6  restart_policy: unless-stopped
7
8nginx:
9  image: "nginx:latest"
10  port: 8080
11  proxy_timeout: 5s
12  contact: ops@swiftdeploy.local
13
14network:
15  name: swiftdeploy-net
16  driver_type: bridge
17
18opa:
19  image: "openpolicyagent/opa:latest"
20  port: 8181
21
22policy_infrastructure:
23  min_disk_free_gb: 10
24  max_cpu_load: 2.0
25
26policy_canary:
27  max_error_rate: 0.01
28  max_p99_latency_ms: 500
29  window_seconds: 30

That is the contract. Everything else is generated or derived from it.

The Design.

SwiftDeploy is a declarative deployment tool. The operator describes what should exist in manifest.yaml, then runs:

bash
1./swiftdeploy init

The tool reads the manifest, validates the required fields, builds a rendering context, and writes the infrastructure files from templates:

text
1manifest.yaml
2  |
3  v
4swiftdeploy init
5  |
6  +--> nginx.conf
7  +--> docker-compose.yml
8  +--> policies/infrastructure.rego
9  +--> policies/canary.rego

That is the "writes its own infrastructure" part. The repository keeps reusable templates under templates/, and the generated files are runtime artifacts. If the manifest changes, init can recreate the stack definition consistently.

The generated Docker Compose file defines three services:

  • app, the FastAPI service.
  • nginx, the public ingress.
  • opa, the local Open Policy Agent sidecar.

Nginx is the only public entry point. The app is exposed only inside the Compose network, and OPA is bound to loopback on the host:

yaml
1opa:
2image: ${opa_image}
3container_name: swiftdeploy-opa
4command:
5  - run
6  - --server
7  - --addr=0.0.0.0:8181
8  - /policies
9ports:
10  - "127.0.0.1:${opa_port}:8181"
11volumes:
12  - ./policies:/policies:ro

That small 127.0.0.1 matters. The CLI can ask OPA for decisions, but the public Nginx port cannot reach or expose the policy API.

The app itself now exposes Prometheus-style metrics:

text
1http_requests_total{method="GET",path="/",status_code="200"} 42
2http_request_duration_seconds_bucket{method="GET",path="/",status_code="200",le="0.5"} 42
3app_uptime_seconds 128.403
4app_mode 1
5chaos_active 2

Those metrics give SwiftDeploy eyes. OPA gives it a brain.

The Guardrails.

The most important rule in the upgrade is this:

The CLI does not decide whether a deploy or promotion is allowed. OPA does.

The CLI gathers facts, sends those facts to OPA, and prints the answer. It does not compare disk against 10GB, CPU against 2.0, error rate against 1%, or latency against 500ms as an allow/deny decision. Those comparisons live in Rego.

The policies are split by domain:

text
1policies/
2infrastructure.rego
3canary.rego

Each domain owns one question:

  • Infrastructure policy answers: "Is this host safe for pre-deploy?"
  • Canary policy answers: "Is this canary safe for pre-promote?"

This separation keeps policy changes local. If the canary promotion threshold changes, the infrastructure policy does not need to know. If the disk requirement changes, the canary policy remains untouched.

The infrastructure policy blocks deployment when:

  • Disk free is below 10GB.
  • CPU load is above 2.0.

The canary policy blocks promotion when:

  • Error rate is above 1%.
  • P99 latency is above 500ms.
  • The metrics are evaluated as a pre-promotion signal over the configured 30 second policy window.

OPA also never returns a bare boolean. A decision needs a reason:

json
1{
2"allowed": false,
3"domain": "canary",
4"question": "pre-promote",
5"reason": "p99 latency 821.40ms is above allowed 500.00ms",
6"violations": [
7  "p99 latency 821.40ms is above allowed 500.00ms"
8]
9}

That is what makes the gate useful. "Denied" is not operationally helpful. "Denied because P99 latency is 821ms and the limit is 500ms" is something an operator can act on.

The isolation matters for the same reason the decision boundary matters. OPA is powerful. It knows deployment rules, thresholds, and runtime safety state. It should be reachable by the local control plane, not by arbitrary public traffic. Nginx exposes the application. The CLI talks to OPA. Those paths stay separate.

The Chaos.

The FastAPI app includes a canary-only chaos endpoint:

bash
1curl -X POST http://127.0.0.1:8080/chaos \
2-H 'Content-Type: application/json' \
3-d '{"mode":"slow","duration":2}'

Slow mode adds latency to canary requests. Once traffic hits the service, /metrics starts showing larger request durations. The status dashboard turns that into an operator view:

text
1swiftdeploy status
2scraped_at: 2026-05-06T19:43:24.328297+00:00
3mode: canary
4chaos: slow
5req/s: 4.50
6p99 latency: 2000.00ms
7error rate: 0.00%
8uptime: 184.2s
9
10Policy Compliance
11
12- [PASS] infrastructure: policy passed
13- [FAIL] canary: p99 latency 2000.00ms is above allowed 500.00ms

That is the system doing what it was built to do. The app is still responding, but the canary is not healthy enough to promote. The policy gate catches the condition before it becomes a stable release problem.

Error chaos is more direct:

bash
1curl -X POST http://127.0.0.1:8080/chaos \
2-H 'Content-Type: application/json' \
3-d '{"mode":"error","rate":0.5}'

Now a percentage of canary requests intentionally return 500. The status view captures both the active chaos state and the policy violation:

text
1swiftdeploy status
2scraped_at: 2026-05-06T19:45:02.114901+00:00
3mode: canary
4chaos: error
5req/s: 6.00
6p99 latency: 120.00ms
7error rate: 48.33%
8uptime: 281.7s
9
10Policy Compliance
11
12- [PASS] infrastructure: policy passed
13- [FAIL] canary: error rate 48.33% is above allowed 1.00%

A promotion during this window is blocked:

text
1pre-promote policy gate:
2[BLOCK] canary: error rate 48.33% is above allowed 1.00%
3  - error rate 48.33% is above allowed 1.00%
4error: policy gate blocked 1 signal(s)

Every status scrape is appended to history.jsonl, which gives the project a memory. Later, the operator can run:

bash
1./swiftdeploy audit

That produces audit_report.md with a timeline of mode and chaos changes, plus a dedicated violations section. It is intentionally plain GitHub-flavored Markdown so it can be attached to an incident note, pull request, or release review without conversion.

The Lessons Learned.

The first lesson is that generation is not only about saving typing. It is about removing ambiguity. If manifest.yaml is the source of truth, then Nginx, Compose, and policy files can be recreated any time from the same declared intent.

The second lesson is that observability and policy need each other. Metrics without gates are dashboards people can ignore. Gates without metrics are guesses. SwiftDeploy combines the two: scrape runtime state, calculate useful signals, ask OPA, and surface the reason.

The third lesson is that policy should be modular. The infrastructure policy should not know how canary latency works. The canary policy should not know how much disk the host has. Each domain asks one question, owns one set of data, and returns one reasoned signal.

The fourth lesson is that failure modes deserve design. OPA can be unavailable, unreachable, slow, misconfigured, or return invalid data. The CLI treats those as human-readable outcomes instead of crashing or hanging. A deployment tool should fail closed, but it should explain itself when it does.

The final lesson is that chaos is only useful when it is visible. Injecting latency or errors is easy. Capturing the failure, showing the policy impact, and preserving the audit trail is where the exercise becomes operationally meaningful.

SwiftDeploy is still intentionally small. It is not Kubernetes, Terraform, Prometheus, or a full policy platform. But it demonstrates the core platform engineering loop:

text
1Declare intent.
2Generate infrastructure.
3Observe runtime behavior.
4Ask policy for a decision.
5Record what happened.

That loop is the heart of safer deployments.

About The Author

Andrew James Okpainmo is a fullstack software engineer who is passionate about building and scaling awesome products and startups. He currently works as a freelance software engineer (with expertise in fullstack software development, cloud engineering, and DevOps), while leading the team at Zed Labs.