GitLab's Terraform Integration for Infrastructure-as-Code Pipelines
AI generated
CI/CD
.yml
GitLab · CI/CD · Infrastructure as Code
GitLab's Terraform integration
Infrastructure as code with a plan diff in the merge request

Running Terraform manually from the command line stops being reliable the moment a team shares the same infrastructure. GitLab's own Terraform integration maps plan and apply as pipeline jobs, manages state centrally, and shows changes as a diff right in the merge request before anyone touches real infrastructure.

17 min read Terraform Infrastructure as Code State backend Merge request widget

1. Why infrastructure-as-code pipelines need more than local Terraform

Running Terraform locally on your own machine works fine as long as only one person makes infrastructure changes. As soon as a team works on the same infrastructure together, typical problems arise: the local state is outdated, two people run apply at the same time and overwrite each other, or nobody can later reconstruct who applied which change and when.

A pipeline-based Terraform execution solves these problems structurally: every change goes through the same, reproducible plan and apply process, the state lives in one central place that all team members reach through the same controlled interface, and every applied change is traceable end to end through the pipeline history.

2. Basics: including GitLab's Terraform template

GitLab ships an official CI template at Terraform/Base.latest.gitlab-ci.yml that predefines the basic Terraform jobs with sensible defaults. include: pulls this template into your own .gitlab-ci.yml, so the entire job logic does not need to be rebuilt by hand. Among other things, the template handles terraform init with the GitLab backend and the JSON output format later needed for the plan widget in the merge request.

In addition to the template, a dedicated image containing Terraform itself is usually needed, such as registry.gitlab.com/gitlab-org/terraform-images/stable, which GitLab maintains to match its own templates. The specific Terraform version can be pinned via the image tag, making version upgrades controllable and reproducible instead of relying on whatever version happens to be installed on the runner.


include:
  - template: Terraform/Base.latest.gitlab-ci.yml

variables:
  TF_ROOT: ${CI_PROJECT_DIR}/infrastructure
  TF_STATE_NAME: production

default:
  image:
    name: registry.gitlab.com/gitlab-org/terraform-images/stable:latest

3. terraform plan as a pipeline job

The plan job calculates what changes would be needed against the real infrastructure without actually applying them. The template automatically produces both a human-readable text output and a machine-readable JSON file, passed to GitLab as artifacts: reports: terraform. This JSON file is the foundation for the plan widget shown later in the merge request.

In every merge request, plan runs automatically, so reviewers can see exactly which resources would be created, changed, or deleted before the actual approval happens. That prevents a code review from relying solely on the .tf files themselves while the real impact on infrastructure only becomes visible during the later apply.


plan:
  extends: .terraform:build
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

plan-json:
  extends: .terraform:build
  script:
    - gitlab-terraform plan-json
  needs: ["plan"]
  artifacts:
    reports:
      terraform: ${TF_ROOT}/plan.json

4. terraform apply as a manual job depending on plan

The apply job actually applies the changes calculated in the plan job. For safety it should practically always use when: manual and explicitly depend on the prior plan job via needs:, so an apply can never run without a corresponding, previously reviewed plan run. A protected environment for the apply job additionally ensures that only authorized team members can approve the actual application.

It matters that apply builds on the exact same Terraform plan that was shown in the merge request, not on a freshly recalculated version that might differ due to changes made to the real infrastructure in the meantime. GitLab's template solves this by passing the plan output as an artifact to the apply job instead of running plan again inside the apply job.


apply:
  extends: .terraform:build
  environment:
    name: production
  needs: ["plan"]
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual

5. Storing the state backend directly in GitLab

Instead of a separately operated backend like S3 or Azure Blob Storage, GitLab offers its own HTTP-based Terraform state backend, addressed through the GitLab API, which stores the state directly in the project, versions it, and secures it with locking. It is configured via a backend "http" block whose address is composed of the GitLab project ID and the chosen TF_STATE_NAME, already predefined in GitLab's template.

The advantage of this managed backend is that no additional cloud resource needs to be operated and secured with its own access rights solely for Terraform state management. Access control runs through the same GitLab project permissions that already apply to the repository, and the state can be inspected directly, and locked if needed, through the GitLab UI under Operate > Terraform States.


terraform {
  backend "http" {
  }
}

# The init call in the template sets the backend parameters automatically:
# terraform init \
#   -backend-config=address=... \
#   -backend-config=lock_address=... \
#   -backend-config=unlock_address=... \
#   -backend-config=username=... \
#   -backend-config=password=$CI_JOB_TOKEN

6. Merge request widget with a plan diff

As soon as the plan-json job provides its report as artifacts: reports: terraform, GitLab automatically shows a dedicated widget in the merge request that summarizes the planned changes: how many resources would be created, changed, and deleted, with the ability to click into the detail of every single resource change. This saves reviewers from manually scrolling through the often long text output of terraform plan in the job log.

This widget is especially valuable for changes that look harmless at first glance but actually trigger a resource being recreated instead of updated in place, for example when an immutable attribute such as an AWS instance type gets changed in a way that forces Terraform to delete and recreate the resource. A reviewer sees such destructive changes clearly flagged in the plan widget before the apply job is even approved.

