docs(vibe): add factory-provisioning guidebook (Ansible + OpenTofu)
Deep, code-grounded tree-docs guidebook under vibe/guidebooks/factory-provisioning/, explored from the actual playbooks/roles and tofu code: - Hub: the two provisioning engines (operator-run Ansible vs CI-applied OpenTofu), a green-field bring-up flow, master index, maintenance rule. - ansible/ sub-tree: ordered pages 01-system .. 06-recover, an inventory & variables concept page, and a Tier-1/Tier-2 roles reference (hashicorp_vault, step_ca, crowdsec, pihole, deploy_docker_compose + the gitea_* family and helpers). - opentofu/ sub-tree: factory-iac (Cloudflare/OVH/GCP/Gitea/Vault edge + cloudflare_token module), postgres-iac (per-app DB/role/pgbouncer lookup), ci-apply-flow (Gitea OIDC-JWT -> Vault -> auto-approve apply). Cross-linked bidirectionally with the lab-ecosystem guidebook and the safe-env ADR/PRD (the sandbox rehearses exactly these engines). 14 mermaid diagrams MCP-validated; zero dead links. Authored by the Lab Cartographer cohort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
114
vibe/guidebooks/factory-provisioning/opentofu/ci-apply-flow.md
Normal file
114
vibe/guidebooks/factory-provisioning/opentofu/ci-apply-flow.md
Normal file
@@ -0,0 +1,114 @@
|
||||
[vibe](../../../README.md) > [Guidebooks](../../README.md) > [Factory provisioning](../README.md) > [OpenTofu](README.md) > **CI apply flow**
|
||||
|
||||
# CI apply flow
|
||||
|
||||
> [!NOTE]
|
||||
> **Status:** ✅ active · **Last Updated:** 2026-06-23
|
||||
> **Upstream:** [`.gitea/workflows/iac.yaml`](../../../../.gitea/workflows/iac.yaml), [`.gitea/workflows/postgres.yaml`](../../../../.gitea/workflows/postgres.yaml)
|
||||
> **Downstream:** [factory iac](factory-iac.md), [postgres iac](postgres-iac.md)
|
||||
> **Related:** [Secrets & Vault](../../lab-ecosystem/secrets-and-vault.md) · [ADR-0001 · Safe prod-like environment](../../../ADR/0001-safe-prod-like-environment.md) · [QA strategy](../../../PRD/safe-prod-like-environment/qa-strategy.md)
|
||||
|
||||
Two Gitea Actions workflows turn every commit that touches the OpenTofu code into a live `apply`. `IAC` ([`.gitea/workflows/iac.yaml`](../../../../.gitea/workflows/iac.yaml)) drives the factory infrastructure under [`iac/`](../../../../iac/); `Postgres` ([`.gitea/workflows/postgres.yaml`](../../../../.gitea/workflows/postgres.yaml)) drives the database stack under [`postgres/iac/`](../../../../postgres/). They share the same two-job shape: a short OIDC-auth job feeds a Vault JWT to a `tofu` job that reads secrets and runs `terraform apply`.
|
||||
|
||||
> [!CAUTION]
|
||||
> **`auto_approve: true` means every merge to `main` applies immediately — there is no plan-gate.** The `dflook/terraform-apply@v1` step skips the interactive approval, so any change that lands on `main` (or any matched `push`) rewrites real cloud and homelab state without a human reviewing the plan. Mitigations are entirely upstream of CI: (1) **mandatory code review** on the PR before merge, and (2) **least-privilege Vault policies** on the `gitea_cicd` role so a runaway apply can only touch the resources its token is scoped to. See [ADR-0001](../../../ADR/0001-safe-prod-like-environment.md): the sandbox lane runs the *same* tofu but **plan-only** against a `sandbox/` state prefix and a throwaway DNS zone, so contributors can validate changes without an auto-apply.
|
||||
|
||||
## Triggers
|
||||
|
||||
Both workflows fire on the same three events; only the watched path globs differ.
|
||||
|
||||
| Event | `IAC` (factory) | `Postgres` |
|
||||
| --- | --- | --- |
|
||||
| `push` | `iac/*.tf`, `iac/*.tfvars`, `iac/**/*.tf`, `iac/**/*.tfvars` | `postgres/**/*.tf`, `postgres/**/*.tfvars` |
|
||||
| `pull_request` | same globs (YAML anchor `*tofuPaths`) | same globs (YAML anchor `*postgresTofuPaths`) |
|
||||
| `workflow_dispatch` | manual, no inputs | manual, no inputs |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `concurrency` is keyed on `${{ github.ref }}-${{ github.workflow }}` with `cancel-in-progress: true`, so a newer push to the same branch cancels an in-flight run. A `pull_request` event triggers the workflow — but the `apply` still runs, so the safety contract is "review **before** merge", not "CI only plans on PRs".
|
||||
|
||||
## Job 1 — `gitea_vault_auth`
|
||||
|
||||
Mints a Gitea OIDC token that Vault will trust. The whole job is one step:
|
||||
|
||||
```bash
|
||||
echo -n "${{ secrets.vault_oauth__sh_b64 }}" | base64 -d | bash
|
||||
```
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Runner | `ubuntu-latest` |
|
||||
| Secret consumed | `vault_oauth__sh_b64` — a base64-encoded shell script |
|
||||
| Step id | `gitea_vault_jwt` |
|
||||
| Output | `gitea_vault_jwt` ← `steps.gitea_vault_jwt.outputs.id_token` |
|
||||
|
||||
The decoded script asks Gitea for an OIDC `id_token` and emits it as a step output. The `tofu` job declares `needs: [gitea_vault_auth]` so it receives `needs.gitea_vault_auth.outputs.gitea_vault_jwt`.
|
||||
|
||||
## Job 2 — `tofu`
|
||||
|
||||
| Field | `IAC` | `Postgres` |
|
||||
| --- | --- | --- |
|
||||
| Job name | `Tofu` | `Tofu - Postgres` |
|
||||
| `needs` | `gitea_vault_auth` | `gitea_vault_auth` |
|
||||
| `OPENTOFU_VERSION` | `1.8.2` | `1.8.2` |
|
||||
| `TERRAFORM_VAULT_AUTH_JWT` | `needs.gitea_vault_auth.outputs.gitea_vault_jwt` | same |
|
||||
| `VAULT_CACERT` | `${{ github.workspace }}/homelab.pem` | same |
|
||||
| Apply path | `iac` | `postgres/iac` |
|
||||
|
||||
Step order inside the job:
|
||||
|
||||
1. **read vault secret** — the shared `*vault_step` anchor (see below).
|
||||
2. **`actions/checkout@v4`** — pull the repo into the workspace.
|
||||
3. **prepare vault self signed cert** — `echo -n "${{ secrets.HOMELAB_CA_CERT }}" | base64 -d > $VAULT_CACERT`, writing the homelab CA to `homelab.pem` so the runner trusts `https://vault.arcodange.lab`.
|
||||
4. **terraform apply** — `dflook/terraform-apply@v1` with the path above and `auto_approve: true`.
|
||||
|
||||
### Vault secret reads (`*vault_step`)
|
||||
|
||||
The `read vault secret` step uses [`arcodange-org/vault-action`](https://gitea.arcodange.lab/arcodange-org/vault-action), authenticating with `method: jwt`, `path: gitea_jwt`, `role: gitea_cicd`, `url: https://vault.arcodange.lab`, `caCertificate: ${{ secrets.HOMELAB_CA_CERT }}`, and `jwtGiteaOIDC` set to the auth job's output. The secrets it exports into the job env differ per workflow:
|
||||
|
||||
| Workflow | Vault path | Selector | Exported as |
|
||||
| --- | --- | --- | --- |
|
||||
| `IAC` | `kvv1/google/credentials` | `credentials` | `GOOGLE_CREDENTIALS` |
|
||||
| `IAC` | `kvv1/admin/gitea` | `token` | `GITEA_TOKEN` |
|
||||
| `IAC` | `kvv1/admin/cloudflare` | `iam_token` | `CLOUDFLARE_API_TOKEN` |
|
||||
| `IAC` | `kvv1/admin/ovh/app` | `*` (all keys) | `OVH_*` |
|
||||
| `Postgres` | `kvv1/google/credentials` | `credentials` | `GOOGLE_BACKEND_CREDENTIALS` |
|
||||
| `Postgres` | `kvv1/postgres/credentials` | `*` (all keys) | `TF_VAR_postgres_*` |
|
||||
|
||||
`GOOGLE_CREDENTIALS` / `GOOGLE_BACKEND_CREDENTIALS` authenticate the GCS state backend; the `TF_VAR_postgres_*` fan-out feeds the Postgres module's input variables directly. See [Secrets & Vault](../../lab-ecosystem/secrets-and-vault.md) for how the `gitea_cicd` role and KV v1 mounts are provisioned.
|
||||
|
||||
## End-to-end flow
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme': 'base'}}%%
|
||||
flowchart TD
|
||||
push["push / PR / workflow_dispatch<br>on iac/** or postgres/** .tf .tfvars"] --> auth["job: gitea_vault_auth<br>base64 -d | bash -> Gitea OIDC id_token"]
|
||||
auth -->|"gitea_vault_jwt output"| tofu["job: tofu<br>OPENTOFU_VERSION 1.8.2"]
|
||||
tofu --> readvault["read vault secret<br>vault-action jwt role gitea_cicd"]
|
||||
readvault -->|"GOOGLE_CREDENTIALS, TF_VAR_postgres_*, ..."| init["tofu init<br>GCS backend, state prefix"]
|
||||
init --> apply["dflook/terraform-apply@v1<br>auto_approve: true"]
|
||||
apply --> state["state updated in GCS<br>real cloud + homelab mutated"]
|
||||
|
||||
classDef trigger fill:#1f3a5f,stroke:#7fb0ff,color:#eaf2ff;
|
||||
classDef job fill:#3a2f5f,stroke:#b39dff,color:#f3eeff;
|
||||
classDef secret fill:#5f3a2f,stroke:#ffb38a,color:#fff1e8;
|
||||
classDef danger fill:#5f1f2f,stroke:#ff8a9d,color:#ffe8ec;
|
||||
class push trigger;
|
||||
class auth,tofu,init job;
|
||||
class readvault secret;
|
||||
class apply,state danger;
|
||||
```
|
||||
|
||||
1. A **push**, **pull_request**, or **workflow_dispatch** event matching the `iac/**` or `postgres/**` path globs starts the workflow.
|
||||
2. Job **`gitea_vault_auth`** runs `base64 -d | bash` on the `vault_oauth__sh_b64` secret to obtain a Gitea OIDC `id_token`, published as the `gitea_vault_jwt` output.
|
||||
3. Job **`tofu`** (gated by `needs: gitea_vault_auth`) starts on `ubuntu-latest` with `OPENTOFU_VERSION 1.8.2` and `TERRAFORM_VAULT_AUTH_JWT` set to that output.
|
||||
4. The **read vault secret** step exchanges the JWT (role `gitea_cicd`, path `gitea_jwt`) for the workflow's secrets and exports them as env vars (`GOOGLE_CREDENTIALS` / `GOOGLE_BACKEND_CREDENTIALS`, `GITEA_TOKEN`, `CLOUDFLARE_API_TOKEN`, `OVH_*`, or `TF_VAR_postgres_*`).
|
||||
5. **`tofu init`** configures the GCS backend, binding the working dir to its state prefix using the Google credentials just read.
|
||||
6. **`dflook/terraform-apply@v1`** runs against `iac` (or `postgres/iac`) with `auto_approve: true` — no plan-gate.
|
||||
7. The **state** in GCS is updated and the real cloud + homelab resources are mutated to match the committed code.
|
||||
|
||||
## Related pages
|
||||
|
||||
- [factory iac](factory-iac.md) — what the `iac/` stack provisions (the `IAC` workflow's target).
|
||||
- [postgres iac](postgres-iac.md) — the `postgres/iac/` database stack (the `Postgres` workflow's target).
|
||||
- [Secrets & Vault](../../lab-ecosystem/secrets-and-vault.md) — the `gitea_cicd` role, OIDC trust, and KV mounts behind every secret read here.
|
||||
- [ADR-0001 · Safe prod-like environment](../../../ADR/0001-safe-prod-like-environment.md) — the sandbox lane runs the same tofu plan-only against a `sandbox/` state prefix and a throwaway zone.
|
||||
Reference in New Issue
Block a user