Enhance Terraform/Tofu PR Automation with GitHub Action
Using OP5dev/TF-via-PR enables low-code workflows to init, plan and apply IaC changes—batteries included.
Provisioning infrastructure-as-code (IaC) in a GitOps framework can feel like walking a tightrope: balancing pipeline security while trying to communicate changes clearly. This blog explores OP5dev/TF-via-PR, which addresses common pitfalls to plan and apply IaC, including:
- Summarize plan changes (with diff)
- Reuse plan file (with encryption)
- Apply on PR merge (before OR after)
Tip: Throughout this blog, ‘TF’ is used to reference both Terraform and OpenTofu interchangeably due to first-class support for both tools.
Summarize plan changes (with diff)
While the plan should be transparent, reviewing 1000s of lines of changes isn’t feasible. On the other hand, a brief 1-liner like “Plan: 2 to add, 2 to change, 2 to destroy” fails to convey much at all. So why not visualize the summary of changes the same way Git does—with diff syntax highlighting.
Right after the color-coded diff, there’s another collapsible section with the trimmed plan output for a more detailed view. Below that, we’ll find a direct link to the full workflow log—handy for when the output exceeds a PR comment’s character limit. Speaking of workflows, the same output is attached to the job summary, even when used in matrix strategy.
This is achieved with the following few lines of GitHub Action.
jobs:
provision:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@4
- name: Setup TF
uses: hashicorp/setup-terraform@v3
- name: Provision TF
uses: op5dev/tf-via-pr@v12
with:
command: plan
arg-lock: false
arg-var-file: env/dev.tfvars
working-directory: stacksBy the way, you may have noticed the oddly-named “terraform[…]tfplan” artifact in the previous screenshot and wondered what it’s all about.
Reuse plan file (with encryption)
Too often, when a plan is approved for merge, the IaC pipeline is rerun to apply changes with auto-approve enabled. This can lead to unpredictable results, especially if configuration drift occurs due to changes made outside the workflow. TF-via-PR takes advantage of workflow artifacts to store and reuse the plan file between runs.
To ensure the triggered workflow picks up the correct plan file artifact, it needs a uniquely identifiable name which accounts for variables, such as:
- Tool: either
terraformortofu. - PR number: so multiple PR branches can plan simultaneously without over-writing each other.
- CLI arguments: working directory, backend-config, var-file, workspace, and destroy.
Additionally, we want to avoid the risk of exposing sensitive data by uploading the plan file as-is. Instead, the file should be encrypted with a secret string before upload. Here’s how we can achieve that.
- name: Provision TF
uses: op5dev/tf-via-pr@v13
with:
command: plan
arg-lock: ${{ github.event_name == 'push' }}
arg-var-file: env/dev.tfvars
working-directory: stacks
plan-encrypt: ${{ secrets.PASSPHRASE }}Tip: For a deeper dive into securing cloud provisioning pipelines, check this blog out.
Speaking of reuse, it’s common for a PR to accumulate several commits before it’s ready to merge. In this case, plan updates can be rendered with:
- Update (default): the existing PR comment is updated in place, complete with a revision history to track changes over time.
- Recreate: the existing PR comment is deleted and replaced with a new one after each commit.
By now, you’ll have noticed that we’re applying changes with the same GitHub Action—the final piece of the ~puzzle~ pipeline.
Apply on PR merge (before OR after)
Whether you decide to apply IaC changes before or after merging, the workflow adapts to fit your needs. By using unique identifiers, we can retrieve the relevant plan file even if the PR branch has been pushed to the default branch, outside of pull_request context. Here’s a complete workflow example to illustrate.
on:
pull_request:
push:
branches: [main]
jobs:
provision:
runs-on: ubuntu-latest
permissions:
actions: read # Required to identify workflow run.
checks: write # Required to add status summary.
contents: read # Required to checkout repository.
pull-requests: write # Required to add comment and label.
steps:
- name: Checkout repository
uses: actions/checkout@4
- name: Setup TF
uses: hashicorp/setup-terraform@v3
# Only plan by default, or apply with lock on merge.
- name: Provision TF
uses: op5dev/tf-via-pr@v13
with:
command: ${{ github.event_name == 'push' && 'apply' || 'plan' }}
arg-lock: ${{ github.event_name == 'push' }}
arg-var-file: env/dev.tfvars
working-directory: stacks
plan-encrypt: ${{ secrets.PASSPHRASE }}We’re not limited to just these workflow triggers; it’s compatible with merge_group using a merge queue to filter out failed apply attempts. Other supported triggers include:
cronfor scheduled configuration drift checks (view example).workflow_dispatchfor manual checks.
Bonus Extras
While this blog has primarily focused on planning and applying changes, we can also perform ‘fmt’ and ‘validate’ checks, along with ‘workspace’ selection. In fact, we can pass the full range of TF options and flags using the ‘arg-’ prefix, such as arg-auto-approve: true, arg-destroy: true, arg-workspace: dev, and arg-parallelism: 20.
For more complex workflows, ‘exitcode’ and ‘identifier’ outputs are provided to help with decision chains or to integrate with linting and security scans. You can find the full list of parameters documented in the Readme.
All forms of contribution are welcome and appreciated for fostering open-source projects. Please feel free to open a discussion to share your ideas, or become a stargazer if you find this project useful.
Whether it’s interpolating dynamic backends, bulk-provisioning environments simultaneously, or triggering actions through PR labels and comments, this workflow offers plenty of flexibility. Is there a specific setup you’d like us to dive into next?
