1+ name : ' Terraform Configuration Drift Detection'
2+
3+ on :
4+ schedule :
5+ - cron : ' 30 13 * * *' # runs every afternoon at 1:30 pm (Depends on Timezone)
6+ workflow_call :
7+ inputs :
8+ working_directory :
9+ description : ' Root directory of the terraform where all resources exist.'
10+ required : true
11+ type : string
12+ default : _example
13+ provider :
14+ description : ' Cloud provider to run the workflow. e.g. azurerm or aws'
15+ required : true
16+ type : string
17+ default : azurerm
18+
19+ # Special permissions required for OIDC authentication
20+ permissions :
21+ id-token : write
22+ contents : read
23+ issues : write
24+
25+ # These environment variables are used by the terraform azure provider to setup OIDD authenticate.
26+ env :
27+ ARM_CLIENT_ID : " ${{ secrets.AZURE_CLIENT_ID }}"
28+ ARM_SUBSCRIPTION_ID : " ${{ secrets.AZURE_SUBSCRIPTION_ID }}"
29+ ARM_TENANT_ID : " ${{ secrets.AZURE_TENANT_ID }}"
30+
31+ jobs :
32+ terraform-plan :
33+ name : ' Terraform Plan'
34+ runs-on : ubuntu-latest
35+ env :
36+ # this is needed since we are running terraform with read-only permissions
37+ ARM_SKIP_PROVIDER_REGISTRATION : true
38+ outputs :
39+ tfplanExitCode : ${{ steps.tf-plan.outputs.exitcode }}
40+
41+ steps :
42+ # Checkout the repository to the GitHub Actions runner
43+ - name : Checkout
44+ uses : actions/checkout@v3
45+
46+ # install AWS-cli
47+ - name : Install AWS CLI
48+ if : ${{ inputs.provider == 'aws' }}
49+ uses : aws-actions/configure-aws-credentials@v1
50+ with :
51+ aws-access-key-id : ${{ secrets.aws_access_key_id }}
52+ aws-secret-access-key : ${{ secrets.aws_secret_access_key }}
53+ aws-region : us-east-2
54+
55+ # Install azure-cli
56+ - name : Install Azure CLI
57+ if : ${{ inputs.provider == 'azurerm' }}
58+ uses : azure/login@v1
59+ with :
60+ creds : ${{ secrets.AZURE_CREDENTIALS }}
61+
62+ # Install the latest version of the Terraform CLI
63+ - name : Setup Terraform
64+ uses : hashicorp/setup-terraform@v2
65+ with :
66+ terraform_wrapper : false
67+
68+ # Run some scripts
69+ - name : Run shell commands
70+ run : ls -la
71+
72+ # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.
73+ - name : " Terraform Init"
74+ uses : hashicorp/terraform-github-actions@master
75+ with :
76+ tf_actions_subcommand : " init"
77+ tf_actions_version : 1.3.6
78+ tf_actions_working_dir : ${{ inputs.working_directory }}
79+ env :
80+ GITHUB_TOKEN : ' ${{ secrets.GITHUB }}'
81+
82+ # Generates an execution plan for Terraform
83+ # An exit code of 0 indicated no changes, 1 a terraform failure, 2 there are pending changes.
84+ - name : Terraform Plan
85+ id : tf-plan
86+ run : |
87+ export exitcode=0
88+ cd ${{ inputs.working_directory }}
89+ terraform plan -detailed-exitcode -no-color -out tfplan || export exitcode=$?
90+
91+ echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
92+
93+ if [ $exitcode -eq 1 ]; then
94+ echo Terraform Plan Failed!
95+ exit 1
96+ else
97+ exit 0
98+ fi
99+
100+ # Save plan to artifacts
101+ - name : Publish Terraform Plan
102+ uses : actions/upload-artifact@v3
103+ with :
104+ name : tfplan
105+ path : tfplan
106+
107+ # Create string output of Terraform Plan
108+ - name : Create String Output
109+ id : tf-plan-string
110+ run : |
111+ cd ${{ inputs.working_directory }}
112+ TERRAFORM_PLAN=$(terraform show -no-color tfplan)
113+
114+ delimiter="$(openssl rand -hex 8)"
115+ echo "summary<<${delimiter}" >> $GITHUB_OUTPUT
116+ echo "## Terraform Plan Output" >> $GITHUB_OUTPUT
117+ echo "<details><summary>Click to expand</summary>" >> $GITHUB_OUTPUT
118+ echo "" >> $GITHUB_OUTPUT
119+ echo '```terraform' >> $GITHUB_OUTPUT
120+ echo "$TERRAFORM_PLAN" >> $GITHUB_OUTPUT
121+ echo '```' >> $GITHUB_OUTPUT
122+ echo "</details>" >> $GITHUB_OUTPUT
123+ echo "${delimiter}" >> $GITHUB_OUTPUT
124+
125+ # Publish Terraform Plan as task summary
126+ - name : Publish Terraform Plan to Task Summary
127+ env :
128+ SUMMARY : ${{ steps.tf-plan-string.outputs.summary }}
129+ run : |
130+ echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY
131+
132+ # If changes are detected, create a new issue
133+ - name : Publish Drift Report
134+ if : steps.tf-plan.outputs.exitcode == 2
135+ uses : actions/github-script@v6
136+ env :
137+ SUMMARY : " ${{ steps.tf-plan-string.outputs.summary }}"
138+ with :
139+ github-token : ${{ secrets.GITHUB }}
140+ script : |
141+ const body = `${process.env.SUMMARY}`;
142+ const title = 'Terraform Configuration Drift Detected';
143+ const creator = 'github-actions[bot]'
144+
145+ // Look to see if there is an existing drift issue
146+ const issues = await github.rest.issues.listForRepo({
147+ owner: context.repo.owner,
148+ repo: context.repo.repo,
149+ state: 'open',
150+ creator: creator,
151+ title: title
152+ })
153+
154+ if( issues.data.length > 0 ) {
155+ // We assume there shouldn't be more than 1 open issue, since we update any issue we find
156+ const issue = issues.data[0]
157+
158+ if ( issue.body == body ) {
159+ console.log('Drift Detected: Found matching issue with duplicate content')
160+ } else {
161+ console.log('Drift Detected: Found matching issue, updating body')
162+ github.rest.issues.update({
163+ owner: context.repo.owner,
164+ repo: context.repo.repo,
165+ issue_number: issue.number,
166+ body: body
167+ })
168+ }
169+ } else {
170+ console.log('Drift Detected: Creating new issue')
171+
172+ github.rest.issues.create({
173+ owner: context.repo.owner,
174+ repo: context.repo.repo,
175+ title: title,
176+ body: body
177+ })
178+ }
179+
180+ # If changes aren't detected, close any open drift issues
181+ - name : Publish Drift Report
182+ if : steps.tf-plan.outputs.exitcode == 0
183+ uses : actions/github-script@v6
184+ with :
185+ github-token : ${{ secrets.GITHUB_TOKEN }}
186+ script : |
187+ const title = 'Terraform Configuration Drift Detected';
188+ const creator = 'github-actions[bot]'
189+
190+ // Look to see if there is an existing drift issue
191+ const issues = await github.rest.issues.listForRepo({
192+ owner: context.repo.owner,
193+ repo: context.repo.repo,
194+ state: 'open',
195+ creator: creator,
196+ title: title
197+ })
198+
199+ if( issues.data.length > 0 ) {
200+ const issue = issues.data[0]
201+
202+ github.rest.issues.update({
203+ owner: context.repo.owner,
204+ repo: context.repo.repo,
205+ issue_number: issue.number,
206+ state: 'closed'
207+ })
208+ }
209+
210+ # Mark the workflow as failed if drift detected
211+ - name : Error on Failure
212+ if : steps.tf-plan.outputs.exitcode == 2
213+ run : exit 1
0 commit comments