LinuxinfrastructureTested on real hardware

Scratch container lifecycle: create, configure, destroy from code

The full test loop for Proxmox infrastructure code — mint a scoped identity, tofu apply a throwaway container, converge it with Ansible, destroy every trace.

DistrosDebian 13, Proxmox VE 9
Shellbash
Updated
Script
bash
# Scratch-container lifecycle — proves your cluster code can BUILD.
# Full walkthrough: /blog/opentofu-scratch-container-lifecycle/
# Run each stage deliberately; this is a checklist, not a fire-and-forget.

### 1 · mint a throwaway identity (on a PVE node, as root)
pveum user add scratch@pve
pveum acl modify /vms/9001 --users scratch@pve --roles PVEAdmin
pveum acl modify /storage/local-lvm --users scratch@pve --roles PVEAdmin
pveum acl modify /nodes/pvelab01 --users scratch@pve --roles PVEAdmin
pveum acl modify / --users scratch@pve --roles PVEAuditor
pveum user token add scratch@pve tf --privsep 0
# copy the secret from the output — it is shown exactly once

### 2 · create the container (on your workstation, in terraform/scratch/)
export PROXMOX_VE_API_TOKEN='scratch@pve!tf=PASTE_SECRET_HERE'
tofu init && tofu apply
# expect: Apply complete! Resources: 1 added.

### 3 · wait for DHCP properly, then read the address (on the node)
pct exec 9001 -- ifup eth0
pct exec 9001 -- ip -4 addr show eth0

### 4 · configure it (workstation; note the trailing comma)
ansible-playbook scratch.yml -i "10.0.0.201,"
# expect: ok>0 changed>0 failed=0 — then SSH in and look yourself

### 5 · destroy it, and verify with a different tool
tofu destroy
pct list                     # on the node: 9001 gone

### 6 · leave nothing behind
pveum user delete scratch@pve   # drops its ACLs and token too
pveum acl list                  # confirm no scratch entries remain
ssh-keygen -R 10.0.0.201        # stale host key off your workstation
unset PROXMOX_VE_API_TOKEN

### 7 · the point: production never noticed
# in your PRODUCTION terraform directory:
tofu plan                    # expect: "No changes."

What this does

Runs one throwaway LXC container through its entire life — created by OpenTofu, configured by Ansible, verified, destroyed, and swept up — to prove that your infrastructure code can actually build things, not just describe them. The final stage re-checks that production was untouched throughout. The reasoning behind every stage lives in A Proxmox Container Born, Configured, and Destroyed From Code.

Prerequisites

  • A terraform/scratch/ configuration with its own state, defining one container with a VMID your production range never uses (the series uses 9001)
  • A small scratch.yml Ansible playbook whose tasks each prove something: package-cache refresh (egress works), one package install (the apt path works), one file write (convergence works)
  • SSH key access: your public key injected into the container via the scratch config’s user_account block

Notes

  • Make these values your own: 9001 is the scratch VMID; pvelab01, local-lvm, and 10.0.0.201 are my documentation node, storage, and DHCP address — substitute what your cluster and lease actually give you. scratch@pve is a temporary account name; PASTE_SECRET_HERE is the token secret shown once at minting — it lives in your shell for the duration of the loop and nowhere else, never in a file or repo. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.
  • Why a separate user instead of a stronger token: Proxmox privilege-separated tokens get the intersection of token and user permissions — a write token minted under a read-only automation user can never write. A disposable user with path-scoped PVEAdmin keeps your standing automation credential a pure auditor while giving the test exactly one container’s worth of blast radius.
  • Separate state is the safety wall: the scratch directory’s state contains one object, so tofu destroy — which removes the objects managed by that particular configuration — cannot reach production even in principle. Never share a state file between throwaway and production resources.
  • DHCP, not static: if your gateway (like mine) only routes clients it leased, a static-IP scratch guest will fail its first apt update with no obvious cause. The blocking ifup is also the clean fix for “container exists before its lease does” — prefer commands that return when ready over sleeps.
  • This exact loop ran end-to-end on the real cluster on 2026-07-24: one added, ok=3 changed=3 failed=0, one destroyed, ACLs clean, production plan still No changes.