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)
Actions Taken
Section titled “Actions Taken”- 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. - Read all current infra files:
infra/main.tf,infra/variables.tf,infra/outputs.tf, all module files. - Created
.sdd/specification/spec-0001-second-k3s-node.md— saved the spec verbatim. - Created
infra/modules/keyvault/directory with 4 files:versions.tf— Terraform >= 1.5, azurerm ~> 4.0variables.tf— location, resource_group_name, name, tenant_id, vm_principal_ids, terraform_object_idmain.tf— azurerm_key_vault, role assignments for VM identities and Terraform calleroutputs.tf— key_vault_id, key_vault_uri, key_vault_name
- Modified
infra/modules/network/main.tf— addedazurerm_public_ip.node2resource (pip-kevinryan-node2). - Modified
infra/modules/network/outputs.tf— addedpublic_ip_address_node2,public_ip_id_node2,vnet_id,vnet_nameoutputs. - Modified
infra/modules/compute/variables.tf— addedvm_name,custom_data; removedacr_login_server,acr_name,github_token. - Modified
infra/modules/compute/main.tf— parameterised NIC/VM/disk names usingvar.vm_name; replaced inlinetemplatefile()withcustom_data = var.custom_data. - Modified
infra/modules/compute/outputs.tf— addedprivate_ip_addressoutput. - Deleted
infra/modules/compute/cloud-init.yaml— replaced by root-level templates. - Created
infra/cloud-init-server.yaml— K3s server cloud-init with Key Vault token retry,helm-controllerin Flux components. - Created
infra/cloud-init-agent.yaml— K3s agent cloud-init with Key Vault token retry, node taint and label. - Modified
infra/modules/registry/variables.tf— replacedvm_principal_id(string) withvm_principal_ids(list of string). - Modified
infra/modules/registry/main.tf— replaced single role assignment withfor_eachovertoset(var.vm_principal_ids). - Rewrote
infra/main.tf— addedrandomprovider, replacedmodule "compute"withmodule "node1"+module "node2", addedmodule "keyvault",random_password.k3s_token,azurerm_key_vault_secret.k3s_token; updated registry to usevm_principal_ids. - Modified
infra/variables.tf— addedkeyvault_namevariable (default:kv-kevinryan-io). - Modified
infra/outputs.tf— addednode1_public_ipandnode2_public_ipoutputs. - Created
.sdd/provenance/spec-0001-second-k3s-node.provenance.md(this file). - Committed all changes to branch
claude/add-second-k3s-node-c8qjoand pushed to remote.
Decisions Made
Section titled “Decisions Made”No autonomous decisions were required — all actions were explicitly specified in the spec.
Deviations from Spec
Section titled “Deviations from Spec”No deviations from spec.
Post-merge Manual Steps
Section titled “Post-merge Manual Steps”These steps must be performed by a human operator after the PR is merged:
terraform init— required to download the newrandomprovider.terraform plan— review the plan carefully (node1 will be replaced if tainted; keyvault, node2, and token secret will be created).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 andhelm-controllerin Flux).terraform apply— creates Key Vault, K3s token secret, node2, and rebuilds node1.- Verify both nodes are ready:
kubectl get nodes - Verify helm-controller is running:
kubectl get pods -n flux-system - 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.
Artifacts Produced
Section titled “Artifacts Produced”| File | Status |
|---|---|
.sdd/specification/spec-0001-second-k3s-node.md | Created |
infra/modules/keyvault/versions.tf | Created |
infra/modules/keyvault/variables.tf | Created |
infra/modules/keyvault/main.tf | Created |
infra/modules/keyvault/outputs.tf | Created |
infra/modules/network/main.tf | Modified |
infra/modules/network/outputs.tf | Modified |
infra/modules/compute/main.tf | Modified |
infra/modules/compute/variables.tf | Modified |
infra/modules/compute/outputs.tf | Modified |
infra/modules/compute/cloud-init.yaml | Deleted |
infra/cloud-init-server.yaml | Created |
infra/cloud-init-agent.yaml | Created |
infra/modules/registry/main.tf | Modified |
infra/modules/registry/variables.tf | Modified |
infra/main.tf | Modified |
infra/variables.tf | Modified |
infra/outputs.tf | Modified |
.sdd/provenance/spec-0001-second-k3s-node.provenance.md | Created |
Validation Results
Section titled “Validation Results”| # | Check | Result |
|---|---|---|
| 1 | .sdd/specification/spec-0001-second-k3s-node.md exists | Pass |
| 2 | infra/modules/keyvault/ has 4 files | Pass |
| 3 | infra/modules/keyvault/main.tf has no data "azurerm_client_config" | Pass |
| 4 | infra/modules/network/main.tf has pip-kevinryan-node2 | Pass |
| 5 | infra/modules/network/outputs.tf exports vnet_id, vnet_name, public_ip_id_node2, public_ip_address_node2 | Pass |
| 6 | infra/modules/compute/main.tf uses vm_name variable and custom_data variable | Pass |
| 7 | infra/modules/compute/variables.tf has no acr_login_server, acr_name, github_token | Pass |
| 8 | infra/modules/compute/outputs.tf includes private_ip_address | Pass |
| 9 | infra/modules/compute/cloud-init.yaml deleted | Pass |
| 10 | infra/cloud-init-server.yaml has helm-controller in Flux --components | Pass |
| 11 | infra/cloud-init-agent.yaml has --node-taint and --node-label in K3s agent install | Pass |
| 12 | Both cloud-init templates have retry loop (30 attempts, 10s sleep, hard failure) | Pass |
| 13 | infra/main.tf has module "node1", module "node2", module "keyvault" | Pass |
| 14 | infra/main.tf has random_password.k3s_token and azurerm_key_vault_secret.k3s_token | Pass |
| 15 | infra/main.tf uses var.keyvault_name (not module.keyvault.key_vault_name) — no circular dependency | Pass |
| 16 | Registry module grants AcrPull via for_each over vm_principal_ids | Pass |
| 17 | terraform fmt -check -recursive infra/ | Not run — terraform binary not available in agent environment |
| 18 | terraform validate | Not run — terraform binary not available in agent environment |
| 19 | No circular dependencies in module wiring — var.keyvault_name used in templates, not module output | Pass |
| 20 | pnpm lint | Not applicable — only Terraform/infrastructure files changed |
| 21 | Provenance record exists with all required sections | Pass |
| 22 | All files committed together | Pass |