Phase: CI Pipeline & Team Hardening

phase-pal-e-platform-ci-hardening Phase

phase

Plan: Platform CI Pipeline & Team Hardening

Vision

The internal developer platform for the pal-e AI agency. A developer adds one entry to var.services, pushes code to Forgejo, and gets: a namespace, CI pipeline, container registry project, GitOps deployment, TLS ingress, monitoring, log aggregation, and alerting. The Terraform is the control plane. The platform is the product.
This plan hardens the Terraform operations layer so that the platform is no longer a one-laptop operation. Infrastructure changes go through CI review, state is backed up, secrets are managed, and a second developer can safely contribute.

Projects & Repos Touched

Project/Repo Platform Role in this plan
pal-e-platform GitHub to Forgejo Mirror to Forgejo, add Woodpecker pipeline, state backup CronJob
pal-e-services GitHub to Forgejo Mirror to Forgejo, add Woodpecker pipeline, state backup CronJob
pal-e-docs (knowledge) Forgejo SOPs for team onboarding, updated deployment docs, DR runbook

Context

The tf-architecture-assessment-2026-02-26 identified critical operational gaps across both Terraform repos. The platform works — 25 resources in pal-e-platform, 36 in pal-e-services, 4 services onboarded — but it's a solo-developer setup. All tofu apply runs happen from one laptop. Two incidents in 48 hours (Grafana CrashLoopBackOff, pal-e-docs Alembic crash) had no automated recovery path. MTTR depended on Lucas being at his machine.
The modularization and Postgres work is covered by plan-2026-02-26-tf-modularize-postgres. This plan covers everything else: the CI pipeline, state protection, secrets management, and team readiness.
Off-host backup gap (identified 2026-02-28): The Salt plan Phase 4 gap analysis revealed that if the NVMe dies, the following are lost: MinIO data (Litestream backups for pal-e-docs), Terraform state (k3s etcd), Forgejo repos (GitHub mirrors exist for some), Harbor container images. Phase 1 of this plan (state backups to MinIO) is a first step but only protects TF state — and MinIO is on the same host. A DR runbook (sop-disaster-recovery) should be written after Phase 1 is complete, documenting what's recoverable and what's not. Off-host replication of MinIO is a future concern.
What's already done:
  • [x] Both repos use kubernetes state backend with locking (tofu-state namespace)
  • [x] Woodpecker CI deployed and proven (builds app container images for 4 services)
  • [x] MinIO deployed (backup target for state)
  • [x] TF assessment complete — 8 deep-dive notes documenting gaps and recommendations
  • [x] Both project pages updated with architecture, decisions, and reference notes
  • [x] Salt plan complete — host is managed, secrets encrypted, firewall active
  • [ ] No CI pipeline for Terraform itself
  • [ ] No automated state backup
  • [ ] No secrets management beyond plaintext tfvars
  • [ ] No developer onboarding docs for TF work
  • [ ] No disaster recovery runbook

Previous Plan

plan-2026-02-24-minio-object-storage — established MinIO as shared object storage. State backups in this plan target MinIO.

Depends On

None — this plan is independently executable. It complements plan-2026-02-26-tf-modularize-postgres but neither blocks the other.

Decisions Made

Decision Rationale
Mirror both repos to Forgejo (not GitHub webhook to Woodpecker) All CI runs on Woodpecker. Consistent with every other repo. GitHub is disaster recovery only. Simpler networking — no cross-network webhook routing.
Woodpecker secrets + <code>TF_VAR_*</code> env vars for CI (not Vault, not SOPS) Lightest path. Woodpecker already manages secrets for app pipelines. <code>TF_VAR_*</code> is native Terraform. Vault is enterprise overhead we don't need. SOPS requires key distribution. Revisit at 5+ developers.
CI runs apply, developers don't (once pipeline is live) Single serialized apply path eliminates state races. Developers run <code>tofu plan</code> locally for feedback. Merge to main triggers apply. No exceptions.
Both repos get pipelines (not just one) Same gaps, same fix. Platform and services share the laptop SPOF. Both need CI.
State backup before pipeline (Phase 1 before Phase 2) Backup is cheap insurance and doesn't depend on Forgejo mirrors. Get it running immediately.
DR runbook after state backups (not before) Salt plan Phase 4 gap analysis (2026-02-28) showed a DR runbook without off-host backups would document an incomplete recovery path. Write the runbook after Phase 1 gives us state backups, so it documents a real (if partial) recovery path. Redistributed from <code>plan-2026-02-26-salt-host-management</code>.

