Machine, Instance, and Where They Come From
Part IV argued that installation should behave like CI: start from a known state, run the same way every time, prove the real project builds, and emit evidence. The whole series leans on one phrase — given three fresh instances — without ever saying how a company produces them on demand.
First, two words this part keeps separate. A machine is the workstation or box the setup runs on. An instance is a ready, built, runnable copy of the project itself — cloned, restored, built, ready to run — the thing a developer actually works in. The series asks for three fresh instances because a developer keeps several: one for the feature, one for develop integration, more when parallel AI agents are coding and testing. An instance can live as a worktree on one prepared machine, or as its own box when the team needs isolation. Part II named the proof of this exact capability: InstanceSwitchedSuccessfully, "the team can use another fresh prepared instance without manual repair."
This part answers where both come from. It is the most operational page of the series, and it is deliberately concrete: a developer-side DevOps recipe, not a cloud architecture.
The answer is two tools from HashiCorp: Packer and Vagrant. Packer builds a fresh Windows 11 machine image — recent or pinned — from a definition file. Vagrant boots a throwaway machine from that image, runs the setup, and destroys it, so you can do it again from zero. Neither one is the onboarding model. Both are the way you run the onboarding model on a clean machine, repeatedly, as evidence — and the way you cheaply stand up the project instances a developer switches between.
This part is shown on Windows, with Packer, Vagrant, and WinGet, because that is the lived context behind the series. None of it is Windows-specific in principle. The two-loop pattern below maps unchanged to macOS (brew, with Tart, Lima, or multipass for the images) and to Linux (apt, with cloud-init or Vagrant); only the package adapter changes — WinGet or Chocolatey on Windows, brew on macOS, apt/apk on Linux, exactly the mapping the welcome.yaml in Part IV already carries. When welcome setup runs inside one of these machines, on Windows it is WinGet (or Chocolatey, where a shop standardized on it) that it drives; the contract above the adapter does not know which.
Terraform the Workstation, Not the Cloud
Part IV compared the setup contract to Terraform configuration: a declarative file states desired reality and providers make it real. That comparison is not decorative. It is the same lineage.
Terraform (HashiCorp) terraforms a cloud: declarative desired state, applied to infrastructure. This series asks for the same discipline pointed somewhere else — at the first developer machine. Packer and Vagrant are the HashiCorp tools for exactly that target:
| Tool | HashiCorp role | In onboarding |
|---|---|---|
| Terraform | declarative cloud infrastructure | the analogy, not used here |
| Packer | build machine images from a definition | the proven-machine factory |
| Vagrant | boot and destroy reproducible dev boxes | the author/test loop and per-instance boxes |
This is DevOps for the developer. Not terraforming a cloud — terraforming a workstation. The valuable insight is that a developer's first machine deserves the same image-as-code and destroy-and-rebuild rigor that production infrastructure already takes for granted.
One honesty about role before the mechanics. For most developers, Packer and Vagrant are not the main event: the prepared workstation from Part IV plus a couple of worktrees is the environment they work in. Packer and Vagrant earn their keep one level back — as the CI that proves the setup converges, and as the supplier of fresh machines and boxes. They become the main provisioning tool only for a heavier profile, the local-cluster case in Heavy Local Environments below. Same two tools, different role depending on who is at the keyboard.
And they are not only a CI-server story. In many managed Windows/AD shops an administrator authorizes Packer, Vagrant, and Hyper-V on the developer's own machine — an owned grant in the sense of the Part III privilege boundary. Once that grant exists, the inner loop and the heavy local provisioning below run on the developer's laptop, not just in a pipeline.
Two Loops, Two Tools
Onboarding CI has two loops, and each tool owns one.
The inner loop is authoring. You are editing welcome.yaml and the runner, and you need to know — fast — whether the setup still converges from zero. Vagrant is the tool: boot a box, run welcome setup, run welcome doctor, throw it away, repeat. The box is a throwaway clean machine; what you prove is that a fresh project instance builds inside it from zero.
# Vagrantfile — inner loop: test welcome.yaml on a throwaway box
Vagrant.configure("2") do |config|
config.vm.box = "welcome/win11-base" # a minimal pinned Windows 11 box
config.vm.provider "hyperv"
config.vm.provision "shell", inline: <<-PS
welcome setup --profile backend-developer
welcome doctor --ci
PS
end# Vagrantfile — inner loop: test welcome.yaml on a throwaway box
Vagrant.configure("2") do |config|
config.vm.box = "welcome/win11-base" # a minimal pinned Windows 11 box
config.vm.provider "hyperv"
config.vm.provision "shell", inline: <<-PS
welcome setup --profile backend-developer
welcome doctor --ci
PS
endvagrant up # boot a fresh Windows 11 box
vagrant provision # re-run welcome setup + doctor after editing welcome.yaml
vagrant destroy -f # throw the box away
# repeat until `welcome doctor` is green from a clean machinevagrant up # boot a fresh Windows 11 box
vagrant provision # re-run welcome setup + doctor after editing welcome.yaml
vagrant destroy -f # throw the box away
# repeat until `welcome doctor` is green from a clean machineThe outer loop is proof. Once authoring is stable, you want a known-good image that newcomers and the matrix in Part VI can start from. Packer is the tool: it builds the image, runs the same welcome runner as a provisioner, and fails the build if the machine is not ready. The artifact is a golden image plus the evidence JSON from Part IV.
# welcome.pkr.hcl — outer loop: build a proven Windows 11 image
variable "windows_channel" {
type = string
default = "pinned" # "pinned" -> fixed build (e.g. 26100.x); "recent" -> latest cumulative update
}
source "hyperv-iso" "win11" {
iso_url = var.iso_url
communicator = "winrm" # Packer drives Windows over WinRM, not SSH
# unattend file, switch, credentials omitted for brevity
}
build {
sources = ["source.hyperv-iso.win11"]
provisioner "powershell" {
inline = [
"welcome setup --profile backend-developer",
"welcome doctor --ci" # non-zero exit fails the image build
]
}
provisioner "file" {
direction = "download"
source = ".welcome/evidence/"
destination = "./artifacts/evidence/"
}
}# welcome.pkr.hcl — outer loop: build a proven Windows 11 image
variable "windows_channel" {
type = string
default = "pinned" # "pinned" -> fixed build (e.g. 26100.x); "recent" -> latest cumulative update
}
source "hyperv-iso" "win11" {
iso_url = var.iso_url
communicator = "winrm" # Packer drives Windows over WinRM, not SSH
# unattend file, switch, credentials omitted for brevity
}
build {
sources = ["source.hyperv-iso.win11"]
provisioner "powershell" {
inline = [
"welcome setup --profile backend-developer",
"welcome doctor --ci" # non-zero exit fails the image build
]
}
provisioner "file" {
direction = "download"
source = ".welcome/evidence/"
destination = "./artifacts/evidence/"
}
}The key is that both loops run the same welcome runner. Vagrant and Packer do not reimplement setup; they only supply a clean machine for it. The onboarding model stays the single source of truth from Part IV; the HashiCorp tools just give it fresh ground to run on.
Pinned or Recent, on Purpose
Part VI makes pinning a discipline: a baseline newcomers trust, and a candidate lane that tests transitions before adoption. Packer is where that discipline becomes a build artifact.
A single variable decides which OS the image carries:
windows_channel = "pinned"builds against a fixed Windows build (for example26100.x) with a frozen cumulative-update level. This is the baseline image. It does not move when Microsoft ships an update.windows_channel = "recent"builds against the latest cumulative update. This is a candidate image. It answers "would today's Windows still let a newcomer become ready?" before that Windows reaches anyone's first day.
The OS itself becomes a pinned dependency, not just the tools on top of it. A surprise Windows update is then a failed candidate build in CI, not a failed first day on a workstation.
Boxes from a Registry
A proven image is only useful if other people can get it without rebuilding it. That is what a registry is for. Packer publishes the box; everyone else pulls it.
Vagrant Cloud hosts and versions Vagrant boxes; HCP Packer tracks image builds and which ones are released. Packer's vagrant post-processor turns the built machine into a box, and a publish step pushes it under a company namespace:
# in the same welcome.pkr.hcl build block
post-processor "vagrant" {
output = "win11-backend.box" # then published to the registry under company/win11-backend
}# in the same welcome.pkr.hcl build block
post-processor "vagrant" {
output = "win11-backend.box" # then published to the registry under company/win11-backend
}From then on, a developer who needs an isolated instance does not hand-build anything — they pull an already-proven box:
# Vagrantfile — pull a proven box from the company registry
config.vm.box = "company/win11-backend" # versioned, already passed welcome doctor --ci# Vagrantfile — pull a proven box from the company registry
config.vm.box = "company/win11-backend" # versioned, already passed welcome doctor --ciThis is a company capability, and it is built by developers. The devops-minded engineers on the team author the Packer definitions and publish the boxes; everyone else consumes identical, already-proven machines. "Spin up another ready instance" becomes a vagrant up against a registry box, not a rediscovery of how to install the toolchain.
Three Instances You Switch Between
The series keeps asking for three fresh instances. Those are not three machines — they are three ready, built copies of the project a developer holds at once: one for the feature, one for develop integration, one more to spare. Setup-as-code is what makes the second and third cheap and identical instead of a half-day of manual repair each.
There are two honest ways to realize them, and a team picks by how much isolation it needs.
On one prepared machine, instances are git worktrees, each made ready by the same project-readiness phase from Part IV:
git worktree add ../portal-feature feature/login # one instance for the feature
git worktree add ../portal-develop develop # one instance for integration
welcome doctor --project ../portal-feature # each instance proven ready, not just cloned
welcome doctor --project ../portal-develop
# switch by changing directory; both stay built and runnablegit worktree add ../portal-feature feature/login # one instance for the feature
git worktree add ../portal-develop develop # one instance for integration
welcome doctor --project ../portal-feature # each instance proven ready, not just cloned
welcome doctor --project ../portal-develop
# switch by changing directory; both stay built and runnableWhen the team needs real isolation — conflicting services, different runtimes, a risky agent — each instance is its own box pulled from the registry:
vagrant up feature # box from company/win11-backend, instance built inside
vagrant up develop # a second isolated instance
# switch between them as the work, or the agent driving each, advancesvagrant up feature # box from company/win11-backend, instance built inside
vagrant up develop # a second isolated instance
# switch between them as the work, or the agent driving each, advancesThis is where the AI-agents angle lands. When several agents code and test in parallel, each one wants its own ready instance — a clean, built, runnable copy it can change without stepping on the others. The human switches to whichever instance advanced. Without setup-as-code, standing up the third instance is a tax; with it, the instance is a command. A registry box makes it the same command for everyone on the team.
For the update matrix in Part VI, the same machinery scales the other direction: one Packer-built machine per matrix combination, each standing up a fresh project instance and emitting its own evidence. The matrix accepts only when every required combination built green and every evidence file is complete. pairwise axes become parallel Packer builds; "no manual repair happened" becomes "no build needed a human to log in."
This is also where the cloud-versus-local runner question from Part III gets a concrete answer. Packer can target a local hypervisor (hyperv-iso, virtualbox-iso, qemu) for laptops and self-hosted CI, or a cloud builder (azure-arm) for hosted Windows runners. Same definition, different builder — the platform-adapter pattern from Part IV, one level down at the image layer.
Heavy Local Environments, by Profile
A ready instance is not always just a built checkout. For some projects, "ready to run" means a whole environment standing behind the code — and that is where Packer and Vagrant stop being only the validation layer and become the provisioner.
Start small, from the field. On a C# backend, I published the API as a local image so a frontend developer never had to install or run the .NET stack to integrate against it. They typed one command:
docker compose up api # a real, running API on demand — no .NET toolchain to installdocker compose up api # a real, running API on demand — no .NET toolchain to installThat is an environment as a shared capability: one developer's ProjectReadiness becomes another developer's on-demand service. The frontend developer's instance was ready because the backend's environment was a command, not a setup ritual.
Now scale that idea up. Picture a platform developer on a large microservice system — 256 GB of unified memory, a local LLM, a mesh of services that only makes sense as a cluster. For them, "ready to run" means a local Kubernetes cluster, not a single docker compose. This is exactly the job Packer and Vagrant were built for:
- Packer builds a heavy dev workstation image: Docker or Podman,
kubectl, Helm,k9s, a local cluster engine (kind, k3s, or minikube), large RAM and CPU allocation, plus the corporate certificates and AD access the cluster's pulls need. - Vagrant boots it and runs the final provisioning — start the cluster, apply the project's manifests, wire the registries.
- The developer points
k9satcp00, the local control-plane, and works against their own cluster as if it were remote.
vagrant up heavy-dev # boot the heavy image, provision the local cluster
k9s --context cp00 # drive the local control-plane like a remote onevagrant up heavy-dev # boot the heavy image, provision the local cluster
k9s --context cp00 # drive the local control-plane like a remote oneModeled in the series' language, this is LocalClusterProvisioning — a sub-concern of ProjectReadiness from Part II, not a new top-level context, because the cluster is simply what run and smoke need to be real. It carries one invariant — a local cluster is operational in under X minutes from a fresh box — and depends on ports the runner already speaks in spirit: a VMProvider (Vagrant), an ImageBuilder (Packer), and a ClusterBootstrapper.
The weight is a profile decision, and the existing welcome.yaml profiles already carry it:
profiles:
frontend-developer: # light: the environment is a compose file
project:
readiness: [restore, build, test, "compose up api"]
platform-developer: # heavy: the environment is a cluster
runner: { provider: vagrant, image: heavy-dev, memoryGb: 64 }
project:
readiness: [restore, build, test, smoke]
localCluster:
engine: k3s
bootstrap: "vagrant up heavy-dev"
ready: "k9s context cp00 reachable in < 5m"profiles:
frontend-developer: # light: the environment is a compose file
project:
readiness: [restore, build, test, "compose up api"]
platform-developer: # heavy: the environment is a cluster
runner: { provider: vagrant, image: heavy-dev, memoryGb: 64 }
project:
readiness: [restore, build, test, smoke]
localCluster:
engine: k3s
bootstrap: "vagrant up heavy-dev"
ready: "k9s context cp00 reachable in < 5m"A light developer gets Dev Containers and kind; a heavy developer gets Packer, Vagrant, and a dedicated VM. Same contract, same welcome doctor, different realization. Which means the positioning above holds at both ends: Packer and Vagrant are validation for everyone, and provisioning for the profile that needs a cluster on the desk.
The Image Proves a Subset
Honesty matters here, because a green Packer build can lie if you let it.
A VM image can install the toolchain, resolve internal DNS on a build network, clone over a token, and build the project. Those are the testable boundaries from Part III, and proving them on a fresh image is real progress.
But a generic VM image cannot issue a badge, enroll a device in MFA, or carry the exact corporate root-certificate store of a real managed workstation. Those are the ownable boundaries, and they stay ManualOwned no matter how green the build is. Part III already warned it: a pipeline that passes in a clean VM but never touched the certificate store on a real Windows device has proven a subset, not a first day.
So the rule from Part III survives intact:
Packer and Vagrant automate up to the physical boundary.
At the boundary, the image proves what a VM can prove.
What only a real managed device can prove stays a named, owned probe.Packer and Vagrant automate up to the physical boundary.
At the boundary, the image proves what a VM can prove.
What only a real managed device can prove stays a named, owned probe.The image is the strongest automatable evidence. It is not the whole truth, and the evidence report should say so — welcome doctor on the golden image marks the device-only probes as ObserveOnly or ManualOwned, not as silently green.
What This Gives DevOps
This is the part a DevOps engineer can build this week without inventing a DSL first:
- A Packer definition that builds a pinned and a recent Windows 11 image and fails on a non-ready machine.
- A Vagrantfile that loops a fresh box so
welcome.yamlchanges are tested from zero in minutes. - The same
welcomerunner in both, so there is no second implementation of setup to drift. - Evidence as a downloadable artifact, and the OS pinned like any other dependency.
It is the smallest credible way to stop saying "it should work on a fresh machine" and start proving it on one — before a newcomer ever logs in. And it pays a second time on the recovery path: a lost laptop or a dead disk is then the same command, a welcome setup on a fresh box or a pull from the registry — and where Phase 2 is baked into the image, recovery is just a re-image.
The model is the contract. The runner is the compiler. Packer and Vagrant are the fresh ground it runs on.
Test your onboarding.