惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
博客园_首页
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
D
Docker
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
量子位
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Blog of Author Tim Ferriss
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
Cloudbric
Cloudbric
博客园 - 司徒正美
H
Help Net Security
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LangChain Blog
Latest news
Latest news
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
博客园 - Franky
S
Security Affairs
W
WeLiveSecurity
F
Full Disclosure
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
Cyberwarzone
Cyberwarzone
美团技术团队
PCI Perspectives
PCI Perspectives
C
Check Point Blog
Spread Privacy
Spread Privacy

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
I Built a Tool That Writes Its Own Infrastructure
Timilehin Ob · 2026-05-06 · via DEV Community

A complete beginner-friendly walkthrough of building SwiftDeploy: a CLI tool that generates Nginx configs, manages Docker containers, enforces deployment policies, and gives you a live metrics dashboard — all from a single YAML file.


Who This Post Is For

If you have heard words like "Docker", "Nginx", "deployment", or "policy-as-code" and felt a little lost — this post is for you. I will explain every concept from scratch before using it. By the end, you will understand not just what I built, but why every piece exists.


The Problem This Solves

Imagine you are deploying a web app. Normally you would have to:

  1. Write a Docker Compose file to describe your containers
  2. Write an Nginx config to set up your web server
  3. Remember to update both files every time something changes
  4. Hope you did not make a typo somewhere

That is a lot of manual work, and manual work leads to mistakes.

What if instead you just wrote one simple file describing what you want, and a smart tool generated everything else?

That is exactly what SwiftDeploy does. You write manifest.yaml. SwiftDeploy does the rest.


Quick Glossary (Read This First)

Before we dive in, here are the key terms used throughout this post:

Docker — A tool that packages your app into a "container" — think of it like a shipping container for software. It runs the same way everywhere.

Container — A lightweight isolated box running your app. Like a mini computer inside your computer.

Docker Compose — A tool that lets you run multiple containers together and define how they connect.

Nginx — A web server that sits in front of your app and handles incoming traffic. Think of it as a receptionist who forwards calls to the right person.

Reverse proxy — When Nginx receives a request from a user and forwards it to your app. The user never talks to your app directly.

YAML — A simple file format using indentation to represent data. Like a structured shopping list.

CLI — Command Line Interface. A tool you run by typing commands in the terminal.

Canary deployment — A technique where you run two versions of your app at the same time — a stable version for most users and a "canary" version for testing. Like sending one canary into a mine before sending all the miners.

OPA (Open Policy Agent) — A policy engine. You write rules in a language called Rego, and OPA decides whether an action is allowed based on those rules.

Prometheus metrics — A standard format for exposing app statistics like request counts and response times. Looks like plain text with specific formatting.


Part 1 — The Foundation: manifest.yaml as the Single Source of Truth

What Is a "Single Source of Truth"?

In software, a "single source of truth" means one place where the authoritative information lives. If you have the same information in multiple files and they disagree, which one is right? Nobody knows. That is a bug waiting to happen.

SwiftDeploy solves this by making manifest.yaml the only file you ever edit. Every other config file is generated from it automatically.

Here is what the manifest looks like:

services:
  image: swift-deploy-1-node:latest   # Which Docker image to run
  port: 3000                          # What port your app uses inside the container
  mode: stable                        # stable or canary
  version: "1.0.0"                    # App version
  restart: unless-stopped             # Restart if it crashes

nginx:
  image: nginx:latest                 # The Nginx web server image
  port: 8080                          # The port the outside world connects to
  proxy_timeout: 30                   # Give up after 30 seconds

network:
  name: swiftdeploy-net               # Name of the internal network
  driver_type: bridge                 # Type of network

contact: "admin@swiftdeploy.local"    # Shown in error messages

Enter fullscreen mode Exit fullscreen mode

Plain English: "Run my app on port 3000, put Nginx in front of it on port 8080, connect them on an internal network."

How Does swiftdeploy init Work?

When you run ./swiftdeploy init, it:

  1. Reads manifest.yaml
  2. Opens templates/nginx.conf.tmpl — a template with placeholder values like {{NGINX_PORT}}
  3. Replaces every placeholder with the real value from the manifest
  4. Writes the result to nginx.conf
  5. Does the same thing for docker-compose.yml

The key insight is string replacement. The template has {{NGINX_PORT}} and the script replaces it with 8080. Simple but powerful.

# This is the core of how it works
nginx_conf = nginx_conf.replace('{{NGINX_PORT}}', nginx_port)
                       .replace('{{PROXY_TIMEOUT}}', proxy_timeout)
                       .replace('{{APP_PORT}}', app_port)