Phases

Phase 1: State Backup CronJob

Slug: phase-2026-02-26-1-state-backup
Goal: Both Terraform state secrets are backed up daily to MinIO. Documented restore procedure.
Owner: Agent (worktree, pal-e-platform repo)
  • Create a CronJob in tofu-state namespace that:
  • Deploy CronJob via Terraform in pal-e-platform (new resource in main.tf or future module)
  • Create MinIO IAM user + policy scoped to litestream-backups/tf-state/ prefix
  • Write restore SOP in pal-e-docs
  • Test: manually trigger CronJob, verify backup exists in MinIO, test restore to a temp secret
Follow-up after Phase 1: Write sop-disaster-recovery in pal-e-docs. This was redistributed from the Salt plan's deferred Phase 4. The runbook should document:
  • Full rebuild sequence: Arch install → clone repo → recover GPG key → Salt bootstrap → Salt apply → tofu apply → verify
  • What's recoverable: TF state (from MinIO backup), pal-e-docs data (Litestream/MinIO — same host caveat), Forgejo repos (GitHub mirrors)
  • What's NOT recoverable without off-host backups: Harbor images, MinIO data if NVMe fails, any non-mirrored Forgejo repos
  • GPG key recovery from physical backup (see todo-gpg-physical-backup)
Issue: create when phase becomes active

Phase 2: Mirror Repos to Forgejo + Validation Pipeline

Slug: phase-2026-02-26-2-forgejo-mirror-validation
Goal: Both TF repos mirrored to Forgejo. PRs run tofu fmt -check and tofu validate. No state access needed. Branch protection enforces PR + CI pass before merge.
Owner: Main session (Forgejo admin tasks) + Agent (pipeline YAML)
  • Create mirror repos on Forgejo:
  • Configure Forgejo mirror sync (Settings, Mirror, pull from GitHub on schedule)
  • Activate both repos in Woodpecker UI
  • Add .woodpecker.yaml to each repo:
    steps:
      validate:
        image: ghcr.io/opentofu/opentofu:1.9
        commands:
          - cd terraform
          - tofu init -backend=false
          - tofu fmt -check -recursive
          - tofu validate
    
  • Enable Forgejo branch protection on main for both repos:
  • Verify: push a branch, PR shows green/red from Woodpecker. Direct push to main is rejected.
Issue: create when phase becomes active

Phase 3: Plan-on-PR Pipeline

Slug: phase-2026-02-26-3-plan-on-pr
Goal: PRs to either repo show tofu plan output as a Forgejo comment. Requires secrets in Woodpecker.
Owner: Agent (worktree, both repos)
  • Add Woodpecker secrets for each repo:
  • Update .woodpecker.yaml to add plan step:
    steps:
      plan:
        image: ghcr.io/opentofu/opentofu:1.9
        commands:
          - mkdir -p ~/.kube
          - echo "$KUBECONFIG_CONTENT" > ~/.kube/config
          - cd terraform
          - tofu init
          - tofu plan -no-color 2>&1 | tee plan.txt
          - # Post plan.txt as PR comment via Forgejo API
        secrets: [kubeconfig_content, tf_var_...]
        when:
          event: pull_request
    
  • Write a small script or use Woodpecker plugin to post plan output as PR comment
  • Verify: open PR with a TF change, plan appears as comment, reviewer can assess impact
Note: This phase solves secrets management for CI. Local dev still uses k3s.tfvars. The two paths coexist — Woodpecker uses TF_VAR_* env vars, developers use -var-file=k3s.tfvars for local plan.
Issue: create when phase becomes active

Phase 4: Apply-on-Merge Pipeline

Slug: phase-2026-02-26-4-apply-on-merge
Goal: Merging to main triggers tofu apply. The laptop SPOF is eliminated.
Owner: Agent (worktree, both repos)
  • Add apply step to .woodpecker.yaml:
    steps:
      apply:
        image: ghcr.io/opentofu/opentofu:1.9
        commands:
          - mkdir -p ~/.kube
          - echo "$KUBECONFIG_CONTENT" > ~/.kube/config
          - cd terraform
          - tofu init
          - tofu apply -auto-approve
        secrets: [kubeconfig_content, tf_var_...]
        when:
          event: push
          branch: main
    
  • Add pipeline ordering: pal-e-platform applies before pal-e-services (manual gate or cross-repo trigger)
  • Add failure notification: Woodpecker webhook or Forgejo notification on apply failure
  • Document the new workflow SOP: "How to make infrastructure changes"
  • Agreement: nobody runs tofu apply from their laptop once this is live (break-glass exception with documented procedure)
  • Verify: merge a trivial change (e.g., add a comment), confirm apply runs and succeeds
