--- # Shared config for the whole k3s_cluster group (both k3s_control_plane and # k3s_workers) — see roles/k3s_node and playbooks/k3s.yml. # Used as the kubeconfig context/cluster name and as the Vault path suffix # below, so the cluster has one name in every place it shows up. k3s_cluster_name: homelab-utils # Pinned, not "latest" — a rebuild months from now should produce the same # cluster it does today, same reasoning as pinning an app's image tag in # common/vars.yml. Bump deliberately; check the current stable release at # https://github.com/k3s-io/k3s/releases first. k3s_version: v1.31.4+k3s1 # One Vault path for the whole cluster, same "one path per stack" convention # as everything under homelab/ — see docs/vault-secrets.md. k3s_vault_path: "homelab/k3s-{{ k3s_cluster_name }}" # The SSH key this group is reached with. A different path from k3s_vault_path # above because it's a different kind of secret: that one is cluster identity # (the join token), this one is access to the hosts, and it lives alongside # the Unraid key under homelab/ci/ rather than with the cluster's own data. # Fetched by playbooks/k3s_ssh_key.yml, which both k3s playbooks import. k3s_ssh_key_vault_path: homelab/ci/ssh-k3s # Where that play writes it, and what every play here then connects with — so # a manual run needs Vault credentials in the environment and nothing else, # rather than the key pre-loaded into ssh-agent. Override on the command line # (-e ansible_ssh_private_key_file=~/.ssh/k3s_ansible) to use a local copy # instead, e.g. if Vault itself is down. # # Under local/, which .gitignore already excludes — same as the kubeconfig # below, and for the same reason: generated credential, never committed. Built # from playbook_dir rather than repo_root, unlike k3s_kubeconfig_local_path: # repo_root is a *play* var set in k3s.yml, so it only resolves inside the two # plays that set it, whereas this is a connection var and has to resolve in # every play in the file. k3s_ssh_key_local_path: "{{ playbook_dir }}/../../../../local/k3s/{{ k3s_cluster_name }}.key" ansible_ssh_private_key_file: "{{ k3s_ssh_key_local_path }}" # Extra INSTALL_K3S_EXEC args appended on every node, both roles. Empty by # default. Only flags valid on *both* `k3s server` and `k3s agent` belong # here — see k3s_server_extra_args below for server-only flags like # `--disable`, which `k3s agent` doesn't understand and will fail to start # with. k3s_extra_args: [] # Extra INSTALL_K3S_EXEC args appended on the control-plane node only. # --disable=servicelb turns off k3s's bundled LoadBalancer controller # (Klipper) in favour of roles/k3s_metallb — the two would otherwise both # try to satisfy the same LoadBalancer Services. Add e.g. '--disable=traefik' # here too if homelab-utils apps end up wanting their own ingress instead. # # Changing this (or k3s_extra_args) on an already-installed cluster is picked # up on the next `ansible-playbook playbooks/k3s.yml` run — roles/k3s_node # tracks the exec line it last installed with and reinstalls (restarting the # k3s/k3s-agent service, not the node) whenever it drifts from what's # configured here. See tasks/server.yml and tasks/agent.yml. k3s_server_extra_args: - "--disable=servicelb" # MetalLB's LoadBalancer IP pool — see roles/k3s_metallb. Must be addresses # on the LAN (192.168.50.0/24 — see inventory/hosts.yml) that nothing else, # DHCP included, will ever hand out. Adjust to a range your DHCP scope # doesn't cover before the first run. k3s_metallb_address_range: "192.168.50.80-192.168.50.99" # The LAN address the shared Postgres primary answers on — see # roles/k3s_postgres, which declares a LoadBalancer Service for it alongside # CNPG's built-in ClusterIP ones. Pinned rather than auto-assigned because # the address gets written down elsewhere (an app's vars.yml, a connection # string in Vault) instead of only being looked up at runtime, so it mustn't # move when the Service is recreated. # # Must be inside the pool above — MetalLB only assigns from its own pools, # and a request for an address outside them leaves the Service pending # forever rather than failing loudly. The caveat of pinning out of the same # pool MetalLB auto-assigns from: it hands unpinned Services the lowest free # address, so if some other LoadBalancer Service is created before this one # and takes .80, this Service is the one left pending. Nothing else here # requests an IP today. If that changes, the fix is a second # IPAddressPool with autoAssign: false in roles/k3s_metallb reserved for # pinned addresses, not a different number here. k3s_postgres_loadbalancer_ip: "192.168.50.81" # Where playbooks/k3s.yml fetches the kubeconfig to, on the Ansible # controller. Under local/, which .gitignore already excludes — it's a # generated credential, never committed. k3s_kubeconfig_local_path: "{{ repo_root }}/local/k3s/{{ k3s_cluster_name }}.kubeconfig" # --- cert-manager (roles/k3s_cert_manager) ---------------------------------- # # TLS for anything this cluster serves over Traefik. See that role's # defaults/main.yml for why DNS-01 rather than HTTP-01, and what each of # these does. # Where Let's Encrypt sends expiry warnings. An ACME account is registered # against it. k3s_cert_manager_acme_email: russell.seymour@turtlesystems.co.uk # >>> SET BEFORE THE FIRST RUN <<< # # How cert-manager proves control of the domain, rendered into the # ClusterIssuer's solvers list as-is. Left empty deliberately — it depends on # who runs DNS for turtlesystems.uk, which this repo has no way to know. # roles/k3s_cert_manager fails fast while it's empty, rather than creating an # issuer that can never satisfy an order. # # Cloudflare, as the most common case — the token needs Zone:DNS:Edit on the # zone, and is stored in Vault (see k3s_cert_manager_vault_path below and # docs/vault-secrets.md) rather than written here: # # k3s_cert_manager_solver: # dns01: # cloudflare: # apiTokenSecretRef: # name: cert-manager-dns-credentials # key: CLOUDFLARE_API_TOKEN # selector: # dnsZones: # - turtlesystems.uk # # For another provider, take the stanza from # https://cert-manager.io/docs/configuration/acme/dns01/ and point its secret # ref at whichever key you stored at that Vault path. # # Use the Let's Encrypt staging directory while working this out — override # k3s_cert_manager_acme_server. Production allows 5 failed validations per # hostname per hour, and exhausting it means waiting, not retrying. k3s_cert_manager_solver: dns01: cloudflare: apiTokenSecretRef: name: cert-manager-dns-credentials key: CLOUDFLARE_API_TOKEN selector: dnsZones: - turtlesystems.uk # --- Traefik (roles/k3s_traefik) -------------------------------------------- # # The ingress controller k3s installs for itself; that role installs nothing, # it only adjusts what's already there. Its defaults are enough for the # dashboard over `kubectl port-forward` — see that role's defaults/main.yml # and README.md → "Dashboard (Traefik)". # >>> SET WHEN AUTHENTIK IS READY TO PROTECT IT <<< # # Uncomment to publish the dashboard on this hostname, behind the forward-auth # middleware below. Until then the port-forward is the only way in, which is # the safe default rather than a limitation. Needs a DNS record pointing at # Traefik's MetalLB address (`kubectl -n kube-system get svc traefik`). # # Before uncommenting, in Authentik: create a Proxy Provider in **forward auth # (domain level)** mode covering turtlesystems.uk, assign it to an # application, and add that application to the embedded outpost. Domain level # rather than single-application for a Kubernetes-specific reason — see # roles/k3s_traefik/defaults/main.yml. # # k3s_traefik_dashboard_host: traefik.turtlesystems.uk # Authentik's embedded outpost, by in-cluster DNS — the `authentik-server` # Service in the `authentik` namespace, i.e. the chart's own service, not a # separately deployed outpost. Unused while the host above is commented out; # set here anyway so enabling the dashboard is one line rather than two. # roles/k3s_traefik refuses to run if the host is set and this isn't. k3s_traefik_dashboard_auth_address: "http://authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik" # --- Apps on the cluster (roles/k3s_app) ------------------------------------ # # The k3s equivalent of `stacks:` in host_vars/nas01.yml and `apps:` in a # Proxmox guest's host_vars — the list of *applications* on this cluster, as # opposed to the k3s_* cluster services above, which are the cluster's own # infrastructure. # # In group_vars rather than host_vars because an app is deployed to the # cluster, not to a node: playbooks/k3s.yml runs roles/k3s_app against # k3s_control_plane only (that's where the manifests directory is), but # nothing about an app belongs to that Pi specifically. # # Same entry shape as the other two platforms — name, src, vault_path, an # optional `db:`, and an optional `state:`. And the same removal rule as # Unraid: set `state: absent` to tear an app down and leave the entry here as # a tombstone; deleting the entry removes nothing, it just stops Ansible # visiting it. See roles/k3s_app/tasks/remove.yml. k3s_apps: - name: authentik src: authentik vault_path: homelab/authentik db: name: authentik user: authentik # The key at homelab/authentik holding this role's password. Named for # the environment variable Authentik itself reads, because # roles/k3s_app passes Vault keys through to the app's Secret verbatim # — so one value serves both the provisioning step and the running app. password_vault_key: AUTHENTIK_POSTGRESQL__PASSWORD admin_vault_path: homelab/shared/postgres # The CNPG LoadBalancer, not the in-cluster -rw Service: these tasks # run on the Ansible controller (delegate_to: localhost), which is off # the cluster and can't resolve or route to a ClusterIP. Authentik # itself uses the ClusterIP — see # src/authentik/ansible/kubernetes/vars.yml. provision_host: "{{ k3s_postgres_loadbalancer_ip }}" provision_port: "5432"