GitLab + Terraform
Learn how to run Terraform in GitLab CI/CD. From validate, plan, and apply workflows to GitLab-managed state and merge request plan reports, build a complete Infrastructure-as-Code pipeline.
Terraform is the standard for Infrastructure as Code (IaC): you define your cloud resources — networks, compute, databases, DNS — in declarative configuration files, and Terraform creates and manages them. GitLab provides the CI/CD platform to run Terraform automatically: every push triggers a validate and plan, every merge request shows the planned changes, and every apply is tracked and auditable. Together, they bring the same rigor to infrastructure that you already apply to application code.
GitLab's integration with Terraform goes beyond just running commands. GitLab can act as the Terraform state backend, storing the state file securely in your project with locking and versioning built in. It can parse the plan output and display a summary directly in the merge request, showing exactly what will be created, updated, or destroyed. And it supports OpenID Connect (OIDC) for keyless authentication to cloud providers, eliminating long-lived access keys from your CI/CD variables [citation:2][citation:18].
It's worth noting that GitLab deprecated its built-in Terraform CI/CD templates in GitLab 18.0, and now recommends using the OpenTofu CI/CD component or building your own pipeline [citation:4][citation:15]. The OpenTofu component provides validate, plan, and apply jobs out of the box, and Terraform users can adapt it or write their own. The core concepts — state management, merge request integration, and OIDC — remain the same regardless of which tool you use.
Validate
Plan
Apply
State Backend
MR Integration
OIDC Auth
The Terraform workflow in CI/CD starts with validation. terraform validate checks the configuration syntax and internal consistency, while terraform fmt -check enforces canonical formatting. These run fast — in seconds — and require no cloud credentials, so they're safe to run on every commit and merge request. A failed validate or format check should block the pipeline immediately, before any slower or more expensive steps run [citation:2][citation:6].
After validation, terraform plan generates an execution plan: a detailed preview of what Terraform will create, update, or destroy. The plan is saved to a file with -out=tfplan, which is critical — the apply step later uses that exact plan, ensuring that what was reviewed is what gets applied. On merge requests, the plan output can be parsed and displayed as a summary, showing reviewers the resource change counts without forcing them to read raw Terraform output [citation:2].
GitLab's merge request integration takes this further. By generating a JSON representation of the plan and declaring it as a terraform report artifact, GitLab parses the plan and displays it directly in the MR widget. Reviewers see "2 to add, 1 to change, 0 to destroy" alongside the code diff, making infrastructure changes as reviewable as application code. This is one of the most valuable features of running Terraform in GitLab: infrastructure changes go through the same review process as code [citation:2].
- Run
terraform validateandfmt -checkon every commit - Use
-out=tfplanand pass the plan file to apply - Generate the
terraformreport artifact for MR display - Use
rulesto run plan on MRs and main - Make apply manual on main, or automatic only with approvals
Terraform state is critical: it maps your configuration to real-world resources and must be shared across pipeline runs. Storing it locally on a runner is fragile and dangerous. GitLab provides a managed Terraform state backend that uses the HTTP backend protocol, storing state in your GitLab project with locking, versioning, and encryption. This means you don't need to set up an S3 bucket or other external storage — GitLab handles it [citation:2][citation:10].
Configuring the backend is a matter of setting CI/CD variables and passing them to terraform init. The key variables are TF_STATE_NAME (which identifies the state file) and TF_ADDRESS (the GitLab API URL for the state). The backend configuration includes lock and unlock addresses, and authentication uses the CI job token. Once configured, every pipeline run reads and writes the same state, ensuring consistency [citation:2].
For multiple environments, each environment gets its own state name. A staging environment uses TF_STATE_NAME=staging and production uses TF_STATE_NAME=production. This isolates state between environments, preventing a staging apply from accidentally modifying production resources. The state name can be set via CI/CD variables scoped to each environment, making the pipeline dynamic without duplicating configuration [citation:10].
- No external storage to manage — state lives in GitLab
- Locking prevents concurrent applies from corrupting state
- Versioning allows rollback to previous state
- Access controlled by project permissions
- Works with both Terraform and OpenTofu