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, optionallyopenresty_git_token - K8s stack:
kubeconfig_path,rabbitmq_password, optionallyloki_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 hostlocal_build— builds a Docker image from a local Dockerfile and sends it over SSHgit_clone— clones a repo at container startup;openresty_git_refmust 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 ofkubernetes_manifest. This is intentional —kubectl_manifestdoes not validate CRD schemas at plan time, avoiding failures when the CRDs are not yet installed.traefik_api_groupvariable (defaulttraefik.io/v1alpha1) controls which Traefik API version is used. Set totraefik.containo.us/v1alpha1for older k3s versions.- Init container copies files from a custom image to the app PVC at startup, but skips
flows.jsonandflows_cred.jsonif they already exist (preserves user-modified Node-RED flows across redeployments). - Grafana Alloy sidecar tails
/data/logs/*.logand ships to Loki. It has its own 100Mi PVC (<appname>-alloy-wal) mounted at/var/lib/alloy/datato 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 usingmock_provider "docker"k8s/tests/k8s_validation.tftest.hcl— K8s stack unit tests usingmock_provider "kubernetes"andmock_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_refhas a built-in validation that rejects:main,master,develop,dev,staging,HEAD,latest,trunk. Always use a version tag or commit SHA.environmentonly accepts"prod"or"dev".openresty_source_typeonly accepts"bind_mount","local_build", or"git_clone".- The K8s stack must be run from the
k8s/directory (it references../modules/). tofu init -backend=falseis required for unit tests and validate — both stacks'backend.tffiles have the backend block commented out by default.