Automating Terraform with GitHub Actions

Automating Terraform with GitHub Actions

CI/CD for Infrastructure as Code


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:

  1. Apply changes if something was modified in infrastructure/.

  2. 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!