7. Advantages over manual Terraform CLI usage

Manual Terraform CLI usage on developer machines brings several structural weaknesses: every person needs local access to cloud credentials with potentially broad permissions, there is no central, enforced four-eyes principle before an apply, and different local Terraform versions between team members can cause slightly diverging behavior. GitLab's integration addresses all of these points structurally, not just through team discipline and conventions.

Teams additionally benefit from a complete audit history: every pipeline documents who created which merge request, who saw which plan, and who approved which apply job, all traceable through the GitLab UI. With manual CLI usage, that traceability usually only exists if someone additionally keeps a manual log, which rarely happens consistently in practice.

8. Automatic apply on staging, manual apply on production

Not every Terraform change needs to go through the same approval process. For a staging environment where misconfigurations are easy to undo and that gets rebuilt regularly anyway, an automatic apply right after a successful plan is often reasonable and considerably speeds up the feedback loop while developing Terraform modules.

For production environments, a manual apply secured by a protected environment remains the right choice, since misconfigurations there can cause real outages or costs. Separate TF_STATE_NAME values per environment combined with rules: conditions reacting to the respective branch let both approval levels coexist cleanly within the same .gitlab-ci.yml, without duplicating code.


apply-staging:
  extends: .terraform:build
  environment:
    name: staging
  variables:
    TF_STATE_NAME: staging
  needs: ["plan-staging"]
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

apply-production:
  extends: .terraform:build
  environment:
    name: production
  variables:
    TF_STATE_NAME: production
  needs: ["plan-production"]
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual

9. Conclusion: Terraform pipelines as the standard for team infrastructure

GitLab's own Terraform integration turns a potentially risky, manual process into a traceable, reviewed pipeline with a clear separation between plan and apply. For any team managing infrastructure together, this approach significantly reduces the risk of unintended or uncoordinated changes.

Moving from local Terraform to the pipeline variant pays off starting from just two people regularly working on the same infrastructure. The managed state backend and the merge request plan widget are not peripheral convenience features here, but the actual core advantages over the classic CLI workflow.

Aspect Manual Terraform CLI usage GitLab Terraform integration
State management Local or self-operated backend GitLab-managed HTTP backend with locking
Review before change Optional, depends on team discipline Plan diff automatically in the merge request widget
Access control Local cloud credentials per person Protected environment, GitLab project permissions
Audit history Usually not present Fully traceable through pipeline and MR history

Mironsoft

CI/CD pipelines, zero-downtime deployments and release automation

Deployments that run without downtime and without the nail-biting?

We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.

Pipeline Review

Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.

Zero-Downtime Deployment

Building symlink releases, health checks and rollback strategies for Magento stores.

CI/CD Automation

Connecting tests, security scans and deployments into one reliable pipeline.

10. Summary

GitLab's Terraform Integration: The Essentials at a Glance

Include the template

include: template: Terraform/Base.latest.gitlab-ci.yml provides predefined plan and apply jobs.

Plan before apply

apply uses needs: on plan and applies exactly the previously calculated and reviewed plan.

Managed state backend

GitLab stores the Terraform state versioned and with locking directly in the project.

Plan diff in the merge request

An automatic widget shows created, changed, and deleted resources before approval.

11. FAQ: GitLab's Terraform Integration: The Essentials at a Glance

1How do I include GitLab's Terraform integration in a pipeline?
Via include: template: Terraform/Base.latest.gitlab-ci.yml in the .gitlab-ci.yml, combined with a Terraform-capable image and variables like TF_ROOT and TF_STATE_NAME.
2Where is the Terraform state stored with GitLab's integration?
In a GitLab-managed, HTTP-based backend that stores the state versioned within the project and secures it against concurrent changes through locking.
3How does GitLab show planned Terraform changes in the merge request?
Through an automatic widget that summarizes created, changed, and deleted resources from the plan job's JSON report, with the ability to click into further detail.
4Does terraform apply always have to be triggered manually?
It is strongly recommended for safety, usually combined with when: manual and a protected environment, so only authorized people can approve real infrastructure changes.
5How does GitLab ensure apply uses the same plan shown in the MR?
The plan output is passed to the apply job as an artifact instead of recalculating the plan inside apply, so no drift can occur from changes made to the infrastructure in the meantime.
6What advantages does GitLab's state backend offer over S3 or Azure Blob Storage?
It requires no separate cloud resource solely for state management and uses the same GitLab project permissions for access control instead of maintaining separate cloud IAM rules.
7How does locking work with GitLab's Terraform backend?
The HTTP backend supports lock and unlock endpoints, so a running apply prevents a second job from modifying the same state at the same time.
8Can I manage multiple Terraform environments like staging and production separately?
Yes, different TF_STATE_NAME values per environment allow separate states within the same project, combined with separate protected environments for each environment's apply jobs.
9Which Terraform version is used in the pipeline?
The version installed in the chosen Terraform image, controllable via the image tag, for example registry.gitlab.com/gitlab-org/terraform-images/stable:1.7, instead of whatever version happens to be on the runner.
10What is the biggest advantage over manual Terraform CLI usage?
The combination of enforced review before every apply, centrally managed state with locking, and a complete audit trail traceable through the pipeline history.