Enter fullscreen mode Exit fullscreen mode

The grader can delete the generated files and run ./swiftdeploy init again — they will be recreated perfectly from the manifest. That is the whole point.


Part 2 — The Architecture: Three Containers, One Network

When you run ./swiftdeploy deploy, three containers start:

Outside World
      |
      | port 8080
      ↓
  ┌─────────┐
  │  Nginx  │  ← Receives all traffic, forwards to app
  └────┬────┘
       │ internal network (swiftdeploy-net)
       ↓
  ┌─────────┐
  │   App   │  ← Python service, port 3000 (never exposed to outside)
  └─────────┘

  ┌─────────┐
  │   OPA   │  ← Policy engine, port 8181 (CLI only, not through Nginx)
  └─────────┘

Enter fullscreen mode Exit fullscreen mode

Why does the app port never get exposed?

This is a security decision. If port 3000 was exposed directly, anyone could bypass Nginx and hit your app without going through timeouts, error handling, logging, or security headers. By keeping it internal, all traffic is forced through Nginx.

Why is OPA isolated from Nginx?

OPA is only for the CLI to query. It is not a user-facing service. If it were accessible through Nginx on port 8080, anyone could query your policies. So OPA lives on the same internal Docker network but is never proxied by Nginx.


Part 3 — The App: A Python HTTP Service

The app (app/main.py) is a pure Python web server — no external frameworks, just Python's built-in http.server. This keeps the Docker image tiny (74MB, well under the 300MB limit).

The Three Endpoints

GET / — Welcome message with mode, version, and timestamp:

{
  "message": "Welcome to SwiftDeploy! Running in canary mode.",
  "mode": "canary",
  "version": "1.0.0",
  "time": "2026-05-05T21:00:00+00:00"
}

Enter fullscreen mode Exit fullscreen mode

GET /healthz — Is the app alive? How long has it been running?

{
  "status": "ok",
  "uptime": 342.15
}

Enter fullscreen mode Exit fullscreen mode

Docker polls this endpoint every 10 seconds. If it fails three times in a row, Docker marks the container "unhealthy" and restarts it. That is why we never apply chaos to /healthz — if we did, Docker would kill our container during a chaos test.

POST /chaos — Only available in canary mode. Simulates failures:

# Make 50% of requests fail with 500 errors
curl -X POST http://localhost:8080/chaos \
  -d '{"mode": "error", "rate": 0.5}'

# Make every request sleep 5 seconds
curl -X POST http://localhost:8080/chaos \
  -d '{"mode": "slow", "duration": 5}'

# Cancel all chaos
curl -X POST http://localhost:8080/chaos \
  -d '{"mode": "recover"}'

Enter fullscreen mode Exit fullscreen mode

Why Only in Canary Mode?

Because canary mode is the "I am testing something risky" mode. Stable mode means production traffic — you should never be able to break it intentionally. Canary mode is the sandbox where chaos makes sense.

Thread Safety

The app handles multiple requests at the same time. If two requests tried to update the chaos state simultaneously, they could corrupt each other. Python's threading.Lock() prevents this:

# Lock means: only one request can change this at a time
with chaos_lock:
    chaos_state["mode"] = "error"
    chaos_state["rate"] = 0.5

Enter fullscreen mode Exit fullscreen mode

This is called "thread safety" — making sure shared data is not corrupted by concurrent access.


Part 4 — The Nginx Configuration

Nginx sits between the user and the app. Here is what our generated nginx.conf does:

Custom Log Format

The task required logs in a specific format. In Nginx, you define log formats with log_format:

log_format swiftdeploy '$time_iso8601 | $status | $request_times | $upstream_addr | $request';

Enter fullscreen mode Exit fullscreen mode

This produces logs like:

2026-05-05T21:00:00+00:00 | 200 | 0.001s | 172.18.0.2:3000 | GET / HTTP/1.1

Enter fullscreen mode Exit fullscreen mode

Timestamp | Status code | How long it took | Where it went | What was requested

JSON Error Responses

When the app is down or slow, Nginx returns an error. By default, Nginx returns HTML error pages — ugly and not useful for APIs. We override this with JSON:

location @err502 {
    default_type application/json;
    return 502 '{"error": "Bad Gateway", "code": 502, "service": "swiftdeploy-api", "contact": "admin@swiftdeploy.local"}';
}

Enter fullscreen mode Exit fullscreen mode

502 means "Bad Gateway" — Nginx could not reach the app. 503 means "Service Unavailable". 504 means "Gateway Timeout" — the app took too long.

The DNS Resolution Trick

