← all work

Iris

A FastAPI control-plane service that provisions per-branch ephemeral environments on AWS — clone to live URL in under four minutes, host-header routed on a shared ALB.

1,567
image builds
98.5%
build success
88.1%
end-to-end
DAU growth

The problem

Engineers were spending hours per branch wiring up an AWS-side environment to test their PR. The team had ~14 active developers; the queue was a meeting, not a system. Iris turned the entire flow into a self-service API call: push to a PR, get back a stable URL.

Architecture

Iris is a control-plane service — it doesn't serve user traffic, it orchestrates AWS resources. One FastAPI process, one Postgres database, and a reconciliation loop holding it all together.

Provisioning pipeline · 8 stages
PR push github webhook Iris API FastAPI · ECS CodeBuild docker build ECR image push ECS Fargate task started ALB rule host-header routing Route53 stable DNS {env}.dev.internal.example URL stable across re-creates Postgres lifecycle state · 30s reconciler Secrets Manager per-env secrets CloudWatch build · runtime metrics
Each stage is idempotent. Failure at any step leaves the system in a state the reconciler can recover from on the next pass.
Stage by stage
  1. 1
    Clone
    Repo clone with on-disk cache to skip cold checkouts.
  2. 2
    Build
    AWS CodeBuild kicks off a Docker image build, tagged with the env-specific IMAGE_TAG.
  3. 3
    Push
    Image goes to ECR; cleanup keeps the repo from drifting.
  4. 4
    Run
    ECS Fargate task spins up with the freshly built image.
  5. 5
    Route
    ALB host-header routing rule attached to a shared listener — no per-env load balancer.
  6. 6
    DNS
    Route53 record created (or updated in place) to keep the URL stable.
  7. 7
    Track
    Lifecycle row in Postgres with stage timestamps. Survives process restarts.
  8. 8
    Reconcile
    Background loop polls ECS state every 30s and reconciles the DB.

Three Core Decisions

1. Host-header routing on a shared ALB, not one ALB per env

The naive design is one ALB per environment. That's clean but the cost is linear in env count — at 14 developers iterating you're paying for ~30 ALBs at any given moment. Iris uses a single shared ALB with one listener and a host-header rule per environment. Cost is essentially flat in env count; latency is unchanged.

2. URL stability across re-creates

A PR is iterative. The most-iterated environment in the dataset absorbed 222 rebuilds at 100% success over its lifetime. If every rebuild churned a new URL, every Jira link and reviewer bookmark would rot.

So Iris detects existing ALB rules and Route53 records on collision and updates them in place. {env}.dev.internal.example stays valid across the full PR lifecycle. This single design call is what made 9.7 average rebuilds per env (p95 37, peak 222) practical instead of chaos.

3. Reconciliation loop survives process restarts

The in-memory active_deployments dict is fast but volatile. Postgres is the durable record, and an EnvironmentMonitor boot scan plus a TaskLifecycleService reconciler that polls ECS every 30s closes the gap. Restart Iris and lifecycle tracking picks up exactly where it left off — no orphan tasks, no leaked routing rules.

What it ran in 12 months

Build time
Average
3.8 min
p95
4.8 min
Devs aren't blocked waiting for env spin-up. p95 stays close to the mean — no fat tail.

Rebuild distribution

Half the envs are one-shot. A third rebuild 6–20 times. Fifteen rebuild more than 20 times. That long tail is the proof that the system absorbs real iterative dev workflows — not just demos that get spun up once for a screenshot.

Rebuild distribution across 161 envs
1 rebuild
81 envs · 50%
2–5 rebuilds
32 envs · 20%
6–20 rebuilds
33 envs · 21%
20+ rebuilds
15 envs · 9%
The 20+ bucket includes the most-iterated environment, which absorbed 222 rebuilds at 100% success.

Stack