← All decisions jacob@stephens.page
Decision Record

Staged declarative provisioning over a single imperative bootstrap script

ADR 0013 · Accepted ยท in production · ~715 words

Context

A new self-hosted service - a multi-service LLM research tool, several containers behind a reverse proxy - needed a home on a single cloud VM. The path of least resistance is imperative: bring the VM up by hand, SSH in, and run a setup.sh that installs the container runtime, sets the firewall, writes the reverse-proxy vhost, and starts the app. It works on the first try and needs no extra tooling.

It also leaves nothing behind. The box's real definition lives only in shell history and the operator's memory; rebuilding it is archaeology, drift is invisible, and the why of each choice is unrecorded. For a host meant to be a reference for provisioning properly, that is the wrong default.

The opposite over-correction fuses everything into one tool and one run - provisioning, host configuration, and app deployment - coupling stages that change at very different rates and for very different reasons.

Decision

Provision declaratively, and split the lifecycle into three stages with explicit boundaries:

Consequences

When I'd revisit

If apply-once first-boot plus live edits starts producing drift I can't see, I'd promote the configure stage to a real config-management tool - an idempotent play run on a schedule - so the running host is continuously reconciled, not just born correct. If the create-only limit ever blocks a legitimate rebuild, I'd widen the token just enough to taint-and-replace, not to a blanket admin credential. And the moment a second operator or machine is involved, local state moves to a shared remote backend first.

Narrative writeup: Importing Live DNS into Terraform Without Downtime. One of a set of architecture decision records. Source markdown lives in the infrastructure-patterns repo, which is the canonical copy.