RedlineKnowledge base

Fridai Terraform and OpenTofu standard

  • Status: Fridai-wide engineering standard
  • Effective: 2026-08-03
  • Owner: Fridai Engineering
  • Review cadence: annually and when Terraform, OpenTofu, GitLab, a provider, or the state-recovery model materially changes
  • Nature: mandatory engineering standard, not corporate policy

Purpose and scope

This standard governs Terraform-compatible infrastructure code across Fridai, including Terraform and OpenTofu roots, modules, state, imports, CI/CD, provider authentication, drift detection, emergency changes, and retirement.

Provider-specific rules may add stricter requirements but cannot weaken this standard. The word Terraform below includes OpenTofu-compatible workflows unless a tool-specific distinction is stated.

Core principles

  1. One resource has one declared owner and one writer.
  2. State boundaries follow ownership, privileges, lifecycle, and blast radius.
  3. A plan is evidence, not authorization to apply.
  4. Existing production resources are discovered and imported before they are changed.
  5. Reproducibility, recovery, and review take precedence over terseness.
  6. Infrastructure metadata follows the Fridai infrastructure resource tagging standard.

Repository and state boundaries

  • Use one repository for a coherent governance boundary, but use multiple independent root configurations and state files when ownership, credentials, lifecycle, or blast radius differs.
  • Do not use Terraform CLI workspaces as the primary environment, tenant, zone, account, or service isolation mechanism.
  • A root MUST have one accountable owner, one remote state name, one credential boundary, and one documented purpose.
  • Account-wide, zone-wide, shared platform, and service-specific resources SHOULD use separate roots and state files.
  • An application repository may own application deployment while an organization-infrastructure repository owns shared network, identity, DNS, or policy resources. The same object MUST NOT be declared in both.
  • Terraform and provider deployment tools such as Wrangler MUST NOT manage the same resource.
  • State names are stable lowercase kebab-case identifiers. Renaming a directory must not silently create a new state.

Each root MUST document:

  • purpose and scope;
  • accountable owner;
  • state name and backend;
  • provider account, zone, project, or equivalent boundary;
  • managed and explicitly excluded resource classes;
  • required credentials and minimum permissions;
  • dependencies and outputs consumed elsewhere;
  • import and validation commands;
  • recovery, rollback, and emergency-change procedures.

Repository layout

The normal layout is:

stacks/
  <scope>/
    <name>/
      <environment>/
        backend.tf
        versions.tf
        providers.tf
        variables.tf
        locals.tf
        main.tf
        outputs.tf
        imports.tf
        ownership.yaml

Files MAY be split by domain when that makes a root easier to review. Filenames use lowercase underscores. Resource and data-source labels use descriptive nouns in lowercase underscores; do not repeat the provider or resource type in the label.

Generated files MUST be clearly marked and reproducible. Generated HCL is reviewed and validated like authored HCL.

Version and dependency management

  • Pin Terraform or OpenTofu to a tested minor line using the repository’s standard version file and CI toolchain image.
  • Every root MUST declare required_version and explicit provider source and version constraints.
  • Production providers MUST be pinned to a tested minor series. Unbounded or floating provider versions are prohibited.
  • Commit .terraform.lock.hcl for every root and every supported platform lock entry.
  • Provider upgrades use a dedicated merge request with release-note review, refreshed locks, validation, and plans for every affected state.
  • Modules are pinned to immutable versions or commit digests. Branch references and floating registry ranges are prohibited in production.
  • Start with explicit roots. Extract a module only after at least two stable, genuinely identical patterns exist and the module has a clear owner and compatibility contract.

HCL style and interfaces

  • terraform fmt -check -recursive and terraform validate MUST pass.
  • TFLint or an approved equivalent SHOULD run with provider-specific rules.
  • Variables MUST have a type and description. Sensitive variables MUST declare sensitive = true; secrets MUST NOT have defaults.
  • Outputs MUST have a description. Sensitive outputs MUST declare sensitive = true and be created only when a documented consumer requires them.
  • Use variable validation and resource preconditions for safety invariants that can be expressed locally.
  • Prefer explicit values and readable locals over dense expressions or excessive indirection.
  • Do not place environment-specific production values in reusable modules.
  • Blanket ignore_changes is prohibited. Every ignored attribute requires a documented external owner and review date.
  • prevent_destroy is a useful final guard, not a substitute for state, permission, and review controls. Its use and recovery implication MUST be documented.
  • Provisioners, local-exec, and remote-exec are prohibited unless an approved, time-limited exception shows that no first-class provider, API pipeline, or image-build mechanism can perform the operation.