One problem we hit: when running nginx -t to validate the config syntax, Nginx tries to resolve the hostname app (the Docker container name). But app only exists inside the Docker network — not during standalone validation. This caused a "host not found" error even though the config was syntactically correct.

The fix is to use a variable for the upstream address:

resolver 127.0.0.11 valid=10s;  # Docker's internal DNS server

location / {
    set $upstream http://app:3000;  # Variable = skip DNS at startup
    proxy_pass $upstream;           # Nginx resolves it per-request instead
}

Enter fullscreen mode Exit fullscreen mode

When proxy_pass uses a variable, Nginx skips DNS resolution at startup and resolves it per-request. This lets nginx -t pass even when the app container does not exist yet.


Part 5 — Stable vs Canary Mode

Canary deployments come from a real practice in mining. Miners used to bring canaries into mines — if the canary died, the air was bad and the miners knew to leave. In software, a "canary" is a small deployment that gets traffic first. If it fails, only a small percentage of users are affected before you roll back.

SwiftDeploy implements a simple version:

  • Stable — normal mode, chaos disabled
  • Canary — test mode, chaos enabled, every response gets X-Mode: canary header

When you promote:

./swiftdeploy promote canary

Enter fullscreen mode Exit fullscreen mode

The script:

  1. Runs a 30-second OPA policy check (explained in Part 7)
  2. Updates mode: canary in manifest.yaml in-place (preserving all comments)
  3. Regenerates docker-compose.yml with the new MODE=canary environment variable
  4. Restarts only the app container — Nginx stays up, no downtime
  5. Polls /healthz to confirm the new mode is active

The X-Mode: canary header indicates to clients that they are talking to the canary version. Nginx forwards it from the app to the user:

proxy_pass_header X-Mode;
add_header X-Mode $upstream_http_x_mode always;

Enter fullscreen mode Exit fullscreen mode


Part 6 — Prometheus Metrics: The "Eyes" of the System

What Are Metrics?

Metrics are numbers that describe how your app is behaving. Things like:

  • How many requests per second?
  • What percentage are failing?
  • How long do requests take?

Prometheus is a popular monitoring system. It expects metrics in a specific text format.

What We Track

http_requests_total — A counter. Counts every request, labelled by method, path, and status code:

http_requests_total{method="GET",path="/",status_code="200"} 142
http_requests_total{method="GET",path="/",status_code="500"} 8

Enter fullscreen mode Exit fullscreen mode

http_request_duration_seconds — A histogram. Groups request durations into buckets:

http_request_duration_seconds_bucket{le="0.1"} 140   # 140 requests took ≤ 0.1s
http_request_duration_seconds_bucket{le="0.5"} 142   # 142 requests took ≤ 0.5s
http_request_duration_seconds_bucket{le="+Inf"} 150  # 150 total requests

Enter fullscreen mode Exit fullscreen mode

A histogram is what allows us to calculate percentile latency. The P99 (99th percentile) means "99% of requests completed within this time." It is more meaningful than the average because averages hide slow outliers.

app_mode — 0 for stable, 1 for canary.

chaos_active — 0 for none, 1 for slow, 2 for error.

How record_request() Works

After every request completes, we call:

record_request("GET", "/", 200, duration)

Enter fullscreen mode Exit fullscreen mode

This updates both the counter and the histogram in one call. We do NOT record /healthz or /metrics — those are infrastructure endpoints, not user traffic.


Part 7 — OPA: The "Brain" That Makes Decisions

What Is OPA?

Open Policy Agent is a general-purpose policy engine. You write rules in a language called Rego (pronounced "ray-go"), and OPA evaluates them against data you send it.

The core principle: The CLI never decides whether to allow or deny. It only collects data, sends it to OPA, and surfaces the result. All decision logic lives in Rego.

Why does this matter? Because it means you can change your policies without touching your deployment code. Policy and mechanics are separated.

The Infrastructure Policy

Before every deployment, we check if the host has enough resources. Here is policies/infrastructure.rego:

package infrastructure

default allow := false

allow if {
    count(deny) == 0    # Allow if there are zero deny reasons
}

deny contains reason if {
    input.disk_free_gb < data.infrastructure.min_disk_free_gb
    reason := sprintf("Disk free %.1fGB is below minimum %dGB",
        [input.disk_free_gb, data.infrastructure.min_disk_free_gb])
}

deny contains reason if {
    input.cpu_load > data.infrastructure.max_cpu_load
    reason := sprintf("CPU load %.2f exceeds maximum %.2f",
        [input.cpu_load, data.infrastructure.max_cpu_load])
}

Enter fullscreen mode Exit fullscreen mode

