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.
|
||||
- Generated files are eslint-ignored: `**/fetch-client.ts`, `packages/web/src/locales`, `dist`,
|
||||
`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
|
||||
}
|
||||
|
||||
variable "region_code" {
|
||||
description = "3-letter region segment of node hostnames (null for global)."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "Region FQDN suffix (null for global)."
|
||||
type = string
|
||||
|
||||
@@ -3,6 +3,7 @@ locals {
|
||||
role = "primary"
|
||||
site_id = 40
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ variable "provider_code" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "region_code" {
|
||||
description = "3-letter region segment of node hostnames (null for global)."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "Region FQDN suffix (null for global)."
|
||||
type = string
|
||||
|
||||
@@ -7,6 +7,7 @@ locals {
|
||||
role = "primary"
|
||||
site_id = null # austin is not a fabric-managed site (no switch fabric)
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
# reachable across the install reboot.
|
||||
|
||||
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
|
||||
kubernetes_version = "v1.36.1"
|
||||
|
||||
@@ -105,9 +107,9 @@ clusters = {
|
||||
}
|
||||
|
||||
nodes = [
|
||||
{ name = "staging-cp1", role = "control-plane", address = "10.10.10.47" },
|
||||
{ name = "staging-cp2", 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.47" },
|
||||
{ role = "control-plane", address = "10.10.10.242" },
|
||||
{ role = "control-plane", address = "10.10.10.117" },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ module "cluster" {
|
||||
source = "../../../../shared/modules/talos-baremetal"
|
||||
|
||||
cluster_name = each.key
|
||||
provider_code = var.provider_code
|
||||
region_code = var.region_code
|
||||
talos_version = each.value.talos_version
|
||||
kubernetes_version = each.value.kubernetes_version
|
||||
talos_schematic_id = each.value.talos_schematic_id
|
||||
|
||||
@@ -51,6 +51,12 @@ variable "provider_code" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "region_code" {
|
||||
description = "3-letter region segment of node hostnames (null for global)."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "Region FQDN suffix (null for global)."
|
||||
type = string
|
||||
|
||||
@@ -154,7 +154,7 @@ variable "netbird_operator_api_token" {
|
||||
}
|
||||
|
||||
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({
|
||||
talos_version = string
|
||||
kubernetes_version = optional(string)
|
||||
|
||||
@@ -5,6 +5,11 @@ terraform {
|
||||
source = "siderolabs/talos"
|
||||
version = "~> 0.11"
|
||||
}
|
||||
# Hostname picks for the talos nodes (node-names module → random_shuffle).
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "~> 3.6"
|
||||
}
|
||||
helm = {
|
||||
source = "hashicorp/helm"
|
||||
version = "~> 3.1"
|
||||
|
||||
@@ -51,6 +51,12 @@ variable "provider_code" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "region_code" {
|
||||
description = "3-letter region segment of node hostnames (null for global)."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "Region FQDN suffix (null for global)."
|
||||
type = string
|
||||
|
||||
@@ -54,6 +54,7 @@ locals {
|
||||
site_id = lookup(local.region_meta, "site_id", null)
|
||||
datacenter = lookup(local.region_meta, "datacenter", 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)
|
||||
}
|
||||
|
||||
@@ -100,5 +101,6 @@ inputs = {
|
||||
site_id = local.site_id
|
||||
datacenter = local.datacenter
|
||||
provider_code = local.provider_code
|
||||
region_code = local.region_code
|
||||
domain = local.domain
|
||||
}
|
||||
|
||||
@@ -15,41 +15,33 @@ terraform {
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
# Wordlist for auto-assigning host names. Names are unique within a cluster
|
||||
# (not globally) — sietch-ceph-laurel and a hypothetical future
|
||||
# a second cluster's *-ceph-laurel could coexist, disambiguated by FQDN.
|
||||
wordlist = compact(split("\n", file("${path.module}/wordlist.txt")))
|
||||
|
||||
# Operator-declared names are reserved — strip them from the pool so
|
||||
# auto-picked names can't collide within this cluster.
|
||||
explicit_names = [for h in var.hosts : h.name if h.name != null]
|
||||
available_words = tolist(setsubtract(toset(local.wordlist), toset(local.explicit_names)))
|
||||
# Auto host names come from the shared node-names inventory (the wordlist used to
|
||||
# live here; it now backs both talos + ceph). cluster_name seeds the shuffle so each
|
||||
# cluster gets its own permutation; explicit host names are excluded from the pool.
|
||||
module "names" {
|
||||
source = "../node-names"
|
||||
cluster_name = var.cluster_name
|
||||
name_seed = var.name_seed
|
||||
names = [for h in var.hosts : h.name]
|
||||
}
|
||||
|
||||
# Full shuffle of available words, seeded by cluster name. Position-indexed:
|
||||
# host[i] with name == null uses result[i]. Adding hosts at the tail is safe;
|
||||
# existing positions keep their names across applies. Bumping name_seed forces
|
||||
# a re-roll of the whole cluster (not recommended once hosts exist).
|
||||
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
|
||||
}
|
||||
# The wordlist + shuffle moved into node-names — preserve the existing shuffle state
|
||||
# so host names don't re-randomize on this refactor.
|
||||
moved {
|
||||
from = random_shuffle.names
|
||||
to = module.names.random_shuffle.names
|
||||
}
|
||||
|
||||
locals {
|
||||
# Resolve each host's final name: explicit or auto-picked from shuffle.
|
||||
hosts_computed = [
|
||||
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
|
||||
bootstrap = coalesce(h.bootstrap, false)
|
||||
roles = h.roles
|
||||
hostname_short = "${var.cluster_name}-${var.role_in_hostname}-${h.name != null ? h.name : random_shuffle.names.result[i]}"
|
||||
fqdn = "${var.cluster_name}-${var.role_in_hostname}-${h.name != null ? h.name : random_shuffle.names.result[i]}.${var.domain}"
|
||||
hostname_short = "${var.cluster_name}-${var.role_in_hostname}-${module.names.resolved[i]}"
|
||||
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,
|
||||
# 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 {
|
||||
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").
|
||||
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).
|
||||
node_map = { for n in var.nodes : n.name => n }
|
||||
cp_node_map = { for k, n in local.node_map : k => n if coalesce(n.role, "control-plane") == "control-plane" }
|
||||
worker_node_map = { for k, n in local.node_map : k => n if coalesce(n.role, "control-plane") == "worker" }
|
||||
node_map = { for n in local.nodes_named : n.hostname => n }
|
||||
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 n.role == "worker" }
|
||||
|
||||
cp_addresses = [for k, n in local.cp_node_map : n.address]
|
||||
|
||||
@@ -158,7 +176,7 @@ locals {
|
||||
apiVersion = "v1alpha1"
|
||||
kind = "HostnameConfig"
|
||||
auto = "off"
|
||||
hostname = n.name
|
||||
hostname = n.hostname
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
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
|
||||
}
|
||||
|
||||
# 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" {
|
||||
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
|
||||
@@ -79,7 +110,7 @@ variable "nodes" {
|
||||
and Terraform stays reachable across the install reboot).
|
||||
EOT
|
||||
type = list(object({
|
||||
name = string
|
||||
name = optional(string) # explicit hostname override; null = auto-pick from the shared inventory
|
||||
role = optional(string, "control-plane")
|
||||
address = string
|
||||
}))
|
||||
|
||||
@@ -5,5 +5,10 @@ terraform {
|
||||
source = "siderolabs/talos"
|
||||
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