GitOps for Heroku Using Terraform and Terrateam
In this guide, we'll implement a GitOps workflow for Heroku using Terraform and Terrateam. Terraform is the industry-standard tool for defining infrastructure as code, while Terrateam adds GitOps automation to Terraform workflows.
You'll learn how to:
- Set up your environment for Terraform and Terrateam
- Defe Heroku infrastructure as code
- Implement a GitOps workflow for infrastructure changes
- Manage Heroku applications using pull requests
- Handle rollbacks through Git
- Configure advanced GitOps controls with Terrateam
Prerequisites
This guide requires:
- A Heroku account with admin access
- A GitHub account with repository admin access
- Google Cloud Platform account (for Terraform state storage)
- The following tools installed on your local machine:
- Git
- Terraform (v1.0.0 or later)
- Heroku CLI
- Google Cloud SDK (gcloud)
We'll assume you have basic familiarity with Git, GitHub, and command-line operations.
Step 1: Setting up your environment
Creating a Heroku authorization token
First, we need to generate a Heroku API token that Terraform will use to authenticate and manage your Heroku resources.
# Log in to Heroku
heroku login
# Create a token for Terraform
heroku authorizations:create --description "Terraform GitOps"
The output will include a token that looks something like:
Creating OAuth Authorization... done
Client: <none>
ID: 35d32c62-5958-4830-99cf-52adda6b76ea
Description: Terraform GitOps
Scope: global
Token: HRKU-059efc2d-2ee1-4ebc-ab96-7a1383a7bb0f
Updated at: Wed Apr 09 2025 16:49:37 GMT+0100 (GMT+01:00) (less than a minute ago)
Save this token securely as we'll use it later when setting up GitHub Secrets.
Setting up Google Cloud Storage for Terraform State
An important aspect of GitOps is maintaining a consistent, accessible state for your infrastructure. For Terraform, this means using a remote backend rather than local state files.
This script sets up GCP resources needed for secure Terraform state management with Terrateam using OIDC authentication. It creates a GCS bucket for storing Terraform state files, a service account with appropriate permissions, and configures workload identity federation between GitHub Actions and GCP. Make sure to replace the placeholder variables with your actual GCP project ID and GitHub organization/repository information before running.
#!/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-heroku-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 (if not already created)
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 (if not already created)
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)")
# Get the full workload identity provider path
WORKLOAD_IDENTITY_PROVIDER=$(gcloud iam workload-identity-pools providers describe "github-provider" \
--project=$GCP_PROJECT_ID \
--location="global" \
--workload-identity-pool="github-pool" \
--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}/*" 2>/dev/null || echo "IAM binding already exists"
GitOps Best Practice: Using OIDC authentication instead of long-lived service account keys improves security by eliminating the need to store sensitive credentials in your repository or CI/CD system.
Initialize a Git Repository
Now create a Git repository to store the Terraform configuration:
mkdir heroku-terraform-gitops
cd heroku-terraform-gitops
git init
Step 2: Configuring Terrateam
Terrateam is the GitOps automation layer that connects your GitHub workflow to Terraform. It automatically runs Terraform when pull requests are created, updated, or merged.
Setting up Terrateam
- Visit terrateam.io to sign up for a Terrateam account
- 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
Click on "Setup" to configure Terrateam:

Creating configuration files
Create the GitHub workflow file shown in Step 1. Copy it to to .github/workflows/terrateam.yml
.

Next, create the Terrateam configuration file. Make sure to use your own GCP_PROJECT_ID
and GCP_PROJECT_NUMBER
values. If you haven’t configured GCP workload identity, refer to the first step of this guide.
Note that we’re using the Terrateam’s OpenID Connect integration features to authenticate and authorize Terraform operations against GCP.
mkdir -p .terrateam
cat > .terrateam/config.yml << 'EOF'
# Automatically run terraform apply when a pull request is merged
when_modified:
autoapply: true
# Use environment variables for GCP authentication
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
This configuration tells Terrateam to automatically run terraform apply when pull requests are merged and configures Google Cloud Storage for state storage.
GitOps Best Practice: The autoapply: true setting ensures that approved changes are automatically applied when merged, maintaining the principle that Git is the source of truth.
Setting up Github Secrets
Terraform needs credentials to access Heroku, but we don't want to store these in our code repository. Instead, we'll use GitHub Secrets:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Add the following secrets, you can get these values from your Heroky account settings:
HEROKU_API_KEY
: The Heroku authorization token you created earlierHEROKU_EMAIL
: Your Heroku account email address

Committing the initial configuration
Let's commit and push our initial configuration to the main branch:
git add .github/workflows/terrateam.yml .terrateam/config.yml
git commit -m "Initial Terrateam configuration"
git push origin main
Step 3: Defining Heroku infrastructure with Terraform
Now we'll define our Heroku infrastructure using Terraform. These configuration files describe the desired state of our infrastructure.
Creating basic Terraform configuration files
First, define the provider requirements and versions:
cat > versions.tf << 'EOF'
terraform {
required_providers {
heroku = {
source = "heroku/heroku"
version = "~> 5.0"
}
}
required_version = ">= 1.0.0"
}
EOF
Next, configure the backend for state storage. Make sure to use your actual GCS bucket value.
cat > backend.tf << EOF
terraform {
backend "gcs" {
bucket = "${TF_STATE_BUCKET}"
prefix = "terraform/state"
}
}
EOF
Set up the Heroku provider:
cat > providers.tf << 'EOF'
# Heroku provider configuration
provider "heroku" {
# Credentials are provided via environment variables:
# HEROKU_API_KEY and HEROKU_EMAIL
}
EOF
Defining a Heroku Resources
Create the main configuration file that defines the Heroku application and its resources:
cat > main.tf << 'EOF'
# Variables
variable "app_name" {
description = "Name of the Heroku application"
type = string
}
variable "region" {
description = "Heroku region"
type = string
default = "us"
}
variable "heroku_team" {
description = "Heroku team name (optional)"
type = string
default = null
}
# Heroku application
resource "heroku_app" "app" {
name = var.app_name
region = var.region
# If a team is specified, create the app in that team
dynamic "organization" {
for_each = var.heroku_team != null ? [1] : []
content {
name = var.heroku_team
}
}
}
# Postgres database add-on
resource "heroku_addon" "database" {
app_id = heroku_app.app.id
plan = "heroku-postgresql:essential-0"
}
# Variables for deployment
variable "github_repo" {
description = "GitHub repository URL containing the application code"
type = string
default = null
}
variable "github_branch" {
description = "GitHub branch to deploy"
type = string
default = "main"
}
# Deploy application from GitHub if repo is specified
resource "heroku_build" "app" {
count = var.github_repo != null ? 1 : 0
app_id = heroku_app.app.id
source {
url = "${var.github_repo}/archive/refs/heads/${var.github_branch}.tar.gz"
version = var.github_branch
}
}
# Configure dyno formation for the app
resource "heroku_formation" "web" {
count = var.github_repo != null ? 1 : 0
app_id = heroku_app.app.id
type = "web"
quantity = 1
size = "basic"
# Ensure the build is complete before scaling
depends_on = [heroku_build.app]
}
# Variables for add-ons
variable "enable_logging" {
description = "Enable Papertrail logging add-on"
type = bool
default = false
}
variable "enable_metrics" {
description = "Enable Metrics add-on"
type = bool
default = false
}
# Papertrail logging add-on
resource "heroku_addon" "logging" {
count = var.enable_logging ? 1 : 0
app_id = heroku_app.app.id
plan = "papertrail:choklad"
}
# Metrics add-on
resource "heroku_addon" "metrics" {
count = var.enable_metrics ? 1 : 0
app_id = heroku_app.app.id
plan = "heroku-metrics:hobby-dev"
}
EOF
Committing your Configuration
Commit and push your Terraform configuration:
git add versions.tf backend.tf providers.tf main.tf terraform.tfvars
git commit -m "Add Terraform configuration for Heroku infrastructure"
git push origin main
Step 4: Managing infrastructure through pull requests
Now that we've configured our GitOps environment, let's deploy our initial Heroku infrastructure through a pull request.
Creating your first Pull Request
In GitOps, even the initial deployment follows the pull request workflow:
Create a feature branch for your initial deployment:
git checkout -b initial-deployment
Create a terraform.tfvars file to provide default values:
cat > terraform.tfvars << 'EOF'
app_name = "terraform-heroku-gitops-demo"
region = "us"
# Uncomment and set these as needed
heroku_team = null # Explicitly set to null to create a personal app
# github_repo = "https://github.com/username/heroku-terraform-gitops.git"
# github_branch = "main"
EOF
Note that heroku_team
is set to null
. If you have a Heroku team defined, you can set it here.
Commit your changes:
git add terraform.tfvars
git commit -m "Configure initial Heroku application deployment"
git push -u origin initial-deployment
Create a pull request on GitHub:
- Navigate to your repository
- Click "Compare & pull request"
- Give your PR a descriptive title like "Initial Heroku Infrastructure Deployment"
- Add details about what resources will be created
- Submit the pull request
Once you create the pull request, Terrateam automatically runs terraform plan and posts the results as a comment on your PR. This gives you and your team visibility into exactly what infrastructure will be created.

Applying Changes
You have two options to apply the changes:
- Comment terrateam apply on the PR to apply before merging
- Merge the PR, and Terrateam will automatically apply the changes (due to our
autoapply: true
setting)
Once applied, you can verify that the resources have been deployed to Heroku:

You can verify that the resources have been deployed to Heroku:

GitOps Best Practice: Review the plan output carefully before approving changes. The plan shows exactly what will be created, modified, or destroyed.
Step 5: Configuring Role-Based Access Control with Terrateam
Now that we have our basic GitOps workflow functioning with Terrateam, let's explore one more configuration option that can make your workflow more efficient and secure. We'll focus on access control to restrict who can plan and apply changes to your infrastructure.
In a team environment, it's essential to control who can make changes to your infrastructure. Terrateam's access control policies allow you to define permissions based on GitHub teams, individual users, or repository roles.
If you're using GitHub teams for access control, you'll need to set them up first:
- Go to your GitHub organization
- Click on "Teams" in the top navigation
- Click "New team"
- Create teams like "infrastructure" and "infrastructure-admins"
- Add appropriate team members
Once that’s done, create a new branch for your configuration changes:
git checkout -b add-access-control
Update your .terrateam/config.yml
file to include access control policies. Make sure you replace YOUR_GITHUB_USERNAME
with your actual username.
# Add access control policies
access_control:
policies:
# Default policy for all directories
- tag_query: ""
# Anyone can run plan
plan: ["*"]
# Only specific team members can apply changes
apply: ["team:infrastructure", "user:YOUR_GITHUB_USERNAME"]
This configuration restricts operations to members of the “infrastructure” team and your user.
Once you have this file ready, commit and push to your branch.
git add .terrateam/config.yml
git commit -m "Add Terrateam configuration with access control rules"
git push -u origin add-access-control
As usual, the next step step is to open a pull request to allow for a Terrateam run:
- Go to your repository on GitHub
- You should see a prompt to create a pull request for your recently pushed branch
- Click "Compare & pull request"
- Title your PR "Add Access Control Terrateam configuration"
- Submit the pull request
Once the PR is created, Terrateam will run a plan operation automatically. Since the changes only affect Terrateam's configuration, there should be no infrastructure changes.
Since you added your username to the apply policy, you should be able to trigger an apply operation:
- Go to your pull request
- Look for the Terrateam plan comment
- Comment terrateam apply on the PR
To test the access restrictions, create a new user or ask a colleague who is not in the "infrastructure" team to comment terrateam apply on the PR. They should receive a permission denied message.
GitOps Best Practice: Configure your GitOps tools to be precise about what triggers infrastructure changes. This reduces unnecessary operations and makes your workflow more efficient.
Conclusion
GitOps brings the best practices of software development to infrastructure management. By implementing GitOps for your Heroku resources using Terraform and Terrateam, you've gained:
- A declarative approach to infrastructure
- Version control for all infrastructure changes
- A peer review process for quality assurance
- Automated application of approved changes
- Simplified rollbacks when needed
- Enhanced security through access controls
The GitOps approach scales well as your infrastructure grows, and provides a consistent, auditable, and secure way to manage your cloud resources.