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 a GitHub Action I maintain—DevSecTop/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 planned changes is simply not feasible. On the other hand, a brief 1-liner like "Plan: 2 to add, 2 to change, 2 to destroy" fails to convey the scope of impact. So why not visualize the summary of changes the same way Git does—with diff syntax highlighting.
Figure: PR comment of TF plan with "Diff of changes" section expanded.
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.
Figure: Workflow log with command-specific job summary.
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: devsectop/tf-via-pr@v12
with:
command: plan
arg-lock: false
arg-var-file: env/dev.tfvars
working-directory: stacks
By 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. Since both the code and pipeline are hosted on GitHub, we can take 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
terraform
ortofu
. - PR number: so multiple PR branches can plan simultaneously without over-writing each other.
- CLI arguments: workspace, working directory, backend-config, var-file, 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 add this to the GitHub Action workflow step from before.
- name: Provision TF
uses: devsectop/tf-via-pr@v12
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.
Secure cloud provisioning pipeline with GitHub automation
Rishav Dhar ・ Oct 20
#infrastructureascode #terraform #githubactions #devops
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 in one of two ways.
- 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.
Figure: PR comment revision history comparing plan and apply outputs.
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: devsectop/tf-via-pr@v12
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 triggers, like 'cron' (for scheduled drift checks) and 'workflow_dispatch' (for manual checks), are also supported, with their plan file shared between workflows.
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, detailed '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.
DevSecTop / TF-via-PR
Plan and apply Terraform/OpenTofu via PR automation, using best practices for secure and scalable IaC workflows.
Terraform/OpenTofu via Pull Request (TF-via-PR)
What does it do?
Who is it for?
Usage
How to get started quickly?
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:
- uses: actions/checkout@4
- uses: hashicorp/setup-terraform@v3
…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 labels and comments, this workflow offers plenty of flexibility. Is there a specific setup you'd like us to dive into next?
Top comments (0)