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 applyandterraform destroyare 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 ownterraform.tfstatein 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):
- Verify the current state is intact:
terraform validate, and where possibleterraform planwith no unexpected changes. - 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" - Store the backup on encrypted storage; for important infrastructure keep at least one copy off the working machine.
- 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.
-
A credentialed refresh records the output into state. Run
terraform refresh(orapply) inenvironments/devwith a service key that can read the global account. This is the only step that addsdirectory_labelstoterraform 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. -
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, anddata.btp_directory_labels.thisreads backvalues: {}— an empty map.local.directory_labelstherefore 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).