Bootstrap Pipelines in an Existing GitLab Project
This guide provides comprehensive instructions for integrating Gruntwork Pipelines into an existing GitLab project with Infrastructure as Code (IaC). This is designed for Gruntwork customers who want to add Pipelines to their current infrastructure projects for streamlined CI/CD management.
To configure Gruntwork Pipelines in an existing GitLab project, complete the following steps (which are explained in detail below):
- (If using a self-hosted GitLab instance) Ensure OIDC configuration and JWKS are publicly accessible.
- Plan your Pipelines setup by identifying all environments and cloud accounts/subscriptions you need to manage.
- Bootstrap core infrastructure in accounts/subscriptions that don't already have the required OIDC and state management resources.
- Configure SCM access using machine users with appropriate Personal Access Tokens (PATs).
- Create
.gruntworkHCL configurations to tell Pipelines how to authenticate and organize your environments. - Create
.gitlab-ci.ymlto configure your GitLab CI/CD pipeline. - Commit and push your changes to activate Pipelines.
Ensure OIDC configuration and JWKS are publicly accessible
This step only applies if you are using a self-hosted GitLab instance that is not accessible from the public internet. If you are using GitLab.com or a self-hosted instance that is publicly accessible, you can skip this step.
- Follow GitLab's instructions for hosting your OIDC configuration and JWKS in a public location (e.g. S3 Bucket). This is necessary for both Gruntwork and the AWS OIDC provider to access the GitLab OIDC configuration and JWKS when authenticating JWT's generated by your custom instance.
- Note the <ISSUER_URL> (stored as
ci_id_tokens_issuer_urlin yourgitlab.rbfile per GitLab's instructions) generated above for reuse in the next steps.
Prerequisites
Before starting, ensure you have:
- An active Gruntwork subscription with Pipelines access. Verify by checking the Gruntwork Developer Portal and confirming access to "pipelines" repositories in your GitHub team.
- Cloud provider credentials with permissions to create OIDC providers and IAM roles in accounts where Pipelines will manage infrastructure.
- Git installed locally for cloning and managing your project.
- Existing IaC project with Terragrunt configurations you want to manage with Pipelines (if you are using OpenTofu/Terraform, and want to start using Terragrunt, read the Quickstart Guide).
Planning Your Pipelines Setup
Before implementing Pipelines, it's crucial to plan your setup by identifying all the environments and cloud resources you need to manage.
Identify Your Environments
Review your existing project structure and identify:
- All environments you want to manage with Pipelines (e.g.,
dev,staging,prod) - Cloud accounts/subscriptions associated with each environment
- Directory paths in your project that contain Terragrunt units for each environment
- Existing OIDC resources that may already be provisioned in your accounts
Determine Required OIDC Roles
For each AWS Account / Azure Subscription you want to manage, you might already have some or all of the following resources provisioned.
- AWS
- Azure
Required AWS Resources:
- An OIDC provider for GitLab
- An IAM role for Pipelines to assume when running Terragrunt plan commands
- An IAM role for Pipelines to assume when running Terragrunt apply commands
Required Azure Resources:
- Entra ID Application for plans with Federated Identity Credential
- Entra ID Application for applies with Federated Identity Credential
- Service Principals with appropriate role assignments
- Storage Account and Container for Terragrunt state storage (if not already existing)
Configuring SCM Access
Pipelines needs the ability to interact with GitLab to fetch resources (e.g. IaC code, reusable CI/CD code and the Pipelines binary itself).
To create machine users for GitLab access, follow our machine users guide to set up the appropriate Personal Access Tokens (PATs) with the required permissions.
Bootstrapping Cloud Infrastructure
If your AWS accounts / Azure subscriptions don't already have all the required OIDC and state management resources, you'll need to bootstrap them. This section provides the infrastructure code needed to set up these resources.
If you already have all the resources listed, you can skip this section.
If you have some of them provisioned, but not all, you can decide to either destroy the resources you already have provisioned and recreate them or import them into state. If you are not sure, please contact Gruntwork support.
Prepare Your Project
Clone your project to your local machine using Git if you haven't already.
If you don't have Git installed, you can install it by following the official guide for Git installation.
For example:
git clone git@gitlab.com:acme/infrastructure-live.git
cd infrastructure-live
To bootstrap your project, we'll use Boilerplate to scaffold it with the necessary IaC code to provision the infrastructure necessary for Pipelines to function.
The easiest way to install Boilerplate is to use mise to install it.
If you don't have mise installed, you can install it by following the official guide for mise installation.
mise use -g boilerplate@latest
If you'd rather install a specific version of Boilerplate, you can use the ls-remote command to list the available versions.
mise ls-remote boilerplate
If you don't already have Terragrunt and OpenTofu installed locally, you can install them using mise:
mise use -g terragrunt@latest opentofu@latest
Cloud-specific bootstrap instructions
- AWS
- Azure
The resources you need provisioned in AWS to start managing resources with Pipelines are:
- An OpenID Connect (OIDC) provider
- An IAM role for Pipelines to assume when running Terragrunt plan commands
- An IAM role for Pipelines to assume when running Terragrunt apply commands
For every account you want Pipelines to manage infrastructure in.
This may seem like a lot to set up, but the content you need to add to your project is minimal. The majority of the work will be pulled from a reusable catalog that you'll reference in your project.
If you want to peruse the catalog that's used in the bootstrap process, you can take a look at the terragrunt-scale-catalog repository.
The process that we'll follow to get these resources ready for Pipelines is:
- Use Boilerplate to scaffold bootstrap configurations in your project for each AWS account
- Use Terragrunt to provision these resources in your AWS accounts
- (Optionally) Bootstrap additional AWS accounts until all your AWS accounts are ready for Pipelines
Bootstrap Your Project for AWS
First, confirm that you have a root.hcl file in the root of your project that looks something like this:
locals {
account_hcl = read_terragrunt_config(find_in_parent_folders("account.hcl"))
state_bucket_name = local.account_hcl.locals.state_bucket_name
region_hcl = read_terragrunt_config(find_in_parent_folders("region.hcl"))
aws_region = local.region_hcl.locals.aws_region
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = local.state_bucket_name
region = local.aws_region
key = "${path_relative_to_include()}/tofu.tfstate"
encrypt = true
use_lockfile = true
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
}
EOF
}
If you don't have a root.hcl file, you might need to customize the bootstrapping process, as the Terragrunt scale catalog expects a root.hcl file in the root of the project. Please contact Gruntwork support for assistance if you need help.
For each AWS account that needs bootstrapping, we'll use Boilerplate to scaffold the necessary content. Run this command from the root of your project for each account:
boilerplate \
--template-url 'github.com/gruntwork-io/terragrunt-scale-catalog//templates/boilerplate/aws/gitlab/account?ref=v1.1.0' \
--output-folder .
You'll need to run this boilerplate command once for each AWS account you want to manage with Pipelines. Boilerplate will prompt you for account-specific values each time.
You can reply y to all the prompts to include dependencies, and accept defaults unless you want to customize something.
Alternatively, you could run Boilerplate non-interactively by passing the --non-interactive flag. You'll need to supply the relevant values for required variables in that case.
e.g.
boilerplate \
--template-url 'github.com/gruntwork-io/terragrunt-scale-catalog//templates/boilerplate/aws/gitlab/account?ref=v1.1.0' \
--output-folder . \
--var 'AccountName=dev' \
--var 'GitLabGroupName=acme' \
--var 'GitLabRepoName=infrastructure-live' \
--var 'GitLabInstanceURL=https://gitlab.com' \
--var 'AWSAccountID=123456789012' \
--var 'AWSRegion=us-east-1' \
--var 'StateBucketName=my-state-bucket' \
--non-interactive
If you're using a self-hosted GitLab instance, you'll want to make sure the issuer is set correctly when calling Boilerplate.
boilerplate \
--template-url 'github.com/gruntwork-io/terragrunt-scale-catalog//templates/boilerplate/aws/gitlab/account?ref=v1.1.0' \
--output-folder . \
--var 'AccountName=dev' \
--var 'GitLabGroupName=acme' \
--var 'GitLabRepoName=infrastructure-live' \
--var 'GitLabInstanceURL=https://gitlab.com' \
--var 'AWSAccountID=123456789012' \
--var 'AWSRegion=us-east-1' \
--var 'StateBucketName=my-state-bucket' \
--var 'Issuer=<ISSUER_URL>' \
--non-interactive
You can also choose to store these values in a YAML file and pass it to Boilerplate using the --var-file flag.
AccountName: dev
GitLabGroupName: acme
GitLabRepoName: infrastructure-live
GitLabInstanceURL: https://gitlab.com
AWSAccountID: 123456789012
AWSRegion: us-east-1
StateBucketName: my-state-bucket
boilerplate \
--template-url 'github.com/gruntwork-io/terragrunt-scale-catalog//templates/boilerplate/aws/gitlab/account?ref=v1.1.0' \
--output-folder . \
--var-file vars.yml \
--non-interactive
Provision AWS Bootstrap Resources
Once you've scaffolded out the accounts you want to bootstrap, you can use Terragrunt to provision the resources in each of these accounts.
Make sure that you authenticate to each AWS account you are bootstrapping using AWS credentials for that account before you attempt to provision resources in it.
You can follow the documentation here to authenticate with the AWS provider. You are advised to choose an authentication method that doesn't require any hard-coded credentials, like assuming an IAM role.
For each account you want to bootstrap, you'll need to run the following commands:
First, make sure that everything is set up correctly by running a plan in the bootstrap directory in name-of-account/_global where name-of-account is the name of the AWS account you want to bootstrap.
terragrunt run --all --non-interactive --provider-cache --backend-bootstrap plan
We're using the --provider-cache flag here to ensure that we don't re-download the AWS provider on every run by leveraging the Terragrunt Provider Cache Server.
We're using the --backend-bootstrap flag here to tell Terragrunt to bootstrap the OpenTofu backend automatically for the account.
Next, apply the changes to your account.
terragrunt run --all --non-interactive --provider-cache apply
The resources you need provisioned in Azure to start managing resources with Pipelines are:
- An Azure Resource Group for OpenTofu state resources
- An Azure Storage Account in that resource group for OpenTofu state storage
- An Azure Storage Container in that storage account for OpenTofu state storage
- An Azure Storage Account in that resource group for OpenTofu state storage
- An Entra ID Application to use for plans
- A Flexible Federated Identity Credential for the application to authenticate with your project on any branch
- A Service Principal for the application to be used in role assignments
- A role assignment for the service principal to access the Azure subscription
- A role assignment for the service principal to access the Azure Storage Account
- An Entra ID Application to use for applies
- A Federated Identity Credential for the application to authenticate with your project on the deploy branch
- A Service Principal for the application to be used in role assignments
- A role assignment for the service principal to access the Azure subscription
This may seem like a lot to set up, but the content you need to add to your project is minimal. The majority of the work will be pulled from a reusable catalog that you'll reference in your project.
If you want to peruse the catalog that's used in the bootstrap process, you can take a look at the terragrunt-scale-catalog repository.
The process that we'll follow to get these resources ready for Pipelines is:
- Use Boilerplate to scaffold bootstrap configurations in your project for each Azure subscription
- Use Terragrunt to provision these resources in your Azure subscription
- Finalizing Terragrunt configurations using the bootstrap resources we just provisioned
- Pull the bootstrap resources into state, now that we have configured a remote state backend
- (Optionally) Bootstrap additional Azure subscriptions until all your Azure subscriptions are ready for Pipelines
Bootstrap Your Project for Azure
For each Azure subscription that needs bootstrapping, we'll use Boilerplate to scaffold the necessary content. Run this command from the root of your project for each subscription:
boilerplate \
--template-url 'github.com/gruntwork-io/terragrunt-scale-catalog//templates/boilerplate/azure/gitlab/subscription?ref=v1.1.0' \
--output-folder .
You'll need to run this boilerplate command once for each Azure subscription you want to manage with Pipelines. Boilerplate will prompt you for subscription-specific values each time.
You can reply y to all the prompts to include dependencies, and accept defaults unless you want to customize something.
Alternatively, you could run Boilerplate non-interactively by passing the --non-interactive flag. You'll need to supply the relevant values for required variables in that case.
e.g.
boilerplate \
--template-url 'github.com/gruntwork-io/terragrunt-scale-catalog//templates/boilerplate/azure/gitlab/subscription?ref=v1.1.0' \
--output-folder . \
--var 'AccountName=dev' \
--var 'GitLabGroupName=acme' \
--var 'GitLabRepoName=infrastructure-live' \
--var 'GitLabInstanceURL=https://gitlab.com' \
--var 'SubscriptionName=dev' \
--var 'AzureTenantID=00000000-0000-0000-0000-000000000000' \
--var 'AzureSubscriptionID=11111111-1111-1111-1111-111111111111' \
--var 'AzureLocation=East US' \
--var 'StateResourceGroupName=pipelines-rg' \
--var 'StateStorageAccountName=mysa' \
--var 'StateStorageContainerName=tfstate' \
--non-interactive
You can also choose to store these values in a YAML file and pass it to Boilerplate using the --var-file flag.
AccountName: dev
GitLabGroupName: acme
GitLabRepoName: infrastructure-live
GitLabInstanceURL: https://gitlab.com
SubscriptionName: dev
AzureTenantID: 00000000-0000-0000-0000-000000000000
AzureSubscriptionID: 11111111-1111-1111-1111-111111111111
AzureLocation: East US
StateResourceGroupName: pipelines-rg
StateStorageAccountName: my-storage-account
StateStorageContainerName: tfstate
boilerplate \
--template-url 'github.com/gruntwork-io/terragrunt-scale-catalog//templates/boilerplate/azure/gitlab/subscription?ref=v1.1.0' \
--output-folder . \
--var-file vars.yml \
--non-interactive
Provision Azure Bootstrap Resources
Once you've scaffolded out the subscriptions you want to bootstrap, you can use Terragrunt to provision the resources in your Azure subscription.
If you haven't already, you'll want to authenticate to Azure using the az CLI.
az login
To dynamically configure the Azure provider with a given tenant ID and subscription ID, ensure that you are exporting the following environment variables if you haven't the values via the az CLI:
ARM_TENANT_IDARM_SUBSCRIPTION_ID
For example:
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"
export ARM_SUBSCRIPTION_ID="11111111-1111-1111-1111-111111111111"
First, make sure that everything is set up correctly by running a plan in the subscription directory.
terragrunt run --all --non-interactive --provider-cache plan
We're using the --provider-cache flag here to ensure that we don't re-download the Azure provider on every run to speed up the process by leveraging the Terragrunt Provider Cache Server.
Next, apply the changes to your subscription.
terragrunt run --all --non-interactive --provider-cache --no-stack-generate apply
We're adding the --no-stack-generate flag here, as Terragrunt will already have the requisite stack configurations generated, and we don't want to accidentally overwrite any configurations while we have state stored locally before we pull them into remote state.
Finalizing Terragrunt configurations
Once you've provisioned the resources in your Azure subscription, you can finalize the Terragrunt configurations using the bootstrap resources we just provisioned.
First, edit the root.hcl file in the root of your project to leverage the storage account we just provisioned.
If your root.hcl file doesn't already have a remote state backend configuration, you'll need to add one that looks like this:
locals {
sub_hcl = read_terragrunt_config(find_in_parent_folders("sub.hcl"))
state_resource_group_name = local.sub_hcl.locals.state_resource_group_name
state_storage_account_name = local.sub_hcl.locals.state_storage_account_name
state_storage_container_name = local.sub_hcl.locals.state_storage_container_name
}
remote_state {
backend = "azurerm"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
resource_group_name = local.state_resource_group_name
storage_account_name = local.state_storage_account_name
container_name = local.state_storage_container_name
key = "${path_relative_to_include()}/tofu.tfstate"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "azurerm" {
features {}
resource_provider_registrations = "none"
}
provider "azuread" {}
EOF
}
Next, finalize the .gruntwork/environment-<name-of-subscription>.hcl file in the root of your project to reference the IDs for the applications we just provisioned.
You can find the values for the plan_client_id and apply_client_id by running terragrunt stack output in the bootstrap directory in name-of-subscription/bootstrap.
terragrunt stack output
The relevant bits that you want to extract from the stack output are the following:
bootstrap = {
apply_app = {
client_id = "33333333-3333-3333-3333-333333333333"
}
plan_app = {
client_id = "44444444-4444-4444-4444-444444444444"
}
}
You can use those values to set the values for plan_client_id and apply_client_id in the .gruntwork/environment-<name-of-subscription>.hcl file.
Pulling the resources into state
Once you've provisioned the resources in your Azure subscription, you can pull the resources into state using the storage account we just provisioned.
terragrunt run --all --non-interactive --provider-cache --no-stack-generate -- init -migrate-state -force-copy
We're adding the -force-copy flag here to avoid any issues with OpenTofu waiting for an interactive prompt to copy up local state.
Creating .gruntwork HCL Configurations
Create HCL configurations in the .gruntwork directory in the root of your project to tell Pipelines how you plan to organize your infrastructure, and how you plan to have Pipelines authenticate with your cloud provider(s).
The repository block
The core configuration that you'll want to start with is the repository block. This block tells Pipelines which branch has the "live" infrastructure you want provisioned. When you merge IaC to this branch, Pipelines will be triggered to update your infrastructure accordingly.
repository {
deploy_branch_name = "main"
}
The environment block
Next, you'll want to define the environments you want to manage with Pipelines using the environment block.
For each environment, you'll want to define a filter block that tells Pipelines which units are part of that environment. You'll also want to define an authentication block that tells Pipelines how to authenticate with your cloud provider(s) for that environment.
- AWS
- Azure
- Custom
environment "production" {
filter {
paths = ["prod/*"]
}
authentication {
aws_oidc {
account_id = "123456789012"
plan_iam_role_arn = "arn:aws:iam::123456789012:role/pipelines-plan"
apply_iam_role_arn = "arn:aws:iam::123456789012:role/pipelines-apply"
}
}
}
Learn more about how Pipelines authenticates to AWS in the Authenticating to AWS page.
Check out the aws block for more information on how to configure Pipelines to reuse common AWS configurations.
environment "production" {
filter {
paths = ["prod/*"]
}
authentication {
azure_oidc {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "11111111-1111-1111-1111-111111111111"
plan_client_id = "33333333-3333-3333-3333-333333333333"
apply_client_id = "44444444-4444-4444-4444-444444444444"
}
}
}
Learn more about how Pipelines authenticates to Azure in the Authenticating to Azure page.
environment "production" {
filter {
paths = ["prod/*"]
}
authentication {
custom {
auth_provider_cmd = "./scripts/custom-auth-prod.sh"
}
}
}
Learn more about how Pipelines can authenticate with custom authentication in the Custom Authentication page.
Creating .gitlab-ci.yml
Create a .gitlab-ci.yml file in the root of your project with the following content:
include:
- project: 'gruntwork-io/gitlab-pipelines-workflows'
file: '/workflows/pipelines.yml'
ref: 'v1'
You can read the Pipelines GitLab CI Workflow to learn how this GitLab CI pipeline calls the Pipelines CLI to run your pipelines.
Commit and Push Your Changes
Commit and push your changes to your project.
You should include [skip ci] in your commit message here to prevent triggering the Pipelines workflow before everything is properly configured.
git add .
git commit -m "Add Pipelines configurations and GitLab CI workflow [skip ci]"
git push
🚀 You've successfully added Gruntwork Pipelines to your existing GitLab project!
Next Steps
You have successfully completed the installation of Gruntwork Pipelines in an existing GitLab project. Proceed to Deploying your first infrastructure change to begin deploying changes.
Troubleshooting Tips
If you encounter issues during the setup process, here are some common troubleshooting steps:
Bootstrap Resources Failure
If your bootstrap resource provisioning fails:
HCL Configuration Issues
If your HCL configurations aren't working as expected:
GitLab CI Pipeline Issues
If your GitLab CI pipeline isn't working as expected: