Skip to content

Terraform Execution

Purpose

The exact commands for every Terraform lifecycle stage, clearly marked by how safe/reversible each one is - and an explicit statement of which ones this documentation portal (and any automated process building it) will never run against shared infrastructure.

Safety classification

Command Touches remote state? Touches real infrastructure? Needs credentials?
terraform fmt -check No No No
terraform validate -backend=false No (skips backend init) No No
terraform init Yes - reads/creates the state file No Yes (state backend credentials)
terraform plan Yes - reads state No (plan-only, makes no changes) Yes
terraform output Yes - reads state No Yes
terraform apply Yes Yes Yes
terraform destroy Yes Yes - destructive Yes

terraform fmt - safe, no credentials

# Linux/Bash, containerized (no local terraform binary required)
docker run --rm -v "$(pwd)/terraform:/tf" -w /tf hashicorp/terraform:1.9 fmt -recursive -check -diff
Exits non-zero if any file isn't canonically formatted; -diff shows exactly what would change. Drop -check to actually reformat in place.

terraform validate - safe, no credentials, no state

docker run --rm -v "$(pwd)/terraform:/tf" -w "/tf/environments/staging" hashicorp/terraform:1.9 init -backend=false -input=false
docker run --rm -v "$(pwd)/terraform:/tf" -w "/tf/environments/staging" hashicorp/terraform:1.9 validate
-backend=false on init is what keeps this safe - it downloads providers/modules for syntax/ type checking only, never contacts the real state backend, never needs real credentials. Mount the whole terraform/ tree (not just one environment directory) so relative module paths like ../../modules/hetzner-network resolve.

# PowerShell equivalent
docker run --rm -v "${PWD}\terraform:/tf" -w "/tf/environments/staging" hashicorp/terraform:1.9 init -backend=false -input=false
docker run --rm -v "${PWD}\terraform:/tf" -w "/tf/environments/staging" hashicorp/terraform:1.9 validate

terraform init (real) / terraform plan / terraform output - need real credentials

These read the real state backend and, for plan, compute what would change - plan itself makes no changes. Requires AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (R2's S3-compatible API keys - see Cloudflare) and the environment's own terraform.tfvars or TF_VAR_* environment variables. Not run as part of building this documentation portal - only by whoever is deliberately planning a real infrastructure change.

terraform apply - destructive potential, real infrastructure change

terraform apply
Provisions/modifies real Hetzner VMs and Cloudflare resources, and costs real money. Never run by this documentation portal, or by any automated process, against platform, staging, or production. A human reviews the plan output first, every time.

terraform destroy - always destructive, marked clearly

terraform destroy
⚠️ Deletes real infrastructure - VMs, DNS records, R2 buckets (and, for a data VM, the MySQL/ Redis data on it, unless backed up first - see Operations → Backup). Never run against any shared environment casually; never run by this documentation portal under any circumstance.

State handling

Backend: Cloudflare R2, via Terraform's S3-compatible backend "s3" support - one state key per environment root inside one bucket, created once by terraform/bootstrap/ (see Environments).

Never commit:

*.tfstate
*.tfstate.*
.terraform/
*.tfplan
Already enforced by terraform/.gitignore. State contains resource attributes derived from sensitive variables (MySQL/Redis passwords end up in a data VM's rendered cloud-init user_data, which state records) - the R2 bucket holding it must be restricted to the same people/CI who'd otherwise have those secrets directly. No state locking is configured (R2 has no DynamoDB-style locking equivalent as of this writing) - avoid two people/pipelines running apply against the same root concurrently.

Secret handling

No credential is ever a literal in any .tf/.tfvars file committed to this repository - every sensitive variable (hcloud_token, cloudflare_api_token, mysql_root_password, mysql_app_password, redis_password) is sensitive = true with no default, supplied via a gitignored terraform.tfvars (copy from the committed terraform.tfvars.example, which holds only <CHANGE_ME>-style placeholders) or TF_VAR_<name> environment variables (preferred for CI/Jenkins).

cp terraform/environments/staging/terraform.tfvars.example terraform/environments/staging/terraform.tfvars
# edit terraform.tfvars with real values - it is gitignored, never committed

What this portal actually runs

Only terraform fmt -check and terraform validate -backend=false - both credential-free and state-free - are run as part of building/validating this documentation. Anything beyond that is explicitly out of scope here; see Terraform overview "Safe commands only".