mirror of
https://github.com/immich-app/yucca.git
synced 2026-09-30 13:33:00 +08:00
feat(k8s): cluster naming (#243)
* chore(netbird): move to kebab naming Render NetBird object names (groups, setup keys, policies, networks, network-resources) as lowercase-kebab instead of UPPER_SNAKE, e.g. YUCCA_PROD_HTZ_FSN1_MGMT → yucca-prod-htz-fsn1-mgmt. The 1Password setup-key item titles stay UPPER_SNAKE (decoupled) so CI/ansible/talos op:// consumers keep resolving. Pin the futo-org/netbird provider to 1.0.2, which fixes the group resources TF→API decode so a resource-tag group (htz-fsn1 `resources`) can be renamed in place — no name pin needed. * update locks * chore(naming): naming names
This commit is contained in:
@@ -187,3 +187,19 @@ highest tag to staging → production is gated behind a reviewed `promote-produc
|
|||||||
trailing commas, width 120.
|
trailing commas, width 120.
|
||||||
- Generated files are eslint-ignored: `**/fetch-client.ts`, `packages/web/src/locales`, `dist`,
|
- Generated files are eslint-ignored: `**/fetch-client.ts`, `packages/web/src/locales`, `dist`,
|
||||||
`build`, `.svelte-kit`.
|
`build`, `.svelte-kit`.
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
- **Cluster names are themed by workload.** Kubernetes clusters → **Star Wars** (`luke` =
|
||||||
|
staging, `father` = the soon-to-be prod). Ceph clusters → **Dune** (`sietch`, `spice`, …).
|
||||||
|
Choose the next themed name when standing up a cluster; it's the Talos/Ceph cluster name +
|
||||||
|
the `<clustername>` hostname segment.
|
||||||
|
- **Node hostnames** follow `<product>-<provider>-<region>-<clustername>-<role>-<nodename>` —
|
||||||
|
e.g. a staging Talos node is `yucca-int-aus-luke-k8s-<word>`:
|
||||||
|
- `product` = `yucca`; `role` = the workload segment (`k8s` for Talos nodes, `ceph` for Ceph).
|
||||||
|
- `provider` / `region` = the **3-letter** `provider_code` / `region_code` from the region's
|
||||||
|
`region.hcl` (austin = `int`/`aus`, htz-fsn1 = `htz`/`fsn`).
|
||||||
|
- `<clustername>` = the themed cluster name (above).
|
||||||
|
- `<nodename>` = auto-picked from the shared name inventory
|
||||||
|
(`tf/shared/modules/node-names/wordlist.txt`) — deterministic per cluster, unique within it;
|
||||||
|
pass an explicit node `name` to override.
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ variable "provider_code" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "region_code" {
|
||||||
|
description = "3-letter region segment of node hostnames (null for global)."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
variable "domain" {
|
variable "domain" {
|
||||||
description = "Region FQDN suffix (null for global)."
|
description = "Region FQDN suffix (null for global)."
|
||||||
type = string
|
type = string
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ locals {
|
|||||||
role = "primary"
|
role = "primary"
|
||||||
site_id = 40
|
site_id = 40
|
||||||
datacenter = "fsn1"
|
datacenter = "fsn1"
|
||||||
provider_code = "htz"
|
provider_code = "htz" # 3-letter provider segment of node hostnames
|
||||||
|
region_code = "fsn" # 3-letter region segment of node hostnames
|
||||||
domain = "prod.fsn1.htz.futo.cloud"
|
domain = "prod.fsn1.htz.futo.cloud"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ variable "provider_code" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "region_code" {
|
||||||
|
description = "3-letter region segment of node hostnames (null for global)."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
variable "domain" {
|
variable "domain" {
|
||||||
description = "Region FQDN suffix (null for global)."
|
description = "Region FQDN suffix (null for global)."
|
||||||
type = string
|
type = string
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ locals {
|
|||||||
role = "primary"
|
role = "primary"
|
||||||
site_id = null # austin is not a fabric-managed site (no switch fabric)
|
site_id = null # austin is not a fabric-managed site (no switch fabric)
|
||||||
datacenter = "austin"
|
datacenter = "austin"
|
||||||
provider_code = "int"
|
provider_code = "int" # 3-letter provider segment of node hostnames
|
||||||
|
region_code = "aus" # 3-letter region segment of node hostnames
|
||||||
domain = "staging.austin.int.futo.cloud"
|
domain = "staging.austin.int.futo.cloud"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
# reachable across the install reboot.
|
# reachable across the install reboot.
|
||||||
|
|
||||||
clusters = {
|
clusters = {
|
||||||
yucca-staging = {
|
# Star Wars-themed cluster name (staging = luke). Node hostnames derive as
|
||||||
|
# yucca-int-aus-luke-k8s-<name> (name auto-picked from the shared inventory).
|
||||||
|
luke = {
|
||||||
talos_version = "1.13.4" # latest stable (v1.13.4); default k8s = 1.36.1
|
talos_version = "1.13.4" # latest stable (v1.13.4); default k8s = 1.36.1
|
||||||
kubernetes_version = "v1.36.1"
|
kubernetes_version = "v1.36.1"
|
||||||
|
|
||||||
@@ -105,9 +107,9 @@ clusters = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nodes = [
|
nodes = [
|
||||||
{ name = "staging-cp1", role = "control-plane", address = "10.10.10.47" },
|
{ role = "control-plane", address = "10.10.10.47" },
|
||||||
{ name = "staging-cp2", role = "control-plane", address = "10.10.10.242" },
|
{ role = "control-plane", address = "10.10.10.242" },
|
||||||
{ name = "staging-cp3", role = "control-plane", address = "10.10.10.117" },
|
{ role = "control-plane", address = "10.10.10.117" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ module "cluster" {
|
|||||||
source = "../../../../shared/modules/talos-baremetal"
|
source = "../../../../shared/modules/talos-baremetal"
|
||||||
|
|
||||||
cluster_name = each.key
|
cluster_name = each.key
|
||||||
|
provider_code = var.provider_code
|
||||||
|
region_code = var.region_code
|
||||||
talos_version = each.value.talos_version
|
talos_version = each.value.talos_version
|
||||||
kubernetes_version = each.value.kubernetes_version
|
kubernetes_version = each.value.kubernetes_version
|
||||||
talos_schematic_id = each.value.talos_schematic_id
|
talos_schematic_id = each.value.talos_schematic_id
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ variable "provider_code" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "region_code" {
|
||||||
|
description = "3-letter region segment of node hostnames (null for global)."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
variable "domain" {
|
variable "domain" {
|
||||||
description = "Region FQDN suffix (null for global)."
|
description = "Region FQDN suffix (null for global)."
|
||||||
type = string
|
type = string
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ variable "netbird_operator_api_token" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "clusters" {
|
variable "clusters" {
|
||||||
description = "Map of bare-metal Talos cluster specs keyed by short cluster name. Declarative: add/modify an entry in clusters.auto.tfvars + tf:apply."
|
description = "Map of bare-metal Talos cluster specs keyed by themed cluster name (Star Wars; e.g. 'luke'). Declarative: add/modify an entry in clusters.auto.tfvars + tf:apply."
|
||||||
type = map(object({
|
type = map(object({
|
||||||
talos_version = string
|
talos_version = string
|
||||||
kubernetes_version = optional(string)
|
kubernetes_version = optional(string)
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ terraform {
|
|||||||
source = "siderolabs/talos"
|
source = "siderolabs/talos"
|
||||||
version = "~> 0.11"
|
version = "~> 0.11"
|
||||||
}
|
}
|
||||||
|
# Hostname picks for the talos nodes (node-names module → random_shuffle).
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.6"
|
||||||
|
}
|
||||||
helm = {
|
helm = {
|
||||||
source = "hashicorp/helm"
|
source = "hashicorp/helm"
|
||||||
version = "~> 3.1"
|
version = "~> 3.1"
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ variable "provider_code" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "region_code" {
|
||||||
|
description = "3-letter region segment of node hostnames (null for global)."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
variable "domain" {
|
variable "domain" {
|
||||||
description = "Region FQDN suffix (null for global)."
|
description = "Region FQDN suffix (null for global)."
|
||||||
type = string
|
type = string
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ locals {
|
|||||||
site_id = lookup(local.region_meta, "site_id", null)
|
site_id = lookup(local.region_meta, "site_id", null)
|
||||||
datacenter = lookup(local.region_meta, "datacenter", null)
|
datacenter = lookup(local.region_meta, "datacenter", null)
|
||||||
provider_code = lookup(local.region_meta, "provider_code", null)
|
provider_code = lookup(local.region_meta, "provider_code", null)
|
||||||
|
region_code = lookup(local.region_meta, "region_code", null)
|
||||||
domain = lookup(local.region_meta, "domain", null)
|
domain = lookup(local.region_meta, "domain", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,5 +101,6 @@ inputs = {
|
|||||||
site_id = local.site_id
|
site_id = local.site_id
|
||||||
datacenter = local.datacenter
|
datacenter = local.datacenter
|
||||||
provider_code = local.provider_code
|
provider_code = local.provider_code
|
||||||
|
region_code = local.region_code
|
||||||
domain = local.domain
|
domain = local.domain
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,41 +15,33 @@ terraform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
# Auto host names come from the shared node-names inventory (the wordlist used to
|
||||||
# Wordlist for auto-assigning host names. Names are unique within a cluster
|
# live here; it now backs both talos + ceph). cluster_name seeds the shuffle so each
|
||||||
# (not globally) — sietch-ceph-laurel and a hypothetical future
|
# cluster gets its own permutation; explicit host names are excluded from the pool.
|
||||||
# a second cluster's *-ceph-laurel could coexist, disambiguated by FQDN.
|
module "names" {
|
||||||
wordlist = compact(split("\n", file("${path.module}/wordlist.txt")))
|
source = "../node-names"
|
||||||
|
cluster_name = var.cluster_name
|
||||||
# Operator-declared names are reserved — strip them from the pool so
|
name_seed = var.name_seed
|
||||||
# auto-picked names can't collide within this cluster.
|
names = [for h in var.hosts : h.name]
|
||||||
explicit_names = [for h in var.hosts : h.name if h.name != null]
|
|
||||||
available_words = tolist(setsubtract(toset(local.wordlist), toset(local.explicit_names)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Full shuffle of available words, seeded by cluster name. Position-indexed:
|
# The wordlist + shuffle moved into node-names — preserve the existing shuffle state
|
||||||
# host[i] with name == null uses result[i]. Adding hosts at the tail is safe;
|
# so host names don't re-randomize on this refactor.
|
||||||
# existing positions keep their names across applies. Bumping name_seed forces
|
moved {
|
||||||
# a re-roll of the whole cluster (not recommended once hosts exist).
|
from = random_shuffle.names
|
||||||
resource "random_shuffle" "names" {
|
to = module.names.random_shuffle.names
|
||||||
input = local.available_words
|
|
||||||
result_count = length(local.available_words)
|
|
||||||
keepers = {
|
|
||||||
cluster_name = var.cluster_name
|
|
||||||
name_seed = var.name_seed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
# Resolve each host's final name: explicit or auto-picked from shuffle.
|
# Resolve each host's final name: explicit or auto-picked from shuffle.
|
||||||
hosts_computed = [
|
hosts_computed = [
|
||||||
for i, h in var.hosts : {
|
for i, h in var.hosts : {
|
||||||
name = h.name != null ? h.name : random_shuffle.names.result[i]
|
name = module.names.resolved[i]
|
||||||
bond_ip = h.bond_ip
|
bond_ip = h.bond_ip
|
||||||
bootstrap = coalesce(h.bootstrap, false)
|
bootstrap = coalesce(h.bootstrap, false)
|
||||||
roles = h.roles
|
roles = h.roles
|
||||||
hostname_short = "${var.cluster_name}-${var.role_in_hostname}-${h.name != null ? h.name : random_shuffle.names.result[i]}"
|
hostname_short = "${var.cluster_name}-${var.role_in_hostname}-${module.names.resolved[i]}"
|
||||||
fqdn = "${var.cluster_name}-${var.role_in_hostname}-${h.name != null ? h.name : random_shuffle.names.result[i]}.${var.domain}"
|
fqdn = "${var.cluster_name}-${var.role_in_hostname}-${module.names.resolved[i]}.${var.domain}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# node-names — the shared name inventory for auto-naming nodes/hosts. Resolves a
|
||||||
|
# per-slot list of names: an explicit override where given, else a deterministic
|
||||||
|
# pick from the wordlist. Names are unique within a cluster (the shuffle is seeded
|
||||||
|
# by cluster_name; explicit names are excluded from the pool so they can't collide).
|
||||||
|
# Position-indexed: slot[i] with no explicit name gets result[i], so appending
|
||||||
|
# slots at the tail is stable (existing slots keep their names across applies).
|
||||||
|
terraform {
|
||||||
|
required_version = ">= 1.6"
|
||||||
|
required_providers {
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cluster_name" {
|
||||||
|
type = string
|
||||||
|
description = "Seeds the shuffle so each cluster gets its own permutation."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name_seed" {
|
||||||
|
type = string
|
||||||
|
default = "v1"
|
||||||
|
description = "Bump to re-roll a cluster's auto-names. Do NOT change once nodes are deployed (renames every auto-named node)."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "names" {
|
||||||
|
type = list(string)
|
||||||
|
description = "Per-slot name: an explicit string to reserve, or null to auto-pick. Resolved positionally in `resolved`."
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
wordlist = compact(split("\n", file("${path.module}/wordlist.txt")))
|
||||||
|
explicit_names = [for n in var.names : n if n != null]
|
||||||
|
available_words = tolist(setsubtract(toset(local.wordlist), toset(local.explicit_names)))
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_shuffle" "names" {
|
||||||
|
input = local.available_words
|
||||||
|
result_count = length(local.available_words)
|
||||||
|
keepers = {
|
||||||
|
cluster_name = var.cluster_name
|
||||||
|
name_seed = var.name_seed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "resolved" {
|
||||||
|
description = "The resolved name per input slot (explicit where given, else auto-picked), in order."
|
||||||
|
value = [for i, n in var.names : n != null ? n : random_shuffle.names.result[i]]
|
||||||
|
}
|
||||||
@@ -11,17 +11,35 @@
|
|||||||
# kernel `ip=` cmdline. Nodes are dialed directly at their maintenance IP,
|
# kernel `ip=` cmdline. Nodes are dialed directly at their maintenance IP,
|
||||||
# which is also pinned as the post-install static IP on the bond.
|
# which is also pinned as the post-install static IP on the bond.
|
||||||
|
|
||||||
|
# Per-node names from the shared inventory (operator override per node, else auto).
|
||||||
|
module "names" {
|
||||||
|
source = "../node-names"
|
||||||
|
cluster_name = var.cluster_name
|
||||||
|
name_seed = var.name_seed
|
||||||
|
names = [for n in var.nodes : n.name]
|
||||||
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
cluster_endpoint = coalesce(var.cluster_endpoint, "https://${var.cluster_vip}:6443")
|
cluster_endpoint = coalesce(var.cluster_endpoint, "https://${var.cluster_vip}:6443")
|
||||||
|
|
||||||
# Prefix length from the subnet CIDR (e.g. "24" from "10.10.10.0/24").
|
# Prefix length from the subnet CIDR (e.g. "24" from "10.10.10.0/24").
|
||||||
netmask = split("/", var.subnet_cidr)[1]
|
netmask = split("/", var.subnet_cidr)[1]
|
||||||
|
|
||||||
# Stable per-name maps (TF map iteration is alphabetical by key, so plan
|
# Each node's hostname per the fleet convention:
|
||||||
|
# <product>-<provider>-<region>-<clustername>-<role>-<nodename>
|
||||||
|
# e.g. yucca-int-aus-luke-k8s-<word>. <nodename> = nodes[i].name or auto-picked.
|
||||||
|
nodes_named = [
|
||||||
|
for i, n in var.nodes : merge(n, {
|
||||||
|
role = coalesce(n.role, "control-plane")
|
||||||
|
hostname = "${var.product}-${var.provider_code}-${var.region_code}-${var.cluster_name}-${var.role_in_hostname}-${module.names.resolved[i]}"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
# Stable per-hostname maps (TF map iteration is alphabetical by key, so plan
|
||||||
# ordering is deterministic across runs).
|
# ordering is deterministic across runs).
|
||||||
node_map = { for n in var.nodes : n.name => n }
|
node_map = { for n in local.nodes_named : n.hostname => n }
|
||||||
cp_node_map = { for k, n in local.node_map : k => n if coalesce(n.role, "control-plane") == "control-plane" }
|
cp_node_map = { for k, n in local.node_map : k => n if n.role == "control-plane" }
|
||||||
worker_node_map = { for k, n in local.node_map : k => n if coalesce(n.role, "control-plane") == "worker" }
|
worker_node_map = { for k, n in local.node_map : k => n if n.role == "worker" }
|
||||||
|
|
||||||
cp_addresses = [for k, n in local.cp_node_map : n.address]
|
cp_addresses = [for k, n in local.cp_node_map : n.address]
|
||||||
|
|
||||||
@@ -158,7 +176,7 @@ locals {
|
|||||||
apiVersion = "v1alpha1"
|
apiVersion = "v1alpha1"
|
||||||
kind = "HostnameConfig"
|
kind = "HostnameConfig"
|
||||||
auto = "off"
|
auto = "off"
|
||||||
hostname = n.name
|
hostname = n.hostname
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,39 @@
|
|||||||
variable "cluster_name" {
|
variable "cluster_name" {
|
||||||
description = "Short cluster identifier (e.g., yucca-staging). Drives the Talos cluster name + node hostname prefix is supplied per-node in `nodes`."
|
description = "Themed cluster name (Star Wars for k8s, e.g. 'luke'/'father'). Used as the Talos cluster name + the <clustername> segment of each node hostname."
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Node hostname convention: <product>-<provider>-<region>-<clustername>-<role>-<nodename>
|
||||||
|
# e.g. yucca-int-aus-luke-k8s-<random>. provider/region are the 3-letter codes
|
||||||
|
# from the region's region.hcl; <nodename> auto-derives from the shared inventory.
|
||||||
|
variable "product" {
|
||||||
|
description = "Product segment of node hostnames."
|
||||||
|
type = string
|
||||||
|
default = "yucca"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "provider_code" {
|
||||||
|
description = "3-letter provider segment of node hostnames (region.hcl provider_code, e.g. 'int' / 'htz')."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region_code" {
|
||||||
|
description = "3-letter region segment of node hostnames (region.hcl region_code, e.g. 'aus' / 'fsn')."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "role_in_hostname" {
|
||||||
|
description = "Workload-role segment of node hostnames ('k8s' for Talos clusters). Distinct from the per-node control-plane/worker role."
|
||||||
|
type = string
|
||||||
|
default = "k8s"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name_seed" {
|
||||||
|
description = "Seed for the per-node random name pick (shared node-names inventory). Do NOT change once nodes are deployed (renames every auto-named node)."
|
||||||
|
type = string
|
||||||
|
default = "v1"
|
||||||
|
}
|
||||||
|
|
||||||
variable "talos_version" {
|
variable "talos_version" {
|
||||||
description = "Talos Linux version to install (e.g., '1.13.3'). Sets machine config schema AND the install image tag (so the installed system is exactly this version, regardless of the maintenance-mode boot image)."
|
description = "Talos Linux version to install (e.g., '1.13.3'). Sets machine config schema AND the install image tag (so the installed system is exactly this version, regardless of the maintenance-mode boot image)."
|
||||||
type = string
|
type = string
|
||||||
@@ -79,7 +110,7 @@ variable "nodes" {
|
|||||||
and Terraform stays reachable across the install reboot).
|
and Terraform stays reachable across the install reboot).
|
||||||
EOT
|
EOT
|
||||||
type = list(object({
|
type = list(object({
|
||||||
name = string
|
name = optional(string) # explicit hostname override; null = auto-pick from the shared inventory
|
||||||
role = optional(string, "control-plane")
|
role = optional(string, "control-plane")
|
||||||
address = string
|
address = string
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -5,5 +5,10 @@ terraform {
|
|||||||
source = "siderolabs/talos"
|
source = "siderolabs/talos"
|
||||||
version = "~> 0.11"
|
version = "~> 0.11"
|
||||||
}
|
}
|
||||||
|
# Used by the node-names child module (random_shuffle) for hostname picks.
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user