[{"data":1,"prerenderedAt":2095},["ShallowReactive",2],{"post:\u002F2026\u002F03\u002F18\u002Ftalos-linux-bootstrapping-a-three-node-kubernetes-cluster-from-bare-metal\u002F":3},{"post":4,"newer":2048,"older":2057,"related":2069,"series":2093},{"id":5,"title":6,"body":7,"canonical":2028,"categories":2029,"date":2031,"description":2032,"extension":2033,"featured":2034,"hero":2035,"image":2035,"meta":2036,"navigation":820,"path":2037,"readingTime":284,"seo":2038,"series":2035,"seriesOrder":2035,"sites":2039,"source":2035,"stem":2040,"tags":2041,"updated":2045,"url":2046,"__hash__":2047},"blog\u002Fblog\u002F2026\u002F03\u002F18\u002Ftalos-linux-bootstrapping-a-three-node-kubernetes-cluster-from-bare-metal.md","Talos Linux: Bootstrapping a Three-Node Cluster on Bare Metal",{"type":8,"value":9,"toc":2019},"minimark",[10,14,66,71,86,89,140,162,166,180,194,197,205,211,334,352,439,446,453,664,674,678,696,700,711,722,740,744,750,1883,1886,1892,1899,1903,1910,1923,1927,2015],[11,12,13],"p",{},"Bringing up a Kubernetes cluster on three bare-metal boxes used to mean an afternoon of installing a base OS, patching it, disabling swap, installing a container runtime, and then finally running kubeadm and hoping the CNI came up clean. Talos Linux collapses most of that into a single step: there's no shell to install anything into, because there isn't a shell. The OS is an immutable, API-managed image whose only job is to run Kubernetes. The first time you bootstrap a cluster this way, the absence of familiar steps is more disorienting than the process itself.",[11,15,16,17,21,22,25,26,25,29,25,32,35,36,39,40,43,44,47,48,25,51,39,54,57,58,61,62,65],{},"This is the sequence I use for a three-node cluster where every node is both control plane and worker, written against Talos 1.14 (the current release, whose ",[18,19,20],"code",{},"talosctl gen config"," defaults to Kubernetes 1.37.0). Talos 1.14 generates a multi-document machine config, so the patches below use the new per-topic documents (",[18,23,24],{},"LinkConfig",", ",[18,27,28],{},"Layer2VIPConfig",[18,30,31],{},"KubeNodeConfig",[18,33,34],{},"UnattendedInstallConfig",") rather than the older ",[18,37,38],{},"machine.network"," and ",[18,41,42],{},"machine.install"," fields. The addressing plan is a single VLAN, ",[18,45,46],{},"10.10.20.0\u002F24",": nodes on ",[18,49,50],{},".11",[18,52,53],{},".12",[18,55,56],{},".13",", a shared API address on ",[18,59,60],{},".10",", gateway on ",[18,63,64],{},".1",".",[67,68,70],"h2",{"id":69},"boot-into-maintenance-mode-and-look-before-you-configure","Boot into maintenance mode and look before you configure",[11,72,73,74,81,82,85],{},"Download an ISO from the ",[75,76,80],"a",{"href":77,"rel":78},"https:\u002F\u002Ffactory.talos.dev\u002F",[79],"nofollow","Image Factory",". If the hardware needs extra drivers or you want system extensions, build a schematic there first, because the installer image you upgrade with later has to carry the same schematic. Boot each machine from it. Until it receives a config, the node runs entirely in RAM in maintenance mode and doesn't touch its disks. Sidero Labs is explicit that the maintenance API is unauthenticated by design: anyone who can reach port 50000 can push a config with ",[18,83,84],{},"--insecure",", so keep that VLAN closed while nodes sit in this state.",[11,87,88],{},"Before writing any patches, ask each node what hardware it actually has:",[90,91,96],"pre",{"className":92,"code":93,"language":94,"meta":95,"style":95},"language-bash shiki shiki-themes github-dark","talosctl get disks --insecure --nodes 10.10.20.11\ntalosctl get links --insecure --nodes 10.10.20.11\n","bash","",[18,97,98,124],{"__ignoreMap":95},[99,100,103,107,111,114,118,121],"span",{"class":101,"line":102},"line",1,[99,104,106],{"class":105},"svObZ","talosctl",[99,108,110],{"class":109},"sU2Wk"," get",[99,112,113],{"class":109}," disks",[99,115,117],{"class":116},"sDLfK"," --insecure",[99,119,120],{"class":116}," --nodes",[99,122,123],{"class":116}," 10.10.20.11\n",[99,125,127,129,131,134,136,138],{"class":101,"line":126},2,[99,128,106],{"class":105},[99,130,110],{"class":109},[99,132,133],{"class":109}," links",[99,135,117],{"class":116},[99,137,120],{"class":116},[99,139,123],{"class":116},[11,141,142,143,39,146,149,150,153,154,157,158,161],{},"Note each node's system disk serial and its NIC's MAC address. I match both by stable properties instead of ",[18,144,145],{},"\u002Fdev\u002Fsda",[18,147,148],{},"enp1s0",", because device names on bare metal change when someone adds a disk or a firmware update renumbers PCI devices. The production notes say the same thing about disks: prefer ",[18,151,152],{},"disk.serial"," or ",[18,155,156],{},"disk.wwid"," over the ",[18,159,160],{},"\u002Fdev\u002Fsd*"," name.",[67,163,165],{"id":164},"secrets-first-then-patches-never-a-hand-edited-config","Secrets first, then patches, never a hand-edited config",[11,167,168,169,172,173,176,177,179],{},"The part I got wrong on my first cluster was treating ",[18,170,171],{},"controlplane.yaml"," as the thing to keep. Talos' own guidance has since become explicit: generate a secrets bundle once, keep small patch files in version control, and regenerate the full machine configs whenever you need them instead of committing them. The reason is drift. ",[18,174,175],{},"talosctl upgrade-k8s"," rewrites component image versions in the live config on each node, so a full ",[18,178,171],{}," in Git goes stale after the first Kubernetes upgrade, and re-applying it later can quietly downgrade components.",[11,181,182,183,186,187,190,191,193],{},"The inputs that make a config reproducible, per the reproducible machine configuration guide, are the ",[18,184,185],{},"secrets.yaml"," bundle, the patches, the cluster name and endpoint, the Kubernetes version, and a fixed ",[18,188,189],{},"--talos-version"," contract. ",[18,192,185],{}," holds the cluster CAs and bootstrap tokens: it belongs in a secrets manager or an encrypted store, not in plain Git.",[11,195,196],{},"The repository layout is small:",[90,198,203],{"className":199,"code":201,"language":202,"meta":95},[200],"language-text","cluster-lab\u002F\n  bootstrap-cluster.sh\n  secrets.yaml            # not committed in plain text\n  patches\u002F\n    common.yaml\n    controlplane.yaml\n    nodes\u002F\n      talos-cp1.yaml\n      talos-cp2.yaml\n      talos-cp3.yaml\n","text",[18,204,201],{"__ignoreMap":95},[11,206,207,210],{},[18,208,209],{},"patches\u002Fcommon.yaml"," holds what every node shares, DNS and time:",[90,212,216],{"className":213,"code":214,"language":215,"meta":95,"style":95},"language-yaml shiki shiki-themes github-dark","apiVersion: v1alpha1\nkind: ResolverConfig\nnameservers:\n  - address: 10.10.20.2\n  - address: 10.10.20.3\n---\napiVersion: v1alpha1\nkind: TimeSyncConfig\nntp:\n  servers:\n    - ntp1.example.internal\n    - ntp2.example.internal\n","yaml",[18,217,218,231,241,250,264,276,282,291,301,309,317,326],{"__ignoreMap":95},[99,219,220,224,228],{"class":101,"line":102},[99,221,223],{"class":222},"s4JwU","apiVersion",[99,225,227],{"class":226},"s95oV",": ",[99,229,230],{"class":109},"v1alpha1\n",[99,232,233,236,238],{"class":101,"line":126},[99,234,235],{"class":222},"kind",[99,237,227],{"class":226},[99,239,240],{"class":109},"ResolverConfig\n",[99,242,244,247],{"class":101,"line":243},3,[99,245,246],{"class":222},"nameservers",[99,248,249],{"class":226},":\n",[99,251,253,256,259,261],{"class":101,"line":252},4,[99,254,255],{"class":226},"  - ",[99,257,258],{"class":222},"address",[99,260,227],{"class":226},[99,262,263],{"class":116},"10.10.20.2\n",[99,265,267,269,271,273],{"class":101,"line":266},5,[99,268,255],{"class":226},[99,270,258],{"class":222},[99,272,227],{"class":226},[99,274,275],{"class":116},"10.10.20.3\n",[99,277,279],{"class":101,"line":278},6,[99,280,281],{"class":105},"---\n",[99,283,285,287,289],{"class":101,"line":284},7,[99,286,223],{"class":222},[99,288,227],{"class":226},[99,290,230],{"class":109},[99,292,294,296,298],{"class":101,"line":293},8,[99,295,235],{"class":222},[99,297,227],{"class":226},[99,299,300],{"class":109},"TimeSyncConfig\n",[99,302,304,307],{"class":101,"line":303},9,[99,305,306],{"class":222},"ntp",[99,308,249],{"class":226},[99,310,312,315],{"class":101,"line":311},10,[99,313,314],{"class":222},"  servers",[99,316,249],{"class":226},[99,318,320,323],{"class":101,"line":319},11,[99,321,322],{"class":226},"    - ",[99,324,325],{"class":109},"ntp1.example.internal\n",[99,327,329,331],{"class":101,"line":328},12,[99,330,322],{"class":226},[99,332,333],{"class":109},"ntp2.example.internal\n",[11,335,336,339,340,343,344,347,348,351],{},[18,337,338],{},"patches\u002Fcontrolplane.yaml"," holds the two decisions that make this a three-node, all-in-one cluster. The first document is a Layer 2 virtual IP for the Kubernetes API. The control plane nodes elect an owner through etcd and that node answers for ",[18,341,342],{},"10.10.20.10"," with gratuitous ARP, so there's no external load balancer to build. The second removes the default ",[18,345,346],{},"NoSchedule"," taint from control plane nodes so workloads can run on them, using the ",[18,349,350],{},"$patch: delete"," form from Talos' \"workloads on control plane\" guide:",[90,353,355],{"className":213,"code":354,"language":215,"meta":95,"style":95},"apiVersion: v1alpha1\nkind: Layer2VIPConfig\nname: 10.10.20.10\nlink: net0\n---\napiVersion: v1alpha1\nkind: KubeNodeConfig\ntaints:\n  node-role.kubernetes.io\u002Fcontrol-plane:\n    $patch: delete\n",[18,356,357,365,374,384,394,398,406,415,422,429],{"__ignoreMap":95},[99,358,359,361,363],{"class":101,"line":102},[99,360,223],{"class":222},[99,362,227],{"class":226},[99,364,230],{"class":109},[99,366,367,369,371],{"class":101,"line":126},[99,368,235],{"class":222},[99,370,227],{"class":226},[99,372,373],{"class":109},"Layer2VIPConfig\n",[99,375,376,379,381],{"class":101,"line":243},[99,377,378],{"class":222},"name",[99,380,227],{"class":226},[99,382,383],{"class":116},"10.10.20.10\n",[99,385,386,389,391],{"class":101,"line":252},[99,387,388],{"class":222},"link",[99,390,227],{"class":226},[99,392,393],{"class":109},"net0\n",[99,395,396],{"class":101,"line":266},[99,397,281],{"class":105},[99,399,400,402,404],{"class":101,"line":278},[99,401,223],{"class":222},[99,403,227],{"class":226},[99,405,230],{"class":109},[99,407,408,410,412],{"class":101,"line":284},[99,409,235],{"class":222},[99,411,227],{"class":226},[99,413,414],{"class":109},"KubeNodeConfig\n",[99,416,417,420],{"class":101,"line":293},[99,418,419],{"class":222},"taints",[99,421,249],{"class":226},[99,423,424,427],{"class":101,"line":303},[99,425,426],{"class":222},"  node-role.kubernetes.io\u002Fcontrol-plane",[99,428,249],{"class":226},[99,430,431,434,436],{"class":101,"line":311},[99,432,433],{"class":222},"    $patch",[99,435,227],{"class":226},[99,437,438],{"class":109},"delete\n",[11,440,441,442,445],{},"If you plan to run MetalLB or another load balancer that should use these nodes as backends, also delete the ",[18,443,444],{},"node.kubernetes.io\u002Fexclude-from-external-load-balancers"," label the same way; Talos adds it to control plane nodes by default.",[11,447,448,449,452],{},"Each node then gets its own patch with a hostname, a link alias pinned to its MAC, a static address, and its install disk selected by serial. This is ",[18,450,451],{},"patches\u002Fnodes\u002Ftalos-cp1.yaml","; the other two differ only in the values:",[90,454,456],{"className":213,"code":455,"language":215,"meta":95,"style":95},"apiVersion: v1alpha1\nkind: HostnameConfig\nhostname: talos-cp1\nauto: off\n---\napiVersion: v1alpha1\nkind: LinkAliasConfig\nname: net0\nselector:\n  match: mac(link.permanent_addr) == \"3c:ec:ef:10:20:11\"\n---\napiVersion: v1alpha1\nkind: LinkConfig\nname: net0\naddresses:\n  - address: 10.10.20.11\u002F24\nroutes:\n  - gateway: 10.10.20.1\n---\napiVersion: v1alpha1\nkind: UnattendedInstallConfig\nprovisioning:\n  diskSelector:\n    match: disk.serial == \"S6XXNX0T100011\"\n",[18,457,458,466,475,485,495,499,507,516,524,531,541,545,553,563,572,580,592,600,613,618,627,637,645,653],{"__ignoreMap":95},[99,459,460,462,464],{"class":101,"line":102},[99,461,223],{"class":222},[99,463,227],{"class":226},[99,465,230],{"class":109},[99,467,468,470,472],{"class":101,"line":126},[99,469,235],{"class":222},[99,471,227],{"class":226},[99,473,474],{"class":109},"HostnameConfig\n",[99,476,477,480,482],{"class":101,"line":243},[99,478,479],{"class":222},"hostname",[99,481,227],{"class":226},[99,483,484],{"class":109},"talos-cp1\n",[99,486,487,490,492],{"class":101,"line":252},[99,488,489],{"class":222},"auto",[99,491,227],{"class":226},[99,493,494],{"class":116},"off\n",[99,496,497],{"class":101,"line":266},[99,498,281],{"class":105},[99,500,501,503,505],{"class":101,"line":278},[99,502,223],{"class":222},[99,504,227],{"class":226},[99,506,230],{"class":109},[99,508,509,511,513],{"class":101,"line":284},[99,510,235],{"class":222},[99,512,227],{"class":226},[99,514,515],{"class":109},"LinkAliasConfig\n",[99,517,518,520,522],{"class":101,"line":293},[99,519,378],{"class":222},[99,521,227],{"class":226},[99,523,393],{"class":109},[99,525,526,529],{"class":101,"line":303},[99,527,528],{"class":222},"selector",[99,530,249],{"class":226},[99,532,533,536,538],{"class":101,"line":311},[99,534,535],{"class":222},"  match",[99,537,227],{"class":226},[99,539,540],{"class":109},"mac(link.permanent_addr) == \"3c:ec:ef:10:20:11\"\n",[99,542,543],{"class":101,"line":319},[99,544,281],{"class":105},[99,546,547,549,551],{"class":101,"line":328},[99,548,223],{"class":222},[99,550,227],{"class":226},[99,552,230],{"class":109},[99,554,556,558,560],{"class":101,"line":555},13,[99,557,235],{"class":222},[99,559,227],{"class":226},[99,561,562],{"class":109},"LinkConfig\n",[99,564,566,568,570],{"class":101,"line":565},14,[99,567,378],{"class":222},[99,569,227],{"class":226},[99,571,393],{"class":109},[99,573,575,578],{"class":101,"line":574},15,[99,576,577],{"class":222},"addresses",[99,579,249],{"class":226},[99,581,583,585,587,589],{"class":101,"line":582},16,[99,584,255],{"class":226},[99,586,258],{"class":222},[99,588,227],{"class":226},[99,590,591],{"class":109},"10.10.20.11\u002F24\n",[99,593,595,598],{"class":101,"line":594},17,[99,596,597],{"class":222},"routes",[99,599,249],{"class":226},[99,601,603,605,608,610],{"class":101,"line":602},18,[99,604,255],{"class":226},[99,606,607],{"class":222},"gateway",[99,609,227],{"class":226},[99,611,612],{"class":116},"10.10.20.1\n",[99,614,616],{"class":101,"line":615},19,[99,617,281],{"class":105},[99,619,621,623,625],{"class":101,"line":620},20,[99,622,223],{"class":222},[99,624,227],{"class":226},[99,626,230],{"class":109},[99,628,630,632,634],{"class":101,"line":629},21,[99,631,235],{"class":222},[99,633,227],{"class":226},[99,635,636],{"class":109},"UnattendedInstallConfig\n",[99,638,640,643],{"class":101,"line":639},22,[99,641,642],{"class":222},"provisioning",[99,644,249],{"class":226},[99,646,648,651],{"class":101,"line":647},23,[99,649,650],{"class":222},"  diskSelector",[99,652,249],{"class":226},[99,654,656,659,661],{"class":101,"line":655},24,[99,657,658],{"class":222},"    match",[99,660,227],{"class":226},[99,662,663],{"class":109},"disk.serial == \"S6XXNX0T100011\"\n",[11,665,666,667,669,670,673],{},"The alias is what lets ",[18,668,171],{}," refer to ",[18,671,672],{},"net0"," on all three machines even if the kernel names the NIC differently on each: Talos treats a link alias as interchangeable with the physical name anywhere in the network configuration.",[67,675,677],{"id":676},"why-the-vip-is-for-kubernetes-only","Why the VIP is for Kubernetes only",[11,679,680,681,684,685,688,689,691,692,695],{},"Two details from the VIP documentation shape the rest of the process. The VIP only comes alive after etcd is bootstrapped, because the election runs through etcd. And Sidero Labs warns not to use the VIP as an endpoint in ",[18,682,683],{},"talosconfig",": if etcd or the API server is what's broken, the VIP is gone too, and you'd lose the Talos API access you need to fix it. So the Kubernetes endpoint is ",[18,686,687],{},"https:\u002F\u002F10.10.20.10:6443",", while ",[18,690,106],{}," talks to the three node addresses directly. The failover is quick when the owner shuts down gracefully and takes up to about a minute after a crash or power loss, which is fine for ",[18,693,694],{},"kubectl"," and irrelevant to pods, since in-cluster clients reach the API server through KubePrism rather than the VIP.",[67,697,699],{"id":698},"apply-then-bootstrap-exactly-once","Apply, then bootstrap exactly once",[11,701,702,703,706,707,710],{},"Applying configuration is a single ",[18,704,705],{},"talosctl apply-config --insecure"," per node, pointed at that node's maintenance-mode address with its generated file. The node validates the config, installs Talos to the selected disk, and reboots into the installed system. If the config is invalid it's rejected over the API with the reason, which is a better failure mode than a kubeadm join hanging on a firewall rule you forgot. I also run ",[18,708,709],{},"talosctl validate --mode metal --strict"," on every generated file first, because a typo in a CEL expression is cheaper to find on a laptop.",[11,712,713,714,717,718,721],{},"Applying configuration to all three nodes gets you three machines whose etcd services sit in a join loop, waiting for a peer. That's where ",[18,715,716],{},"talosctl bootstrap"," comes in. You run it against one control plane node, once. It tells that node to stop waiting and start etcd as a single-member cluster; the other two join once Kubernetes is up on the first. The docs put it in capitals: run it once, on a single node. Skip it and every etcd service stays stuck; Talos' troubleshooting guide lists \"all etcd services are stuck in Pre state\" with the fix \"make sure that a single member was bootstrapped.\" Bootstrap is also the command you'd use later for disaster recovery, with ",[18,719,720],{},"--recover-from"," pointing at an etcd snapshot, which is one more reason to treat it as a deliberate act rather than something a retry loop fires twice.",[11,723,724,725,728,729,732,733,736,737,65],{},"Once it settles, ",[18,726,727],{},"talosctl kubeconfig"," pulls an admin kubeconfig from the node and ",[18,730,731],{},"talosctl health"," confirms etcd, the control plane components, and node readiness. Nodes show ",[18,734,735],{},"NotReady"," until the CNI (Flannel by default) is running, then flip to ",[18,738,739],{},"Ready",[67,741,743],{"id":742},"the-whole-sequence-as-one-script","The whole sequence as one script",[11,745,746,747,749],{},"This is the script I run from the repository root. It generates the talosconfig and one config per node, validates them, applies them, waits for the installed systems to answer on the authenticated API, bootstraps the first node, and waits for health. It assumes DHCP reservations hand each node the same address in maintenance mode that its ",[18,748,24],{}," makes permanent; if yours differ, keep a second map of maintenance addresses for step 4.",[90,751,753],{"className":92,"code":752,"language":94,"meta":95,"style":95},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# bootstrap-cluster.sh\n#\n# Generates one machine config per node from secrets.yaml + patches, applies\n# each config to a node in maintenance mode, bootstraps etcd once, then pulls\n# a kubeconfig and waits for the cluster to report healthy.\n#\n# Needs: talosctl matching the Talos version on the ISO, the patches\u002F tree\n# described above, and DHCP reservations that give each node the same address\n# in maintenance mode that its LinkConfig patch assigns permanently.\nset -euo pipefail\n\nCLUSTER_NAME=\"lab\"\nENDPOINT=\"https:\u002F\u002F10.10.20.10:6443\"   # the Layer 2 VIP from patches\u002Fcontrolplane.yaml\nTALOS_CONTRACT=\"v1.14\"                 # keep fixed for reproducible configs\nK8S_VERSION=\"1.37.0\"\nBOOTSTRAP_NODE=\"talos-cp1\"\nOUT=\"_out\"                             # generated files: never commit this directory\n\ndeclare -A NODES=(\n    [talos-cp1]=\"10.10.20.11\"\n    [talos-cp2]=\"10.10.20.12\"\n    [talos-cp3]=\"10.10.20.13\"\n)\n\nmkdir -p \"$OUT\"\nexport TALOSCONFIG=\"$OUT\u002Ftalosconfig\"\n\n# 1. Cluster secrets: generated once, then stored somewhere safer than this repo.\nif [[ ! -f secrets.yaml ]]; then\n    talosctl gen secrets --output-file secrets.yaml\nfi\n\n# 2. The talosctl client config, with every control plane node as an endpoint.\ntalosctl gen config \"$CLUSTER_NAME\" \"$ENDPOINT\" \\\n    --with-secrets secrets.yaml \\\n    --talos-version \"$TALOS_CONTRACT\" \\\n    --output-types talosconfig \\\n    --output \"$TALOSCONFIG\" \\\n    --force\ntalosctl config endpoint \"${NODES[@]}\"\n\n# 3. One controlplane config per node, validated before anything touches hardware.\nfor name in \"${!NODES[@]}\"; do\n    talosctl gen config \"$CLUSTER_NAME\" \"$ENDPOINT\" \\\n        --with-secrets secrets.yaml \\\n        --kubernetes-version \"$K8S_VERSION\" \\\n        --talos-version \"$TALOS_CONTRACT\" \\\n        --config-patch @patches\u002Fcommon.yaml \\\n        --config-patch @patches\u002Fcontrolplane.yaml \\\n        --config-patch \"@patches\u002Fnodes\u002F${name}.yaml\" \\\n        --output-types controlplane \\\n        --output \"$OUT\u002F${name}.yaml\" \\\n        --force\n    talosctl validate --config \"$OUT\u002F${name}.yaml\" --mode metal --strict\ndone\n\n# 4. Apply over the unauthenticated maintenance API. This is the only --insecure step.\nfor name in \"${!NODES[@]}\"; do\n    echo \"applying $OUT\u002F${name}.yaml to ${NODES[$name]}\"\n    talosctl apply-config --insecure --nodes \"${NODES[$name]}\" --file \"$OUT\u002F${name}.yaml\"\ndone\n\n# 5. Wait for every node to install, reboot and answer on the authenticated API.\nfor name in \"${!NODES[@]}\"; do\n    ip=\"${NODES[$name]}\"\n    for attempt in $(seq 1 60); do\n        if talosctl --nodes \"$ip\" version >\u002Fdev\u002Fnull 2>&1; then\n            echo \"$name ($ip) is up\"\n            break\n        fi\n        if [[ $attempt -eq 60 ]]; then\n            echo \"$name ($ip) did not come up after 10 minutes\" >&2\n            exit 1\n        fi\n        sleep 10\n    done\ndone\n\n# 6. Bootstrap etcd exactly once, on exactly one node.\ntalosctl bootstrap --nodes \"${NODES[$BOOTSTRAP_NODE]}\"\n\n# 7. Kubeconfig to a separate file, then wait for the whole cluster.\ntalosctl kubeconfig \"$OUT\u002Fkubeconfig\" --nodes \"${NODES[$BOOTSTRAP_NODE]}\" --merge=false --force\ntalosctl health \\\n    --nodes \"${NODES[$BOOTSTRAP_NODE]}\" \\\n    --control-plane-nodes \"$(IFS=,; echo \"${NODES[*]}\")\" \\\n    --wait-timeout 15m\n\nKUBECONFIG=\"$OUT\u002Fkubeconfig\" kubectl get nodes\n",[18,754,755,761,766,771,776,781,786,790,795,800,805,816,822,834,847,860,870,880,893,897,913,927,939,951,956,961,979,998,1003,1009,1030,1048,1054,1059,1065,1092,1103,1118,1129,1144,1150,1175,1180,1186,1217,1240,1250,1265,1279,1290,1300,1315,1326,1345,1351,1381,1387,1392,1398,1423,1450,1486,1491,1496,1502,1527,1546,1574,1608,1626,1632,1638,1656,1675,1684,1689,1698,1704,1709,1714,1720,1741,1746,1752,1785,1795,1813,1847,1856,1861],{"__ignoreMap":95},[99,756,757],{"class":101,"line":102},[99,758,760],{"class":759},"sAwPA","#!\u002Fusr\u002Fbin\u002Fenv bash\n",[99,762,763],{"class":101,"line":126},[99,764,765],{"class":759},"# bootstrap-cluster.sh\n",[99,767,768],{"class":101,"line":243},[99,769,770],{"class":759},"#\n",[99,772,773],{"class":101,"line":252},[99,774,775],{"class":759},"# Generates one machine config per node from secrets.yaml + patches, applies\n",[99,777,778],{"class":101,"line":266},[99,779,780],{"class":759},"# each config to a node in maintenance mode, bootstraps etcd once, then pulls\n",[99,782,783],{"class":101,"line":278},[99,784,785],{"class":759},"# a kubeconfig and waits for the cluster to report healthy.\n",[99,787,788],{"class":101,"line":284},[99,789,770],{"class":759},[99,791,792],{"class":101,"line":293},[99,793,794],{"class":759},"# Needs: talosctl matching the Talos version on the ISO, the patches\u002F tree\n",[99,796,797],{"class":101,"line":303},[99,798,799],{"class":759},"# described above, and DHCP reservations that give each node the same address\n",[99,801,802],{"class":101,"line":311},[99,803,804],{"class":759},"# in maintenance mode that its LinkConfig patch assigns permanently.\n",[99,806,807,810,813],{"class":101,"line":319},[99,808,809],{"class":116},"set",[99,811,812],{"class":116}," -euo",[99,814,815],{"class":109}," pipefail\n",[99,817,818],{"class":101,"line":328},[99,819,821],{"emptyLinePlaceholder":820},true,"\n",[99,823,824,827,831],{"class":101,"line":555},[99,825,826],{"class":226},"CLUSTER_NAME",[99,828,830],{"class":829},"snl16","=",[99,832,833],{"class":109},"\"lab\"\n",[99,835,836,839,841,844],{"class":101,"line":565},[99,837,838],{"class":226},"ENDPOINT",[99,840,830],{"class":829},[99,842,843],{"class":109},"\"https:\u002F\u002F10.10.20.10:6443\"",[99,845,846],{"class":759},"   # the Layer 2 VIP from patches\u002Fcontrolplane.yaml\n",[99,848,849,852,854,857],{"class":101,"line":574},[99,850,851],{"class":226},"TALOS_CONTRACT",[99,853,830],{"class":829},[99,855,856],{"class":109},"\"v1.14\"",[99,858,859],{"class":759},"                 # keep fixed for reproducible configs\n",[99,861,862,865,867],{"class":101,"line":582},[99,863,864],{"class":226},"K8S_VERSION",[99,866,830],{"class":829},[99,868,869],{"class":109},"\"1.37.0\"\n",[99,871,872,875,877],{"class":101,"line":594},[99,873,874],{"class":226},"BOOTSTRAP_NODE",[99,876,830],{"class":829},[99,878,879],{"class":109},"\"talos-cp1\"\n",[99,881,882,885,887,890],{"class":101,"line":602},[99,883,884],{"class":226},"OUT",[99,886,830],{"class":829},[99,888,889],{"class":109},"\"_out\"",[99,891,892],{"class":759},"                             # generated files: never commit this directory\n",[99,894,895],{"class":101,"line":615},[99,896,821],{"emptyLinePlaceholder":820},[99,898,899,902,905,908,910],{"class":101,"line":620},[99,900,901],{"class":829},"declare",[99,903,904],{"class":116}," -A",[99,906,907],{"class":226}," NODES",[99,909,830],{"class":829},[99,911,912],{"class":226},"(\n",[99,914,915,918,921,924],{"class":101,"line":629},[99,916,917],{"class":226},"    [",[99,919,920],{"class":105},"talos-cp1",[99,922,923],{"class":226},"]=",[99,925,926],{"class":109},"\"10.10.20.11\"\n",[99,928,929,931,934,936],{"class":101,"line":639},[99,930,917],{"class":226},[99,932,933],{"class":105},"talos-cp2",[99,935,923],{"class":226},[99,937,938],{"class":109},"\"10.10.20.12\"\n",[99,940,941,943,946,948],{"class":101,"line":647},[99,942,917],{"class":226},[99,944,945],{"class":105},"talos-cp3",[99,947,923],{"class":226},[99,949,950],{"class":109},"\"10.10.20.13\"\n",[99,952,953],{"class":101,"line":655},[99,954,955],{"class":226},")\n",[99,957,959],{"class":101,"line":958},25,[99,960,821],{"emptyLinePlaceholder":820},[99,962,964,967,970,973,976],{"class":101,"line":963},26,[99,965,966],{"class":105},"mkdir",[99,968,969],{"class":116}," -p",[99,971,972],{"class":109}," \"",[99,974,975],{"class":226},"$OUT",[99,977,978],{"class":109},"\"\n",[99,980,982,985,988,990,993,995],{"class":101,"line":981},27,[99,983,984],{"class":829},"export",[99,986,987],{"class":226}," TALOSCONFIG",[99,989,830],{"class":829},[99,991,992],{"class":109},"\"",[99,994,975],{"class":226},[99,996,997],{"class":109},"\u002Ftalosconfig\"\n",[99,999,1001],{"class":101,"line":1000},28,[99,1002,821],{"emptyLinePlaceholder":820},[99,1004,1006],{"class":101,"line":1005},29,[99,1007,1008],{"class":759},"# 1. Cluster secrets: generated once, then stored somewhere safer than this repo.\n",[99,1010,1012,1015,1018,1021,1024,1027],{"class":101,"line":1011},30,[99,1013,1014],{"class":829},"if",[99,1016,1017],{"class":226}," [[ ",[99,1019,1020],{"class":829},"!",[99,1022,1023],{"class":829}," -f",[99,1025,1026],{"class":226}," secrets.yaml ]]; ",[99,1028,1029],{"class":829},"then\n",[99,1031,1033,1036,1039,1042,1045],{"class":101,"line":1032},31,[99,1034,1035],{"class":105},"    talosctl",[99,1037,1038],{"class":109}," gen",[99,1040,1041],{"class":109}," secrets",[99,1043,1044],{"class":116}," --output-file",[99,1046,1047],{"class":109}," secrets.yaml\n",[99,1049,1051],{"class":101,"line":1050},32,[99,1052,1053],{"class":829},"fi\n",[99,1055,1057],{"class":101,"line":1056},33,[99,1058,821],{"emptyLinePlaceholder":820},[99,1060,1062],{"class":101,"line":1061},34,[99,1063,1064],{"class":759},"# 2. The talosctl client config, with every control plane node as an endpoint.\n",[99,1066,1068,1070,1072,1075,1077,1080,1082,1084,1087,1089],{"class":101,"line":1067},35,[99,1069,106],{"class":105},[99,1071,1038],{"class":109},[99,1073,1074],{"class":109}," config",[99,1076,972],{"class":109},[99,1078,1079],{"class":226},"$CLUSTER_NAME",[99,1081,992],{"class":109},[99,1083,972],{"class":109},[99,1085,1086],{"class":226},"$ENDPOINT",[99,1088,992],{"class":109},[99,1090,1091],{"class":116}," \\\n",[99,1093,1095,1098,1101],{"class":101,"line":1094},36,[99,1096,1097],{"class":116},"    --with-secrets",[99,1099,1100],{"class":109}," secrets.yaml",[99,1102,1091],{"class":116},[99,1104,1106,1109,1111,1114,1116],{"class":101,"line":1105},37,[99,1107,1108],{"class":116},"    --talos-version",[99,1110,972],{"class":109},[99,1112,1113],{"class":226},"$TALOS_CONTRACT",[99,1115,992],{"class":109},[99,1117,1091],{"class":116},[99,1119,1121,1124,1127],{"class":101,"line":1120},38,[99,1122,1123],{"class":116},"    --output-types",[99,1125,1126],{"class":109}," talosconfig",[99,1128,1091],{"class":116},[99,1130,1132,1135,1137,1140,1142],{"class":101,"line":1131},39,[99,1133,1134],{"class":116},"    --output",[99,1136,972],{"class":109},[99,1138,1139],{"class":226},"$TALOSCONFIG",[99,1141,992],{"class":109},[99,1143,1091],{"class":116},[99,1145,1147],{"class":101,"line":1146},40,[99,1148,1149],{"class":116},"    --force\n",[99,1151,1153,1155,1157,1160,1163,1166,1169,1172],{"class":101,"line":1152},41,[99,1154,106],{"class":105},[99,1156,1074],{"class":109},[99,1158,1159],{"class":109}," endpoint",[99,1161,1162],{"class":109}," \"${",[99,1164,1165],{"class":226},"NODES",[99,1167,1168],{"class":109},"[",[99,1170,1171],{"class":829},"@",[99,1173,1174],{"class":109},"]}\"\n",[99,1176,1178],{"class":101,"line":1177},42,[99,1179,821],{"emptyLinePlaceholder":820},[99,1181,1183],{"class":101,"line":1182},43,[99,1184,1185],{"class":759},"# 3. One controlplane config per node, validated before anything touches hardware.\n",[99,1187,1189,1192,1195,1198,1200,1202,1204,1206,1208,1211,1214],{"class":101,"line":1188},44,[99,1190,1191],{"class":829},"for",[99,1193,1194],{"class":226}," name ",[99,1196,1197],{"class":829},"in",[99,1199,1162],{"class":109},[99,1201,1020],{"class":829},[99,1203,1165],{"class":226},[99,1205,1168],{"class":109},[99,1207,1171],{"class":829},[99,1209,1210],{"class":109},"]}\"",[99,1212,1213],{"class":226},"; ",[99,1215,1216],{"class":829},"do\n",[99,1218,1220,1222,1224,1226,1228,1230,1232,1234,1236,1238],{"class":101,"line":1219},45,[99,1221,1035],{"class":105},[99,1223,1038],{"class":109},[99,1225,1074],{"class":109},[99,1227,972],{"class":109},[99,1229,1079],{"class":226},[99,1231,992],{"class":109},[99,1233,972],{"class":109},[99,1235,1086],{"class":226},[99,1237,992],{"class":109},[99,1239,1091],{"class":116},[99,1241,1243,1246,1248],{"class":101,"line":1242},46,[99,1244,1245],{"class":116},"        --with-secrets",[99,1247,1100],{"class":109},[99,1249,1091],{"class":116},[99,1251,1253,1256,1258,1261,1263],{"class":101,"line":1252},47,[99,1254,1255],{"class":116},"        --kubernetes-version",[99,1257,972],{"class":109},[99,1259,1260],{"class":226},"$K8S_VERSION",[99,1262,992],{"class":109},[99,1264,1091],{"class":116},[99,1266,1268,1271,1273,1275,1277],{"class":101,"line":1267},48,[99,1269,1270],{"class":116},"        --talos-version",[99,1272,972],{"class":109},[99,1274,1113],{"class":226},[99,1276,992],{"class":109},[99,1278,1091],{"class":116},[99,1280,1282,1285,1288],{"class":101,"line":1281},49,[99,1283,1284],{"class":116},"        --config-patch",[99,1286,1287],{"class":109}," @patches\u002Fcommon.yaml",[99,1289,1091],{"class":116},[99,1291,1293,1295,1298],{"class":101,"line":1292},50,[99,1294,1284],{"class":116},[99,1296,1297],{"class":109}," @patches\u002Fcontrolplane.yaml",[99,1299,1091],{"class":116},[99,1301,1303,1305,1308,1310,1313],{"class":101,"line":1302},51,[99,1304,1284],{"class":116},[99,1306,1307],{"class":109}," \"@patches\u002Fnodes\u002F${",[99,1309,378],{"class":226},[99,1311,1312],{"class":109},"}.yaml\"",[99,1314,1091],{"class":116},[99,1316,1318,1321,1324],{"class":101,"line":1317},52,[99,1319,1320],{"class":116},"        --output-types",[99,1322,1323],{"class":109}," controlplane",[99,1325,1091],{"class":116},[99,1327,1329,1332,1334,1336,1339,1341,1343],{"class":101,"line":1328},53,[99,1330,1331],{"class":116},"        --output",[99,1333,972],{"class":109},[99,1335,975],{"class":226},[99,1337,1338],{"class":109},"\u002F${",[99,1340,378],{"class":226},[99,1342,1312],{"class":109},[99,1344,1091],{"class":116},[99,1346,1348],{"class":101,"line":1347},54,[99,1349,1350],{"class":116},"        --force\n",[99,1352,1354,1356,1359,1362,1364,1366,1368,1370,1372,1375,1378],{"class":101,"line":1353},55,[99,1355,1035],{"class":105},[99,1357,1358],{"class":109}," validate",[99,1360,1361],{"class":116}," --config",[99,1363,972],{"class":109},[99,1365,975],{"class":226},[99,1367,1338],{"class":109},[99,1369,378],{"class":226},[99,1371,1312],{"class":109},[99,1373,1374],{"class":116}," --mode",[99,1376,1377],{"class":109}," metal",[99,1379,1380],{"class":116}," --strict\n",[99,1382,1384],{"class":101,"line":1383},56,[99,1385,1386],{"class":829},"done\n",[99,1388,1390],{"class":101,"line":1389},57,[99,1391,821],{"emptyLinePlaceholder":820},[99,1393,1395],{"class":101,"line":1394},58,[99,1396,1397],{"class":759},"# 4. Apply over the unauthenticated maintenance API. This is the only --insecure step.\n",[99,1399,1401,1403,1405,1407,1409,1411,1413,1415,1417,1419,1421],{"class":101,"line":1400},59,[99,1402,1191],{"class":829},[99,1404,1194],{"class":226},[99,1406,1197],{"class":829},[99,1408,1162],{"class":109},[99,1410,1020],{"class":829},[99,1412,1165],{"class":226},[99,1414,1168],{"class":109},[99,1416,1171],{"class":829},[99,1418,1210],{"class":109},[99,1420,1213],{"class":226},[99,1422,1216],{"class":829},[99,1424,1426,1429,1432,1434,1436,1438,1441,1443,1445,1448],{"class":101,"line":1425},60,[99,1427,1428],{"class":116},"    echo",[99,1430,1431],{"class":109}," \"applying ",[99,1433,975],{"class":226},[99,1435,1338],{"class":109},[99,1437,378],{"class":226},[99,1439,1440],{"class":109},"}.yaml to ${",[99,1442,1165],{"class":226},[99,1444,1168],{"class":109},[99,1446,1447],{"class":226},"$name",[99,1449,1174],{"class":109},[99,1451,1453,1455,1458,1460,1462,1464,1466,1468,1470,1472,1475,1477,1479,1481,1483],{"class":101,"line":1452},61,[99,1454,1035],{"class":105},[99,1456,1457],{"class":109}," apply-config",[99,1459,117],{"class":116},[99,1461,120],{"class":116},[99,1463,1162],{"class":109},[99,1465,1165],{"class":226},[99,1467,1168],{"class":109},[99,1469,1447],{"class":226},[99,1471,1210],{"class":109},[99,1473,1474],{"class":116}," --file",[99,1476,972],{"class":109},[99,1478,975],{"class":226},[99,1480,1338],{"class":109},[99,1482,378],{"class":226},[99,1484,1485],{"class":109},"}.yaml\"\n",[99,1487,1489],{"class":101,"line":1488},62,[99,1490,1386],{"class":829},[99,1492,1494],{"class":101,"line":1493},63,[99,1495,821],{"emptyLinePlaceholder":820},[99,1497,1499],{"class":101,"line":1498},64,[99,1500,1501],{"class":759},"# 5. Wait for every node to install, reboot and answer on the authenticated API.\n",[99,1503,1505,1507,1509,1511,1513,1515,1517,1519,1521,1523,1525],{"class":101,"line":1504},65,[99,1506,1191],{"class":829},[99,1508,1194],{"class":226},[99,1510,1197],{"class":829},[99,1512,1162],{"class":109},[99,1514,1020],{"class":829},[99,1516,1165],{"class":226},[99,1518,1168],{"class":109},[99,1520,1171],{"class":829},[99,1522,1210],{"class":109},[99,1524,1213],{"class":226},[99,1526,1216],{"class":829},[99,1528,1530,1533,1535,1538,1540,1542,1544],{"class":101,"line":1529},66,[99,1531,1532],{"class":226},"    ip",[99,1534,830],{"class":829},[99,1536,1537],{"class":109},"\"${",[99,1539,1165],{"class":226},[99,1541,1168],{"class":109},[99,1543,1447],{"class":226},[99,1545,1174],{"class":109},[99,1547,1549,1552,1555,1557,1560,1563,1566,1569,1572],{"class":101,"line":1548},67,[99,1550,1551],{"class":829},"    for",[99,1553,1554],{"class":226}," attempt ",[99,1556,1197],{"class":829},[99,1558,1559],{"class":226}," $(",[99,1561,1562],{"class":105},"seq",[99,1564,1565],{"class":116}," 1",[99,1567,1568],{"class":116}," 60",[99,1570,1571],{"class":226},"); ",[99,1573,1216],{"class":829},[99,1575,1577,1580,1583,1585,1587,1590,1592,1595,1598,1601,1604,1606],{"class":101,"line":1576},68,[99,1578,1579],{"class":829},"        if",[99,1581,1582],{"class":105}," talosctl",[99,1584,120],{"class":116},[99,1586,972],{"class":109},[99,1588,1589],{"class":226},"$ip",[99,1591,992],{"class":109},[99,1593,1594],{"class":109}," version",[99,1596,1597],{"class":829}," >",[99,1599,1600],{"class":109},"\u002Fdev\u002Fnull",[99,1602,1603],{"class":829}," 2>&1",[99,1605,1213],{"class":226},[99,1607,1029],{"class":829},[99,1609,1611,1614,1616,1618,1621,1623],{"class":101,"line":1610},69,[99,1612,1613],{"class":116},"            echo",[99,1615,972],{"class":109},[99,1617,1447],{"class":226},[99,1619,1620],{"class":109}," (",[99,1622,1589],{"class":226},[99,1624,1625],{"class":109},") is up\"\n",[99,1627,1629],{"class":101,"line":1628},70,[99,1630,1631],{"class":829},"            break\n",[99,1633,1635],{"class":101,"line":1634},71,[99,1636,1637],{"class":829},"        fi\n",[99,1639,1641,1643,1646,1649,1651,1654],{"class":101,"line":1640},72,[99,1642,1579],{"class":829},[99,1644,1645],{"class":226}," [[ $attempt ",[99,1647,1648],{"class":829},"-eq",[99,1650,1568],{"class":116},[99,1652,1653],{"class":226}," ]]; ",[99,1655,1029],{"class":829},[99,1657,1659,1661,1663,1665,1667,1669,1672],{"class":101,"line":1658},73,[99,1660,1613],{"class":116},[99,1662,972],{"class":109},[99,1664,1447],{"class":226},[99,1666,1620],{"class":109},[99,1668,1589],{"class":226},[99,1670,1671],{"class":109},") did not come up after 10 minutes\"",[99,1673,1674],{"class":829}," >&2\n",[99,1676,1678,1681],{"class":101,"line":1677},74,[99,1679,1680],{"class":116},"            exit",[99,1682,1683],{"class":116}," 1\n",[99,1685,1687],{"class":101,"line":1686},75,[99,1688,1637],{"class":829},[99,1690,1692,1695],{"class":101,"line":1691},76,[99,1693,1694],{"class":105},"        sleep",[99,1696,1697],{"class":116}," 10\n",[99,1699,1701],{"class":101,"line":1700},77,[99,1702,1703],{"class":829},"    done\n",[99,1705,1707],{"class":101,"line":1706},78,[99,1708,1386],{"class":829},[99,1710,1712],{"class":101,"line":1711},79,[99,1713,821],{"emptyLinePlaceholder":820},[99,1715,1717],{"class":101,"line":1716},80,[99,1718,1719],{"class":759},"# 6. Bootstrap etcd exactly once, on exactly one node.\n",[99,1721,1723,1725,1728,1730,1732,1734,1736,1739],{"class":101,"line":1722},81,[99,1724,106],{"class":105},[99,1726,1727],{"class":109}," bootstrap",[99,1729,120],{"class":116},[99,1731,1162],{"class":109},[99,1733,1165],{"class":226},[99,1735,1168],{"class":109},[99,1737,1738],{"class":226},"$BOOTSTRAP_NODE",[99,1740,1174],{"class":109},[99,1742,1744],{"class":101,"line":1743},82,[99,1745,821],{"emptyLinePlaceholder":820},[99,1747,1749],{"class":101,"line":1748},83,[99,1750,1751],{"class":759},"# 7. Kubeconfig to a separate file, then wait for the whole cluster.\n",[99,1753,1755,1757,1760,1762,1764,1767,1769,1771,1773,1775,1777,1779,1782],{"class":101,"line":1754},84,[99,1756,106],{"class":105},[99,1758,1759],{"class":109}," kubeconfig",[99,1761,972],{"class":109},[99,1763,975],{"class":226},[99,1765,1766],{"class":109},"\u002Fkubeconfig\"",[99,1768,120],{"class":116},[99,1770,1162],{"class":109},[99,1772,1165],{"class":226},[99,1774,1168],{"class":109},[99,1776,1738],{"class":226},[99,1778,1210],{"class":109},[99,1780,1781],{"class":116}," --merge=false",[99,1783,1784],{"class":116}," --force\n",[99,1786,1788,1790,1793],{"class":101,"line":1787},85,[99,1789,106],{"class":105},[99,1791,1792],{"class":109}," health",[99,1794,1091],{"class":116},[99,1796,1798,1801,1803,1805,1807,1809,1811],{"class":101,"line":1797},86,[99,1799,1800],{"class":116},"    --nodes",[99,1802,1162],{"class":109},[99,1804,1165],{"class":226},[99,1806,1168],{"class":109},[99,1808,1738],{"class":226},[99,1810,1210],{"class":109},[99,1812,1091],{"class":116},[99,1814,1816,1819,1822,1825,1827,1830,1833,1835,1837,1839,1842,1845],{"class":101,"line":1815},87,[99,1817,1818],{"class":116},"    --control-plane-nodes",[99,1820,1821],{"class":109}," \"$(",[99,1823,1824],{"class":226},"IFS",[99,1826,830],{"class":829},[99,1828,1829],{"class":109},",; ",[99,1831,1832],{"class":116},"echo",[99,1834,1162],{"class":109},[99,1836,1165],{"class":226},[99,1838,1168],{"class":109},[99,1840,1841],{"class":829},"*",[99,1843,1844],{"class":109},"]}\")\"",[99,1846,1091],{"class":116},[99,1848,1850,1853],{"class":101,"line":1849},88,[99,1851,1852],{"class":116},"    --wait-timeout",[99,1854,1855],{"class":109}," 15m\n",[99,1857,1859],{"class":101,"line":1858},89,[99,1860,821],{"emptyLinePlaceholder":820},[99,1862,1864,1867,1869,1871,1873,1875,1878,1880],{"class":101,"line":1863},90,[99,1865,1866],{"class":226},"KUBECONFIG",[99,1868,830],{"class":829},[99,1870,992],{"class":109},[99,1872,975],{"class":226},[99,1874,1766],{"class":109},[99,1876,1877],{"class":105}," kubectl",[99,1879,110],{"class":109},[99,1881,1882],{"class":109}," nodes\n",[11,1884,1885],{},"On a healthy run, the last lines look like this:",[90,1887,1890],{"className":1888,"code":1889,"language":202,"meta":95},[200],"waiting for all k8s nodes to report ready: OK\nwaiting for all control plane static pods to be running: OK\nwaiting for all control plane components to be ready: OK\nwaiting for kube-proxy to report ready: OK\nwaiting for coredns to report ready: OK\nwaiting for all k8s nodes to report schedulable: OK\nNAME        STATUS   ROLES           AGE   VERSION\ntalos-cp1   Ready    control-plane   6m    v1.37.0\ntalos-cp2   Ready    control-plane   5m    v1.37.0\ntalos-cp3   Ready    control-plane   5m    v1.37.0\n",[18,1891,1889],{"__ignoreMap":95},[11,1893,1894,1895,1898],{},"Two follow-ups belong on day one rather than \"later.\" Take the first etcd snapshot (",[18,1896,1897],{},"talosctl -n 10.10.20.11 etcd snapshot db.snapshot",") and schedule them; the disaster recovery guide recommends snapshots on a schedule so you can restore the latest one. And unplug the USB stick or unmount the ISO. The production notes suggest doing it before applying configs, so the installer can't mistake the boot media for the install disk; either way, the nodes should boot from their own disks from here on.",[67,1900,1902],{"id":1901},"what-immutability-actually-buys-you-here","What immutability actually buys you here",[11,1904,1905,1906,1909],{},"The payoff isn't visible on day one. It shows up months later when a node needs to be reimaged after a hardware swap: boot the ISO, update the MAC and disk serial in that node's patch, regenerate, apply. The process is identical to the first bootstrap, because there was never any drift to reconcile. The inputs are the only source of truth, applied the same way every time. Compare that to a traditional bare-metal node where six months of ad hoc ",[18,1907,1908],{},"apt install"," and manual kubelet flag tweaks make a \"clean rebuild\" a multi-hour archaeology project. Talos trades the flexibility of a general-purpose OS for a much smaller, much more predictable surface, and for a cluster whose only job is running Kubernetes, that trade is an easy one to make.",[11,1911,1912,1913,1917,1918,1922],{},"Once the cluster is up, ",[75,1914,1916],{"href":1915},"\u002F2026\u002F02\u002F04\u002Fpython-talos-health-checking-a-kubernetes-cluster-from-a-cron-job\u002F","Python: Talos – Health-Checking a Kubernetes Cluster from a Cron Job"," covers keeping an eye on it, and ",[75,1919,1921],{"href":1920},"\u002F2026\u002F08\u002F26\u002Ftalos-linux-upgrading-a-cluster-without-a-maintenance-window\u002F","Talos Linux: Upgrading a Cluster Without a Maintenance Window"," covers the first upgrade.",[67,1924,1926],{"id":1925},"references","References",[1928,1929,1930,1943,1954,1961,1968,1975,1987,1994,2001,2008],"ul",{},[1931,1932,1933,1938,1939,1942],"li",{},[75,1934,1937],{"href":1935,"rel":1936},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fgetting-started\u002Fgetting-started",[79],"Getting Started"," (maintenance mode, ",[18,1940,1941],{},"apply-config --insecure",", bootstrap once)",[1931,1944,1945,1620,1950,1953],{},[75,1946,1949],{"href":1947,"rel":1948},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fgetting-started\u002Fprodnotes",[79],"Production Clusters",[18,1951,1952],{},"gen secrets",", endpoints, install disk selection)",[1931,1955,1956],{},[75,1957,1960],{"href":1958,"rel":1959},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fconfigure-your-talos-cluster\u002Fsystem-configuration\u002Freproducible-machine-configuration",[79],"Reproducible Machine Configuration",[1931,1962,1963],{},[75,1964,1967],{"href":1965,"rel":1966},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fconfigure-your-talos-cluster\u002Fsystem-configuration\u002Fpatching",[79],"Configuration Patches",[1931,1969,1970],{},[75,1971,1974],{"href":1972,"rel":1973},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fnetworking\u002Fadvanced\u002Fvip",[79],"Virtual (shared) IP",[1931,1976,1977,39,1982],{},[75,1978,1981],{"href":1979,"rel":1980},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fnetworking\u002Fconfiguration\u002Faliases",[79],"Link Aliases",[75,1983,1986],{"href":1984,"rel":1985},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fnetworking\u002Fconfiguration\u002Fstatic",[79],"Static Addressing",[1931,1988,1989],{},[75,1990,1993],{"href":1991,"rel":1992},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Fdeploy-and-manage-workloads\u002Fworkloads-on-controlplane",[79],"Enable workloads on your control plane nodes",[1931,1995,1996],{},[75,1997,2000],{"href":1998,"rel":1999},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Freference\u002Fconfiguration\u002Fruntime\u002Funattendedinstallconfig",[79],"UnattendedInstallConfig reference",[1931,2002,2003],{},[75,2004,2007],{"href":2005,"rel":2006},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Freference\u002Fcli",[79],"talosctl CLI reference",[1931,2009,2010],{},[75,2011,2014],{"href":2012,"rel":2013},"https:\u002F\u002Fdocs.siderolabs.com\u002Ftalos\u002Fv1.14\u002Ftroubleshooting\u002Ftroubleshooting",[79],"Troubleshooting",[2016,2017,2018],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":95,"searchDepth":126,"depth":126,"links":2020},[2021,2022,2023,2024,2025,2026,2027],{"id":69,"depth":126,"text":70},{"id":164,"depth":126,"text":165},{"id":676,"depth":126,"text":677},{"id":698,"depth":126,"text":699},{"id":742,"depth":126,"text":743},{"id":1901,"depth":126,"text":1902},{"id":1925,"depth":126,"text":1926},"techcolumnist",[2030],"engineering","2026-03-18T14:00:00Z","Bringing up a three-node Talos Kubernetes cluster on bare metal: secrets, per-node patches, a Layer 2 VIP, apply-config, a single bootstrap, and a full script.","md",false,null,{},"\u002Fblog\u002F2026\u002F03\u002F18\u002Ftalos-linux-bootstrapping-a-three-node-kubernetes-cluster-from-bare-metal",{"title":6,"description":2032},[2028],"blog\u002F2026\u002F03\u002F18\u002Ftalos-linux-bootstrapping-a-three-node-kubernetes-cluster-from-bare-metal",[2042,2043,2044],"talos","kubernetes","cluster","2026-09-19T16:11:56Z","\u002F2026\u002F03\u002F18\u002Ftalos-linux-bootstrapping-a-three-node-kubernetes-cluster-from-bare-metal\u002F","4rQaYyP3IY4c2rcBCZCQUi1Iou2KrND-rCX1PKoY3YI",{"title":2049,"description":2050,"date":2051,"url":2052,"categories":2053,"tags":2055,"image":2035,"readingTime":278,"canonical":2028,"sites":2056,"series":2035,"seriesOrder":2035},"Talos Linux: What Immutable Infrastructure Buys You in Production","Where an immutable, API-managed OS pays off under Kubernetes and where it bites: drift, upgrades, try-mode config changes, and debugging without SSH.","2026-03-25T14:00:00Z","\u002F2026\u002F03\u002F25\u002Ftalos-linux-what-immutable-infrastructure-actually-buys-you-in-production\u002F",[2030,2054],"strategy",[2042,2043],[2028],{"title":2058,"description":2059,"date":2060,"url":2061,"categories":2062,"tags":2064,"image":2035,"readingTime":243,"canonical":2028,"sites":2068,"series":2035,"seriesOrder":2035},"Node.js: Discovery – Diffing Two Network Scans to Flag New Hosts","A dependency-free Node.js script that diffs two nmap XML scans and flags new, missing and moved hosts plus port changes, with an exit code for cron or CI.","2026-03-11T14:00:00Z","\u002F2026\u002F03\u002F11\u002Fnode-js-discovery-diffing-two-network-scans-to-flag-new-hosts\u002F",[2063,2030],"scripts",[2065,2066,2067],"nodejs","discovery","network",[2028],[2070,2079,2085],{"title":2071,"description":2072,"date":2073,"url":2074,"categories":2075,"tags":2076,"image":2035,"readingTime":266,"canonical":2028,"sites":2078,"series":2035,"seriesOrder":2035},"Hyper-V to Talos: Rethinking the Virtualization Stack","Why moving workloads off Hyper-V VMs onto a Talos Kubernetes cluster changed the whole stack, with an inventory script and a VM-to-manifest example.","2026-09-16T14:00:00Z","\u002F2026\u002F09\u002F16\u002Fhyper-v-to-talos-rethinking-the-virtualization-stack\u002F",[2030,2054],[2077,2042,2043],"hyper-v",[2028],{"title":1921,"description":2080,"date":2081,"url":1920,"categories":2082,"tags":2083,"image":2035,"readingTime":284,"canonical":2028,"sites":2084,"series":2035,"seriesOrder":2035},"Rolling Talos and Kubernetes upgrades through a cluster one node at a time: preflight checks, etcd snapshots, Image Factory installers, and scripts.","2026-08-26T14:00:00Z",[2030],[2042,2043],[2028],{"title":2086,"description":2087,"date":2088,"url":2089,"categories":2090,"tags":2091,"image":2035,"readingTime":284,"canonical":2028,"sites":2092,"series":2035,"seriesOrder":2035},"Kubernetes: Running Talos Alongside a Legacy Hyper-V Estate","Introducing a Talos Kubernetes cluster into an existing Hyper-V estate: VM build script, Image Factory schematic, VLAN, VIP, firewall and storage boundaries.","2026-07-08T14:00:00Z","\u002F2026\u002F07\u002F08\u002Fkubernetes-running-talos-alongside-a-legacy-hyper-v-estate\u002F",[2030],[2042,2043,2077],[2028],{"doc":2035,"posts":2094},[],1790052513813]