Notice: the thresholds (min_disk_free_gb, max_cpu_load) are not written in the Rego file. They come from data.json:

{
  "infrastructure": {
    "min_disk_free_gb": 10,
    "max_cpu_load": 2.0,
    "max_mem_percent": 90
  }
}

Enter fullscreen mode Exit fullscreen mode

This separation means: change the threshold by editing data.json, not the policy logic. Environments have different needs — a production server and a test server should not have the same disk requirements.

The Canary Safety Policy

Before every promotion, we check if the running service is healthy enough to switch modes. This is policies/canary.rego:

package canary

default allow := false

allow if { count(deny) == 0 }

deny contains reason if {
    input.error_rate_percent > data.canary.max_error_rate_percent
    reason := sprintf("Error rate %.2f%% exceeds maximum %.2f%% over last 30s",
        [input.error_rate_percent, data.canary.max_error_rate_percent])
}

deny contains reason if {
    input.p99_latency_ms > data.canary.max_p99_latency_ms
    reason := sprintf("P99 latency %.0fms exceeds maximum %dms over last 30s",
        [input.p99_latency_ms, data.canary.max_p99_latency_ms])
}

Enter fullscreen mode Exit fullscreen mode

How the 30-Second Window Works

The requirement says "over the last 30 seconds." But Prometheus metrics are cumulative — they count from when the app started, not just the last 30 seconds.

The trick: take two snapshots 30 seconds apart and subtract them.

Snapshot 1 (time T)    →    wait 30s    →    Snapshot 2 (time T+30)

delta = Snapshot2 - Snapshot1   ←  this is what happened in the last 30 seconds

Enter fullscreen mode Exit fullscreen mode

delta_total  = sum(totals2.values()) - sum(totals1.values())
delta_errors = sum(errors in snapshot2) - sum(errors in snapshot1)
error_rate_percent = (delta_errors / delta_total * 100)

Enter fullscreen mode Exit fullscreen mode

This gives us a true "what happened in the last 30 seconds" measurement, regardless of historical traffic.

How the CLI Talks to OPA

OPA exposes a REST API. The CLI sends a POST request with the data:

curl -X POST http://127.0.0.1:8181/v1/data/infrastructure \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "disk_free_gb": 11.0,
      "cpu_load": 0.18,
      "mem_percent": 53.5
    }
  }'

Enter fullscreen mode Exit fullscreen mode

OPA responds with:

{
  "result": {
    "allow": true,
    "deny": []
  }
}

Enter fullscreen mode Exit fullscreen mode

Or if blocked:

{
  "result": {
    "allow": false,
    "deny": ["Disk free 4.0GB is below minimum 10GB"]
  }
}

Enter fullscreen mode Exit fullscreen mode

The CLI reads allow to know what to do, and prints the deny reasons so the operator knows exactly why something was blocked.


Part 8 — What Happened When I Injected Chaos

This is where it gets interesting. After deploying in canary mode and activating error chaos:

./swiftdeploy promote canary

curl -X POST http://localhost:8080/chaos \
  -d '{"mode": "error", "rate": 0.5}'

Enter fullscreen mode Exit fullscreen mode

The status dashboard started showing failures:

══════════════════════════════════════════════════════
  SwiftDeploy Status Dashboard
  2026-05-05T21:55:12Z
══════════════════════════════════════════════════════
  Mode:        canary
  Chaos:       error

  Total reqs:  89
  Error rate:  48.31%
  P99 latency: 10000ms

  Policy Compliance:
    ✅ infrastructure: PASS (disk=11GB, cpu=0.22, mem=54.1%)
    ❌ canary: FAIL (error=48.31%, p99=10000ms)

Enter fullscreen mode Exit fullscreen mode

Then when I tried to promote to stable while the chaos was running:

── OPA Pre-Promote Policy Check ──────────────────────
  Sampling metrics over a 30-second window...
  Last 30s: 80 requests, 36 errors
  Error rate: 45.0000%, P99 latency: 10000.00ms
  ❌ BLOCK: Canary policy denied promotion
    ↳ Error rate 45.00% exceeds maximum 1.00% over last 30s
    ↳ P99 latency 10000ms exceeds maximum 500ms over last 30s
❌ Promotion blocked by policy.

Enter fullscreen mode Exit fullscreen mode

The policy worked. The system refused to let me promote while the service was actively failing. This is the whole point — you cannot accidentally promote a broken service.

After recovering:

curl -X POST http://localhost:8080/chaos -d '{"mode": "recover"}'
./swiftdeploy promote stable  # Now succeeds

Enter fullscreen mode Exit fullscreen mode


Part 9 — The Audit Trail

