GitOps for Feature Flags with LaunchDarkly and Terraform

Malcolm Matalka avatar

Malcolm Matalka

GitOps for Feature Flags with LaunchDarkly and Terraform blog post

Feature flags are a powerful tool for modern software development, enabling controlled feature rollouts, A/B testing, and quick rollbacks. However, they can become a source of confusion and technical debt without proper management. This guide shows how to implement GitOps principles for LaunchDarkly feature flags using Terraform and Terrateam, creating a workflow where flags are version-controlled, peer-reviewed, and automatically deployed.

Feature Flags and GitOps: A Perfect Match

Feature flags deserve the same care and process as your application code and infrastructure. By managing flags through GitOps:

  • Your Git repository becomes the definitive record of all flag configurations
  • Changes go through pull requests, ensuring visibility and quality
  • Git history provides a complete record of who changed what and when
  • Approved changes are automatically applied to your environments
  • Flag configurations stay in sync across development, staging, and production

LaunchDarkly’s Terraform provider, combined with Terrateam, makes this approach straightforward to implement. Terrateam enhances Terraform workflows by automating operations when pull requests are created or merged, posting plan results as comments, and providing access controls for different operations.

Prerequisites

Before we begin, you’ll need:

  • A LaunchDarkly account with admin access
  • A GitHub account with repository admin access
  • Google Cloud Platform account (for Terraform state storage)
  • The following tools installed locally:
    • Git
    • Terraform (v1.0.0 or later)
    • Google Cloud SDK (gcloud)

Step 1: Setting Up the Environment with Security in Mind

Creating a LaunchDarkly API Token

First, generate an API token that Terraform will use to manage your LaunchDarkly resources:

  • Log in to LaunchDarkly
  • Go to Account/Organization Settings > Authorization
  • Click “Create token”
  • Give it a name like “Terraform GitOps”
  • Make sure it has “Writer” permissions
  • Copy and save the token securely

Setting Up Google Cloud Storage for Terraform State

To do GitOps with Terraform, we need a reliable, shared location to store Terraform state. We’ll use Google Cloud Storage with OIDC authentication for security:

#!/bin/bash
# === Set up GCS for Terraform state with OIDC authentication ===
# Set your variables
export GCP_PROJECT_ID="your-gcp-project-id"
export GITHUB_ORG="your-github-org"
export GITHUB_REPO="your-github-repo"
export TF_STATE_BUCKET="terraform-launchdarkly-gitops-state-$GCP_PROJECT_ID"
# Authenticate with Google Cloud if needed
gcloud auth login
# Enable required APIs
gcloud services enable iamcredentials.googleapis.com --project=$GCP_PROJECT_ID
gcloud services enable iam.googleapis.com --project=$GCP_PROJECT_ID
gcloud services enable cloudresourcemanager.googleapis.com --project=$GCP_PROJECT_ID
# Create bucket with versioning enabled
gcloud storage buckets create gs://$TF_STATE_BUCKET \
--project=$GCP_PROJECT_ID \
--location=us-central1 \
--uniform-bucket-level-access 2>/dev/null || echo "Bucket already exists"
gcloud storage buckets update gs://$TF_STATE_BUCKET --versioning
# Create service account
gcloud iam service-accounts create terrateam-state \
--display-name="Terrateam Terraform State Access" \
--project=$GCP_PROJECT_ID 2>/dev/null || echo "Service account already exists"
# Grant storage admin permissions to the bucket
gcloud storage buckets add-iam-policy-binding gs://$TF_STATE_BUCKET \
--member="serviceAccount:terrateam-state@$GCP_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.admin"
# Create Workload Identity Pool
gcloud iam workload-identity-pools create "github-pool" \
--project=$GCP_PROJECT_ID \
--location="global" \
--display-name="GitHub Actions" 2>/dev/null || echo "Workload identity pool already exists"
# Create the identity provider
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
--project=$GCP_PROJECT_ID \
--location="global" \
--workload-identity-pool="github-pool" \
--display-name="GitHub Actions" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository_owner=='$GITHUB_ORG'" \
--issuer-uri="https://token.actions.githubusercontent.com" 2>/dev/null || echo "Workload identity provider already exists"
# Get the full workload identity pool ID
POOL_ID=$(gcloud iam workload-identity-pools describe "github-pool" \
--project=$GCP_PROJECT_ID \
--location="global" \
--format="value(name)")
# Create service account binding
gcloud iam service-accounts add-iam-policy-binding \
"terrateam-state@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--project=$GCP_PROJECT_ID \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/${POOL_ID}/attribute.repository=${GITHUB_ORG}/${GITHUB_REPO}" 2>/dev/null || echo "IAM binding already exists"

GitOps Best Practice: OIDC authentication eliminates the need to store long-lived service account keys in your repository, improving security.

