Using Terraform with GitLab
This guide describes how to use Terraform with GitLab, covering CI/CD, State, Modules, and Security.
This guide explores almost all GitLab Terraform integration features, from pipelines to state management to private module registries and even managing GitLab itself using Terraform. We also cover best practices for maintainability and security.
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.
- Leveraging 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 of this post, you'll understand how to transform your infrastructure management into a streamlined, automated, and secure operation.
GitLab as a VCS for Terraform
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:
- Tracking Changes: Every modification to your infrastructure definition is tracked with Git commits, providing a clear audit trail of who changed what, when, and why.
- Collaboration: GitLab enables multiple developers and operations team members to work on the same Terraform configuration concurrently using feature branches.
- History and Rollback: Maintain a complete history of your infrastructure's evolution. If issues arise, you can easily review past changes or roll back to a previous known-good state of your Terraform files.
- 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: Facilitate peer reviews of your Terraform code before changes are applied, improving code quality and catching potential errors early. This is a 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 leverage Terraform workspaces (more on this in the CI/CD section).
More on structuring Terraform code here.
Managing Terraform State in GitLab
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.
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: Seamlessly 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 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:
- 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 theinit
command. - 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 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
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: manual
Authenticating with 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
- GitLab CI Generates a JWT: For jobs configured to use OIDC, GitLab generates a JSON Web Token (
CI_JOB_JWT_V2
or other specific ID tokens likeGITLAB_OIDC_TOKEN
). - 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_dev
can 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 significantly improves your security posture by avoiding 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
gpg
within 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 runningplan
orapply
. - 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.cache
Directory-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
cd
into 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_DIR
environment 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.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
Custom 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 plan
automatically when a merge request is created or updated. - Use tools or scripts (e.g.,
gitlab-terraform
helpers if still applicable, or custom scripts withjq
andcurl
) 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: 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
.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
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
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"
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.
7 Security Best Practices for Terraform and GitLab
Security should be paramount throughout your IaC lifecycle.
- 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_base
for 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: false
orartifacts: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.
Conclusion
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.