7.8 KiB
Plan — cc-ci on DigitalOcean: terraform/ + nixos-infect + Nix provisioning
Status: PROPOSED. Add a terraform/ folder to the cc-ci product repo
(recipe-maintainers/cc-ci) that provisions the cc-ci server on DigitalOcean (8 GB droplet),
converts it to NixOS via nixos-infect, then applies the existing cc-ci flake config — making the
CI server reproducible-from-scratch on real cloud hosting. Owner: a cc-ci-repo infra task
(implementable by the Builder/Adversary loops as an infra unit, or by the assistant). This file:
/srv/cc-ci/cc-ci-plan/plan-cc-ci-digitalocean-terraform.md.
0. Why
cc-ci currently runs as the Incus VM cc-nix-test on b1 — a small, shared 4-core host (the contention
we kept hitting). A dedicated DO 8 GB droplet gives standard, reliable hosting with a public
IP, fully reproducible via Terraform + the existing cc-ci NixOS flake. "Spin up cc-ci from nothing"
becomes a terraform apply instead of hand-driven Incus API calls.
1. What already exists — build ON this, don't reinvent
- cc-ci is a flake-based NixOS system:
flake.nix→nixosConfigurations.cc-ci(pinned nixpkgs 24.11) →nix/hosts/cc-ci/{configuration.nix, hardware.nix}+nix/modules/*(proxy/traefik, drone, drone-runner, bridge, dashboard, backupbot, swarm, abra, harness, warm-keycloak, secrets). - From-scratch install is already VERIFIED (D8,
docs/install.md): a blank NixOS host + the two repos (cc-ci cloned--recursiveso thecc-ci-secretssubmodule atsecrets/comes too) + the one bootstrap age key at/var/lib/sops-nix/key.txt→ a singlenixos-rebuild switchconverges the whole server (0 failed units; serialized reconcile oneshots proxy→drone→bridge→dashboard→ backupbot). The wildcard TLS cert + all secrets are sops-encrypted incc-ci-secrets(not out-of-band). - So "provision via Nix in the expected way" = that exact D8 flow: clone
--recursive+ bootstrap age key +nixos-rebuild switch --flake .#<host>. - The current
nix/hosts/cc-ci/hardware.nixis Incus-VM-specific — DO needs its own hardware/bootloader/networking, which nixos-infect generates.
2. terraform/ layout (in recipe-maintainers/cc-ci)
terraform/
versions.tf # terraform + digitalocean/digitalocean provider, pinned
variables.tf # do_token(sensitive), region, size, ssh_key_id, ts_auth_key(sensitive), hostname
main.tf # digitalocean_droplet + ssh key + user_data
outputs.tf # droplet ipv4, id
user-data.sh # cloud-init stage-1: run nixos-infect (pinned)
README.md # apply instructions + operator inputs
.gitignore # *.tfstate*, *.auto.tfvars, .terraform/ (NEVER commit secrets/state)
- Droplet:
digitalocean_droplet— sizes-4vcpu-8gb(8 GB RAM / 4 vCPU), region (e.g.nyc3), base imageubuntu-24-04-x64(nixos-infect supports it),ssh_keys=[var.ssh_key_id],user_data=file("user-data.sh"), a stable name + tag. Optional:monitoring=true. - Secrets (DO token, TS key) are sensitive vars via
TF_VAR_*env or a gitignored*.auto.tfvars;terraform.tfstateis gitignored (it can hold secrets). Mirrors cc-ci's no-secrets-in-git rule.
3. Stage 1 — nixos-infect (base Ubuntu → NixOS)
user-data.sh runs on first boot of the droplet:
#!/usr/bin/env bash
set -euo pipefail
export NIX_CHANNEL=nixos-24.11
curl -fsSL https://raw.githubusercontent.com/elitak/nixos-infect/<PINNED_SHA>/nixos-infect | bash -x
nixos-infect converts the droplet to NixOS in place, generates /etc/nixos/{configuration.nix, hardware-configuration.nix, networking.nix} (DO-correct: bootloader on the DO disk, public-IP
networking via the DO metadata), and reboots into NixOS. Pin the nixos-infect revision — do not
curl | bash master blind. After this, the droplet is bare NixOS on DO, ssh-able as root.
4. Stage 2 — provision via Nix (bare NixOS → converged cc-ci) — "the expected way"
- Capture DO hardware into the flake. Take the
hardware-configuration.nix+networking.nixnixos-infect generated and add them as a flake host. Cleaner: a new hostnix/hosts/cc-ci-do/that imports the sharednix/modules/*+ the DO hardware, withnixosConfigurations.cc-ci-doinflake.nix(keeps the Incuscc-cihost buildable during transition). Make DO the canonicalcc-ciafter cutover. - Run the D8 install flow on the droplet: clone
recipe-maintainers/cc-ci--recursive(bringscc-ci-secrets), provision the bootstrap age key at/var/lib/sops-nix/key.txt, thennixos-rebuild switch --flake .#cc-ci-do. The reconcile oneshots converge the swarm. - Where stage 2 runs (recommendation): v1 = documented operator step (Terraform provisions +
infects; the age-key placement +
nixos-rebuildis the manual step, exactly likedocs/install.md— the age key is operator-provided anyway). Automate later via a Terraformremote-execprovisioner or a cloud-init second stage once the key-delivery story is settled.
5. Operator inputs (class-A1 — provide at apply, NEVER commit)
- DO API token (
TF_VAR_do_token). - DO SSH key (registered on DO; the operator/agent holds the private half to ssh + run stage 2).
TS_AUTH_KEY— tailnet join (cc-ci enables tailscale; the droplet joins the same tailnet so the orchestrator/loops reach it exactly as today, direct peer).- Bootstrap age key →
/var/lib/sops-nix/key.txton the droplet (decryptscc-ci-secretsincl. the wildcard TLS cert). The single out-of-band secret perdocs/install.md.
6. DNS / gateway — a simplification DO enables (open decision)
Today *.ci.commoninternet.net reaches the Incus VM (no public IP) via an external nginx
TLS-passthrough gateway → MagicDNS. A DO droplet has a public IP, so point
ci.commoninternet.net + the *.ci wildcard A record straight at the droplet and drop the
gateway — Traefik terminates TLS directly. The pre-issued sops wildcard cert still works as-is; or,
with a public IP, switch Traefik to ACME (Let's Encrypt) and retire the manual cert + renewal.
v1: keep the sops cert (no behavior change); evaluate ACME-on-public-IP as a follow-up. Record in
DECISIONS.md.
7. Open decisions (log in DECISIONS.md)
- Replace vs. parallel: stand DO up in parallel, verify a full
!testme+ the D-gates green on it, then cut DNS over and retire the Incuscc-nix-test. Nothing stateful is lost — recipes redeploy, warm canonicals re-seed on first green runs. - Flake host: parallel
cc-ci-dohost until cutover, then make DO the canonicalcc-ci. - Droplet size/region; ACME vs sops cert (§6); stage-2 automation (§4.3).
8. Definition of Done
terraform/in the cc-ci repo;terraform apply(with operator inputs) creates an 8 GB DO droplet and nixos-infect converts it to NixOS.- Given the bootstrap age key, the droplet converges to a full cc-ci via
nixos-rebuild switch --flake .#<host>(the D8 flow) — 0 failed units; traefik/drone/bridge/dashboard/backupbot up. - A real recipe
!testmeruns green on the DO cc-ci; the dashboard +*.ci.commoninternet.netreachable via the chosen DNS path. terraform/README.mddocuments apply + operator inputs; no secrets/state committed.- Adversary-verifiable: from-scratch reproducibility (the D8 guarantee) holds on DO.
9. Guardrails
- No secrets in git (DO token, TS key, age key, tfstate all out-of-band/gitignored) — cc-ci's rule.
- Pin everything (provider, nixos-infect rev; nixpkgs already pinned) — reproducible, no drift.
- Don't break the running Incus cc-ci until the DO one is verified green (parallel bring-up + cutover).
- Real Nix provisioning (the flake), not hand-installed packages.