Manage Talos the GitOps Way!
Talm is just like Helm, but for Talos Linux
While developing Talm, we aimed to achieve the following goals:
-
Automatic Discovery: In a bare-metal environment, each server may vary slightly in aspects such as disks and network interfaces. Talm enables discovery of node information, which is then used to generate patches.
-
Ease of Customization: You can customize templates to create your unique configuration based on your environment. The templates use the standard Go templates syntax, enhanced with widely-known Helm templating logic.
-
GitOps Friendly: The patches generated do not contain sensitive data, allowing them to be stored in Git in an unencrypted, open format. For scenarios requiring complete configurations, the
--fulloption allows the obtain a complete config that can be used for matchbox and other solutions. -
Simplicity of Use: You no longer need to pass connection options for each specific server; they are saved along with the templating results into a separate file. This allows you to easily apply one or multiple files in batch using a syntax similar to
kubectl apply -f node1.yaml -f node2.yaml. -
Compatibility with talosctl: We strive to maintain compatibility with the upstream project in patches and configurations. The configurations you obtain can be used with the official tools like talosctl and Omni.
For macOS and Linux users, the recommended way to install talm is with Homebrew.
brew install talmDownload binary from Github releases page
Or use simple script to install it:
curl -sSL https://github.com/cozystack/talm/raw/refs/heads/main/hack/install.sh | sh -sWindows is supported. Download the talm-windows-*.zip archive from the
releases page and
extract talm.exe. On Windows, template paths passed to the -t /
--template flag accept either \ or / separators, so
-t templates\controlplane.yaml and -t templates/controlplane.yaml
are equivalent. Other path flags (--talosconfig, -f / --file)
are delegated to the underlying OS file loader and follow standard
Windows path rules.
Create new project
mkdir newcluster
cd newcluster
talm init -p cozystack -N myawesomeclusterEdit values.yaml to set your cluster's control-plane endpoint. This
is the URL every node's kubelet and kube-proxy will dial. The chart
leaves it empty on purpose so a missed override fails loudly instead
of silently embedding a placeholder. For cozystack VIP setups set
endpoint and floatingIP together (same IP, single shared VIP);
for single-node clusters use that node's routable IP and leave
floatingIP blank; for multi-node with an external load balancer
use the LB URL and leave floatingIP blank. Subnet-selector fields
(kubelet.validSubnets, etcd.advertisedSubnets) are derived
automatically from the node's default-gateway-bearing link, so no
override is needed unless you have a multi-homed node that requires
a specific subnet pinned.
Boot Talos Linux node, let's say it has address 192.0.2.4. Then:
# values.yaml (single-node example matching the 192.0.2.4 node below)
endpoint: "https://192.0.2.4:6443"
floatingIP: ""Gather node information:
talm -n 192.0.2.4 -e 192.0.2.4 template -t templates/controlplane.yaml -i > nodes/node1.yamlEdit nodes/node1.yaml file:
# talm: nodes=["192.0.2.4"], endpoints=["192.0.2.4"], templates=["templates/controlplane.yaml"]
machine:
network:
# -- Discovered interfaces:
# enx9c6b0047066c:
# name: enp193s0f0
# mac:9c:6b:00:47:06:6c
# bus:0000:c1:00.0
# driver:bnxt_en
# vendor: Broadcom Inc. and subsidiaries
# product: BCM57414 NetXtreme-E 10Gb/25Gb RDMA Ethernet Controller)
# enx9c6b0047066d:
# name: enp193s0f1
# mac:9c:6b:00:47:06:6d
# bus:0000:c1:00.1
# driver:bnxt_en
# vendor: Broadcom Inc. and subsidiaries
# product: BCM57414 NetXtreme-E 10Gb/25Gb RDMA Ethernet Controller)
interfaces:
- interface: enx9c6b0047066c
addresses:
- 192.0.2.4/26
routes:
- network: 0.0.0.0/0
gateway: 192.0.2.1
nameservers:
- 8.8.8.8
- 8.8.4.4
install:
# -- Discovered disks:
# /dev/nvme0n1:
# model: SAMSUNG MZQL21T9HCJR-00A07
# serial: S64GNE0RB00153
# wwid: eui.3634473052b001530025384500000001
# size: 1.75 TB
# /dev/nvme1n1:
# model: SAMSUNG MZQL21T9HCJR-00A07
# serial: S64GNE0R811820
# wwid: eui.36344730528118200025384500000001
# size: 1.75 TB
disk: /dev/nvme0n1
type: controlplane
cluster:
clusterName: talm
controlPlane:
endpoint: https://192.0.2.4:6443Note: The output format depends on the Talos version configured in
Chart.yaml(templateOptions.talosVersion) or via the--talos-versionCLI flag. For Talos < v1.12, the output is a single YAML document withmachine.networkandmachine.registriessections (as shown above). For Talos >= v1.12, the output uses the multi-document format with separate typed documents instead of the deprecated monolithic fields.HostnameConfig,ResolverConfigand a network interface document (LinkConfig,BondConfig, orVLANConfig— depending on topology) are always emitted;Layer2VIPConfigappears on controlplane nodes whenfloatingIPis set;RegistryMirrorConfigis emitted only by the cozystack chart.
Apply config:
talm apply -f nodes/node1.yaml -iUpgrade node:
talm upgrade -f nodes/node1.yamlShow diff:
talm apply -f nodes/node1.yaml --dry-runRe-template and update generated file in place (this will overwrite it):
talm template -f nodes/node1.yaml -I
Per-node patches inside node files. A node file can carry Talos config below its modeline (for example, a custom
hostname, secondary interfaces withdeviceSelector, VIP placement, or extra etcd args). Whentalm apply -f node.yamlruns the template-rendering branch, that body is applied as a strategic merge patch on top of the rendered template before the result is sent to the node — so per-node fields survive even when the template auto-generates conflicting values (e.g.hostname: talos-XXXXX).Talos v1.12+ caveat. The multi-document output format introduced in v1.12 splits network configuration into typed documents (
LinkConfig,BondConfig,VLANConfig,Layer2VIPConfig,HostnameConfig,ResolverConfig). Legacy node-body fields undermachine.network.interfaceshave no safe 1:1 mapping to those types, so the multi-doc path does not translate them — if you target a v1.12+ Talos node, pin per-node network settings by patching the typed resources (e.g. aLinkConfigdocument below the modeline) rather than legacymachine.network.interfaces. Fields outside the network area (machine.network.hostnameviaHostnameConfig,machine.install.disk, extra etcd args, etc.) still merge as expected.One body, one node. A non-empty body is a per-node pin, so the modeline for that file must target exactly one node.
talm applyrefuses a multi-node modeline when the body is non-empty; modeline- only files (no body) are still allowed and drive the same rendered template on every listed target.
talm template -f node.yaml(with or without-I) does not apply the same overlay: its output is the rendered template plus the modeline and the auto-generated warning, byte-identical to what the template alone would produce. Routing it through the patcher would drop every YAML comment (including the modeline) and re-sort keys, breaking downstream commands that read the file back. Useapply --dry-runif you want to preview the exact bytes that will be sent to the node.
Talm offers a similar set of commands to those provided by talosctl. However, you can specify the --file option for them.
For example, to run a dashboard for three nodes:
talm dashboard -f node1.yaml -f node2.yaml -f node3.yaml
You're free to edit template files in ./templates directory.
All the Helm and Sprig functions are supported, including lookup for talos resources!
Lookup function example:
{{ lookup "nodeaddresses" "network" "default" }}
- is equivalent to:
talosctl get nodeaddresses --namespace=network defaultQuerying disks map example:
{{ range .Disks }}{{ if .system_disk }}{{ .device_name }}{{ end }}{{ end }}
- will return the system disk device name
Talm provides built-in encryption support using age encryption. Sensitive files are encrypted with their values stored in SOPS format (ENC[AGE,data:...]), while YAML keys remain unencrypted for better readability.
To encrypt all sensitive files (secrets.yaml, talosconfig, kubeconfig):
talm init --encrypt
# or
talm init -eThis command will:
- Generate
talm.keyif it doesn't exist - Encrypt
secrets.yaml→secrets.encrypted.yaml - Encrypt
talosconfig→talosconfig.encrypted - Encrypt
kubeconfig→kubeconfig.encrypted(if exists) - Update
.gitignorewith sensitive files
To decrypt all encrypted files:
talm init --decrypt
# or
talm init -dThis command will:
- Decrypt
secrets.encrypted.yaml→secrets.yaml - Decrypt
talosconfig.encrypted→talosconfig - Decrypt
kubeconfig.encrypted→kubeconfig(if exists) - Update
.gitignorewith sensitive files
The talm.key file is generated in age keygen format and contains:
- Creation timestamp
- Public key (for sharing)
- Private key (keep secure!)
Important: Always backup your talm.key file! Without it, you won't be able to decrypt your encrypted secrets. The key file is automatically added to .gitignore to prevent accidental commits.
Encrypted files (*.encrypted.yaml, *.encrypted) can be safely committed to Git, while plain files (secrets.yaml, talosconfig, kubeconfig, talm.key) are ignored.