Every significant event is written to history.jsonl:

{"timestamp":"2026-05-05T21:00:00Z","event":"deploy","details":{"mode":"stable"}}
{"timestamp":"2026-05-05T21:10:00Z","event":"promote","details":{"mode":"canary"}}
{"timestamp":"2026-05-05T21:15:00Z","event":"policy_check","details":{"policy":"canary","result":"fail","error_rate_percent":45.0}}
{"timestamp":"2026-05-05T21:20:00Z","event":"promote","details":{"mode":"stable"}}

Enter fullscreen mode Exit fullscreen mode

Running ./swiftdeploy audit transforms this into a readable Markdown report:

# SwiftDeploy Audit Report

Generated: 2026-05-05T21:30:00Z
Total events: 24

## Timeline

| Timestamp | Event | Details |
|-----------|-------|---------|
| 2026-05-05T21:00:00Z | deploy | mode=stable |
| 2026-05-05T21:10:00Z | promote | → canary |
| 2026-05-05T21:15:00Z | policy_check | canary: fail |

## Policy Violations

| Timestamp | Policy | Reasons |
|-----------|--------|---------|
| 2026-05-05T21:15:00Z | canary | Error rate 45.00% exceeds maximum 1.00% over last 30s |

Enter fullscreen mode Exit fullscreen mode


Part 10 — Lessons Learned

1. The DNS Resolution Problem Was the Biggest Surprise

I spent a long time debugging why nginx -t kept failing with "host not found" even though my config was correct. The issue was that Nginx tries to resolve hostnames at startup — but app is a Docker hostname that only exists inside a running Docker network.

The fix (using set $upstream as a variable) was not obvious. It is an Nginx-specific trick that delays DNS resolution from startup to per-request. Learning this saved me from a broken validation check.

2. Capabilities Matter More Than You Think

Running containers as non-root is well-known advice. But dropping capabilities (like cap_drop: ALL) is less discussed. When I dropped all capabilities from Nginx, it crashed on startup with:

chown("/var/cache/nginx/client_temp", 101) failed (1: Operation not permitted)

Enter fullscreen mode Exit fullscreen mode

Nginx needs CHOWN to set up its temp directories. Dropping ALL capabilities silently breaks things. The lesson: drop specific capabilities you know the container does not need, not everything at once.

3. OPA's sprintf Handles Integers and Floats Differently

When my Rego policy tried to format an integer from data.json using %.0f, OPA produced %!f(int=500) — a formatting error. The fix was to use %d for integers from data.json and %.0f only for float inputs. OPA does not silently convert types the way Python does.

4. Cumulative Metrics Need Careful Handling

Prometheus metrics are cumulative counters — they only go up. To calculate "what happened in the last 30 seconds," you cannot just look at the current value. You need two snapshots and a delta. This is obvious in retrospect, but took a few failed attempts to get right.

5. The Manifest as the Single Source of Truth Actually Works

The grader requirement was: delete generated files, run ./swiftdeploy init, and verify they regenerate correctly. This forced a discipline that turned out to be genuinely useful. When you know everything comes from one file, debugging is much easier. There is only one place to look.


Complete Setup Instructions

# Clone the repo
git clone https://github.com/YOUR_USERNAME/swiftdeploy-automation.git
cd swiftdeploy-automation

# Install Python dependency
pip install pyyaml

# Build the Docker image
docker build -t swift-deploy-1-node:latest .

# Make CLI executable
chmod +x swiftdeploy

# Deploy
./swiftdeploy deploy

# Try everything
curl http://localhost:8080/
curl http://localhost:8080/healthz
curl http://localhost:8080/metrics
./swiftdeploy status
./swiftdeploy promote canary
./swiftdeploy promote stable
./swiftdeploy audit
./swiftdeploy teardown --clean

Enter fullscreen mode Exit fullscreen mode


Conclusion

SwiftDeploy taught me that the most valuable thing in infrastructure is not the tools themselves — it is the discipline they enforce.

Making the manifest the single source of truth means you cannot have config drift. Putting all policy logic in OPA means your deployment tool cannot make silent decisions. Requiring metrics before promotion means you cannot promote a broken service by accident.

Each of these constraints feels like a restriction at first. But each one also prevents a whole category of mistakes.

If you want to replicate this project, the full source code is at the GitHub link below. Every file is commented and explained. Start with manifest.yaml, read swiftdeploy from top to bottom, then look at the Rego policies.

The best way to learn is to break it intentionally — fill up the disk, inject chaos, watch the policies fire. That is what the chaos endpoint is for.


Source code: github.com/devops-timi/swiftdeploy-automation