GitOps for Feature Flags with LaunchDarkly and Terraform

Malcolm Matalka

On this page
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 variablesexport 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 neededgcloud auth login
# Enable required APIsgcloud services enable iamcredentials.googleapis.com --project=$GCP_PROJECT_IDgcloud services enable iam.googleapis.com --project=$GCP_PROJECT_IDgcloud services enable cloudresourcemanager.googleapis.com --project=$GCP_PROJECT_ID
# Create bucket with versioning enabledgcloud 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 accountgcloud 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 bucketgcloud 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 Poolgcloud 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 providergcloud 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 IDPOOL_ID=$(gcloud iam workload-identity-pools describe "github-pool" \ --project=$GCP_PROJECT_ID \ --location="global" \ --format="value(name)")
# Create service account bindinggcloud 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:
mkdir launchdarkly-terraform-gitopscd launchdarkly-terraform-gitopsgit 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
- Visit https://terrateam.io to sign up
- Install the Terrateam GitHub App from the dashboard
- Select the GitHub organization or account where your repository is located
- 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:
mkdir -p .terrateam
cat > .terrateam/config.yml << 'EOF'# Automatically run terraform apply when a pull request is mergedwhen_modified: autoapply: true
# Disable cost estimation for feature flag changes (not needed for LaunchDarkly)cost_estimation: enabled: false
# Custom workflow steps for Terraform operationsworkflows: - 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: applyEOF
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:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Add the following secret:
LAUNCHDARKLY_ACCESS_TOKEN
: The API token you created earlier
Committing the Initial Configuration
Commit and push the initial configuration:
git add .github/workflows/terrateam.yml .terrateam/config.ymlgit 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:
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:
cat > backend.tf << EOFterraform { backend "gcs" { bucket = "${TF_STATE_BUCKET}" prefix = "terraform/state" }}EOF
Set up the LaunchDarkly provider:
cat > providers.tf << 'EOF'# LaunchDarkly provider configurationprovider "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:
cat > main.tf << 'EOF'# Variablesvariable "project_key" { description = "LaunchDarkly project key" type = string}
# LaunchDarkly projectresource "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 Definitionresource "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 settingsresource "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 settingsresource "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:
cat > terraform.tfvars << 'EOF'project_key = "example-project"EOF
Commit your Terraform configuration:
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:
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
:
cat > terraform.tfvars << 'EOF'project_key = "my-project"EOF
Commit and push your changes:
git add terraform.tfvarsgit commit -m "Configure initial LaunchDarkly feature flag deployment"git push -u origin initial-deploy
Create a pull request on GitHub:
- Navigate to your repository
- Click “Compare & pull request”
- Give your PR a descriptive title like “Initial LaunchDarkly Flag Deployment”
- Submit the pull request
Once the PR is created, Terrateam will automatically run terraform plan
and post the results as a comment.
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 will add more comments to show that the apply was successful:
At this point, you can check your LaunchDarkly dashboard to see the project deployed as configured:
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:
git checkout -b prod-rollout
Modify the new_checkout_prod
resource in the main.tf
file as follows:
# Production environment flag settingsresource "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:
git add main.tfgit commit -m "Roll out new checkout experience to 10% of production users"git push -u origin prod-rollout
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:
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.
You should be able to see the change in LaunchDarkly:
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.