Issue: create when phase becomes active

Phase 5: Developer Onboarding & DORA Baseline

Slug: phase-2026-02-26-5-onboarding-dora
Goal: A new developer can set up their environment and contribute TF changes safely. DORA metrics baseline established.
Owner: Main session (docs in pal-e-docs)
  • Write developer onboarding SOP in pal-e-docs:
  • Create RBAC: per-developer kubeconfig with read-only cluster access (sufficient for tofu plan)
  • Establish DORA baseline from pipeline data:
  • Optional: scheduled tofu plan (drift detection) that alerts if plan shows unexpected changes
  • Update both project pages with pipeline status and onboarding link
Issue: create when phase becomes active

Key Files

Phase File Repo Change
1 <code>terraform/main.tf</code> pal-e-platform Add CronJob + MinIO IAM for state backup
2 <code>.woodpecker.yaml</code> pal-e-platform Create — fmt + validate pipeline
2 <code>.woodpecker.yaml</code> pal-e-services Create — fmt + validate pipeline
3 <code>.woodpecker.yaml</code> both repos Add plan step with secrets
4 <code>.woodpecker.yaml</code> both repos Add apply step on main push
5 pal-e-docs notes pal-e-docs Developer onboarding SOP, DORA baseline

Verification

  • [ ] Phase 1: State backup exists in MinIO for both repos. Restore SOP tested. DR runbook written.
  • [ ] Phase 2: Both repos mirrored on Forgejo. PR triggers fmt+validate in Woodpecker. Branch protection prevents direct push to main.
  • [ ] Phase 3: PR to either repo gets a tofu plan output posted as comment.
  • [ ] Phase 4: Merge to main triggers tofu apply. Trivial change applies successfully.
  • [ ] Phase 5: Onboarding SOP written. DORA baseline captured. A second developer could follow the doc and submit a TF change.

Next Plan Seeds

  • Drift detection — scheduled tofu plan that alerts on unexpected changes (manual kubectl edits, Helm drift)
  • DORA metrics Grafana dashboard — Woodpecker build metadata to Prometheus to Grafana visualization
  • SOPS or Vault for secrets — if team grows beyond 2-3 developers, Woodpecker secrets become unwieldy. SOPS + age encrypts tfvars in git.
  • State splitting — for larger teams, split pal-e-platform state per module (monitoring, forgejo, harbor) to reduce blast radius and enable parallel applies
  • Platform dev cluster — second k3s for testing Helm upgrades and TF changes before prod. Requires modularization (covered by other plan).
  • GitHub to Forgejo full migration — move pal-e-platform and pal-e-services source of truth from GitHub to Forgejo. GitHub becomes read-only mirror.
  • Off-host MinIO replication — replicate MinIO bucket to external S3 (Backblaze B2, Hetzner Object Storage) so NVMe failure doesn't lose all backups. Critical for a complete DR story.
  • plan-2026-02-26-tf-modularize-postgres — companion plan covering modularization and Postgres. Independent but complementary.
  • plan-2026-02-25-platform-observability — alerting from Phase 3 of that plan feeds into MTTR measurement in Phase 5 of this plan.
  • plan-2026-02-26-salt-host-management (COMPLETE) — DR runbook redistributed from that plan's deferred Phase 4. Salt CI is a future extension of TF CI.
  • tf-architecture-assessment-2026-02-26 — the assessment that generated this plan (hub note linking 8 deep-dives).
  • tf-pipeline-design — detailed pipeline design note with Woodpecker YAML examples and challenge table.
  • tf-rollback-strategy — rollback mechanisms. Phase 4 enables git-revert-based rollback.
  • tf-team-readiness — the 7 blockers this plan addresses.
  • tf-best-practices-comparison — priority order that informed phase sequencing.
  • service-onboarding-sop — the service-level SOP. This plan creates the infrastructure-level equivalent.
  • todo-deployment-safety — the incident that motivated this work.
  • todo-gpg-physical-backup — GPG backup prerequisite for DR runbook.
  • platform-maturity-matrix — maps this plan's phases to enterprise capability targets.