Files
OpenTofuPlayground/CLAUDE.md
Lutz Finsterle 3bf5960302
Some checks failed
Deploy / Update K8s Apps / Detect changed K8s tfvars (push) Failing after 13s
Deploy / Update Apps / Detect changed tfvars files (push) Failing after 13s
Test / Static Analysis (push) Failing after 11s
Test / Unit Tests — Docker Stack (push) Has been skipped
Test / Unit Tests — K8s Stack (push) Has been skipped
Deploy / Update K8s Apps / Deploy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update K8s Apps / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update Apps / Deploy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update Apps / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Test / Integration Test — K8s (k3d) (push) Has been skipped
Initial Commit
2026-03-06 19:17:15 +01:00

6.1 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Common Commands

All commands run from the repo root unless noted.

Validate & format

make validate          # tofu validate on both stacks (Docker root + k8s/)
make fmt-check         # tofu fmt -check -recursive
make fmt-fix           # auto-format all .tf files
make lint              # tflint on both stacks (requires tflint in PATH)

Unit tests (no infrastructure required)

make test-unit                        # both stacks
make test-unit-docker                 # Docker stack only  (runs from repo root)
make test-unit-k8s                    # K8s stack only     (runs from k8s/)
tofu test -filter=<run_block_name>    # single test by name
tofu test -verbose                    # verbose output

Integration tests (real infrastructure)

make test-integration-k8s     # spins up local k3d cluster, applies, verifies, destroys
make test-integration-docker  # requires Docker + SSH to localhost

Full fast suite (no infra)

make test-all    # validate + fmt-check + unit tests
make clean       # remove .terraform dirs and local state files

Manual tofu workflow (local deploy, bypassing CI)

# Docker stack
tofu init -backend=false    # or with -backend-config flags for SeaweedFS
tofu workspace select myapp || tofu workspace new myapp
tofu apply -var-file="apps/myapp.tfvars" -var="ssh_key_path=/tmp/key" -var="db_password=..."

# K8s stack (always run from k8s/)
cd k8s
tofu init -backend=false
tofu workspace select myapp || tofu workspace new myapp
tofu apply -var-file="apps/myapp.tfvars" -var="kubeconfig_path=/tmp/kubeconfig" ...

Architecture

This is a GitOps multi-stack infrastructure blueprint. There is no application code — only OpenTofu (Terraform-compatible) HCL and CI/CD pipelines.

Two independent stacks

Stack Root dir Module What it deploys
Docker / (repo root) modules/app-openresty-pg-redis OpenResty + PostgreSQL + Redis on a remote Docker host via SSH
Kubernetes k8s/ modules/app-k8s-nodered-rabbitmq Node-RED + optional RabbitMQ on a k3s cluster via kubeconfig

Each stack is a fully independent OpenTofu root. They share no state, no providers, and no modules.

One app = one .tfvars file = one workspace

The CI/CD pipelines detect which apps/*.tfvars (or k8s/apps/*.tfvars) files were added, changed, or deleted, then run tofu apply or tofu destroy per file in its own Tofu workspace. App name is derived from the filename (apps/myapp.tfvars → workspace myapp).

State backend

Both backend.tf files default to local state (no backend block active). To use SeaweedFS S3, uncomment the terraform { backend "s3" {} } block and run scripts/setup-backend.sh, or pass -backend-config flags manually. The backend key pattern is apps/<appname>.tfstate (Docker) and apps-k8s/<appname>.tfstate (K8s).

Secrets pattern

No secrets in the repository. Three values are always injected at CI runtime from gopass:

  • Docker stack: ssh_key_path, db_password, optionally openresty_git_token
  • K8s stack: kubeconfig_path, rabbitmq_password, optionally loki_auth_token

These are passed as -var flags and never appear in .tfvars files.

Module: modules/app-openresty-pg-redis

Three OpenResty source modes controlled by openresty_source_type:

  • bind_mount — mounts an existing path from the remote host
  • local_build — builds a Docker image from a local Dockerfile and sends it over SSH
  • git_clone — clones a repo at container startup; openresty_git_ref must be a pinned tag or SHA (branch names are rejected by variable validation)

environment = "prod" creates named Docker volumes; "dev" uses ephemeral containers.

Module: modules/app-k8s-nodered-rabbitmq

Key design decisions:

  • kubectl_manifest (gavinbunney/kubectl provider) is used for all Traefik CRDs (IngressRoute, Middleware) instead of kubernetes_manifest. This is intentional — kubectl_manifest does not validate CRD schemas at plan time, avoiding failures when the CRDs are not yet installed.
  • traefik_api_group variable (default traefik.io/v1alpha1) controls which Traefik API version is used. Set to traefik.containo.us/v1alpha1 for older k3s versions.
  • Init container copies files from a custom image to the app PVC at startup, but skips flows.json and flows_cred.json if they already exist (preserves user-modified Node-RED flows across redeployments).
  • Grafana Alloy sidecar tails /data/logs/*.log and ships to Loki. It has its own 100Mi PVC (<appname>-alloy-wal) mounted at /var/lib/alloy/data to persist WAL across pod restarts.
  • RabbitMQ is a StatefulSet (single replica) deployed only when enable_rabbitmq = true.

Test files

  • tests/docker_validation.tftest.hcl — Docker stack unit tests using mock_provider "docker"
  • k8s/tests/k8s_validation.tftest.hcl — K8s stack unit tests using mock_provider "kubernetes" and mock_provider "kubectl"

CI/CD

Two equivalent implementations — use whichever matches your git host:

  • Gitea Actions: .gitea/workflows/{test,deploy,deploy-k8s}.yml
  • GitLab CI: .gitlab-ci.yml (root entry point) + .gitlab/workflows/{test,deploy,deploy-k8s}.gitlab-ci.yml

Key difference: Gitea uses dynamic matrix jobs (one job per changed tfvars). GitLab CI uses a for loop in a single job (CI_COMMIT_BEFORE_SHA + git show to recover deleted tfvars without checking out the previous commit).

Important constraints

  • openresty_git_ref has a built-in validation that rejects: main, master, develop, dev, staging, HEAD, latest, trunk. Always use a version tag or commit SHA.
  • environment only accepts "prod" or "dev".
  • openresty_source_type only accepts "bind_mount", "local_build", or "git_clone".
  • The K8s stack must be run from the k8s/ directory (it references ../modules/).
  • tofu init -backend=false is required for unit tests and validate — both stacks' backend.tf files have the backend block commented out by default.