Skip to content

Operations

Placeholder document created by the SPEC-TF-001 scaffold. Operational runbooks will be added as environments and modules are implemented.

Non-destructive checks

Run from the repository root:

make check            # fmt check + init (no backend) + validate for all root modules
./scripts/validate.sh # equivalent standalone script

Recommended pull-request sequence once linting and testing are adopted (Terraform Testing skill note):

1. terraform fmt -check -recursive
2. terraform init -backend=false
3. terraform validate
4. tflint --recursive
5. security and policy scan
6. terraform test (plan-based or mocked)
7. integration tests where required
8. environment plan + human approval
9. controlled apply

State safety

  • Never commit state, plan files, or crash logs (see .gitignore).
  • Do not modify state manually unless following an approved recovery procedure.
  • terraform apply and terraform destroy are intentionally absent from all automation in this scaffold; no Makefile target or script runs them.

Local backend and state backup (ADR-TF-003)

The project uses Terraform's local backend during the initial development phase (ADR-TF-003 "Local Terraform Backend Strategy"). Consequences:

  • Each executable root module (repository root, environments/dev) keeps its own terraform.tfstate in its own directory. State is never shared between environments and is never committed to Git.
  • Concurrent operators are prohibited: only one person may run state-modifying commands against a given root module at a time.

Backup procedure for important infrastructure

Before any operation that could damage or lose state (provider upgrades, refactors, terraform state surgery, machine maintenance):

  1. Verify the current state is intact: terraform validate, and where possible terraform plan with no unexpected changes.
  2. Copy the state file to a protected location outside the repository: bash BACKUP_DIR="${HOME}/btp-terraform-backups" mkdir -p "${BACKUP_DIR}" STAMP="$(date +%Y%m%d-%H%M%S)" cp terraform.tfstate "${BACKUP_DIR}/$(basename "$(pwd)")-${STAMP}.tfstate" shasum -a 256 "terraform.tfstate" > "${BACKUP_DIR}/$(basename "$(pwd)")-${STAMP}.tfstate.sha256"
  3. Store the backup on encrypted storage; for important infrastructure keep at least one copy off the working machine.
  4. Record the backup location, timestamp, and checksum in the corresponding implementation record or operations log.

Restore by copying the backed-up file back to terraform.tfstate in the root module directory, then running terraform plan to verify consistency. Do not edit state files manually outside an approved recovery procedure.

Migration triggers (see Risks.md R004)

The local backend must be reviewed and migrated before: multiple operators, CI/CD apply operations, shared environments, or production infrastructure.

Why a newly declared output is missing from terraform output -json

Terraform does not write root module outputs into state when the output is declared in configuration. Outputs are recorded during terraform apply and terraform refresh, and nothing else. Declaring output "directory_labels" in environments/dev/outputs.tf therefore changes nothing visible to terraform output -json until one of those two commands runs.

cd environments/dev
terraform output -json | jq 'has("directory_labels")'   # false after adding the block

This is expected behaviour, not a configuration defect. terraform validate passing says nothing about state contents: validation reads configuration only.

The documentation pipeline inherits the same constraint. scripts/extract_outputs.sh runs terraform output -json against local state and needs no credentials, so it cannot create the missing key either — it faithfully reports whatever the last credentialed run left behind. Without a refresh the absent key stays absent indefinitely.

Do not seed, hand-edit, or otherwise fabricate terraform.tfstate to make an output appear. State describes a real global account; writing to it corrupts the record that every later plan is diffed against.

Directory Labels page: the two independent preconditions

The Directory Labels page stays empty until both of the following hold. They are unrelated, and satisfying one does nothing for the other.

  1. A credentialed refresh records the output into state. Run terraform refresh (or apply) in environments/dev with a service key that can read the global account. This is the only step that adds directory_labels to terraform output -json. These commands are never run by any Makefile target or script in this repository, and they change state — back it up first per the procedure above.

  2. Labels exist in the BTP account. A human must assign labels to a directory in the BTP console or via the API. As recorded on 2026-10-02, the global account has exactly one directory, 99eb5432-1be0-4df1-9b94-7186e327ee86, and data.btp_directory_labels.this reads back values: {} — an empty map. local.directory_labels therefore evaluates to [], and the generated page renders:

``text !!! info "No data available" Thedirectory_labels` section is present in this capture but holds no rows: no labels were reported for any directory in this global account.

   This page does not claim why. The capture cannot distinguish a data

```

That message is correct behaviour for absent data, and it deliberately asserts no cause: the capture cannot tell a data source that returned nothing from a directory walk that dropped rows. A refresh alone will make the output key appear carrying an empty list and the page will still show "No data available" — with the present-but-empty wording rather than the absent-section wording.

Unverified row-shape contract

local.directory_labels in environments/dev/locals.tf produces one row per (directory, label key) with values normalised via sort(tolist(...)). That shape was inferred from the provider schema, not observed against real provider output. The renderer fixture used during development proved render_directory_labels() in scripts/generate_docs.py handles the shape; it did not prove the provider emits it.

The first credentialed refresh is therefore the point at which the assumption is actually tested. If the page renders but shows no rows (or throws), suspect the shape in environments/dev/locals.tf (local.directory_labels) and the consumer in scripts/generate_docs.py (render_directory_labels()), and compare against the raw btp_directory_labels attributes in state. Inspecting state read-only is safe and needs no credentials:

terraform state show 'data.btp_directory_labels.this["99eb5432-1be0-4df1-9b94-7186e327ee86"]'

Repository guards

Run from the repository root (both are non-destructive):

./scripts/check-tracked-state.sh         # fails if git tracks state/plan artifacts
./scripts/check-provider-constraint.sh   # enforces the ADR-TF-002 provider constraint

Wire both into CI/CD when a pipeline is introduced; CI must run terraform init without -upgrade during normal execution (ADR-TF-002).