# Ansible Roles in Detail
The playbook `pb_all.yml` runs seven roles in sequence. Each role is fully idempotent —
it checks state before acting and does nothing if the desired state is already achieved.
## Execution order
```mermaid
%%{init: {'themeVariables': {'fontSize': '30px'}, 'flowchart': {'useMaxWidth': false}}}%%
flowchart TD
T["tools
localhost"] --> F["flash
turing_pis"]
F --> KH["known_hosts
all_nodes + turing_pis"]
KH --> MF["move_fs
all_nodes"]
MF --> UP["update_packages
all_nodes"]
UP --> K["k3s
all_nodes"]
K --> C["cluster
localhost"]
```
Each role is tagged with its own name, so you can run individual stages:
```bash
ansible-playbook pb_all.yml --tags tools
ansible-playbook pb_all.yml --tags flash
# etc.
```
The `servers` tag covers both `move_fs` and `update_packages`.
---
## `tools` — CLI tool installation
**Runs on:** localhost (devcontainer)
**Tag:** `tools`
Installs command-line tools needed to manage the cluster:
| Tool | Version | Purpose |
|------|---------|---------|
| `helm` | 3.20.0 | Kubernetes package manager |
| `kubectl` | latest stable | Kubernetes CLI |
| `kubeseal` | 0.35.0 | Sealed Secrets CLI |
| `helm-diff` | plugin | Shows Helm upgrade diffs |
Also creates:
- Shell completions for helm, kubectl (bash + zsh)
- `k` alias for kubectl
- Port-forward helper scripts: `argo.sh`, `grafana.sh`, `dashboard.sh`, `longhorn.sh`
- Sets PATH to include `$BIN_DIR`
The role is split across multiple task files:
- `shell.yml` — PATH, zsh theme
- `helm.yml` — Helm binary + helm-diff plugin
- `kubectl.yml` — kubectl binary + completions
- `kubeseal.yml` — kubeseal binary
- `scripts.yml` — port-forward helper scripts
---
## `flash` — BMC-based OS flashing
**Runs on:** `turing_pis` (BMC hosts)
**Tag:** `flash`
**Guard:** only runs when `do_flash` is true (`-e do_flash=true` or `-e flash_force=true`)
Flashes Ubuntu 24.04 LTS onto Turing Pi compute modules via the BMC's `tpi` CLI.
### How it works
1. **Discover nodes** — looks for the inventory group `_nodes`
(e.g. `turingpi_nodes` for BMC host `turingpi`).
2. **For each node:**
- Check if the node is already contactable via SSH (skip if so, unless force).
- Download the OS image (RK1 or CM4) to `/tmp` on the devcontainer.
- SCP the image to the BMC at `/mnt/sdcard/images/`.
- Power off the node.
- Run `tpi flash --node ` (async, up to 600 seconds).
- Wait for flash to complete.
3. **Bootstrap cloud-init:**
- Enter MSD mode (mount the node's eMMC as USB storage on the BMC).
- Render `cloud.cfg` with the node's hostname and the `ansible` user + SSH key.
- SCP the config to the node's filesystem.
- Clear cloud-init cache, reboot, and wait for SSH.
### OS images
| Type | Image | Source |
|------|-------|--------|
| RK1 | Ubuntu 24.04 (rockchip) | `github.com/Joshua-Riek/ubuntu-rockchip` |
| CM4 | Ubuntu 24.04 Server | `cdimage.ubuntu.com` |
### Idempotency
- Node is pinged first — if contactable and `flash_force` is not set, flashing is skipped.
- Images are only downloaded if not already present.
---
## `known_hosts` — SSH host key management
**Runs on:** `all_nodes`, `turing_pis`
**Tag:** `known_hosts`
**Constraint:** `serial: 1` (must not run in parallel)
Updates `~/.ssh/known_hosts` with fresh SSH host keys for each node:
1. Look up the node's IP via `dig`.
2. Remove old entries (by hostname and IP).
3. Scan for current SSH host keys (`ssh-keyscan`).
4. Add the new keys.
:::{warning}
This role must run with `serial: 1` because parallel writes to `~/.ssh/known_hosts`
cause race conditions and file corruption.
:::
---
## `move_fs` — OS migration to NVMe
**Runs on:** `all_nodes`
**Tag:** `servers`
**Guard:** only activates for nodes with `root_dev` defined in the inventory
Migrates the root filesystem from eMMC to NVMe using `ubuntu-rockchip-install`:
1. Check the current root device.
2. If not already on the target device, run `ubuntu-rockchip-install` to the NVMe.
3. Reboot.
:::{note}
eMMC always remains the bootloader for RK1 nodes. Re-flashing via BMC (`tpi flash`)
still works because it writes to eMMC. After a re-flash, the `move_fs` role will
re-migrate to NVMe on the next playbook run.
:::
---
## `update_packages` — OS preparation
**Runs on:** `all_nodes`
**Tag:** `servers`
Prepares each node for K3s:
1. `dpkg --configure -a` (fix any interrupted package operations)
2. `apt dist-upgrade` (full OS upgrade)
3. Reboot if required (kernel updates)
4. `apt autoremove` (clean up)
5. Install required packages:
- `unattended-upgrades` — automatic security updates
- `open-iscsi` — required by Longhorn for iSCSI storage
- `original-awk` — required by some K3s scripts
6. **NVIDIA GPU nodes only** (when `nvidia_gpu_node: true` in inventory):
- Install `ubuntu-drivers-common` and run `ubuntu-drivers install` to install the GPU driver
- Add the NVIDIA container toolkit apt repository and install `nvidia-container-toolkit`
- Write `/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl` with the NVIDIA
runtime set as the default containerd runtime. k3s regenerates `config.toml` on
every agent restart from this template, so the configuration persists across reboots.
- Restart `k3s-agent` to apply the new containerd config
---
## `k3s` — Kubernetes installation
**Runs on:** `all_nodes`
**Tag:** `k3s`
Installs K3s with one control plane node and the rest as workers.
### Control plane (`control.yml`)
- Downloads the K3s install script.
- Runs: `k3s server --disable=traefik --cluster-init`
- Traefik is disabled because this project uses NGINX Ingress.
- `--cluster-init` enables embedded etcd.
### Workers (`worker.yml`)
- Checks if the node is already in the cluster (skip if so, unless force).
- Gets the join token from the control plane.
- Runs the K3s agent installer.
- Labels RK1 nodes with `node-type=rk1` (used by the rkllama DaemonSet selector).
- Creates `/opt/rkllama/models` directory on RK1 nodes.
- Labels NVIDIA GPU nodes with `nvidia.com/gpu.present=true` (when `nvidia_gpu_node: true`
in inventory). This bootstraps scheduling for the NVIDIA device plugin DaemonSet, which
then takes over and advertises `nvidia.com/gpu` allocatable resources to the scheduler.
### Kubeconfig (`kubeconfig.yml`)
- Copies `k3s.yaml` from the control plane to `~/.kube/config` on the devcontainer.
- Replaces `127.0.0.1` with the control plane's actual IP.
### Force reinstall
With `-e k3s_force=true`, K3s is uninstalled first (`k3s-uninstall.sh` on control plane,
`k3s-agent-uninstall.sh` on workers), then reinstalled.
---
## `cluster` — ArgoCD and service deployment
**Runs on:** localhost (devcontainer)
**Tag:** `cluster`
Bootstraps ArgoCD and the entire service stack:
1. **Taint the control plane** (multi-node only) — applies `NoSchedule` taint so
workloads only run on worker nodes. Skipped for single-node clusters.
2. **Install ArgoCD** — deploys the ArgoCD OCI Helm chart (v7.8.3).
3. **Patch ConfigMap** — adds a custom Lua health check for `monitoring.coreos.com/Prometheus`
(respects a `skip-health-check` annotation).
4. **Create AppProject** — creates the `kubernetes` ArgoCD project allowing access to
all repos, namespaces, and cluster-scoped resources.
5. **Create root Application** — creates `all-cluster-services` pointing at
`kubernetes-services/` in the repository. Passes `repo_remote`, `cluster_domain`,
and `domain_email` as Helm values.
6. **Create ArgoCD Ingress** — creates an Ingress for `argocd.` with
SSL passthrough.
After this role completes, ArgoCD takes over and syncs all services defined in
`kubernetes-services/templates/`.
The `cluster_install_list` variable in `group_vars/all.yml` controls which services
the Ansible role installs directly (currently just `argocd`). Everything else is
managed by ArgoCD once it's running.