Initialize a Git Repository

Create a Git repository to store your Terraform configuration:

Terminal window
mkdir launchdarkly-terraform-gitops
cd launchdarkly-terraform-gitops
git init

Step 2: Setting Up Terrateam with Access Controls

Terrateam connects your GitHub workflow to Terraform, automating operations when pull requests are created, updated, or merged. Importantly, it provides fine-grained access control to ensure only authorized team members can change sensitive environments.

Setting Up Terrateam

  1. Visit https://terrateam.io to sign up
  2. Install the Terrateam GitHub App from the dashboard
  3. Select the GitHub organization or account where your repository is located
  4. Choose the repository you want to use with Terrateam and click on “Setup” to configure Terrateam.

Creating Configuration Files

Create a .github/workflows directory and copy the terrateam.yml file to it.

Next, create the Terrateam configuration file:

Terminal window
mkdir -p .terrateam
cat > .terrateam/config.yml << 'EOF'
# Automatically run terraform apply when a pull request is merged
when_modified:
autoapply: true
# Disable cost estimation for feature flag changes (not needed for LaunchDarkly)
cost_estimation:
enabled: false
# Custom workflow steps for Terraform operations
workflows:
- tag_query: ""
plan:
- type: oidc
provider: gcp
service_account: "terrateam-state@{GCP_PROJECT_ID}.iam.gserviceaccount.com"
workload_identity_provider: "projects/{GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
- type: init
- type: plan
apply:
- type: oidc
provider: gcp
service_account: "terrateam-state@{GCP_PROJECT_ID}.iam.gserviceaccount.com"
workload_identity_provider: "projects/{GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
- type: init
- type: apply
EOF

Note: Replace {GCP_PROJECT_ID} and {GCP_PROJECT_NUMBER} with your actual values.

This configuration:

  • Automatically applies changes when pull requests are merged
  • Only triggers on relevant pull requests
  • Disables cost estimation (not needed for feature flags)
  • Configures custom workflow steps with GCP connectivity for Terraform State

Setting Up GitHub Secrets

Terraform needs credentials to access LaunchDarkly:

  1. Go to your GitHub repository → Settings → Secrets and variables → Actions
  2. Add the following secret:
    • LAUNCHDARKLY_ACCESS_TOKEN: The API token you created earlier

Committing the Initial Configuration

Commit and push the initial configuration:

Terminal window
git add .github/workflows/terrateam.yml .terrateam/config.yml
git commit -m "Initial Terrateam configuration"
git push origin main

Step 3: Defining LaunchDarkly Infrastructure with Terraform

Now we’ll define the LaunchDarkly resources using Terraform. These configuration files describe the desired state of your feature flags.

Creating Basic Terraform Configuration Files

First, define the provider requirements:

Terminal window
cat > versions.tf << 'EOF'
terraform {
required_providers {
launchdarkly = {
source = "launchdarkly/launchdarkly"
version = "~> 2.0"
}
}
required_version = ">= 1.0.0"
}
EOF

Configure the backend for state storage:

Terminal window
cat > backend.tf << EOF
terraform {
backend "gcs" {
bucket = "${TF_STATE_BUCKET}"
prefix = "terraform/state"
}
}
EOF

Set up the LaunchDarkly provider:

Terminal window
cat > providers.tf << 'EOF'
# LaunchDarkly provider configuration
provider "launchdarkly" {
# Credentials are provided via environment variables:
# LAUNCHDARKLY_ACCESS_TOKEN
}
EOF

Defining LaunchDarkly Resources

Create the base project configuration, which defines two environments, development and production:

Terminal window
cat > main.tf << 'EOF'
# Variables
variable "project_key" {
description = "LaunchDarkly project key"
type = string
}
# LaunchDarkly project
resource "launchdarkly_project" "example" {
key = var.project_key
name = "Example Project"
tags = ["managed-by-terraform"]
# Define environments within the project
environments {
key = "production"
name = "Production"
color = "EEEEEE"
tags = ["managed-by-terraform"]
}
environments {
key = "development"
name = "Development"
color = "7B42FF"
tags = ["managed-by-terraform"]
}
}
# Feature Flag Definition
resource "launchdarkly_feature_flag" "new_checkout_experience" {
project_key = launchdarkly_project.example.key
key = "new-checkout-experience"
name = "New Checkout Experience"
description = "Enable the new checkout UI"
variation_type = "boolean"
variations {
value = true
name = "Enabled"
description = "The new checkout experience"
}
variations {
value = false
name = "Disabled"
description = "The old checkout experience"
}
defaults {
on_variation = 0 # Use the "Enabled" variation when targeting is on
off_variation = 1 # Use the "Disabled" variation when targeting is off
}
tags = ["managed-by-terraform", "checkout", "ui"]
}
# Development environment flag settings
resource "launchdarkly_feature_flag_environment" "new_checkout_dev" {
flag_id = launchdarkly_feature_flag.new_checkout_experience.id
env_key = "development"
on = true
# Always enabled in development
fallthrough {
variation = 0
}
off_variation = 1
}
# Production environment flag settings
resource "launchdarkly_feature_flag_environment" "new_checkout_prod" {
flag_id = launchdarkly_feature_flag.new_checkout_experience.id
env_key = "production"
on = true
# Initially disabled in production
fallthrough {
variation = 1
}
# Beta testers get the feature enabled
rules {
description = "Beta testers"
clauses {
attribute = "email"
op = "endsWith"
values = ["@example.com"]
}
variation = 0
}
off_variation = 1
}
EOF

