Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A safe Terraform pipeline in GitLab separates configuration checks, planning, review, and infrastructure changes. The plan job creates a saved plan; an authorized person reviews it; and a gated apply job consumes that same plan. Use persistent remote state, backend locking where available, tightly scoped credentials, and restricted artifacts so the pipeline does not turn a review step into an uncontrolled infrastructure mutation.

How the Terraform pipeline works

Terraform’s core workflow is init, plan, and apply. Initialization configures the working directory, backend, providers, and modules. Planning compares the configuration with state and infrastructure and previews proposed changes without applying them. Applying a saved plan carries out the actions in that plan. HashiCorp’s Terraform documentation describes this sequence in Terraform CLI and Running Terraform in automation.

  1. Check the proposed configuration. Run formatting and validation checks so basic errors are caught before an infrastructure plan is produced.
  2. Initialize and plan. Configure the intended backend and run terraform plan -out=tfplan to save the proposed actions.
  3. Review the result. Make the plan available to the people responsible for the change, and obtain the approval required by your production policy.
  4. Apply the reviewed plan. The apply job should use the saved plan from the plan job, not generate a fresh plan after approval.

GitLab defines pipelines in a project’s .gitlab-ci.yml. Jobs run on runners, stages run in sequence, and jobs in the same stage can run in parallel. Pipelines can be triggered by events such as branch pushes and merge requests, or started manually. GitLab documents these pipeline concepts in CI/CD pipelines and Pipeline architectures.

A cautious GitLab CI example

This skeleton illustrates the job boundaries and saved-plan handoff; it is not a drop-in configuration for every repository. It assumes Terraform is installed on the selected runner, the repository contains the configuration, and the backend is configured for the environment. Adapt the runner, backend, branch rules, artifact controls, and credential integration to your GitLab and Terraform versions. Test the rules and artifact handoff before relying on the pipeline for production.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
stages:
  - validate
  - plan
  - apply

validate:
  stage: validate
  script:
    - terraform fmt -check -recursive
    - terraform init -backend=false -input=false
    - terraform validate

plan:
  stage: plan
  script:
    - terraform init -input=false
    - terraform plan -input=false -out=tfplan
  artifacts:
    paths:
      - tfplan
      - .terraform/
      - .terraform.lock.hcl

apply:
  stage: apply
  needs:
    - job: plan
      artifacts: true
  script:
    - terraform apply -input=false tfplan
  when: manual
  allow_failure: false
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual

The example makes the apply job manual and restricts it to the default branch, but a manual job alone does not define who is authorized to approve a production change. Configure GitLab permissions and any environment approval controls to match your policy. A merge-request plan is useful feedback, but the plan to approve and apply should be generated against the current shared branch and state: merge ordering or infrastructure changes can make an earlier proposal stale.

Why the plan job exports more than the plan file

When plan and apply run in different jobs, they may run on different machines. The later job needs the saved plan and the initialized working-directory artifacts it relies on. The example transfers .terraform/ as well as tfplan; passing only the plan file is not guaranteed to provide everything the apply job needs. Confirm the required files and backend behavior for your Terraform version and runner setup.

Keep dependencies consistent

Commit .terraform.lock.hcl so provider selections are recorded and future initialization uses those selections by default. Pin the Terraform version in the runner environment rather than silently inheriting whatever version happens to be installed. If you use a GitLab CI/CD component or another included pipeline definition, pin it to a specific version where possible and review how included configuration merges with your jobs; same-named jobs or settings can interact unexpectedly.

Choose and protect the state backend

Terraform state maps resource addresses in configuration to real infrastructure objects. CI needs persistent state that later runs can retrieve and update; local state on an ephemeral runner is not an adequate team workflow. Choose a backend based on persistence, locking, access controls, backup and recovery, operational ownership, and fit with your deployment architecture.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a backend that supports locking when concurrent operations are possible. HashiCorp explains in Running Terraform in automation that locking protects against race conditions from concurrent runs; not every backend supports it. Avoid disabling locking as a workaround for competing runs, because two writers can interfere with state.

GitLab Self-Managed state is one option

GitLab documents Terraform state storage for GitLab Self-Managed installations. Its administration documentation says state files are encrypted before storage and that the feature is enabled by default. The documented default is local storage for relevant installations, with supported object-storage configurations also available; Helm chart installations use external object-storage configuration. These details apply to GitLab Self-Managed, not every GitLab deployment, and should be checked against the current administration documentation for the installation.

Rank #3

For Self-Managed administrators, verify the storage arrangement and backup plan before migrating state: GitLab warns that migration from object storage back to local storage is not possible. Recovery also depends on access to encrypted state files and the database; the documented decryption process requires the application secret and project ID. Treat state recovery as an operational responsibility, not merely a pipeline setting.

Control who can change infrastructure

A plan is a preview; an apply changes infrastructure. For production, HashiCorp recommends human review and warns about automatic approval when unintended destructive actions could cause downtime. The important control is not simply that an approval occurred, but that the apply job consumes the exact saved plan that was reviewed. If the plan is regenerated after approval, the reviewer may be authorizing a different set of actions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A practical design is to use merge-request pipelines for early feedback, then create the production plan from the merged branch and current state. Make production apply a gated job and limit who can run or approve it through the project’s permissions and environment policy. The precise GitLab controls available depend on the project configuration and product setup.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Handle credentials and artifacts as sensitive data

Terraform credentials should be available only to the jobs and environments that need them. GitLab describes CI/CD variables as less secure than secrets-management providers: they can be overridden, may be accessible to people with settings access if not hidden, or may be exposed through a misconfigured pipeline. Prefer a secrets manager for highly sensitive secrets. If sensitive values must be CI/CD variables, GitLab advises masking, hiding, and protecting them where possible; scope them narrowly and avoid printing them in scripts or logs.

Saved plan files and initialized directories are operational artifacts and may contain sensitive values or configuration details. Restrict artifact access to the jobs and people who need it, and set retention in line with your policy. Do not assume a plan report is safe to publish merely because it is formatted for a merge request.

Plan visibility and OpenTofu reports

GitLab documents a terraform report path for an OpenTofu tfplan.json file that can display information in a merge-request widget. Its documentation requires JQ processing to remove credentials before the report is uploaded. This is a documented OpenTofu JSON report workflow, not a general instruction to upload any Terraform binary plan file as that report. Protect the report input and inspect what it exposes before enabling the integration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Scale the pipeline without losing control

For a small repository, separate validation, plan, and apply jobs or stages are usually easier to inspect. GitLab stages provide a straightforward sequence; needs can express more direct job dependencies and artifact handoffs. For larger setups, GitLab supports parent-child pipelines to split work within a project and multi-project pipelines to coordinate across projects. Whichever structure you choose, keep the production plan-to-apply relationship explicit and ensure that the correct state and credentials reach only the intended jobs.

  • Backend: verify persistence, locking support, access control, backups, recovery, and who operates it.
  • Approval: decide who reviews production plans and who can start the apply job.
  • Artifacts: transfer the saved plan and necessary initialized files, while limiting access and retention.
  • Credentials: prefer a secrets manager for highly sensitive values; otherwise use protected, hidden, masked, and narrowly scoped variables where available.
  • Dependencies: commit the provider lock file and pin Terraform and reusable pipeline components deliberately.

GitLab and HashiCorp product details, report support, configuration labels, and Terraform behavior can change. Check the current documentation for your versions and validate the pipeline in a non-production environment before using it to change production infrastructure.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.