When working with Infrastructure as Code (IaC), the goal is to automate everything to reduce manual intervention, ensure consistency, and eliminate human errors. Terraform already gives us declarative infrastructure management, but without a CI/CD pipeline, we still rely on manually running terraform plan
and terraform apply
, which leaves room for mistakes.
So, I decided to integrate Terraform into a GitHub Actions CI/CD pipeline to make it truly automated. While doing this, I also wanted an easy way to destroy the environment when I no longer needed it without manually running terraform destroy
.
I know, I know. Terraform destroy in a CI/CD pipeline sounds dangerous. If misconfigured, it could wipe out production infrastructure in one bad push. Not ideal. But in my case, I was experimenting with hosting a static website on AWS using S3 and CloudFront, and I needed an automated way to clean up environments when I was done.
So, here's how I automated Terraform deployments AND controlled when to destroy my environment safely.
Step 1: Detecting Infrastructure Changes
All my infrastructure code lived in an infrastructure/
folder, so I needed my pipeline to run Terraform only when changes were made to this folder. Instead of running Terraform on every push, I used the dorny/path-filter
GitHub Action. This action checks which files changed in a push and sets an output variable indicating whether Terraform should run:
- name: Detect changed files
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
infrastructure:
- "infrastructure/**"
Now, Terraform only runs when something changes inside the infrastructure/
folder.
Step 2: Deciding Whether to Apply or Destroy Infrastructure
I wanted two ways to trigger Terraform:
Apply changes if something was modified in
infrastructure/
.Destroy infrastructure if I explicitly requested it (without running Terraform manually).
To do this, I added a step that sets an environment variable (TF_RUN
) if Terraform should execute:
- name: Set Terraform Execution Flag
id: set-flag
run: |
if [[ "${{ steps.changes.outputs.infrastructure }}" == "true" || "${{ contains(github.event.head_commit.message, 'destroy infra') }}" == "true" ]]; then
echo "TF_RUN=true" >> $GITHUB_ENV
else
echo "TF_RUN=false" >> $GITHUB_ENV
fi
This way Terraform will only run if something changed in infrastructure/
or I pushed a commit with the message destroy infra
. If neither condition is met, Terraform does nothing, preventing unnecessary runs.
Step 3: Running Terraform Based on the Flag
I then used TF_RUN
in every Terraform-related step to ensure that it only runs when necessary:
- name: Setup Terraform
if: env.TF_RUN == 'true'
uses: hashicorp/setup-terraform@v3.1.2
with:
terraform_version: "1.10.5"
- name: Initialize Terraform
if: env.TF_RUN == 'true'
working-directory: infrastructure
run: terraform init -input=false
This way, terraform won’t initialize unless needed and no random Terraform runs will happen if infra wasn’t changed.
Step 4: Handling terraform apply
and terraform destroy
Safely
For Terraform apply, I added an extra condition to ensure it doesn’t run if the commit message requests a destroy:
- name: Terraform Apply
if: env.TF_RUN == 'true' && !contains(github.event.head_commit.message, 'destroy infra')
working-directory: infrastructure
run: terraform apply tfplan
And for destroy, I made sure it only runs if explicitly triggered:
- name: Terraform Destroy
if: contains(github.event.head_commit.message, 'destroy infra')
working-directory: infrastructure
run: terraform destroy --auto-approve
Wrapping up
This setup fully automates Terraform while keeping control over when infrastructure is applied or destroyed. I no longer have to manually run Terraform, just commit my changes, push, and let GitHub Actions handle it.
Here’s the complete GitHub Actions YAML if you’re interested: [terraform-ci-cd.yml]
What do you think?
This setup worked well for me, but I know there are many ways to automate Terraform, specifically terraform destroy
as evident through this reddit post. How do you handle Terraform CI/CD? Do you automate Terraform destroy, or do you prefer a different approach? Let me know!