Secrets and provider authentication

  • Use short-lived or narrowly scoped credentials where the platform supports them. Prefer workload identity and account-owned service tokens over personal credentials.
  • Use separate read-only discovery credentials and write credentials.
  • Split write credentials further when account, zone, product, or service boundaries permit it.
  • Credentials are injected through protected CI/CD variables or an approved secret manager. They are never embedded in HCL, backend configuration, variable files, plans, state backups, scripts, container images, or examples.
  • CI logs MUST not print credential-bearing environment variables, request headers, backend URLs with embedded credentials, or raw sensitive API responses.
  • A local operator uses an approved credential helper or ephemeral environment variables and clears them after the session.

State and locking

  • Production state MUST use a remote backend with locking, access control, encryption, version history, and auditability.
  • Fridai GitLab-hosted repositories SHOULD use GitLab-managed Terraform/OpenTofu HTTP state with one state per root.
  • Backend declarations contain no credentials. GitLab CI uses job credentials; authorized local recovery uses a time-limited token supplied through backend environment variables.
  • Local state, .terraform/, saved plans, crash logs, and credential-bearing variable files MUST be ignored and MUST NOT be committed.
  • State and plans are sensitive even when all declared variables are marked sensitive. Access is limited to the smallest operational group.
  • State MUST have an encrypted off-host backup and a tested restore procedure. Back up state before every import batch, state move, provider migration, or other state surgery.
  • Loss or outage of the GitLab state service must not alter live infrastructure, but it prevents reliable plans and applies. Do not bypass the backend during an outage.
  • State commands that remove, move, or replace bindings require a reviewed runbook, a fresh backup, exact resource addresses, and recorded evidence.

Discovery, adoption, and imports

  • Discovery is read-only and paginated. Raw responses containing secrets, identities, or sensitive policy content are not committed.
  • Every discovered object is classified before import as import-now, import-later, project-owned, observe-only, retiring, or unknown-owner.
  • Discovery does not imply Terraform ownership.
  • Import small, dependency-aware batches. Use import blocks when supported so the intent is reviewable and repeatable.
  • Generated configuration is a starting point, not authoritative HCL. Verify it against the pinned provider schema and the live API.
  • The first refreshed plan after import MUST be a no-op or contain only understood, documented normalization. Replacement and deletion are prohibited until configuration completeness is proven.
  • Use moved blocks for reviewed address refactors. Do not perform blind state manipulation to silence a plan.
  • Resources with unknown ownership remain observe-only. Retiring resources are not mutated merely to make Terraform or tag inventory look complete.

Plan, apply, and verification

  • Merge-request pipelines run formatting, linting, validation, policy checks, secret scanning, and speculative plans for affected roots.
  • Plans MUST use the same pinned toolchain, lock file, variables, and credentials model as apply.
  • A reviewed plan expires when source, variables, provider locks, live state, or relevant external dependencies change.
  • The protected default-branch pipeline MUST create a fresh plan. Production apply uses that exact saved plan in the same pipeline.
  • Applies are blocking manual jobs. There is no automatic production apply and no generic or automatic destroy job.
  • Operations are serialized per state. Concurrent plans may be informational, but applies and state-changing imports MUST hold the state-specific lock and CI resource group.
  • After apply, CI refreshes state, verifies critical provider behavior through a read-only API check, records deployment evidence, and reports unexplained drift.
  • Rollback normally means a new reviewed configuration change. Reverting source does not guarantee provider-side rollback and MUST be planned before apply.

GitLab CE 18.11 control model

Fridai’s self-managed GitLab instance currently runs GitLab CE 18.11.2. The following design uses only features documented for the Free tier.

