Terraform Quickstart: Part 5 - Automate Terraform with GitHub Actions
Use GitHub Actions to Automate Your Terraform Workflow and Resource Management
Introduction
So far, you’ve used Terraform to manage GitHub repositories manually from the command line. But running Terraform by hand every time you make a change takes time and can lead to mistakes. In this chapter, you’ll learn how to use GitHub Actions to automate your Terraform workflow.
What are GitHub Actions?
GitHub Actions is a built-in CI/CD (Continuous Integration/Continuous Delivery) tool provided by GitHub. It lets you automate development workflows—like building, testing, deploying applications, and even managing infrastructure.
You define workflows using YAML files stored in your repository. These workflows are version-controlled and easy to share with your team. They can run automatically when certain events happen, such as commits, pull requests, or on a schedule.
Repository Management at Scale with Terraform GitHub Provider
Organizations with many repositories use Terraform to create and configure them consistently. They can apply standardized settings, branch protection rules, and team access automatically. This helps enforce organizational policies without manual work.
In this chapter, you’ll build a real-world automation workflow using Terraform and GitHub Actions. Here’s what you’ll automate:
Creating and configuring a GitHub repository with Terraform.
Automatically adding a
CODEOWNERS
file and setting branch protection.Using GitHub Actions to run Terraform automatically when changes are pushed.
We can reuse the main.tf
from the previous chapter, and enhance it with more instructions. Read the comments for more information on each step.
terraform {
required_providers {
github = {
source = "integrations/github"
version = "~> 5.0"
}
}
}
provider "github" {
token = var.github_token
owner = var.github_owner
}
# Define standard repository settings using a local variable
locals {
standard_topics = ["managed-by-terraform", "organization-standard"]
# Standard repository configuration settings
standard_settings = {
visibility = "public"
has_issues = true
has_projects = true
has_wiki = true
allow_merge_commit = true
allow_squash_merge = true
allow_rebase_merge = true
delete_branch_on_merge = true
vulnerability_alerts = true
auto_init = true # Initialize with README
}
# Standard branch protection settings
standard_branch_protection = {
required_approving_review_count = 1
dismiss_stale_reviews = true
require_code_owner_reviews = false
require_linear_history = true
allow_force_pushes = false
allow_deletions = false
}
# List of repositories to create with their specific customizations
repositories = {
"service-a" = {
description = "Microservice A for customer data processing"
topics = ["microservice", "customer-data"]
template = null
},
"service-b" = {
description = "Microservice B for order processing"
topics = ["microservice", "order-processing"]
template = null
},
"frontend-app" = {
description = "Main frontend application"
topics = ["frontend", "react"]
template = null
},
"documentation" = {
description = "Central documentation repository"
topics = ["docs", "markdown"]
template = null
},
"shared-libraries" = {
description = "Shared code libraries and utilities"
topics = ["library", "shared-code"]
template = null
}
}
}
# Create repositories with standard settings and customizations
resource "github_repository" "repos" {
for_each = local.repositories
name = each.key
description = each.value.description
# Merge standard settings with any repo-specific overrides
visibility = local.standard_settings.visibility
has_issues = local.standard_settings.has_issues
has_projects = local.standard_settings.has_projects
has_wiki = local.standard_settings.has_wiki
allow_merge_commit = local.standard_settings.allow_merge_commit
allow_squash_merge = local.standard_settings.allow_squash_merge
allow_rebase_merge = local.standard_settings.allow_rebase_merge
delete_branch_on_merge = local.standard_settings.delete_branch_on_merge
vulnerability_alerts = local.standard_settings.vulnerability_alerts
auto_init = local.standard_settings.auto_init
# Apply template if specified
dynamic "template" {
for_each = each.value.template != null ? [each.value.template] : []
content {
owner = template.value.owner
repository = template.value.name
}
}
# Combine standard topics with repo-specific topics
topics = concat(local.standard_topics, each.value.topics)
}
# Apply standard branch protection to all repositories
resource "github_branch_protection" "main_branch" {
for_each = github_repository.repos
repository_id = each.value.name
pattern = "main"
required_pull_request_reviews {
required_approving_review_count = local.standard_branch_protection.required_approving_review_count
dismiss_stale_reviews = local.standard_branch_protection.dismiss_stale_reviews
require_code_owner_reviews = local.standard_branch_protection.require_code_owner_reviews
}
allows_force_pushes = local.standard_branch_protection.allow_force_pushes
allows_deletions = local.standard_branch_protection.allow_deletions
}
# Create a .github/CODEOWNERS file in each repository
resource "github_repository_file" "codeowners" {
for_each = github_repository.repos
repository = each.value.name
branch = "main"
file = ".github/CODEOWNERS"
content = "# Default code owners for this repository\n* @${var.github_owner}/admins\n"
commit_message = "Add CODEOWNERS file"
commit_author = "Terraform"
commit_email = "terraform@example.com"
# Wait for repository initialization
depends_on = [github_repository.repos]
}
This time, Terraform will create multiple repositories with additional settings like CODEOWNERS, README.md, main branch protection, Wiki pages, etc.
Change this output.tf
to this:
# Output the repository URLs
output "repository_urls" {
value = {
for name, repo in github_repository.repos : name => repo.html_url
}
}
You can reuse the terraform.tf
and the variables.tf
files from the previous chapter.
It’s always a good idea to test the resource creation manually before pushing it to GitHub.
Run terraform apply
. Open the URL from the output.
For example, you should see something like this:
Now that we know everything works as expected, we can destroy the resources and start the automation. Execute terraform destroy
to get rid of the newly created repos.
Configuring the Automation
Create a new repository
repo-management-demo
to host the Terraform config and clone it:
git clone https://github.com/your-username/repo-management-demo.git
cd repo-management-demo
Create the directory structure:
mkdir -p terraform .github/workflows
Add the
.tf
filesterraform
directory.
Don’t forget to add
terraform.tfvars
to.gitingore
.
Create the following GitHub Actions workflow in your repository at .github/workflows/terraform.yml
:
name: Terraform CI/CD Automation
on:
push:
branches: [main]
paths:
- 'terraform/**'
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform
env:
TF_VAR_github_token: ${{ secrets.GH_TERRAFORM_TOKEN }}
TF_VAR_github_owner: ${{ github.repository_owner }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Terraform CLI
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.6
- name: Terraform Initialization
run: terraform init
- name: Terraform Plan
run: terraform plan -input=false
- name: Terraform Apply
run: terraform apply -auto-approve -input=false
Since GitHub Actions doesn’t know the values for the secret variables, we have to add them manually through the UI:
Setting Secrets for GitHub Actions
Go to your GitHub repository
Navigate to "Settings" > "Secrets and variables" > "Actions"
Click "New repository secret"
Add your GitHub token as a secret named
TF_VAR_github_token
github.repository_owner
is a built-in GitHub Actions variable, providing the username or organization name automatically.
When you push to the main branch, Terraform will run the workflow automatically and create the repositories.
If you try to update the main.tf
, you’ll see an error: in the pipeline
Error: 422 Repository creation failed. [name already exists on this account]
This occurs because Terraform lost track of the infrastructure it previously created. Terraform didn't recognize the existing repositories because the Terraform state file wasn't preserved between pipeline runs. We’ll talk about the state file in the next chapter.
Conclusion
In this chapter, you automated Terraform with GitHub Actions and learned best practices for managing secrets in your workflow.
Next, we’ll move on to more advanced topics like managing Terraform state. You’ll also start working with other providers beyond GitHub.