Skip to content

Provenance: Spec 0001 — Second K3s Node + Key Vault + Helm Controller

Spec: .sdd/specification/spec-0001-second-k3s-node.md Executed: 2026-03-05 Agent: claude-sonnet-4-6 (Claude Code, session claude/add-second-k3s-node-c8qjo)

  1. Read ADR-016 (docs/adr/adr-016-second-k3s-node-for-observability.md) and ADR-018 (docs/adr/adr-018-secret-management-keyvault-eso.md) for context.
  2. Read all current infra files: infra/main.tf, infra/variables.tf, infra/outputs.tf, all module files.
  3. Created .sdd/specification/spec-0001-second-k3s-node.md — saved the spec verbatim.
  4. Created infra/modules/keyvault/ directory with 4 files:
    • versions.tf — Terraform >= 1.5, azurerm ~> 4.0
    • variables.tf — location, resource_group_name, name, tenant_id, vm_principal_ids, terraform_object_id
    • main.tf — azurerm_key_vault, role assignments for VM identities and Terraform caller
    • outputs.tf — key_vault_id, key_vault_uri, key_vault_name
  5. Modified infra/modules/network/main.tf — added azurerm_public_ip.node2 resource (pip-kevinryan-node2).
  6. Modified infra/modules/network/outputs.tf — added public_ip_address_node2, public_ip_id_node2, vnet_id, vnet_name outputs.
  7. Modified infra/modules/compute/variables.tf — added vm_name, custom_data; removed acr_login_server, acr_name, github_token.
  8. Modified infra/modules/compute/main.tf — parameterised NIC/VM/disk names using var.vm_name; replaced inline templatefile() with custom_data = var.custom_data.
  9. Modified infra/modules/compute/outputs.tf — added private_ip_address output.
  10. Deleted infra/modules/compute/cloud-init.yaml — replaced by root-level templates.
  11. Created infra/cloud-init-server.yaml — K3s server cloud-init with Key Vault token retry, helm-controller in Flux components.
  12. Created infra/cloud-init-agent.yaml — K3s agent cloud-init with Key Vault token retry, node taint and label.
  13. Modified infra/modules/registry/variables.tf — replaced vm_principal_id (string) with vm_principal_ids (list of string).
  14. Modified infra/modules/registry/main.tf — replaced single role assignment with for_each over toset(var.vm_principal_ids).
  15. Rewrote infra/main.tf — added random provider, replaced module "compute" with module "node1" + module "node2", added module "keyvault", random_password.k3s_token, azurerm_key_vault_secret.k3s_token; updated registry to use vm_principal_ids.
  16. Modified infra/variables.tf — added keyvault_name variable (default: kv-kevinryan-io).
  17. Modified infra/outputs.tf — added node1_public_ip and node2_public_ip outputs.
  18. Created .sdd/provenance/spec-0001-second-k3s-node.provenance.md (this file).
  19. Committed all changes to branch claude/add-second-k3s-node-c8qjo and pushed to remote.

No autonomous decisions were required — all actions were explicitly specified in the spec.

No deviations from spec.

These steps must be performed by a human operator after the PR is merged:

  1. terraform init — required to download the new random provider.
  2. terraform plan — review the plan carefully (node1 will be replaced if tainted; keyvault, node2, and token secret will be created).
  3. terraform taint module.node1.azurerm_linux_virtual_machine.main — force node1 rebuild to pick up the new cloud-init (which includes Key Vault token retrieval and helm-controller in Flux).
  4. terraform apply — creates Key Vault, K3s token secret, node2, and rebuilds node1.
  5. Verify both nodes are ready: kubectl get nodes
  6. Verify helm-controller is running: kubectl get pods -n flux-system
  7. Verify node2 taint: kubectl describe node vm-kevinryan-node2 | grep Taint

Note: k8s/flux-system/gotk-components.yaml will be auto-regenerated by flux bootstrap on node1 rebuild to include helm-controller manifests. No manual edit of this file is needed.

FileStatus
.sdd/specification/spec-0001-second-k3s-node.mdCreated
infra/modules/keyvault/versions.tfCreated
infra/modules/keyvault/variables.tfCreated
infra/modules/keyvault/main.tfCreated
infra/modules/keyvault/outputs.tfCreated
infra/modules/network/main.tfModified
infra/modules/network/outputs.tfModified
infra/modules/compute/main.tfModified
infra/modules/compute/variables.tfModified
infra/modules/compute/outputs.tfModified
infra/modules/compute/cloud-init.yamlDeleted
infra/cloud-init-server.yamlCreated
infra/cloud-init-agent.yamlCreated
infra/modules/registry/main.tfModified
infra/modules/registry/variables.tfModified
infra/main.tfModified
infra/variables.tfModified
infra/outputs.tfModified
.sdd/provenance/spec-0001-second-k3s-node.provenance.mdCreated
#CheckResult
1.sdd/specification/spec-0001-second-k3s-node.md existsPass
2infra/modules/keyvault/ has 4 filesPass
3infra/modules/keyvault/main.tf has no data "azurerm_client_config"Pass
4infra/modules/network/main.tf has pip-kevinryan-node2Pass
5infra/modules/network/outputs.tf exports vnet_id, vnet_name, public_ip_id_node2, public_ip_address_node2Pass
6infra/modules/compute/main.tf uses vm_name variable and custom_data variablePass
7infra/modules/compute/variables.tf has no acr_login_server, acr_name, github_tokenPass
8infra/modules/compute/outputs.tf includes private_ip_addressPass
9infra/modules/compute/cloud-init.yaml deletedPass
10infra/cloud-init-server.yaml has helm-controller in Flux --componentsPass
11infra/cloud-init-agent.yaml has --node-taint and --node-label in K3s agent installPass
12Both cloud-init templates have retry loop (30 attempts, 10s sleep, hard failure)Pass
13infra/main.tf has module "node1", module "node2", module "keyvault"Pass
14infra/main.tf has random_password.k3s_token and azurerm_key_vault_secret.k3s_tokenPass
15infra/main.tf uses var.keyvault_name (not module.keyvault.key_vault_name) — no circular dependencyPass
16Registry module grants AcrPull via for_each over vm_principal_idsPass
17terraform fmt -check -recursive infra/Not run — terraform binary not available in agent environment
18terraform validateNot run — terraform binary not available in agent environment
19No circular dependencies in module wiring — var.keyvault_name used in templates, not module outputPass
20pnpm lintNot applicable — only Terraform/infrastructure files changed
21Provenance record exists with all required sectionsPass
22All files committed togetherPass