Run Terraform from Gitlab CI

Bits Lovers
Written by Bits Lovers on
Run Terraform from Gitlab CI

GitLab is more than a code repo. You can build, test, and deploy straight from it. If you are already working with Infrastructure as Code, you probably use Terraform locally. But running it inside GitLab CI solves a bunch of headaches you don’t think about until your team grows.

We already covered how to deploy Cloud Formation using GitLab. This time we are doing the same with Terraform.

Why Run Terraform from GitLab CI?

Running Terraform on your laptop is fine when you are the only person on the project. But that changes fast.

The Terraform State Problem

Here is the scenario that got me to move Terraform into CI in the first place. Terraform releases new versions all the time. The Terraform State format can change between major versions. So if one person upgrades to Terraform 1.5 and runs terraform apply, it bumps the state file. Now everyone else on the team also has to upgrade, or they get errors.

Multiply that by a team of ten or twenty people deploying several times a day. It is not fun.

Running Terraform in CI fixes this. Everyone uses the same version, pinned in the pipeline. No more “works on my machine” problems with the state.

You also get auditability. GitLab keeps logs of every plan and apply. You can see who deployed what and when. That is hard to track when everyone runs Terraform from their own machine.

Another benefit: once the pipeline is set up, anyone on the team can trigger a deployment. They do not need to know the variable names, the backend config, or how to install the right provider plugins. They just click a button.

Let’s build a pipeline that runs Plan, Apply, and Destroy.

Terraform GitLab CI YAML

variables:
  AWS_REGION: us-east-1
  PHASE: BUILD
before_script:
  - apk add --no-cache curl jq python3 py3-pip git
  - pip install awscli
  - aws configure set region ${AWS_REGION}
stages:
  - plan
  - deploy
Plan:
  image:
    name: hashicorp/terraform:1.5
    entrypoint: [""]
  stage: plan
  artifacts:
    paths:
    - plan.bin
    - app_config.zip
    expire_in: 2 week
  script:
    - terraform --version
    - terraform init
    - terraform plan -input=false -out=plan.bin
  only:
    variables:
      - $PHASE == "BUILD"
Apply:
  image:
    name: hashicorp/terraform:1.5
    entrypoint: [""]
  when: manual
  stage: deploy
  script:
    - terraform init
    - terraform apply -auto-approve -input=false plan.bin
  only:
    variables:
      - $PHASE == "BUILD"
  environment:
    name: snunv
Destroy:
  image:
    name: hashicorp/terraform:1.5
    entrypoint: [""]
  stage: deploy
  script:
    - terraform init
    - terraform destroy -auto-approve
  only:
    variables:
      - $PHASE == "DESTROY"
  environment:
    name: snunv
    action: stop

A few things worth noting about this setup.

Both Apply and Destroy are set to when: manual. That is intentional. The Plan job runs automatically on every commit so you can review the changes. But applying or destroying infrastructure requires someone to click the button.

This is useful when you are iterating on Terraform code. You commit, Plan runs, you review the output. If it looks right, you trigger Apply. If not, you push another commit and try again.

I also pinned the Terraform image to hashicorp/terraform:1.5. Do not use the latest tag. Terraform updates frequently and a version bump can break your state or your provider plugins. Always pin to a specific version.

Note: If you are starting a new project in 2025 or later, you might want to look at the GitLab OpenTofu CI/CD component instead. GitLab has been moving toward a component-based approach for IaC pipelines, with official support for both Terraform and OpenTofu. The YAML above still works, but the component approach handles more of the boilerplate for you.

GitLab Terraform Destroy – The Destroy Phase

Destroying an environment by accident is a bad day. That is why the Destroy job is gated behind a CI variable instead of running automatically.

To trigger it, go to your project’s Build > Pipelines page and click Run pipeline.

run terraform in gitlab ci

Select the branch you want to run the pipeline on. Then add a CI Variable with the key PHASE and the value DESTROY. Click Run pipeline and follow the logs.

terraform with gitlab

You can find all available Terraform Docker image tags on Docker Hub. Again, pin to a specific version. Do not use latest.

Conclusion

Running Terraform from GitLab CI takes a bit of upfront work but pays off quickly. Your team uses the same Terraform version, every change is logged, and anyone can trigger a deployment without touching their local setup.

Boost your Terraform skills:

Split your resources using Terraform Modules, and improve the usability and maintainability.

How to create complex expressions using Terraform Template.

What is the difference between Locals and Terraform Variables?

How to use the Terraform Data on your modules.

What are the advantages of Terraform Output?

Learn how to create multiple copies from the same resource on Terraform.

GitLab CI + Terraform: A Production IaC Pipeline in 2026 — covers OIDC auth, multi-account deployments, plan artifacts, and drift detection in a real pipeline

How to create a Pipeline on Gitlab to execute a Terraform code?

How to use the Gitlab CI Variables

Effective Cache Management with Maven Projects on Gitlab.

Pipeline to build Docker in Docker on Gitlab.

How to Autoscaling the Gitlab Runner.

How to execute Cloud Formation on Gitlab.

Terraform and Ansible: The Integration That Actually Works — the pull model, S3 handoff, and bootstrap script patterns that make provisioning and configuration work together.

Terraform Debug: Inspect State and Troubleshoot — state inspection, common failure modes, and the taint/import workflows that save you from re-creating infrastructure.

Terraform lookup(): Access Map Values Without Headaches — HCL map access patterns, defaults, and nested variable resolution.

Bits Lovers

Bits Lovers

Professional writer and blogger. Focus on Cloud Computing.

Comments

comments powered by Disqus