mirror of
https://github.com/vhaudiquet/homeprod.git
synced 2026-09-10 06:20:58 +00:00
infra: remove stale old kube files, infra/kube is new truth
This commit is contained in:
@@ -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]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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 ]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user