Skip to content
Back to skills

Ansible

ASecurity

Ansible playbook structure, tags, topology rules, branch switching, and operational foot-guns for the K3s cluster.

  • 32 stars
  • 0 votes
  • 0 copies
  • 7 views
  • Added September 19, 2026
devopsgoshellbashnodekubernetesapi

Works with

  • terminal
  • api
  • mcp

Security analysis

A100/100

Scanned September 19, 2026

npx -y skills add gilesknap/tpi-k3s-ansible --skill ansible --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ansible?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ansible
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gilesknap-ansible/badge)](https://www.skillsdirectory.com/skills/gilesknap-ansible)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ansible
description: Ansible playbook structure, tags, topology rules, branch switching, and operational foot-guns for the K3s cluster.
---

# Ansible

## Playbook Structure
- Entry point: `pb_all.yml` (NOT `site.yml`)
- Decommission: `pb_decommission.yml`
- Vars: `group_vars/all.yml` — single source of truth for cluster-wide settings
- Inventory: `hosts.yml`
- ArgoCD apps: `kubernetes-services/templates/`; extra manifests in
  `kubernetes-services/additions/`. Reusable ingress sub-chart at
  `kubernetes-services/additions/ingress/`.
- SSH to nodes: `ssh ansible@<node>` (not root).

## Common Tags
- `servers` — system packages, workstation taint/label tasks, NVIDIA
  container runtime on GPU nodes. **Not `update_packages`** — that
  tag silently does nothing.
- `cluster` — ArgoCD root app + all child apps (branch switching,
  service toggles). Sanctioned path for touching ArgoCD-managed state.
- `k3s` — K3s install/configure.
- `known_hosts` — populate `/etc/ssh/ssh_known_hosts`.

## Operational patterns

- **Ansible reaches nodes via the host SSH agent**, forwarded into the
  devcontainer by VS Code Dev Containers (`SSH_AUTH_SOCK` is set
  automatically). Make sure your ansible key is loaded into the host
  agent before opening the container; the sandbox running Claude
  cannot see it, but ordinary devcontainer terminals can.
- **Commits need `uv run`** — pre-commit hooks live in the uv venv.
- **Adding a node**: run `/add-node`.
- **Full bootstrap**: run `/bootstrap-cluster`.
- **Rebuild cluster**: run `/rebuild-cluster`.

## Branch switching

`repo_branch` in `group_vars/all.yml` **must always be `main`** — never
edit it. To point the cluster at a feature branch, use
`just switch-branch <branch>`, which passes `-e repo_branch=<branch>`
as an override. Revert with `just switch-branch main`.

### Check current tracked revision before branching new work

The cluster may already be on a feature branch (e.g. mid-validation of
an open PR), not `main`. Branching new work off `origin/main` then
running `just switch-branch <new-branch>` will silently roll back any
commits that exist only on the previously-tracked branch. The most
painful case is sealed secrets re-sealed during a rebuild: the new
branch applies older (un-decryptable) versions from `main`, the
controller logs `no key could decrypt secret`, and consumer pods drift
from their live Secrets.

Before creating a new working branch:

```bash
kubectl -n argo-cd get application all-cluster-services \
  -o jsonpath='{.spec.sources[*].targetRevision}'
```

If it's not `main`, **stop and ask the user** whether to:
(a) merge the tracked branch to main first, then branch off main, or
(b) base the new work on the currently-tracked branch.

## Topology rules

### Multi-homed nodes
K3s and flannel auto-detect the IP from the default route, which may be
the wrong subnet. Set `node_ip` and `flannel_iface` in `hosts.yml` for
any node with multiple NICs — otherwise CNI traffic goes out the wrong
interface and pod networking silently breaks.

### Control-plane `NoSchedule` taint
The control plane (e.g. node01) is tainted `NoSchedule`. DaemonSets
without a matching toleration won't schedule there, so it can safely
be skipped when running `--tags servers` for node-level drivers (DRA
plugins, etc.).

### Workstation taint (ws03)
Workstation nodes carry `workstation=true:NoSchedule` because they may
reboot unexpectedly. This has three consequences you must design for:

- **DaemonSets that need ws03** must tolerate the taint. The
  `nvidia-device-plugin` template already does; audit any new DaemonSet
  that needs to run there.
- **Longhorn does not tolerate the taint** — no Longhorn storage runs
  on ws03. Anything with a Longhorn PVC must not be scheduled there.
- **Monitoring statefulsets** (Grafana, Prometheus) tolerate the taint
  so they can scrape workstation metrics, but must have a `nodeAffinity`
  rule **excluding** ws03. Otherwise their Longhorn PVCs get provisioned
  on ws03 and fail to attach.
- **amd64-only workloads land on nuc2 by default.** ws03 and nuc2 are the
  only amd64 nodes; ws03's taint means a chart with
  `nodeSelector: kubernetes.io/arch: amd64` pins to nuc2 without any
  explicit hostname selector. Useful for charts whose schema forbids
  `kubernetes.io/hostname` overrides (e.g. thoth).

### Longhorn replica count vs node count
Replica counts in `kubernetes-services/values.yaml` must match the
number of Longhorn-capable nodes (i.e. excluding ws03). Going too high
leaves volumes permanently Degraded; going too low under-replicates on
adds. Update after any `/add-node` run.

## BMC power operations

When a node is `NotReady` and SSH times out, `ansible -m reboot` and the
graceful procedures in `docs/how-to/node-operations.md` cannot help —
both the kubelet and `sshd` on the node are gone. Power-cycle via the
Turing Pi BMC instead.

The BMC is reachable as `{{ tpi_user }}@turingpi` (default `root@turingpi`,
defined in `group_vars/all.yml`). Each slot maps to a node via
`slot_num` in `hosts.yml`: node01→1, node02→2, node03→3, node04→4.
`nuc2` and `ws03` are not on the BMC.

```bash
# What the BMC thinks of all four slots
ssh root@turingpi 'tpi power status'

# Power-cycle a single node (off → wait → on) — the form used by roles/flash
ssh root@turingpi 'tpi power off -n 4 && sleep 15 && tpi power on -n 4'
```

Notes:
- Don't try `kubectl drain` first — the node is unreachable, so the
  drain will just hang. Just power-cycle and let workloads reschedule
  (or come back, for DaemonSets pinned to that node like `rkllama`).
- Hard power-cycle is unclean for in-flight writes. If the node hosts
  live local-PV data (see CLAUDE.md "Local PV data paths are sacred":
  Prometheus on node02, Grafana on node03, Open-WebUI on node04),
  expect fsck on boot — check pod logs once the node returns.
- For the Claude sandbox specifically: the BMC SSH key isn't reachable
  from the sandbox (the `--clearenv` bwrap wrapper strips `SSH_AUTH_SOCK`
  and the BMC host key isn't in the sandbox's `known_hosts`). The `!`
  prompt prefix runs inside the same sandbox, so it doesn't help — ask
  the user to run `ssh root@turingpi 'tpi power ...'` from a regular
  devcontainer terminal or the host shell.

## Foot-guns

- **`known_hosts` task must run `serial: 1`** — parallel writes cause
  race conditions on the shared file.
- **Traefik is disabled** — `--disable=traefik` with NGINX Ingress. Do
  not assume Traefik CRDs exist.
- **No automated tests** — validate by running playbook tags against
  the live cluster.
- **Ansible `k8s` module merges annotations** — it never removes
  annotations that were previously set on an Ingress. If you remove
  annotations from a template (e.g. `ssl-passthrough`), you must
  `kubectl delete` the old Ingress first, then re-run the playbook to
  recreate it cleanly.
- **ArgoCD `valuesObject` overrides `values.yaml`** — for child apps
  like `open-brain-mcp`, the image tag is set in `templates/*.yaml`
  under `valuesObject`, not in `additions/*/values.yaml`. Editing
  `values.yaml` alone has no effect — check the template first.
- **`admin_emails` is duplicated** — must be kept in sync between
  `kubernetes-services/values.yaml` (Helm) and `group_vars/all.yml`
  (Ansible). After changing the Ansible copy, re-run `--tags cluster`.
- **Prometheus admission webhook secret** — `kube-prometheus-stack`'s
  TLS secret (`grafana-prometheus-kube-pr-admission`) is not created by
  the Helm hook job under ArgoCD. The `cluster` role creates it via
  `scripts/create-prometheus-admission-secret`. If the operator is
  still CrashLooping after a rebuild, run
  `just create-prometheus-admission-secret` directly then delete the
  stuck pod.
- **Decommission before ArgoCD** — when tearing down the cluster, the
  decommission playbook deletes ArgoCD Applications (orphan cascade)
  *before* scaling down workloads. Doing it in the other order means
  ArgoCD reconciliation re-creates pods faster than you can remove
  them. After controller uninstall, strip finalizers from Longhorn
  CRD resources (volumes, engines, etc.) since the controller is gone.
- **`gh pr edit` fails on this repo** — classic projects warning
  causes a GraphQL error. Use
  `gh api repos/OWNER/REPO/pulls/N -X PATCH -f body=...` instead.
- **`local-nvme` PVs get stuck `Released` after ArgoCD app delete**
  — chart-generated PVCs (StatefulSet `volumeClaimTemplates`) are
  owned by the consuming ArgoCD app, so `argocd app delete` cascades
  to the PVC via `resources-finalizer.argocd.argoproj.io`. The
  static PV (`Retain` policy, pre-bound via `claimRef`) keeps the
  **dead PVC's `uid`** in `spec.claimRef.uid`. New PVCs get fresh
  UIDs and can't bind → pod `Pending`, scheduler reports `didn't
  find available persistent volumes to bind`. Fix:
  `kubectl patch pv <name> --type=json -p '[{"op":"remove","path":"/spec/claimRef/uid"},{"op":"remove","path":"/spec/claimRef/resourceVersion"}]'`.
  (This is the rare authorised exception to the no-mutate-ArgoCD-
  managed rule — you're clearing kube-managed runtime state, not
  changing template intent.) Long-term fix tracked in issue #397.
  Prefer `argocd app sync --replace` over `argocd app delete`/recreate
  for the five `local-nvme`-backed apps: open-webui, grafana,
  prometheus, plus the three supabase-* claims.

## Key files
- `pb_all.yml` — main playbook
- `pb_decommission.yml` — teardown
- `group_vars/all.yml` — all variables
- `hosts.yml` — inventory with node vars
- `roles/` — Ansible roles

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.