How to create an Automated Testing environment with Terraform and GitHub Actions
This tutorial shows how to create a pipeline that uses Terraform and GitHub Actions to spin up an ephemeral test environment and automatically tear it down. Useful to test infrastructure changes without polluting the Azure/AWS account and to reduce costs.
Prerequisites
- Azure or AWS account with permissions to create resources (or a local environment like Docker for a simplified example).
- GitHub repository with permissions for GitHub Actions.
- Terraform installed locally to test and validate the files.
- Basic knowledge of Terraform and GitHub Actions.
Step 1: Concept and file organization
Explains the why: an ephemeral environment is created per branch/pull request, used for testing and removed on merge/close. We will separate the Terraform configuration and the GitHub Actions workflow.
# Estrutura mínima do repositório
/
├─ terraform/
│ ├─ main.tf
│ ├─ variables.tf
│ └─ outputs.tf
└─ .github/workflows/terraform-testing.yml
Step 2: Minimal Terraform for example environment
Here is a simple example that creates a VM/instance or, to avoid cloud complexity, a local resource (e.g.: null_resource). Use this to validate create/destroy logic.
# terraform/main.tf
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}
provider "null" {}
resource "null_resource" "test_env" {
triggers = {
branch = var.branch_name
}
}
output "env_id" {
value = null_resource.test_env.id
}
# terraform/variables.tf
variable "branch_name" {
type = string
default = "local"
}
Step 3: GitHub Actions workflow to create and destroy
The workflow runs terraform init/plan/apply when opening a pull request and runs destroy when the PR is closed or when the branch is removed. It uses a unique naming per branch.
# .github/workflows/terraform-testing.yml
name: Terraform Testing Env
on:
pull_request:
types: [opened, reopened, synchronize]
pull_request_target:
types: [closed]
jobs:
apply:
if: github.event_name == 'pull_request' && github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: '1.5.0'
- name: Terraform Init
working-directory: terraform
run: terraform init -input=false
- name: Terraform Apply
working-directory: terraform
env:
TF_VAR_branch_name: ${{ github.head_ref }}
run: terraform apply -auto-approve -input=false
destroy:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: '1.5.0'
- name: Terraform Init
working-directory: terraform
run: terraform init -input=false
- name: Terraform Destroy
working-directory: terraform
env:
TF_VAR_branch_name: ${{ github.head_ref }}
run: terraform destroy -auto-approve -input=false
Step 4: State and secrets management
If using real cloud resources, never store state locally on the runner. Configure a remote backend (e.g.: Azure Storage, S3) and use GitHub secrets for credentials. Quick example for an S3 backend:
# Adicionar ao terraform/main.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "envs/${var.branch_name}.tfstate"
region = "eu-west-1"
}
}
Step 5: Common errors and how to avoid them
Main issues: state conflicts when multiple runs use the same key, lack of permissions for the runner account, forgetting to destroy abandoned branches. Solutions: backend per branch, roles/minimal responsibility associations, periodic cleanup with a cron script.
Verify the result
When you open a pull request you will see the 'Terraform Testing Env' job run and apply the resources. Confirm the env_id in the job outputs. When closing the PR, verify that the destroy job ran and that the resource was removed (or that the corresponding key does not exist in the state backend).
Conclusion
You now have a basic flow to create ephemeral environments with Terraform and GitHub Actions: create on PR, test and destroy on close. Next steps: replace null_resource with real resources (VM, RDS, Storage), add automated tests that run against the environment and support state locks. Tip: start by testing locally with terraform plan and then simulate the runner using act to validate the workflow.