Create default values files:

Terminal window
cat > terraform.tfvars << 'EOF'
project_key = "example-project"
EOF

Commit your Terraform configuration:

Terminal window
git commit -am "Initial Terraform configuration"
git push origin main

Step 4: Managing Feature Flags Through Pull Requests

Now let’s deploy our LaunchDarkly resources through a pull request.

Creating Your First Pull Request

Create a feature branch for your initial deployment:

Terminal window
git checkout -b initial-deploy

Make a change to differentiate this branch from main. For example, you could modify the project key in terraform.tfvars:

Terminal window
cat > terraform.tfvars << 'EOF'
project_key = "my-project"
EOF

Commit and push your changes:

Terminal window
git add terraform.tfvars
git commit -m "Configure initial LaunchDarkly feature flag deployment"
git push -u origin initial-deploy

Create a pull request on GitHub:

  1. Navigate to your repository
  2. Click “Compare & pull request”
  3. Give your PR a descriptive title like “Initial LaunchDarkly Flag Deployment”
  4. Submit the pull request

Once the PR is created, Terrateam will automatically run terraform plan and post the results as a comment. Terrateam Plan Output

After the PR is reviewed, merge it. Terrateam will automatically apply the changes (due to our autoapply: true setting). This will trigger the workflow on the main branch. Terrateam In Progress

Terrateam will add more comments to show that the apply was successful:

Terrateam Apply

At this point, you can check your LaunchDarkly dashboard to see the project deployed as configured:

Launch Darkly Environments Launch Darkly Environments

Rolling Out a Feature Gradually

Now, let’s say you want to roll out the new checkout experience to production users gradually. Create a new branch for this change:

Terminal window
git checkout -b prod-rollout

Modify the new_checkout_prod resource in the main.tf file as follows:

# Production environment flag settings
resource "launchdarkly_feature_flag_environment" "new_checkout_prod" {
flag_id = launchdarkly_feature_flag.new_checkout_experience.id
env_key = "production"
on = true
# Roll out to 10% of users
fallthrough {
rollout_weights = [10000, 90000] # 10% enabled, 90% disabled (in thousandths of a percent)
}
# Beta testers always get the new experience
rules {
description = "Beta testers"
clauses {
attribute = "email"
op = "endsWith"
values = ["@example.com"]
}
variation = 0
}
off_variation = 1
}

Commit the changes:

Terminal window
git add main.tf
git commit -m "Roll out new checkout experience to 10% of production users"
git push -u origin prod-rollout

main-tf diff

Create a pull request for this change. Terrateam will run the plan and show you exactly how the flag configuration will change. In this case, it will show an in-place update:

Terrateam Plan Output 2

After review, merge the PR, or comment with a terraform apply, and Terrateam will automatically apply the changes. LaunchDarkly will immediately update the flag settings without requiring a code deployment.

Terrateam Apply Output 2

You should be able to see the change in LaunchDarkly:

Launch Darkly New Checkout Experience

Conclusion

By managing LaunchDarkly feature flags with Terraform and Terrateam, you’ve integrated them into a GitOps workflow that provides the same level of care, governance, and automation as your application code and infrastructure.

This approach offers several benefits:

  • Single source of truth: All flag configurations are defined in your Git repository.
  • Version control: Every flag change is tracked with a complete history.
  • Peer review: Changes undergo the same review process as your application code.
  • Automated deployment: Flag changes are automatically deployed after approval.
  • Consistent environments: Flag configurations stay in sync across environments.
  • Quick rollbacks: You can revert changes by simply reverting your PR.

Most importantly, by treating feature flags as code, you acknowledge their critical role in your application architecture. Feature flags are too important to manage through ad-hoc processes in the UI. With this GitOps approach, your flags become a first-class citizen in your development workflow, with the same rigor, visibility, and controls as the rest of your system.

As your system grows, you can extend this approach with more complex flag configurations, custom rules, and even integration with your CI/CD pipelines to coordinate code and flag deployments.

GitOps-First Infrastructure as Code

Ready to get started?

Build, manage, and deploy infrastructure with GitHub pull requests.