Article ยท part of a guide
Using Terraform with GitLab
This guide describes how to use Terraform with GitLab, covering CI/CD, State, Modules, and Security.

Key takeaways
- GitLab can store Terraform state remotely via its built-in HTTP backend, with encryption, versioning, and automatic state locking to prevent concurrent modifications.
- GitLab CI/CD pipelines can validate, plan, and apply Terraform, and OIDC lets CI jobs obtain short-lived cloud credentials instead of hardcoding static access keys.
- GitLab provides a private Terraform module registry for publishing and consuming versioned modules, and the GitLab Terraform provider can manage GitLab itself.
- The post lists 7 security best practices, including securing state, using OIDC, securing plan artifacts, least privilege, and IaC code scanning with tools like tfsec or checkov.
This guide covers the main ways GitLab and Terraform work together, along with the security and maintenance habits that go with them. Here's what we'll get through:
What We'll Cover:
- GitLab as a Version Control System (VCS) for your Terraform files.
- Securely managing Terraform state files with GitLab's built-in HTTP backend.
- Building GitLab CI/CD pipelines for Terraform automation, including OIDC for cloud authentication.
- Using GitLab as a private Terraform Module Registry.
- Using the GitLab Terraform Provider to manage GitLab resources.
- 7 security best practices for your Terraform and GitLab workflows.
By the end you'll have a Terraform workflow that runs entirely on GitLab, from version control through to applying changes.
Want to run this workflow on a managed platform instead of maintaining it yourself? Scalr pricing is per run, free up to 50 runs a month.
GitLab as a VCS for Terraform
It starts with version control. Keeping your *.tf and *.tfvars files in a GitLab project gives you the usual benefits:
Every change to your infrastructure definition lands as a Git commit, so you get a clear audit trail of who changed what, when, and why. Several people can work on the same Terraform configuration at once using feature branches. And because GitLab keeps the full history, you can review past changes or roll back to a known-good version of your Terraform files whenever something breaks.
The rest is about workflow:
- Branching and Merging: Use Git branching strategies (e.g., Gitflow, GitLab Flow) to manage new features, bug fixes, or environment-specific configurations. Merge requests provide a formal process for proposing and reviewing changes.
- Code Review: Have peers review your Terraform code before changes are applied. It catches errors early and is good practice for any IaC setup.
Structuring 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, likedev.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 Terraform workspaces (more on this in the CI/CD section).
More on structuring Terraform code here.
Managing Terraform State in GitLab
Terraform's state file (terraform.tfstate) maps your configuration to the real resources it manages, so it's worth looking after. Keeping it on a local disk is risky, and it doesn't work once more than one person is involved. That's what remote backends are for, and GitLab has its own: GitLab-Managed Terraform State.
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:
- Centralized Storage: State files are stored within your GitLab project, providing a single location for both your code and its corresponding state.
- Security: State files are encrypted at rest and in transit (via HTTPS).
- Versioning: GitLab automatically versions your state files, allowing you to track changes and potentially roll back if needed.
- State Locking: Automatic state lock prevents concurrent modifications, reducing the risk of state corruption.
- Integration: Integrates with GitLab CI/CD and user permissions.
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 use 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}"
TF_HTTP_ADDRESS: "${TF_ADDRESS}"
TF_HTTP_LOCK_ADDRESS: "${TF_ADDRESS}/lock"
TF_HTTP_UNLOCK_ADDRESS: "${TF_ADDRESS}/lock"
TF_HTTP_USERNAME: "gitlab-ci-token"
TF_HTTP_PASSWORD: "${CI_JOB_TOKEN}"
TF_HTTP_LOCK_METHOD: "POST"
TF_HTTP_UNLOCK_METHOD: "DELETE"
TF_HTTP_RETRY_WAIT_MIN: "5"
# ... inside a script section for init job ...
terraform initHere, 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:
- Developer Role: Can read the state (e.g., for
terraform plan -lock=false). - Maintainer Role: Can write to the state (e.g.,
terraform apply) and manage locks. - Guest Role: Importantly, if your project is public or internal, users with Guest access might be able to view job artifacts like Terraform plan outputs, which can contain sensitive information. We'll address securing plan files later.
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.
- Ensure your current Terraform configuration is initialized with the old backend.
- Update your
backend "http" {}block if necessary, but primarily you'll provide the new configuration during theinitcommand. - Run
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.
Building GitLab Pipelines for Terraform, with OIDC
GitLab CI/CD is where automating your Terraform operations (1) really pays off. A well-built CD pipeline (1) can lint, validate, plan, and apply your infrastructure changes securely and reliably.
Core Pipeline Structure
A typical GitLab pipeline (.gitlab-ci.yml) 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: manualAuthenticating with OIDC
Hardcoding static cloud provider access tokens or access keys in CI/CD variables is a security risk. OpenID Connect (OIDC) lets GitLab CI jobs get short-lived, temporary credentials from cloud providers like AWS, GCP, and Azure, so you don't have to keep long-lived secrets around.
How OIDC Works with GitLab CI
- GitLab CI Generates a JWT: For jobs that declare an
id_tokensblock, GitLab generates a JSON Web Token in the variable you name (e.g.GITLAB_OIDC_TOKEN). The older predefinedCI_JOB_JWT_V2was removed in GitLab 17.0, soid_tokensis now the supported mechanism. - Cloud Provider Setup:
- Configure your cloud provider (e.g., AWS IAM, GCP Workload Identity Federation, Azure AD Federated Credentials) to trust GitLab as an OIDC identity provider.
- Create an IAM Role (or equivalent) in your cloud provider that Terraform will assume.
- Define a trust policy for this role that specifies conditions based on the JWT claims (e.g., GitLab project path, branch, tag) to enforce the principle of least privilege. This means a CI job from
project_A/branch_devcan only assume roles meant for that context.
- Terraform Assumes Role: The Terraform provider in your CI job uses the JWT to request temporary credentials from the cloud provider by assuming the configured role.
Example: OIDC for AWS in .gitlab-ci.yml and Terraform:
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.gitlab-ci.yml
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!
}providers.tf
This is a big security win because you're not storing static credentials for your AWS accounts or other cloud services.
Managing Plan Artifacts
The terraform plan -out=plan.cache command saves the execution plan. This plan file is a critical job artifact.
Security Risks: Plan files can contain sensitive data from your configuration or existing state. By default, in GitLab, anyone with Guest role access to a public/internal project can download artifacts.
Securing Plan Artifacts:
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.- Project Visibility: Keep projects with sensitive IaC private.
- Disable Public Pipelines: If your project is public, restrict pipeline visibility.
- Encryption (Advanced): For extremely sensitive data, consider encrypting the plan file itself using tools like
gpgwithin your CI job before declaring it as an artifact, and decrypting it in the apply job. This adds complexity.
Handling Multiple Environments
Managing different environments is crucial. Two common approaches:
Terraform Workspaces:
- Workspaces allow you to use the same Terraform code to manage multiple distinct sets of infrastructure resources, each with its own state file.
- In GitLab CI, you can select a workspace using
terraform workspace select <name>before runningplanorapply. - The workspace name can be tied to the Git branch or a CI variable.
- Use Case: Best when environments are very similar in configuration, differing mainly by variables (e.g., instance sizes, counts).
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.cacheDirectory-Based Structure:
- Create separate directories for each environment (e.g.,
environments/dev,environments/staging,environments/prod). - Each directory can have its own
main.tf,variables.tf, and backend configuration (though ideally, the backend config points to different state names/paths within the same GitLab HTTP backend). - This allows for more significant differences in configuration between environments.
- Your CI pipeline would
cdinto the appropriate directory based on the branch or target environment. - Use Case: When environments have substantially different resource configurations or require different provider versions/configurations. This is a common way to achieve split state.
Provider Plugin Management
Caching: Terraform downloads provider plugins during terraform init. To speed up CI jobs:
- Set the
TF_PLUGIN_CACHE_DIRenvironment variable in your.gitlab-ci.yml(e.g.,TF_PLUGIN_CACHE_DIR: "${CI_PROJECT_DIR}/.terraform-plugin-cache"). - Configure GitLab CI caching for this directory, keyed by the
.terraform.lock.hclfile:
cache:
key:
files:
- ${TF_ROOT}/.terraform.lock.hcl
paths:
- ${TF_ROOT}/.terraform-plugin-cache
policy: pull-push # Or pull for non-default branchesCustom Providers: For private or custom-built providers not in public registries, you can use Terraform's filesystem mirror capabilities by placing the provider binary in a known location (e.g., ~/.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
- Configure CI jobs to run
terraform planautomatically when a merge request is created or updated. - Use tools or scripts (e.g.,
gitlab-terraformhelpers if still applicable, or custom scripts withjqandcurl) 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
- Examine job logs in the GitLab UI carefully for error messages from Terraform or the runner.
- Enable CI/CD debug logging in GitLab for more verbose output.
- Common issues include authentication problems (OIDC setup, token permissions), incorrect paths in
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)
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
}terraform/main.tf
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:
name: hashicorp/terraform:latest
entrypoint: [""]
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com # Must match the audience configured on your AWS IAM OIDC provider
before_script:
- cd ${TF_ROOT}
- echo "${GITLAB_OIDC_TOKEN}" > ${AWS_WEB_IDENTITY_TOKEN_FILE} # ID token for AWS OIDC (CI_JOB_JWT_V2 was removed in GitLab 17.0)
- 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 .gitlab-ci.yml
(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).
Using GitLab's Terraform Module Registry
Modules are how you build reusable, versioned pieces of your infrastructure that are easy to maintain. GitLab has its own private Terraform Registry to host and share your custom modules across your organization.
Publishing Modules
Via GitLab CI/CD (Recommended):
Structure your module in a dedicated GitLab project.
Use a .gitlab-ci.yml file that triggers on Git tags (which should follow semantic versioning, e.g., v1.0.1).
The CI job will package your module (usually a .tgz archive) and use GitLab's API (with CI_JOB_TOKEN) to publish it to the registry.
GitLab provides CI/CD templates for this (e.g., Terraform-Module.gitlab-ci.yml), though with templates being deprecated, you might adapt this logic or use newer component-based approaches if available.
Via API Manually:
You can also use curl and the Packages API with a GitLab Personal Access Token, Project Access Token, or Deploy Token (with read_package_registry and write_package_registry scopes) to upload your packaged module.
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.
Using ~/.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>"
}Environment Variables (for CI/CD): Set 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.
Managing GitLab with the GitLab Terraform Provider
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
- Managing GitLab projects: create, configure settings (visibility, merge methods, CI/CD settings), project variables.
- Managing GitLab groups and memberships.
- Configuring users and their permissions.
- Setting up CI/CD variables at project or group levels.
- Managing repository files, branches, and branch protections.
- Configuring deploy keys and deploy tokens.
- Managing GitLab Runners (registering, enabling for projects).
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"
# The boolean *_enabled arguments were removed from gitlab_project;
# use the *_access_level arguments ("disabled", "private", or "enabled") instead.
issues_access_level = "enabled"
merge_requests_access_level = "enabled"
wiki_access_level = "disabled"
snippets_access_level = "disabled"
container_registry_access_level = "enabled"
}
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.
7 Security Best Practices for Terraform and GitLab
A handful of security practices matter more than the rest when you're running Terraform through GitLab. These seven are the ones worth getting right:
- Secure State: Use GitLab-Managed Terraform State (HTTP backend) for its built-in encryption and access controls, and make sure you understand the role of
db_key_basefor self-managed instances. - OIDC for Cloud Credentials: Prioritize OIDC over static cloud access tokens in GitLab CI. This is a cornerstone of modern, secure CI/CD.
- Secure Plan Artifacts: Control access to job artifacts (
plan file) usingartifacts:public: falseorartifacts:access. Be mindful of project visibility settings and their impact on artifact and code visibility. - Manage Sensitive Data: Use GitLab CI/CD variables (masked and protected) for secrets needed by Terraform that aren't cloud credentials handled by OIDC. For more complex secret management, consider integrating HashiCorp Vault (though this adds another layer of infrastructure).
- Principle of Least Privilege (PoLP): Grant only necessary permissions to CI jobs via OIDC roles. Assign GitLab users the minimum required roles (developer role, maintainer role).
- Code Scanning (SAST for IaC): Integrate static analysis security testing (SAST) tools like
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. - Regular Review: Periodically review GitLab user permissions and CI/CD pipeline configurations. Keep Terraform, providers, and GitLab versions up-to-date.
Where This Leaves You
Run the setup in this guide and your entire Terraform workflow lives in GitLab. Code sits under version control, state goes to the HTTP backend with locking and versioning, pipelines authenticate to your cloud with OIDC instead of static keys, and shared modules come from the private registry. You can even point Terraform back at GitLab to manage projects, groups, and CI/CD variables as code.
What you're maintaining, though, is the glue. The backend config, the OIDC trust policies, the artifact rules, and the approval logic in your .gitlab-ci.yml are all yours to keep working as your team grows. When that upkeep starts eating more time than it saves, a dedicated management platform usually takes those pieces off your hands. Our guide to Terraform Cloud alternatives compares the options, and all of them integrate with GitLab as the VCS provider.
About the author

CEO at Scalr
Sebastian Stadil is the CEO of Scalr with 15+ years of DevOps experience. He started with AWS in 2004 and advised early Microsoft Azure and Google Cloud.
Part of this guide
25 sheets
CI/CD and GitOps for Terraform & OpenTofu
- Migrating GitHub Actions from an S3 Backend to Scalr
- Terraform Testing: terraform test, tofu test, Terratest
- Custom Hooks for Terraform and OpenTofu: Pre-Plan, Post-Apply, and the Hooks Registry
- Integrating Terraform with Backstage
- The No-Nonsense Guide to ArgoCD
- Top 10 Continuous Delivery Tools
- Top 10 GitOps Tools: A Comprehensive Guide
- Everything you need to know about using Terraform or OpenTofu with Slack
- Key DevOps Metrics You Should Be Tracking
- Terraform and OpenTofu with Dependabot
- Terraform Notifications in Microsoft Teams
- The Complete Guide to DevOps Monitoring Tools: Choosing the Right Solution for Your Infrastructure
- Top Jenkins Alternatives & Specialized IaC Tools
- Why You Should Use Dependabot with Terraform and OpenTofu
- Mastering Terraform at Scale: A Developer's Guide to Reliable Infrastructure
- Integrating Terraform Events w/ AWS EventBridge
- How to use the Azure DevOps Terraform Provider
- Deploying your infrastructure with Scalr and GitHub Actions
- Terraform Configuration Ingestion
- Setting up Scalr & Azure DevOps Part 3 - Add Azure credentials
- Setting up Scalr & Azure DevOps Part 3 - Execute Your Terraform Code and Create a Workspace
- Setting up Scalr & Azure DevOps Part 1 - Picking a Workflow
- Terraform Cost Estimation in 2021: The Definitive Guide