Terraform apply from GitHub Actions is a powerful way to automate your infrastructure deployments, but it’s easy to get wrong and end up with unintended changes or security vulnerabilities.
Let’s look at a typical workflow:
name: Terraform Apply
on:
push:
branches:
- main
jobs:
terraform_apply:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.5.7
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Plan
run: terraform plan -out=tfplan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Apply
run: terraform apply -auto-approve tfplan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
This looks straightforward, but the terraform apply -auto-approve is a huge red flag. It bypasses the critical human review step, which is where most safety nets lie. The real power comes from understanding how to integrate Terraform’s planning and review process within GitHub Actions, not just blindly executing apply.
The core idea is to decouple the plan from the apply. You want to generate a detailed plan, have someone approve it (or a system confirm it), and then apply that specific, pre-approved plan. This prevents drift and ensures that only what was intended gets deployed.
Here’s how you can structure this for safety:
-
Generate the Plan and Store it as an Artifact:
- The
terraform plancommand creates a plan file (.tfplan). This file is a binary representation of exactly what Terraform will do. - You can upload this
.tfplanfile as a GitHub Actions artifact. This makes it available for download and inspection later.
# ... (checkout, setup_terraform, terraform_init steps) ... - name: Terraform Plan id: plan run: terraform plan -out=tfplan -input=false env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Upload plan artifact uses: actions/upload-artifact@v3 with: name: terraform-plan path: tfplanid: plangives this step a reference.-input=falseis crucial in CI/CD to prevent Terraform from prompting for input.
- The
-
Create a Manual Approval Step:
- GitHub Actions has a built-in feature for environments and manual approvals. You can configure an environment in your repository settings (e.g., "Production") and require approvers.
- This means the workflow will pause at a specific step until one or more designated users approve it.
# ... (previous steps) ... - name: Deploy to Production if: github.ref == 'refs/heads/main' # Only deploy from main branch uses: "marvinpinto/action-automatic-releases@latest" # Placeholder, you'll use a custom deploy job with: repo_token: "${{ secrets.GITHUB_TOKEN }}" automatic_release_tag: "latest" prerelease: true title: "Automated Release" files: "/path/to/your/release/files" # This is where you'd download the plan- Correction: The
marvinpinto/action-automatic-releasesis not the right tool here. You need a job that waits for approval and then applies. The correct way is to use a separate job with anenvironmentblock.
Corrected Approach for Manual Approval:
# ... (previous steps for plan generation) ... jobs: terraform_apply: runs-on: ubuntu-latest # ... (checkout, setup_terraform, terraform_init, terraform_plan steps as above) ... # This job will run after the plan is generated and uploaded as an artifact. # It requires manual approval. deploy_production: needs: terraform_apply # Ensure plan job completes first runs-on: ubuntu-latest environment: Production # Configure "Production" environment in GitHub Repo Settings -> Environments steps: - name: Download plan artifact uses: actions/download-artifact@v3 with: name: terraform-plan path: ./artifacts - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.5.7 - name: Terraform Init run: terraform init env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Terraform Apply run: terraform apply -input=false -auto-approve ./artifacts/tfplan env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}- Key Changes:
- A new job
deploy_productionis introduced. environment: Productionis specified. Go to your GitHub repository’s Settings -> Environments. Create an environment named "Production". Under "Environment protection rules", check "Required reviewers" and add the users or teams who must approve deployments.needs: terraform_applyensures this job only runs after theterraform_applyjob (which generates the plan) has finished.actions/download-artifact@v3is used to retrieve thetfplanfile generated in the previous job.- The
terraform applycommand now uses the downloadedtfplanfile.-auto-approveis still present here because the manual approval step itself is the gatekeeper. The approval means you are explicitly agreeing to apply this specific plan.
- A new job
-
Secure Your Credentials:
- Never hardcode credentials. Use GitHub Secrets.
- For cloud providers like AWS, consider using IAM roles for GitHub Actions if your CI/CD runner can assume them, which is more secure than long-lived access keys.
# In your GitHub Repository Settings -> Secrets and variables -> Actions # Add: # AWS_ACCESS_KEY_ID # AWS_SECRET_ACCESS_KEYIf using IAM roles (preferred for AWS):
- You don’t need to pass
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYas environment variables. - Your Terraform configuration would typically be set up to use the instance profile credentials.
- The GitHub Actions runner needs to be configured to assume an IAM role. This often involves an extra step in your workflow or runner setup.
The most surprising true thing about automating Terraform apply is that the real safety comes not from the automation itself, but from integrating human review and explicit approval into the automated pipeline. Blind automation is dangerous; a guided, approved automation is powerful.
By generating a plan artifact and requiring manual approval through GitHub Environments, you create a robust process:
- Code Change: A developer pushes Terraform code to your repository.
- Plan Generation: A GitHub Action runs
terraform plan, creates a.tfplanfile, and uploads it as an artifact. - Pull Request Review: The code change is reviewed via a Pull Request.
- Manual Approval: Once merged to
main(or your deployment branch), a new workflow triggers. This workflow pauses at thedeploy_productionjob, waiting for designated reviewers to approve the deployment. - Apply: Upon approval, the workflow downloads the previously generated
.tfplanartifact and executesterraform apply -auto-approveusing that specific plan.
This ensures that what gets deployed has been explicitly reviewed at the code level (via PR) and explicitly approved at the execution level (via environment approval), using a plan generated from the exact code that was merged.
The next thing you’ll likely encounter is managing drift between your Terraform state and the actual infrastructure, especially if manual changes are made outside of Terraform.