GitLab's built-in CI/CD is one of the most complete pipelines-as-code systems available — and it integrates cleanly with Terraform for infrastructure deployments. This post walks through bootstrapping a private GitLab group, registering runners, and setting up a full Terraform CI/CD pipeline from scratch.
GitLab group and project structure
Start with a group to organize related repositories under shared CI/CD variables and runners. In GitLab, groups are the right scope for enterprise settings — you define runners, variables, and access controls once at the group level rather than per-project.
# Recommended structure for an infrastructure group
infra-group/
├── terraform-modules/ # shared module library
├── networking/ # VPC, subnets, DNS
├── platform/ # ECS, RDS, core services
└── apps/ # application-layer infra
Add group-level CI/CD variables (Settings → CI/CD → Variables) for shared secrets: AWS credentials, Terraform backend config, Vault tokens. These inherit to all projects in the group so you don't repeat them.
Registering a GitLab Runner
For infrastructure pipelines touching AWS, I prefer self-hosted runners over GitLab's shared runners. They run inside your VPC, can use instance profiles instead of long-lived credentials, and aren't subject to shared runner resource limits.
# Install on an EC2 instance (Amazon Linux 2)
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
sudo yum install gitlab-runner -y
# Register the runner
sudo gitlab-runner register \
--url "https://gitlab.com/" \
--registration-token "YOUR_GROUP_TOKEN" \
--executor "docker" \
--docker-image "hashicorp/terraform:1.5" \
--description "infra-runner" \
--tag-list "infra,terraform" \
--run-untagged false \
--locked false
Tag the runner with infra and terraform. Only jobs that specify these tags in .gitlab-ci.yml will route to this runner — keeping infra jobs isolated from application build jobs.
The .gitlab-ci.yml pipeline
A Terraform pipeline has four natural stages: validate, plan, apply (with manual gate), and destroy (restricted to authorized users).
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
TF_STATE_NAME: ${CI_PROJECT_NAME}
default:
image: hashicorp/terraform:1.5
tags:
- infra
- terraform
before_script:
- cd ${TF_ROOT}
- terraform init
-backend-config="bucket=${TF_STATE_BUCKET}"
-backend-config="key=${TF_STATE_NAME}/terraform.tfstate"
-backend-config="region=us-east-1"
-input=false
validate:
stage: validate
script:
- terraform validate
- terraform fmt -check -recursive
plan:
stage: plan
script:
- terraform plan -out=tfplan -input=false
- terraform show -no-color tfplan | tee plan.txt
artifacts:
name: tfplan
paths:
- ${TF_ROOT}/tfplan
- ${TF_ROOT}/plan.txt
expire_in: 1 week
apply:
stage: apply
script:
- terraform apply -input=false tfplan
dependencies:
- plan
when: manual
only:
- main
Environment-specific pipelines with rules
For multi-environment setups, use GitLab's rules to control which jobs run on which branches:
plan:dev:
extends: .plan_template
variables:
TF_WORKSPACE: dev
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
plan:prod:
extends: .plan_template
variables:
TF_WORKSPACE: prod
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Storing plan output as an artifact
Always store the plan file as a GitLab artifact. This does two things: the apply stage can use the exact saved plan (no drift between plan and apply), and reviewers can download plan.txt from the pipeline UI before approving the manual apply job.
A manual apply gate is non-negotiable for production environments. The reviewer should read the plan output, not just click approve because the pipeline is green.
Handling AWS credentials securely
Prefer IAM roles over long-lived access keys wherever possible. For self-hosted runners on EC2, attach an instance profile with the required Terraform permissions — no credentials needed in CI/CD variables at all. For GitLab.com shared runners, use OIDC federation with AWS so the runner gets short-lived tokens per job instead of static keys.
Wrapping up
GitLab + Terraform is a solid combination for infrastructure CI/CD. The key pieces: a tagged self-hosted runner inside your network, group-level variable management, a pipeline with an explicit plan artifact and manual apply gate, and proper credential handling. Get those right and you have a production-grade IaC pipeline with a clean audit trail.