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.
Table of Contents
- 1. Why infrastructure-as-code pipelines need more than local Terraform
- 2. Basics: including GitLab's Terraform template
- 3. terraform plan as a pipeline job
- 4. terraform apply as a manual job depending on plan
- 5. Storing the state backend directly in GitLab
- 6. Merge request widget with a plan diff
- 7. Advantages over manual Terraform CLI usage
- 8. Automatic apply on staging, manual apply on production
- 9. Conclusion: Terraform pipelines as the standard for team infrastructure
- 10. Summary
- 11. FAQ
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.