Your costs = usage. Period.

Terraform has revolutionized Infrastructure as Code (IaC), allowing teams to define and manage infrastructure declaratively. GitLab, a complete DevSecOps platform, provides the ideal environment to develop, secure, and operate that Terraform code. Integrating these two powerhouses unlocks incredible efficiency, collaboration, and security for your infrastructure workflows.
While storing your Terraform code in a GitLab project is a fundamental first step, the synergy goes much deeper. This guide will explore the full spectrum of GitLab Terraform integration features, from robust CI/CD pipelines and secure state management to private module registries and even managing GitLab itself using Terraform. We'll cover best practices to ensure your infrastructure configuration is managed effectively and securely from your local machine to the cloud.
What We'll Cover:
By the end of this post, you'll understand how to transform your infrastructure management into a streamlined, automated, and secure operation.
The absolute cornerstone of using GitLab with Terraform is version controlling your Terraform code. Storing your *.tf
and *.tfvars
files in a GitLab project provides numerous advantages:
Structuring Your Terraform Code in GitLab:
A common way to structure your Terraform code within a GitLab project involves:
main.tf
: Defines your primary set of Terraform resources.variables.tf
: Declares input variables.outputs.tf
: Defines outputs from your configuration.versions.tf
: Specifies Terraform version and provider plugins versions.providers.tf
: Configures Terraform providers (e.g., AWS, Azure, GCP, GitLab).terraform.tfvars
(often environment-specific, like dev.tfvars
, prod.tfvars
): Contains variable values. Ensure sensitive values are not committed here; use GitLab CI/CD variables instead..gitlab-ci.yml
: Defines your GitLab CI/CD pipeline for Terraform automation.For multi-environment setups, you might use separate directories per environment or leverage Terraform workspaces (more on this in the CI/CD section).
Terraform state files (terraform.tfstate
) are critical. They store the mapping between your Terraform configuration and the real-world resources. Storing state locally on your local machine or in local storage is risky and unsuitable for team collaboration. This is where Terraform remote backends come into play, and GitLab offers a powerful, integrated solution: GitLab-Managed Terraform State.
Introducing GitLab-Managed Terraform State (HTTP Backend)
GitLab can act as a remote backend for your Terraform state files using its built-in Terraform HTTP backend. This offers several advantages:
Setup and Configuration:
1. Terraform Backend Configuration:
In your Terraform project (e.g., in providers.tf or a dedicated backend.tf file), declare the HTTP backend:
terraform {
backend "http" {
// Configuration will be provided during init
}
}
2. Initializing the Backend (Local Machine):
To initialize this backend from your local terminal for the first time or for local development against a GitLab-managed state:
# Ensure you have a GitLab Personal Access Token with 'api' scope
export GITLAB_ACCESS_TOKEN="your_personal_access_token"
export GITLAB_USERNAME="your_gitlab_username"
terraform init \
-backend-config="address=https://gitlab.com/api/v4/projects/<YOUR_PROJECT_ID>/terraform/state/<YOUR_STATE_NAME>" \
-backend-config="lock_address=https://gitlab.com/api/v4/projects/<YOUR_PROJECT_ID>/terraform/state/<YOUR_STATE_NAME>/lock" \
-backend-config="unlock_address=https://gitlab.com/api/v4/projects/<YOUR_PROJECT_ID>/terraform/state/<YOUR_STATE_NAME>/lock" \
-backend-config="username=${GITLAB_USERNAME}" \
-backend-config="password=${GITLAB_ACCESS_TOKEN}" \
-backend-config="lock_method=POST" \
-backend-config="unlock_method=DELETE" \
-backend-config="retry_wait_min=5"
Replace <YOUR_PROJECT_ID>
with your GitLab project's ID and <YOUR_STATE_NAME>
with a descriptive name for your state (e.g., production
, vpc
). The address
is the URL of the remote state backend.
3. Initializing the Backend (GitLab CI/CD):
Within a GitLab CI job, initialization is simpler as GitLab provides necessary environment variables. The gitlab-terraform helper scripts (though templates are being deprecated, the underlying mechanism is similar) or direct terraform init commands can leverage CI_API_V4_URL, CI_PROJECT_ID, and CI_JOB_TOKEN.
A common setup in .gitlab-ci.yml
using direct Terraform commands:
variables:
TF_STATE_NAME: "default" # Or make this dynamic per environment
TF_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}"
# ... inside a script section for init job ...
terraform init \
-backend-config="address=${TF_ADDRESS}" \
-backend-config="lock_address=${TF_ADDRESS}/lock" \
-backend-config="unlock_address=${TF_ADDRESS}/lock" \
-backend-config="username=gitlab-ci-token" \
-backend-config="password=${CI_JOB_TOKEN}" \
-backend-config="lock_method=POST" \
-backend-config="unlock_method=DELETE" \
-backend-config="retry_wait_min=5"
Here, CI_JOB_TOKEN
is used for authentication, which is a secure, short-lived access token.
Permissions and Roles:
GitLab uses its standard project roles for controlling access to the Terraform state:
terraform plan -lock=false
).terraform apply
) and manage locks.Self-Managed GitLab Considerations:
For self-managed GitLab instances, GitLab administrators need to configure Terraform state storage. This can be a local filesystem path on the GitLab server or an object storage service like Amazon S3 or Google Cloud Storage. Using object storage is recommended for clustered GitLab deployments to avoid split-brain scenarios.
Migrating to GitLab-Managed State:
If you have an existing state file in a different state storage backend (e.g., local, S3 bucket) or want to change the state name or location, Terraform supports state migration.
backend "http" {}
block if necessary, but primarily you'll provide the new configuration during the init
command.terraform init -migrate-state
. Terraform will prompt you for the configuration of the new backend (your GitLab HTTP backend details) and ask for confirmation to copy the old state to the new location. This is useful if you're starting with an empty state in GitLab but want to bring in an existing setup.
# Example: Migrating from a local state to GitLab-managed state
# First, ensure your backend "http" {} block is in your .tf files.
# Then, run init with the new GitLab backend config and the -migrate-state flag:
terraform init -migrate-state \
-backend-config="address=https://gitlab.com/api/v4/projects/<PROJECT_ID>/terraform/state/<NEW_STATE_NAME>" \
# ... other backend-config parameters as shown before ...
Terraform will detect the change and ask to copy the state.
Accessing Remote State Across Configurations:
Often, you need to share outputs from one Terraform configuration (e.g., network setup) with another (e.g., application deployment). The terraform_remote_state
Terraform data source allows this.
# variables.tf
variable "network_state_address" {
description = "URL of the network Terraform state in GitLab"
type = string
}
variable "gitlab_api_user" {
description = "GitLab username for accessing remote state"
type = string
}
variable "gitlab_api_token" {
description = "GitLab token (PAT or CI_JOB_TOKEN) for accessing remote state"
type = string
sensitive = true
}
# data.tf
data "terraform_remote_state" "network" {
backend = "http"
config = {
address = var.network_state_address # e.g., "https://gitlab.com/api/v4/projects/YOUR_NET_PROJECT_ID/terraform/state/network"
username = var.gitlab_api_user # "gitlab-ci-token" if in CI, else your username
password = var.gitlab_api_token # ${CI_JOB_TOKEN} if in CI, else your PAT
lock_method = "POST"
unlock_method= "DELETE"
retry_wait_min = 5
}
}
# main.tf
resource "aws_instance" "app_server" {
# ... other configurations ...
subnet_id = data.terraform_remote_state.network.outputs.app_subnet_id
}
You would pass the variable values via .tfvars
files (kept out of VCS if containing PATs) or GitLab CI/CD variables.
GitLab CI/CD is where the real power of automating your Terraform operations (1) shines. A well-structured CD pipeline (1) can lint, validate, plan, and apply your infrastructure changes securely and reliably.
Core Pipeline Structure (.gitlab-ci.yml
):
A typical GitLab pipeline for Terraform includes several stages:
stages:
- validate
- build # Often called 'plan'
- deploy
- cleanup # Optional, for destroy operations
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform # Directory where Terraform files are
TF_STATE_NAME: "default"
TF_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}"
# For OIDC (example for AWS)
# AWS_ROLE_ARN: "arn:aws:iam::123456789012:role/GitLabCIRoleForTerraform"
# AWS_WEB_IDENTITY_TOKEN_FILE: "${CI_PROJECT_DIR}/web_identity_token" # For newer Terraform AWS provider versions
default:
image:
name: hashicorp/terraform:latest # Use a specific version in production
entrypoint: [""]
before_script:
- cd ${TF_ROOT}
# OIDC Token setup for AWS (example, adjust for your cloud)
# - echo "${CI_JOB_JWT_V2}" > ${AWS_WEB_IDENTITY_TOKEN_FILE}
# - export AWS_ROLE_SESSION_NAME="GitLabCI-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
# For GitLab HTTP Backend
- export GITLAB_TF_ADDRESS="-backend-config=address=${TF_ADDRESS}"
- export GITLAB_TF_LOCK_ADDRESS="-backend-config=lock_address=${TF_ADDRESS}/lock"
- export GITLAB_TF_UNLOCK_ADDRESS="-backend-config=unlock_address=${TF_ADDRESS}/lock"
- export GITLAB_TF_USERNAME="-backend-config=username=gitlab-ci-token"
- export GITLAB_TF_PASSWORD="-backend-config=password=${CI_JOB_TOKEN}"
- export GITLAB_TF_LOCK_METHOD="-backend-config=lock_method=POST"
- export GITLAB_TF_UNLOCK_METHOD="-backend-config=unlock_method=DELETE"
- export GITLAB_TF_RETRY="-backend-config=retry_wait_min=5"
- terraform --version
- terraform init -input=false ${GITLAB_TF_ADDRESS} ${GITLAB_TF_LOCK_ADDRESS} ${GITLAB_TF_UNLOCK_ADDRESS} ${GITLAB_TF_USERNAME} ${GITLAB_TF_PASSWORD} ${GITLAB_TF_LOCK_METHOD} ${GITLAB_TF_UNLOCK_METHOD} ${GITLAB_TF_RETRY}
validate:
stage: validate
script:
- terraform validate -json
plan:
stage: build # 'build' stage often used for plan generation
script:
- terraform plan -out=tfplan.cache -input=false
# Optionally, convert plan to JSON for further processing or MR comments
- terraform show -json tfplan.cache > tfplan.json
artifacts:
name: "plan_artifacts_${CI_COMMIT_REF_SLUG}"
paths:
- ${TF_ROOT}/tfplan.cache
- ${TF_ROOT}/tfplan.json
# By default, artifacts are public. Secure them!
public: false # Restricts access to project members (Reporter+)
# For more granular control (GitLab Premium+):
# access: developer # Or maintainer
expire_in: 1 week
apply:
stage: deploy
script:
- terraform apply -input=false tfplan.cache
dependencies:
- plan # Ensures 'plan' job artifacts are available
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Apply only on default branch
when: manual # Manual trigger for production changes is a good practice
- if: $CI_COMMIT_BRANCH =~ /^env\// # Example: auto-apply for 'env/*' branches
when: on_success
destroy: # Optional
stage: cleanup
script:
- terraform destroy -auto-approve
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $DESTROY_INFRA == "true" # Require explicit variable
when: manual
Secure Cloud Authentication with OpenID Connect (OIDC):
Hardcoding static cloud provider access tokens or access keys in CI/CD variables is a security risk. OpenID Connect (OIDC) allows GitLab CI jobs to obtain short-lived, temporary credentials from cloud providers like AWS, GCP, and Azure, eliminating the need for long-lived sensitive data.
How OIDC Works with GitLab CI:
CI_JOB_JWT_V2
or other specific ID tokens like GITLAB_OIDC_TOKEN
).project_A/branch_dev
can only assume roles meant for that context.Example: OIDC for AWS in .gitlab-ci.yml
and Terraform:
.gitlab-ci.yml
(relevant parts):
variables:
AWS_ROLE_ARN: "arn:aws:iam::YOUR_ACCOUNT_ID:role/YourGitLabCIRole"
AWS_WEB_IDENTITY_TOKEN_FILE: "${CI_PROJECT_DIR}/aws_oidc_token.json" # Standard practice
AWS_ROLE_SESSION_NAME: "GitLabCI-${CI_JOB_ID}"
default:
id_tokens: # Request an OIDC token
GITLAB_OIDC_TOKEN:
aud: # Optional: specify audience if required by your IdP config in AWS
- sts.amazonaws.com
# - https://gitlab.com (if you configured GitLab instance as audience)
before_script:
- echo "${GITLAB_OIDC_TOKEN}" > ${AWS_WEB_IDENTITY_TOKEN_FILE}
# Terraform AWS provider will automatically pick up AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE
No explicit provider configuration for credentials is needed if AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE are set as environment variables. The AWS provider SDK will handle the token exchange.
provider "aws" {
region = "us-east-1"
# No explicit access_key or secret_key needed!
}
This significantly improves your security posture by avoiding static credentials for your AWS accounts or other cloud services.
Managing Plan Artifacts (terraform plan data
):
The terraform plan -out=plan.cache
command saves the execution plan. This plan file is a critical job artifact.
artifacts:public: false
: In your .gitlab-ci.yml
, set this on your plan artifact definition. This restricts download access to project members with at least Reporter access.artifacts:access: developer
/ maintainer
: For GitLab Premium/Ultimate, you can use this for more granular control.gpg
within your CI job before declaring it as an artifact, and decrypting it in the apply job. This adds complexity.Handling Multiple Environments (Dev, Staging, Prod) & Split State
:
Managing different environments is crucial. Two common approaches:
1. Terraform Workspaces:
terraform workspace select <name>
before running plan
or apply
.
variables:
TF_WORKSPACE: "${CI_COMMIT_REF_NAME}" # Use branch name as workspace
script:
- terraform workspace select ${TF_WORKSPACE} || terraform workspace new ${TF_WORKSPACE}
- terraform plan -out=tfplan.cache
2. Directory-Based Structure:
environments/dev
, environments/staging
, environments/prod
).main.tf
, variables.tf
, and backend configuration (though ideally, the backend config points to different state names/paths within the same GitLab HTTP backend).cd
into the appropriate directory based on the branch or target environment.Provider Plugin Management in CI:
terraform init
. To speed up CI jobs:TF_PLUGIN_CACHE_DIR
environment variable in your .gitlab-ci.yml
(e.g., TF_PLUGIN_CACHE_DIR: "${CI_PROJECT_DIR}/.terraform-plugin-cache"
)..terraform.lock.hcl
file:
cache:
key:
files:
- ${TF_ROOT}/.terraform.lock.hcl
paths:
- ${TF_ROOT}/.terraform-plugin-cache
policy: pull-push # Or pull for non-default branches
~/.terraform.d/plugins
or a project subdirectory) and configuring provider_installation
in a CLI configuration file or using the TF_CLI_PLUGIN_PATH
environment variable.Integrating with Merge Requests:
terraform plan
automatically when a merge request is created or updated.gitlab-terraform
helpers if still applicable, or custom scripts with jq
and curl
) to parse the JSON plan output and post a summary as a comment on the merge request. This allows reviewers to see the proposed changes directly in the MR.Troubleshooting CI Jobs:
TF_ROOT
, state locking conflicts (rare with GitLab backend if CI jobs are serialized correctly for applies), or syntax errors in Terraform code.Example: Provisioning a Virtual Machine with GitLab CI and OIDC (AWS EC2)
1. Terraform Code (terraform/main.tf
):
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "example_vm" {
ami = "ami-0c55b31ad2c675a0a" # Example: Amazon Linux 2 AMI (HVM)
instance_type = "t2.micro"
tags = {
Name = "GitLab-CI-VM-Example"
Environment = "Demo"
}
}
output "instance_id" {
value = aws_instance.example_vm.id
}
output "instance_public_ip" {
value = aws_instance.example_vm.public_ip
}
2. .gitlab-ci.yml
(relevant sections):
stages:
- validate
- plan
- deploy
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
TF_STATE_NAME: "vm-example"
TF_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}"
AWS_ROLE_ARN: "arn:aws:iam::YOUR_ACCOUNT_ID:role/YourGitLabCIRoleForVMs" # Set this in GitLab CI/CD variables
AWS_WEB_IDENTITY_TOKEN_FILE: "${CI_PROJECT_DIR}/aws_oidc_token.json"
AWS_DEFAULT_REGION: "us-east-1" # Or your desired region
AWS_ROLE_SESSION_NAME: "GitLabCI-VM-${CI_JOB_ID}"
default:
image: hashicorp/terraform:latest
entrypoint: [""]
before_script:
- cd ${TF_ROOT}
- echo "${CI_JOB_JWT_V2}" > ${AWS_WEB_IDENTITY_TOKEN_FILE} # Using CI_JOB_JWT_V2 for AWS
- export GITLAB_TF_ADDRESS="-backend-config=address=${TF_ADDRESS}"
- export GITLAB_TF_LOCK_ADDRESS="-backend-config=lock_address=${TF_ADDRESS}/lock"
# ... other GITLAB_TF_ backend configs as shown previously ...
- terraform init -input=false ${GITLAB_TF_ADDRESS} ${GITLAB_TF_LOCK_ADDRESS} # ... etc.
validate_vm:
stage: validate
script:
- terraform validate
plan_vm:
stage: plan
script:
- terraform plan -out=tfplan.cache
artifacts:
paths:
- ${TF_ROOT}/tfplan.cache
public: false
apply_vm:
stage: deploy
script:
- terraform apply -input=false tfplan.cache
dependencies:
- plan_vm
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
(Ensure your AWS IAM Role YourGitLabCIRoleForVMs
has permissions to create EC2 instances and is configured to trust JWTs from your GitLab project/branch as per OIDC setup).
Terraform modules are essential for creating reusable, maintainable, and versioned components of your infrastructure. GitLab provides its own private Terraform Registry to host and share your custom modules within your organization.
Publishing Modules:
.gitlab-ci.yml
file that triggers on Git tags (which should follow semantic versioning, e.g., v1.0.1
)..tgz
archive) and use GitLab's API (with CI_JOB_TOKEN
) to publish it to the registry.Terraform-Module.gitlab-ci.yml
), though with templates being deprecated, you might adapt this logic or use newer component-based approaches if available.Example: Referencing a Module from GitLab Registry in your Terraform code:
module "vpc" {
source = "gitlab.com/your-group/your-vpc-module/aws" # Or gitlab.your-instance.com
version = "1.0.1"
# Module inputs
vpc_cidr = "10.0.0.0/16"
# ...
}
Authentication for Consuming Modules:
When terraform init
runs, it needs to authenticate to your GitLab instance to download private modules.
~/.terraformrc
or terraform.rc
: Create this file with credentials:
credentials "gitlab.com" { // Or your self-managed instance hostname
token = "<YOUR_GITLAB_PERSONAL_ACCESS_TOKEN_OR_DEPLOY_TOKEN>"
}
TF_TOKEN_gitlab_com
(replace gitlab_com
with your instance, underscores for dots) with a suitable token. CI_JOB_TOKEN
can often be used if the job has appropriate permissions.Beyond managing external cloud infrastructure, you can use Terraform to manage GitLab itself using the official GitLab Terraform Provider (gitlabhq/gitlab
). This is IaC for your DevSecOps platform!
Common Use Cases:
Example: Creating a GitLab Project:
provider "gitlab" {
# token = var.gitlab_admin_token # Can be set via env var GITLAB_TOKEN
}
resource "gitlab_project" "example_app" {
name = "My Example App"
description = "An example application project managed by Terraform."
visibility_level = "private"
default_branch = "main"
issues_enabled = true
merge_requests_enabled = true
wiki_enabled = false
snippets_enabled = false
container_registry_enabled = true # Requires GitLab 13.6+ for provider to set
}
resource "gitlab_project_variable" "app_env" {
project = gitlab_project.example_app.id
key = "APP_ENVIRONMENT"
value = "development"
protected = false
masked = false
environment_scope = "*" # All environments
}
For an exhaustive list of resources and data sources, refer to the official GitLab Provider documentation on the Terraform Registry.
Security should be paramount throughout your IaC lifecycle.
db_key_base
for self-managed instances.plan file
) using artifacts:public: false
or artifacts:access
.tfsec
, checkov
, or GitLab's own SAST capabilities into your GitLab CI pipeline to catch misconfigurations and security vulnerabilities in your Terraform code before they reach production.Combining Terraform's powerful infrastructure provisioning capabilities with GitLab's comprehensive DevSecOps platform creates a robust, secure, and efficient workflow for managing the entire lifecycle of your state of your infrastructure.
By leveraging GitLab for version control, adopting its secure HTTP backend for Terraform state files, building sophisticated GitLab CI/CD pipelines with OIDC, utilizing the private Terraform Module Registry, and even managing GitLab itself with the GitLab Terraform Provider, you can significantly enhance collaboration, automation, and security.
Embrace these best practices and GitLab Terraform integration features to ensure your team can confidently and rapidly evolve your cloud infrastructure, focusing on delivering value rather than wrestling with manual processes or security concerns.