Terraform Configuration Drift Detection #736
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: 'Terraform Configuration Drift Detection' | |
on: | |
workflow_dispatch: | |
schedule: | |
- cron: '41 3 * * *' # runs nightly at 3:41 am | |
#Special permissions required for OIDC authentication | |
permissions: | |
id-token: write | |
contents: read | |
issues: write | |
#These environment variables are used by the terraform azure provider to setup OIDD authenticate. | |
env: | |
ARM_CLIENT_ID: "${{ secrets.AZURE_CLIENT_ID }}" | |
ARM_SUBSCRIPTION_ID: "${{ secrets.AZURE_SUBSCRIPTION_ID }}" | |
ARM_TENANT_ID: "${{ secrets.AZURE_TENANT_ID }}" | |
jobs: | |
terraform-plan: | |
name: 'Terraform Plan' | |
runs-on: ubuntu-latest | |
env: | |
#this is needed since we are running terraform with read-only permissions | |
ARM_SKIP_PROVIDER_REGISTRATION: true | |
outputs: | |
tfplanExitCode: ${{ steps.tf-plan.outputs.exitcode }} | |
steps: | |
# Checkout the repository to the GitHub Actions runner | |
- name: Checkout | |
uses: actions/checkout@v4 | |
# Install the latest version of the Terraform CLI | |
- name: Setup Terraform | |
uses: hashicorp/setup-terraform@v3 | |
with: | |
terraform_wrapper: false | |
# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. | |
- name: Terraform Init | |
run: terraform init | |
# Generates an execution plan for Terraform | |
# An exit code of 0 indicated no changes, 1 a terraform failure, 2 there are pending changes. | |
- name: Terraform Plan | |
id: tf-plan | |
run: | | |
export exitcode=0 | |
terraform plan -detailed-exitcode -no-color -out tfplan || export exitcode=$? | |
echo "exitcode=$exitcode" >> $GITHUB_OUTPUT | |
if [ $exitcode -eq 1 ]; then | |
echo Terraform Plan Failed! | |
exit 1 | |
else | |
exit 0 | |
fi | |
# Save plan to artifacts | |
- name: Publish Terraform Plan | |
uses: actions/upload-artifact@v4 | |
with: | |
name: tfplan | |
path: tfplan | |
# Create string output of Terraform Plan | |
- name: Create String Output | |
id: tf-plan-string | |
run: | | |
TERRAFORM_PLAN=$(terraform show -no-color tfplan) | |
delimiter="$(openssl rand -hex 8)" | |
echo "summary<<${delimiter}" >> $GITHUB_OUTPUT | |
echo "## Terraform Plan Output" >> $GITHUB_OUTPUT | |
echo "<details><summary>Click to expand</summary>" >> $GITHUB_OUTPUT | |
echo "" >> $GITHUB_OUTPUT | |
echo '```terraform' >> $GITHUB_OUTPUT | |
echo "$TERRAFORM_PLAN" >> $GITHUB_OUTPUT | |
echo '```' >> $GITHUB_OUTPUT | |
echo "</details>" >> $GITHUB_OUTPUT | |
echo "${delimiter}" >> $GITHUB_OUTPUT | |
# Publish Terraform Plan as task summary | |
- name: Publish Terraform Plan to Task Summary | |
env: | |
SUMMARY: ${{ steps.tf-plan-string.outputs.summary }} | |
run: | | |
echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY | |
# If changes are detected, create a new issue | |
- name: Publish Drift Report | |
if: steps.tf-plan.outputs.exitcode == 2 | |
uses: actions/github-script@v7 | |
env: | |
SUMMARY: "${{ steps.tf-plan-string.outputs.summary }}" | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const body = `${process.env.SUMMARY}`; | |
const title = 'Terraform Configuration Drift Detected'; | |
const creator = 'github-actions[bot]' | |
// Look to see if there is an existing drift issue | |
const issues = await github.rest.issues.listForRepo({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
state: 'open', | |
creator: creator, | |
title: title | |
}) | |
if( issues.data.length > 0 ) { | |
// We assume there shouldn't be more than 1 open issue, since we update any issue we find | |
const issue = issues.data[0] | |
if ( issue.body == body ) { | |
console.log('Drift Detected: Found matching issue with duplicate content') | |
} else { | |
console.log('Drift Detected: Found matching issue, updating body') | |
github.rest.issues.update({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: issue.number, | |
body: body | |
}) | |
} | |
} else { | |
console.log('Drift Detected: Creating new issue') | |
github.rest.issues.create({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
title: title, | |
body: body | |
}) | |
} | |
# If changes aren't detected, close any open drift issues | |
- name: Publish Drift Report | |
if: steps.tf-plan.outputs.exitcode == 0 | |
uses: actions/github-script@v7 | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const title = 'Terraform Configuration Drift Detected'; | |
const creator = 'github-actions[bot]' | |
// Look to see if there is an existing drift issue | |
const issues = await github.rest.issues.listForRepo({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
state: 'open', | |
creator: creator, | |
title: title | |
}) | |
if( issues.data.length > 0 ) { | |
const issue = issues.data[0] | |
github.rest.issues.update({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: issue.number, | |
state: 'closed' | |
}) | |
} | |
# Mark the workflow as failed if drift detected | |
- name: Error on Failure | |
if: steps.tf-plan.outputs.exitcode == 2 | |
run: exit 1 |