Required Free-tier controls

  • Keep the infrastructure project private.
  • Protect the default branch: Maintainers may merge; direct pushes and force pushes are disabled.
  • Enable Pipelines must succeed.
  • Use merge-request pipelines and workflow:rules to avoid duplicate pipelines.
  • Generate Terraform plan reports for the merge-request widget. Plan artifacts use artifacts:access: maintainer and the shortest practical expiry, normally 24 hours.
  • Use parent-child pipelines to fan out changed roots. The parent trigger uses strategy: mirror. Plan and apply for a root remain in the same child pipeline; do not depend on paid cross-pipeline artifact fetching.
  • Use resource_group with one stable value per state to serialize mutations.
  • Use a blocking manual production apply with when: manual, allow_failure: false, and manual_confirmation. Because it runs only in a protected-branch pipeline, only a user allowed to merge that protected branch may run it.
  • Store credentials as protected, masked, and hidden CI/CD variables. Masking is log protection, not access control.
  • Use typed pipeline spec:inputs for controlled operator inputs and minimize or disable ad-hoc pipeline variables.
  • Use scheduled pipelines for read-only drift and inventory checks.
  • Use GitLab environments and deployment records for audit visibility, without treating them as an authorization boundary.
  • Use self-managed runners with dedicated infrastructure tags. Privileged runners require a documented exception.
  • Publish the pinned IaC toolchain image to the project Container Registry and refer to it by immutable digest. Protect stable container tags.
  • Use local CI includes initially. Reusable components MAY be published to the same self-managed GitLab instance after the pattern stabilizes.
  • Track adoption and exceptions with Free-tier issues, scoped labels, milestones, and a project issue board. Dependencies that GitLab CE cannot enforce use issue links plus a checked dependency list in the issue description.
  • Create protected Git tags and GitLab releases for verified infrastructure-code baselines when a meaningful operational baseline is reached.

GitLab CE Free does not enforce required merge-request approvals, protected environments, deployment approvals, issue weights, iterations, advanced issue board lists, or blocked-issue indicators. Fridai therefore MUST NOT describe any of those as active controls.

The compensating control is:

  1. record a human plan review in the merge request by convention;
  2. restrict merge to Maintainers on the protected default branch;
  3. require the successful merge-request pipeline;
  4. create a fresh plan on the protected default branch;
  5. require a permitted operator to confirm the blocking manual apply; and
  6. retain pipeline, deployment, issue, and release evidence.

This provides a deliberate two-step workflow but does not technically enforce two different people. If enforceable separation of duties becomes mandatory, Fridai must adopt an external approval control or a GitLab tier that supports required approvals and protected deployment approvals.

Do not rely on GitLab.com CI/CD component mirroring from self-managed GitLab or cross-pipeline artifact download features that require a paid tier.

Standard GitLab labels and milestones

Infrastructure projects SHOULD create these scoped labels:

type::inventory
type::adoption
type::change
type::drift
type::exception
type::retirement
state::discovered
state::classified
state::planned
state::ready-for-apply
state::applied
state::verified
state::blocked
risk::low
risk::medium
risk::high
risk::critical
scope::account
scope::zone
scope::service

Use milestones for outcomes with a bounded completion criterion, such as bootstrap-inventory, first-controlled-import, and production-management. Do not recreate paid weights or iterations through arbitrary numeric labels.

Drift, emergency changes, and retirement

  • Scheduled drift checks are read-only and open or update a GitLab issue with the affected state, resource address, risk, evidence, and accountable owner.
  • Drift is never auto-applied.
  • An emergency manual change requires an incident or emergency-change issue, named approver, exact scope, timestamp, rollback approach, and reconciliation merge request.
  • Normal applies pause until emergency drift is understood.
  • Retirement requires dependency evidence, a reviewed destroy plan for exact resources, current state backup, explicit manual confirmation, post-removal verification, and registry tombstones. There is no reusable CI destroy job.

Required checks before readiness

A root is not ready for production management until:

  • formatting, validation, lint, policy, and secret scans pass;
  • provider locks are committed;
  • state and plans are absent from Git;
  • backend access and locking are verified;
  • off-host state restore has been tested;
  • inventory is complete for the declared scope and every object is classified;
  • every resource has one owner and satisfies the tagging policy or has a recorded fallback;
  • imports have no unexplained change;
  • no plan contains an unapproved replacement or deletion;
  • CI restricts artifacts, serializes the state, and protects apply;
  • provider credentials meet the documented minimum permissions; and
  • recovery, rollback, emergency change, and retirement runbooks are actionable.

Exceptions

Exceptions require a GitLab issue recording the affected roots and resources, reason, risk, compensating control, accountable owner, approver, and expiry or review date. Exceptions cannot authorize secret disclosure, unreviewed production deletion, dual ownership, or bypassing state locking.

References

Git history

Loading the page's history…