| name | terraform-engineer |
| description | Terraform engineering practices and conventions for loic-roux-404/k3s-paas. Covers mono-repo module structure, local relative sources, tf-root-* composition, standard module files, Contabo/Libvirt/GitHub/Kubernetes provider constraints, inputs via Terragrunt env.hcl, local-only state management, SOPS-age secrets, and local make orchestration.
|
| metadata | {"version":"2.0.0","domain":"infrastructure","triggers":"terraform, terragrunt, tf-modules, tf-root, main.tf, variables.tf, outputs.tf, contabo, dmacvicar/libvirt, github, kubernetes, helm, local state, backend.tf, sops_decrypt_file, env.hcl, root.hcl, make terragrunt\n","role":"infrastructure-engineer","scope":"implementation","output-format":"code"} |
Module Structure (Mono-repo, Domain-prefixed)
All Terraform modules live as sibling directories inside the mono-repo, prefixed with tf-modules-<domain>:
tf-modules-cloud/ # VPS/VM provisioning (Contabo provider)
tf-modules-github/ # GitHub repo/OIDC variable configuration
tf-modules-k8s/ # Kubernetes platform resources
tf-modules-monitoring/ # Monitoring stack configuration
tf-modules-nix/ # Nix-specific provisioning helpers
tf-modules-services/ # Application-level services
Modules are not versioned via Git tags and not consumed via remote source URLs. They are consumed via relative local paths inside terragrunt.hcl terraform.source blocks. There is no module registry in use.
Root Stacks (tf-root-*)
Deployable units are prefixed tf-root-<layer>:
tf-root-apps/ # GitHub repo/OIDC wiring
tf-root-network/ # k3s/RKE2 network, DNS
tf-root-paas/ # Dex, cert-manager, platform services
These are not invoked directly with terraform apply. They are always driven through Terragrunt. Direct terraform invocations are only used for debugging.
Standard Module File Layout
Every tf-modules-* and tf-root-* directory follows:
main.tf # resources
variables.tf # input declarations (validation blocks optional but preferred)
outputs.tf # output values consumed by downstream layers
No providers.tf at module level — providers are configured at the Terragrunt-generated backend.tf layer. No versions.tf at module level — version constraints are absent from most modules.
Provider Usage
The project uses non-HashiCorp providers matching its infrastructure choices:
- Contabo provider for VPS provisioning (
tf-modules-cloud/)
- Kubernetes/Helm providers for cluster resources (
tf-modules-k8s/)
- GitHub provider for repository and OIDC variable management (
tf-modules-github/)
- Libvirt provider for local QEMU/KVM development VMs (
terragrunt/cloud/local/)
Variable & Input Passing Convention
Variables flow exclusively through the Terragrunt inputs block, fed from env.hcl locals:
# env.hcl
locals {
secret_vars = yamldecode(sops_decrypt_file(find_in_parent_folders("secrets/<env>.yaml")))
env = "contabo"
input_vars = {
node_ip = local.secret_vars.node_ip
domain = "example.com"
arch = get_env("ARCH", "x86_64")
}
}
root.hcl propagates inputs = local.env.locals.input_vars to all child modules. No .tfvars files are used. No terraform.tfvars or *.auto.tfvars exist in the codebase.
State Management
State is always local, never remote. The backend is generated by Terragrunt:
remote_state {
backend = "local"
config = {
path = "${get_parent_terragrunt_dir()}/.terragrunt/${local.env.locals.env}/${path_relative_to_include()}/terraform.tfstate"
}
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
}
State files land at .terragrunt/<env>/<layer>/terraform.tfstate. They are gitignored. No state locking, no remote backend, no DynamoDB, no GCS bucket. Back up state manually before destructive operations (cp -r .terragrunt/ .terragrunt.bak/).
Secrets Handling
Secrets are managed with SOPS + Age, decrypted inline at plan/apply time:
secret_vars = yamldecode(sops_decrypt_file(find_in_parent_folders("secrets/<env>.yaml")))
The SOPS_AGE_KEY environment variable must be present. It is only injected inside nix develop (via .envrc + direnv). Never run terragrunt apply outside the nix devShell — SOPS decryption will silently fail or error.
Secrets live in secrets/<env>.yaml (SOPS-encrypted YAML). The Age private key is never committed.
CI/CD Integration
The .github/ directory exists. Terraform/Terragrunt runs are primarily local, driven by make targets. GitHub Actions may exist for linting or docs (MkDocs site), but apply workflows are manual and operator-driven, consistent with a single-operator platform.
Environment Management
Environments map to cloud providers/targets, not lifecycle stages:
terragrunt/cloud/contabo/ # production VPS on Contabo
terragrunt/cloud/local/ # local QEMU/libvirt dev VM
There is no dev/staging/prod branching strategy. The env key in env.hcl is get_env("ENV_NAME", "prod") and names the secrets file (secrets/<env>.yaml, i.e. prod/local); the directory names the target (contabo/local). The state path is scoped by directory.