infra: remove stale old kube files, infra/kube is new truth

This commit is contained in:
2026-08-21 20:36:53 +02:00
parent 8f97feb3e9
commit 19fb650b8d
7 changed files with 0 additions and 1067 deletions
-187
View File
@@ -1,187 +0,0 @@
# Talos node for the P330 — joins the r740 "kube" cluster.
terraform {
required_providers {
talos = {
source = "siderolabs/talos"
version = "0.9.0"
}
null = {
source = "hashicorp/null"
version = "3.2.3"
}
}
}
# Read the r740 kube module state to reuse the cluster secrets & endpoint.
# The r740 module exposes: client_configuration, machine_secrets, cluster_name,
# cluster_endpoint, kube_host.
data "terraform_remote_state" "r740_kube" {
backend = var.r740_backend
config = var.r740_backend == "local" ? {
path = "${var.r740_state_path}/terraform.tfstate"
} : var.r740_backend_config
}
locals {
cluster_name = data.terraform_remote_state.r740_kube.outputs.cluster_name
cluster_endpoint = data.terraform_remote_state.r740_kube.outputs.cluster_endpoint
machine_secrets = data.terraform_remote_state.r740_kube.outputs.machine_secrets
client_config = data.terraform_remote_state.r740_kube.outputs.client_configuration
# kubeconfig produced by the r740 kube module — used to wait for the node and
# apply labels. There is no in-tree kubernetes provider here on
# purpose: managing a `kubernetes_node` resource conflicts with the node
# object that kubelet itself creates, so we use a null_resource with kubectl
# to wait + label idempotently.
kubeconfig_path = "${var.r740_state_path}/kubeconfig"
# Network config: static if node_subnet is provided, otherwise Talos DHCPs.
static_network = var.node_subnet == null ? {} : {
interfaces = [{
interface = var.network_interface
addresses = [var.node_subnet]
routes = var.node_gateway == null ? [] : [{ gateway = var.node_gateway }]
}]
}
network_patch = {
nameservers = var.nameservers
}
network_patch_merged = merge(local.network_patch, local.static_network)
machine_patch = {
install = {
image = var.installer_image
disk = var.install_disk
}
network = merge(local.network_patch_merged, {
# Pin the Kubernetes node name. Talos otherwise auto-generates a hostname
# (e.g. "talos-8ec-vd1"), so the node registers with that random name
# instead of var.p330_node_name — and our label null_resource waits
# for the wrong node. Setting machine.network.hostname fixes the node name.
hostname = var.p330_node_name
})
# Kernel modules required by Longhorn (iSCSI + ext4) — must match the
# control-plane nodes so Longhorn can schedule replicas on the failover node.
kernel = {
modules = [
{ name = "iscsi_tcp" },
{ name = "libiscsi" },
{ name = "scsi_transport_iscsi" },
{ name = "ext4" },
]
}
sysctls = {
"fs.inotify.max_user_instances" = "1024"
"fs.inotify.max_user_watches" = "1048576"
}
}
}
# Control-plane machine configuration. machine_type = "controlplane" makes
# Talos generate a join config that runs the apiserver/controller-manager/
# scheduler AND joins the existing etcd cluster as a new member (the cluster
# was already bootstrapped by the r740 module's talos_machine_bootstrap).
data "talos_machine_configuration" "p330" {
cluster_name = local.cluster_name
machine_type = "controlplane"
cluster_endpoint = local.cluster_endpoint
machine_secrets = local.machine_secrets
config_patches = [
yamlencode({
machine = local.machine_patch
}),
yamlencode({
cluster = {
network = {
cni = {
name = "none"
}
}
}
})
]
}
# Rendered config is written to disk so it can also be applied manually with
# `talosctl apply-config --nodes <p330_host> --file p330.yaml` if needed.
resource "local_file" "p330_machine_config" {
filename = "${path.module}/p330.yaml"
content = data.talos_machine_configuration.p330.machine_configuration
}
# Apply the machine config to the running (maintenance-mode) node over the
# Talos API. Because the config patch contains a `machine.install` block, when
# Talos receives this config on a node booted from the USB (maintenance) image
# it installs itself to install.disk and reboots into the installed system.
# For a controlplane node it then joins the existing etcd cluster as a new
# member and runs the control-plane components; for a worker it just registers
# via kubelet.
resource "talos_machine_configuration_apply" "p330" {
client_configuration = local.client_config
machine_configuration_input = data.talos_machine_configuration.p330.machine_configuration
node = var.p330_host
depends_on = [local_file.p330_machine_config]
}
# Emit a talosconfig scoped to this node for ad-hoc `talosctl` use.
data "talos_client_configuration" "p330" {
cluster_name = local.cluster_name
client_configuration = local.client_config
nodes = [var.p330_host]
}
resource "local_file" "talosconfig" {
content = data.talos_client_configuration.p330.talos_config
filename = "${path.module}/talosconfig"
depends_on = [data.talos_client_configuration.p330]
}
# Wait for the node to register with Kubernetes (kubelet creates the Node
# object after Talos installs and reboots), then label it. This is idempotent:
# kubectl exits 0 if the label already exists.
resource "null_resource" "p330_node_label" {
triggers = {
node = var.p330_node_name
kubeconfig = local.kubeconfig_path
}
provisioner "local-exec" {
# Wait for the node to show up, then label. The wait loop is bounded
# by kubectl --timeout; tune it via TF_LOG / re-run if the node is slow to
# join (a controlplane node must first complete the etcd join handshake).
command = <<-EOT
set -euo pipefail
KUBECONFIG="${local.kubeconfig_path}"
export KUBECONFIG
NODE="${var.p330_node_name}"
echo "Waiting for node $NODE to be registered (kubelet creates the Node object once Talos has installed, rebooted and joined etcd)..."
# kubectl wait --for=condition=Ready fails instantly with NotFound if the
# node object doesn't exist yet, so poll for existence first.
# /bin/sh (dash) has no $SECONDS, so count iterations with a bounded loop.
tries=240 # 240 * 5s = 20 minutes max
until kubectl get node "$NODE" >/dev/null 2>&1; do
tries=$((tries - 1))
if [ "$tries" -le 0 ]; then
echo "Timed out waiting for node $NODE to register." >&2
exit 1
fi
sleep 5
done
echo "Node $NODE registered. Waiting for it to become Ready..."
# Now wait for Ready (a controlplane node needs etcd joined + apiserver up).
kubectl wait --for=condition=Ready "node/$NODE" --timeout=20m || \
kubectl wait --for=jsonpath='{.status.conditions[?(@.reason=="KubeletReady")].status}'=True "node/$NODE" --timeout=20m
# Failover marker label (applied to both controlplane and worker nodes).
kubectl label --overwrite node "$NODE" homeprod.io/failover=true
echo "Node $NODE ready and labeled for failover scheduling."
EOT
}
depends_on = [talos_machine_configuration_apply.p330]
}
-96
View File
@@ -1,96 +0,0 @@
# Variables for the P330 Talos worker node that joins the r740 cluster.
variable "p330_host" {
description = "Reachable IP/hostname of the P330 Talos node (for Talos API access)."
type = string
}
variable "p330_node_name" {
description = "Kubernetes/Talos node name for the P330 (e.g. p330)."
type = string
default = "p330"
}
variable "r740_state_path" {
description = <<EOT
Path to the Terraform state of the r740 kube module, used by terraform_remote_state
to read the cluster secrets and endpoint so this node can join the existing cluster.
Path is resolved by terraform_remote_state relative to the working directory where
terraform runs (this module dir). The default points two levels up to the repo
root and back down to the r740 kube module.
EOT
type = string
default = "../../r740/kube"
}
variable "r740_backend" {
description = <<EOT
Terraform backend type used by the r740 kube module.
Set to "local" (default) when r740 uses a local tfstate file in its own directory,
or the matching remote backend name ("s3", "remote", ...) if the r740 module uses
a configured backend.
EOT
type = string
default = "local"
}
variable "r740_backend_config" {
description = <<EOT
Backend configuration map passed to terraform_remote_state when r740_backend is
not "local". For a local backend this is ignored.
EOT
type = map(string)
default = {}
}
variable "installer_image" {
description = <<EOT
Talos installer image to use on the P330 (bare metal).
Must be a **metal** Image Factory build that includes ixgbe.allow_unsupported_sfp=1
in the kernel command line (sd-boot/UKI ignores machine.install.extraKernelArgs, so
the param must be baked into the image). The default is a custom factory build
(a18165114...).
EOT
type = string
default = "factory.talos.dev/installer/a18165114f80c28601d05bc4ff1f6ea6d6b214882c5b9af7928aaf4d09741beb:v1.13.6"
}
variable "install_disk" {
description = "Block device path to install Talos on (e.g. /dev/sda, /dev/nvme0n1)."
type = string
default = "/dev/nvme0n1"
}
variable "node_subnet" {
description = <<EOT
Static IPv4 address in CIDR notation for the P330 node (e.g. 10.1.2.132/24).
Set to null to use DHCP. A static address is recommended for a failover node so
DNS/affinity rules stay stable.
EOT
type = string
default = "10.1.2.132/24"
}
variable "node_gateway" {
description = "IPv4 gateway for the P330 node. Ignored when node_subnet is null."
type = string
default = "10.1.2.1"
}
variable "network_interface" {
description = <<EOT
Primary network interface name on the P330. Defaults to enp3s0f1 (the 10G Intel
X520 NIC), which must be on the same L2/subnet as the r740 control plane so etcd
peer traffic (TLS-verified against the r740's etcd cert SANs) doesn't cross a
router. eno1 (1G) is left unconfigured.
EOT
type = string
default = "enp3s0f1"
}
variable "nameservers" {
description = "DNS nameservers configured on the node (must work independently of kube)."
type = list(string)
default = ["10.1.2.148", "1.1.1.1"]
}
-365
View File
@@ -1,365 +0,0 @@
terraform {
required_providers {
talos = {
source = "siderolabs/talos"
version = "0.9.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "2.36.0"
}
helm = {
source = "hashicorp/helm"
version = "2.17.0"
}
}
}
# Talos configuration
provider "talos" {}
# Kubernetes configuration
provider "kubernetes" {
config_path = "${path.module}/kubeconfig"
}
# Helm configuration
provider "helm" {
kubernetes {
config_path = "${path.module}/kubeconfig"
}
}
resource "talos_machine_secrets" "kube" {}
data "talos_machine_configuration" "kube" {
cluster_name = "kube-${var.physical_hostname}"
machine_type = "controlplane"
cluster_endpoint = "https://${var.kube_host}:6443"
machine_secrets = talos_machine_secrets.kube.machine_secrets
config_patches = [
yamlencode({
machine = {
install = {
# Image Factory image with iSCSI extension for Longhorn.
# Generated at https://factory.talos.dev — siderolabs/iscsi-tools + qemu-guest-agent
image = "factory.talos.dev/installer/dc7b152cb3ea99b821fcb7340ce7168313ce393d663740b791c36f6e95fc8586:v1.13.6"
}
network = {
nameservers = [
# We need a set of nameservers that can work independently of kube
# to bootstrap.
"10.1.2.148",
"1.1.1.1"
]
}
certSANs = [
"${var.kube_host}", "${var.kube_hostname}"
]
# Kernel modules required by Longhorn (iSCSI + ext4)
kernel = {
modules = [
{
name = "iscsi_tcp"
},
{
name = "libiscsi"
},
{
name = "scsi_transport_iscsi"
},
{
name = "ext4"
},
]
}
# Sysctls for Longhorn
sysctls = {
"fs.inotify.max_user_instances" = "1024"
"fs.inotify.max_user_watches" = "1048576"
}
}
cluster = {
clusterName = "kube-${var.physical_hostname}"
allowSchedulingOnControlPlanes = true
apiServer = {
certSANs = [
"${var.kube_host}", "${var.kube_hostname}"
]
}
network = {
dnsDomain = "cluster.local"
cni = {
name: "none"
}
}
proxy = {
disabled = true
}
}
})
]
}
data "talos_client_configuration" "kube" {
cluster_name = "kube-${var.physical_hostname}"
client_configuration = talos_machine_secrets.kube.client_configuration
nodes = ["${var.kube_host}"]
}
resource "talos_machine_configuration_apply" "kube" {
client_configuration = talos_machine_secrets.kube.client_configuration
machine_configuration_input = data.talos_machine_configuration.kube.machine_configuration
node = var.kube_host
depends_on = [ talos_machine_secrets.kube ]
}
resource "talos_machine_bootstrap" "kube" {
node = var.kube_host
client_configuration = talos_machine_secrets.kube.client_configuration
depends_on = [ talos_machine_configuration_apply.kube, talos_machine_secrets.kube ]
}
resource "talos_cluster_kubeconfig" "kube" {
node = var.kube_host
depends_on = [ talos_machine_bootstrap.kube ]
client_configuration = talos_machine_secrets.kube.client_configuration
}
output "kubeconfig" {
sensitive = true
value = talos_cluster_kubeconfig.kube.kubeconfig_raw
}
output "client_configuration" {
description = "Talos client configuration (sensitive) used to manage nodes."
sensitive = true
value = talos_machine_secrets.kube.client_configuration
}
output "machine_secrets" {
description = "Talos machine secrets (sensitive) used to generate node configs."
sensitive = true
value = talos_machine_secrets.kube.machine_secrets
}
output "cluster_name" {
description = "Name of the Talos cluster the worker joins."
value = "kube-${var.physical_hostname}"
}
output "cluster_endpoint" {
description = "Endpoint (host:port) of the Talos/Kubernetes API on the cluster."
value = "https://${var.kube_host}:6443"
}
output "kube_host" {
description = "Reachable IP/hostname of the control-plane node."
value = var.kube_host
}
resource "local_file" "kubeconfig" {
content = "${talos_cluster_kubeconfig.kube.kubeconfig_raw}"
filename = "${path.module}/kubeconfig"
depends_on = [ talos_cluster_kubeconfig.kube ]
}
data "talos_client_configuration" "talosconfig" {
cluster_name = "kube-${var.physical_hostname}"
client_configuration = talos_machine_secrets.kube.client_configuration
nodes = [var.kube_host]
}
resource "local_file" "talosconfig" {
content = "${data.talos_client_configuration.talosconfig.talos_config}"
filename = "${path.module}/talosconfig"
depends_on = [ data.talos_client_configuration.talosconfig ]
}
# TODO : Wait for talos_cluster_kubeconfig...
resource "helm_release" "cilium" {
name = "cilium"
namespace = "kube-system"
repository = "https://helm.cilium.io/"
chart = "cilium"
wait = false
depends_on = [ local_file.kubeconfig, talos_cluster_kubeconfig.kube ]
set {
name = "ipam.mode"
value = "kubernetes"
}
set {
name = "kubeProxyReplacement"
value = true
}
set {
name = "securityContext.capabilities.ciliumAgent"
value = "{CHOWN,KILL,NET_ADMIN,NET_RAW,IPC_LOCK,SYS_ADMIN,SYS_RESOURCE,DAC_OVERRIDE,FOWNER,SETGID,SETUID}"
}
set {
name = "securityContext.capabilities.cleanCiliumState"
value = "{NET_ADMIN,SYS_ADMIN,SYS_RESOURCE}"
}
set {
name = "cgroup.autoMount.enabled"
value = false
}
set {
name = "cgroup.hostRoot"
value = "/sys/fs/cgroup"
}
set {
name = "k8sServiceHost"
value = "localhost"
}
set {
name = "k8sServicePort"
value = 7445
}
set {
name = "etcd.clusterDomain"
value = "cluster.local"
}
set {
name = "hubble.relay.enabled"
value = true
}
# Enable hubble ui
set {
name = "hubble.ui.enabled"
value = true
}
# Gateway API support
set {
name = "gatewayAPI.enabled"
value = true
}
set {
name = "gatewayAPI.enableAlpn"
value = true
}
set {
name = "gatewayAPI.enableAppProtocol"
value = true
}
# Gateway API trusted hops : for reverse proxy
set {
name = "gatewayAPI.xffNumTrustedHops"
value = 1
}
# Single-node cluster, so 1 operator only
set {
name = "operator.replicas"
value = 1
}
# L2 announcements
set {
name = "l2announcements.enabled"
value = true
}
set {
name = "externalIPs.enabled"
value = true
}
# Disable ingress controller (traefik will be used for now)
set {
name = "ingressController.enabled"
value = false
}
set {
name = "ingressController.loadbalancerMode"
value = "shared"
}
# Ingress controller for external : behind reverse proxy, trust 1 hop
set {
name = "envoy.xffNumTrustedHopsL7PolicyIngress"
value = 1
}
# Set cilium as default ingress controller
set {
name = "ingressController.default"
value = true
}
set {
name = "ingressController.service.externalTrafficPolicy"
value = "Local"
}
}
resource "kubernetes_namespace" "flux-system" {
metadata {
name = "flux-system"
}
lifecycle {
ignore_changes = [ metadata[0].annotations, metadata[0].labels ]
}
depends_on = [ talos_cluster_kubeconfig.kube, local_file.kubeconfig, helm_release.cilium ]
}
resource "kubernetes_secret" "flux-sops" {
metadata {
name = "flux-sops"
namespace = "flux-system"
}
type = "generic"
data = {
"sops.asc"=var.sops_private_key
}
depends_on = [ kubernetes_namespace.flux-system ]
}
resource "helm_release" "flux-operator" {
name = "flux-operator"
namespace = "flux-system"
repository = "oci://ghcr.io/controlplaneio-fluxcd/charts"
chart = "flux-operator"
wait = true
depends_on = [ kubernetes_secret.flux-sops ]
}
resource "helm_release" "flux-instance" {
name = "flux"
namespace = "flux-system"
repository = "oci://ghcr.io/controlplaneio-fluxcd/charts"
chart = "flux-instance"
values = [
file("values/components.yaml")
]
set {
name = "instance.distribution.version"
value = "2.x"
}
set {
name = "instance.distribution.registry"
value = "ghcr.io/fluxcd"
}
set {
name = "instance.sync.name"
value = "homeprod"
}
set {
name = "instance.sync.kind"
value = "GitRepository"
}
set {
name = "instance.sync.url"
value = "https://github.com/vhaudiquet/homeprod"
}
set {
name = "instance.sync.path"
value = "kubernetes/"
}
set {
name = "instance.sync.ref"
value = "refs/heads/main"
}
depends_on = [ helm_release.flux-operator ]
}
-34
View File
@@ -1,34 +0,0 @@
instance:
components:
- source-controller
- kustomize-controller
- helm-controller
- notification-controller
- image-reflector-controller
- image-automation-controller
cluster:
type: kubernetes
multitenant: false
networkPolicy: true
domain: "cluster.local"
kustomize:
patches:
- target:
kind: Deployment
name: "(kustomize-controller|helm-controller)"
patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --concurrent=10
- op: add
path: /spec/template/spec/containers/0/args/-
value: --requeue-dependency=10s
- patch: |
- op: add
path: /spec/decryption
value:
provider: sops
secretRef:
name: flux-sops
target:
kind: Kustomization
-16
View File
@@ -1,16 +0,0 @@
variable "sops_private_key" {
description = "Private SOPS GPG key for flux/kubernetes to decrypt secrets"
type = string
}
variable "kube_hostname" {
description = "Kubernetes cluster hostname"
type = string
}
variable "kube_host" {
description = "Kubernetes cluster host"
type = string
}
variable "physical_hostname" {
description = "Host name of the physical host for the kubernetes VM"
type = string
}
-223
View File
@@ -1,223 +0,0 @@
# Talos control-plane node for the Raspberry Pi 4 — joins the r740 "kube" cluster
# as a third etcd member to restore quorum (2-of-3 majority). Unlike the p330
# failover node, this node is tainted "quorum" so no user workloads are ever
# scheduled on it; only essential DaemonSets (Cilium, etc.) that tolerate the
# taint land here for cluster networking.
#
# Secret handling: the cluster machine secrets are provided via
# var.machine_secrets_file (a local, gitignored JSON file in the provider's
# machine_secrets format). They are consumed by EPHEMERAL resources and
# WRITE-ONLY attributes so they never land in Terraform state. See
# variables.tf and scripts/extract-talos-secrets.sh for how to produce the
# file from the live r740 node.
terraform {
required_providers {
talos = {
source = "siderolabs/talos"
version = "0.11.0"
}
null = {
source = "hashicorp/null"
version = "3.2.3"
}
}
}
locals {
# Load the machine secrets from the gitignored JSON file. This local is only
# ever referenced by ephemeral resources / write-only attributes, so the
# values are never persisted to state.
machine_secrets = jsondecode(file(var.machine_secrets_file))
# Network config: static if node_subnet is provided, otherwise Talos DHCPs.
# The rpi4 uses DHCP (node_subnet = null), so only nameservers are patched in.
static_network = var.node_subnet == null ? {} : {
interfaces = [{
interface = var.network_interface
addresses = [var.node_subnet]
routes = var.node_gateway == null ? [] : [{ gateway = var.node_gateway }]
}]
}
network_patch = {
nameservers = var.nameservers
}
network_patch_merged = merge(local.network_patch, local.static_network)
machine_patch = {
install = {
image = var.installer_image
disk = var.install_disk
}
network = local.network_patch_merged
# NOTE: no Longhorn iSCSI/ext4 kernel modules here. This is a quorum-only
# node: the quorum taint keeps user workloads (and Longhorn replicas) off
# it, so the storage stack is not needed. Essential DaemonSets such as
# Cilium still run here for cluster networking and tolerate the taint.
sysctls = {
"fs.inotify.max_user_instances" = "1024"
"fs.inotify.max_user_watches" = "1048576"
}
kubelet = {
# Register the node already tainted so the scheduler never admits user
# workloads even before the null_resource below runs. NoSchedule is
# sufficient: essential DaemonSets (Cilium, etc.) tolerate it, but no
# user pods are admitted.
extraArgs = {
"register-with-taints" = "${var.quorum_taint_key}=${var.quorum_taint_value}:${var.quorum_taint_effect}"
}
}
}
}
# --- Ephemeral resources: secrets never stored in state ---------------------
#
# talos_machine_configuration generates the control-plane join config from the
# provided machine_secrets. The output (machine_configuration) is an ephemeral
# value — it can only flow into write-only attributes or provisioners, never
# into a persisted resource attribute.
ephemeral "talos_machine_configuration" "rpi4" {
cluster_name = var.cluster_name
machine_type = "controlplane"
cluster_endpoint = var.cluster_endpoint
machine_secrets = local.machine_secrets
config_patches = [
yamlencode({
machine = local.machine_patch
}),
# Pin the Kubernetes node name via a HostnameConfig document (Talos v1.13+).
# The old machine.network.hostname field conflicts with the default
# HostnameConfig document ("static hostname is already set"), so we use the
# document-based config with auto: off + an explicit hostname instead.
yamlencode({
apiVersion = "v1alpha1"
kind = "HostnameConfig"
hostname = var.rpi4_node_name
auto = "off"
}),
yamlencode({
cluster = {
network = {
cni = {
name = "none"
}
}
}
})
]
}
# talos_client_configuration generates a Talos client config (talosconfig) from
# the machine_secrets, scoped to the rpi4 node. Also ephemeral — used only to
# drive the write-only client_configuration_wo on the apply resource.
ephemeral "talos_client_configuration" "rpi4" {
cluster_name = var.cluster_name
machine_secrets = local.machine_secrets
nodes = [var.rpi4_host]
}
# --- Apply the config to the node (write-only attrs → no secrets in state) --
#
# machine_configuration_input_wo and client_configuration_wo are write-only:
# Terraform uses them during apply but does NOT persist them to state. Only a
# hash of the machine config (machine_configuration_hash) is stored, for drift
# detection. Because the config patch contains a `machine.install` block, when
# Talos receives this config on a node booted from the SD card (maintenance)
# image it installs itself to install.disk and reboots into the installed
# system. As a controlplane node it then joins the existing etcd cluster as a
# new member and runs the control-plane components. With r740 + p330 + rpi4 the
# etcd cluster reaches 3 members → 2-of-3 quorum.
resource "talos_machine_configuration_apply" "rpi4" {
node = var.rpi4_host
client_configuration_wo = ephemeral.talos_client_configuration.rpi4.client_configuration
machine_configuration_input_wo = ephemeral.talos_machine_configuration.rpi4.machine_configuration
}
# --- Write the rendered config to disk for manual use ----------------------
#
# local_file.content cannot accept an ephemeral value (it would persist to
# state), so we use a null_resource local-exec provisioner instead —
# provisioners do not persist their arguments to state. This writes rpi4.yaml
# so the config can also be applied manually with
# `talosctl apply-config --nodes <rpi4_host> --file rpi4.yaml` if needed.
resource "null_resource" "rpi4_machine_config_file" {
triggers = {
# Re-run only when the (non-secret) inputs that shape the config change.
node = var.rpi4_node_name
install_disk = var.install_disk
installer_image = var.installer_image
taint = "${var.quorum_taint_key}=${var.quorum_taint_value}:${var.quorum_taint_effect}"
}
provisioner "local-exec" {
command = <<-EOT
set -euo pipefail
cat > "${path.module}/rpi4.yaml" <<'YAMLEOF'
${ephemeral.talos_machine_configuration.rpi4.machine_configuration}
YAMLEOF
echo "Wrote ${path.module}/rpi4.yaml"
EOT
}
depends_on = [talos_machine_configuration_apply.rpi4]
}
# --- Wait for the node, then label + taint ---------------------------------
#
# Wait for the node to register with Kubernetes (kubelet creates the Node
# object after Talos installs and reboots), then label it and (re)apply the
# quorum taint. This is idempotent: kubectl exits 0 if the label/taint already
# exists. The taint is also set via kubelet `register-with-taints`, so this
# null_resource is a safety net for manual edits / drift. The kubeconfig path
# is only used inside the provisioner (not persisted to state).
resource "null_resource" "rpi4_node_label_and_taint" {
triggers = {
node = var.rpi4_node_name
key = var.quorum_taint_key
value = var.quorum_taint_value
effect = var.quorum_taint_effect
kubeconfig = var.kubeconfig_path
}
provisioner "local-exec" {
# Wait for the node to show up, then label + taint. The wait loop is bounded
# by kubectl --timeout; tune it via TF_LOG / re-run if the node is slow to
# join (a controlplane node must first complete the etcd join handshake).
command = <<-EOT
set -euo pipefail
KUBECONFIG="${var.kubeconfig_path}"
export KUBECONFIG
NODE="${var.rpi4_node_name}"
echo "Waiting for node $NODE to be registered (kubelet creates the Node object once Talos has installed, rebooted and joined etcd)..."
# kubectl wait --for=condition=Ready fails instantly with NotFound if the
# node object doesn't exist yet, so poll for existence first.
# /bin/sh (dash) has no $SECONDS, so count iterations with a bounded loop.
tries=240 # 240 * 5s = 20 minutes max
until kubectl get node "$NODE" >/dev/null 2>&1; do
tries=$((tries - 1))
if [ "$tries" -le 0 ]; then
echo "Timed out waiting for node $NODE to register." >&2
exit 1
fi
sleep 5
done
echo "Node $NODE registered. Waiting for it to become Ready..."
# Now wait for Ready (a controlplane node needs etcd joined + apiserver up).
kubectl wait --for=condition=Ready "node/$NODE" --timeout=20m || \
kubectl wait --for=jsonpath='{.status.conditions[?(@.reason=="KubeletReady")].status}'=True "node/$NODE" --timeout=20m
# Quorum marker + taint (applied to the controlplane node).
kubectl label --overwrite node "$NODE" homeprod.io/quorum=true
# Apply the taint idempotently (kubectl taint --overwrite is a no-op if it exists).
kubectl taint --overwrite node "$NODE" \
"${var.quorum_taint_key}=${var.quorum_taint_value}:${var.quorum_taint_effect}"
echo "Node $NODE ready, labeled and tainted for quorum-only scheduling."
EOT
}
depends_on = [talos_machine_configuration_apply.rpi4]
}
-146
View File
@@ -1,146 +0,0 @@
# Variables for the Raspberry Pi 4 Talos control-plane node that joins the r740
# "kube" cluster as a third etcd member to restore quorum (2-of-3 majority).
#
# Secret handling: the cluster machine secrets (cluster id/secret, etcd/k8s
# certs, bootstrap token) are NOT read from terraform state (the r740 state is
# stale) and are NOT generated here (that would create a new, incompatible
# cluster). Instead they are provided via `machine_secrets_file` — a local,
# gitignored JSON file in the Talos provider's machine_secrets format. The
# file is produced once from the live r740 node (see
# scripts/extract-talos-secrets.sh) and stored in a real secret manager
# (Bitwarden); you paste it back to disk when running this module. Ephemeral
# resources + write-only attributes ensure the secrets never land in Terraform
# state.
variable "rpi4_host" {
description = "Reachable IP/hostname of the rpi4 Talos node (for Talos API access). With DHCP this is the leased IP (e.g. 10.1.2.135)."
type = string
}
variable "rpi4_node_name" {
description = "Kubernetes/Talos node name for the rpi4 (e.g. rpi4). Pinned via machine.network.hostname so the node registers with this name regardless of DHCP."
type = string
default = "rpi4"
}
# --- Cluster identity (no terraform_remote_state — state is stale) ----------
variable "cluster_name" {
description = "Name of the existing Talos cluster the rpi4 joins. Must match the cluster the r740 bootstrapped (kube-r740)."
type = string
default = "kube-r740"
}
variable "cluster_endpoint" {
description = "Endpoint (host:port) of the Talos/Kubernetes API on the cluster. Must match the r740 bootstrap endpoint."
type = string
default = "https://kube-r740.lan:6443"
}
# --- Secrets (provided manually, never in state) ---------------------------
variable "machine_secrets_file" {
description = <<EOT
Path to a local, gitignored JSON file containing the cluster machine secrets in
the Talos provider's machine_secrets format (cluster.id, cluster.secret, certs,
secrets.bootstrap_token, secrets.secretbox_encryption_secret, trustdinfo.token).
Generate it once from the live r740 node with
scripts/extract-talos-secrets.sh, store the contents in Bitwarden, and paste it
back to this file when running this module. The file MUST be gitignored — it
contains the cluster root of trust.
EOT
type = string
default = "secrets.json"
}
# --- Install / network -----------------------------------------------------
variable "installer_image" {
description = <<EOT
Talos installer image to use on the rpi4 (bare metal, ARM64).
Must be an ARM64 Image Factory build (schematic generated at
https://factory.talos.dev) for the Raspberry Pi 4 platform. Unlike the x86
control-plane nodes, this quorum node does NOT need the iSCSI/Longhorn
extensions because no user workloads or Longhorn replicas are scheduled on it
(the quorum taint keeps it empty); only essential DaemonSets (Cilium, etc.)
land here.
EOT
type = string
default = "factory.talos.dev/installer/ee21ef4a5ef808a9b7484cc0dda0f25075021691c8c09a276591eedb638ea1f9:v1.13.6"
}
variable "install_disk" {
description = "Block device path to install Talos on. For the rpi4 booting from the SD card this is /dev/mmcblk0."
type = string
default = "/dev/mmcblk0"
}
variable "node_subnet" {
description = <<EOT
Static IPv4 address in CIDR notation for the rpi4 node (e.g. 10.1.2.135/24).
Set to null (default) to use DHCP. The rpi4 uses DHCP, so a static address is
not required; the node registers with Kubernetes under rpi4_node_name regardless
of the leased IP.
EOT
type = string
default = null
}
variable "node_gateway" {
description = "IPv4 gateway for the rpi4 node. Ignored when node_subnet is null (DHCP)."
type = string
default = null
}
variable "network_interface" {
description = <<EOT
Primary network interface name on the rpi4. The built-in Ethernet port is eth0.
EOT
type = string
default = "eth0"
}
variable "nameservers" {
description = "DNS nameservers configured on the node (must work independently of kube)."
type = list(string)
default = ["10.1.2.148", "1.1.1.1"]
}
# --- Quorum taint ----------------------------------------------------------
variable "quorum_taint_key" {
description = "Taint key applied to the node to reserve it as a quorum-only member (no user workloads)."
type = string
default = "dedicated"
}
variable "quorum_taint_value" {
description = "Taint value applied to the node."
type = string
default = "quorum"
}
variable "quorum_taint_effect" {
description = "Taint effect applied to the node. NoSchedule is sufficient: essential DaemonSets (Cilium, etc.) tolerate it for networking, but no user workloads are admitted."
type = string
default = "NoSchedule"
validation {
condition = contains(["NoSchedule", "PreferNoSchedule", "NoExecute"], var.quorum_taint_effect)
error_message = "quorum_taint_effect must be NoSchedule, PreferNoSchedule or NoExecute."
}
}
# --- Kubeconfig for the label/taint null_resource --------------------------
variable "kubeconfig_path" {
description = <<EOT
Path to a kubeconfig for the cluster, used by the null_resource that waits for
the node and applies the quorum label/taint. This is NOT stored in state — it is
only referenced inside a local-exec provisioner. Point it at the r740 kube
module's kubeconfig (../../r740/kube/kubeconfig) or any valid kubeconfig for the
cluster.
EOT
type = string
default = "../../r740/kube/kubeconfig"
}