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 Validate, Plan & Apply for Azure | |
on: | |
workflow_call: | |
secrets: | |
infracost-api-key: | |
description: 'The API key for infracost' | |
required: false | |
GH_TOKEN: | |
description: 'The GitHub Token' | |
required: true | |
inputs: | |
AZURE_CLIENT_ID: | |
description: 'The Azure Client ID' | |
required: true | |
type: string | |
AZURE_TENANT_ID: | |
description: 'The Azure Tenant ID' | |
required: true | |
type: string | |
terraform-subscription-id: | |
description: 'The azure subscription ID to deploy to' | |
required: true | |
type: number | |
# terraform-backend-subscription-id: | |
# description: 'Azure subscription ID for the backend storage account' | |
# type: string | |
# required: true | |
terraform-state-key: | |
default: '${{ github.event.repository.name }}.tfstate' | |
description: 'The key of the terraform state' | |
required: false | |
type: string | |
terraform-backend-storage-account-name: | |
description: 'The name of the state storage account' | |
required: true | |
type: string | |
terraform-backend-container-name: | |
description: 'The name of the state storage account container' | |
required: false | |
type: string | |
description: 'tfstate' | |
state-resource-group-name: | |
type: string | |
required: false | |
description: 'The name of the state resource group' | |
default: 'rg-alz-state' | |
azure-primary-region: | |
type: string | |
required: false | |
description: 'The primary Azure region to deploy to' | |
default: 'uksouth' | |
environment: | |
type: string | |
required: true | |
description: 'The environment to deploy to' | |
terraform-values-file: | |
default: 'values/production.tfvars' | |
description: 'The values file to use' | |
required: false | |
type: string | |
terraform-version: | |
default: '1.5.7' | |
description: 'The version of terraform to use' | |
required: false | |
type: string | |
enable-infracost: | |
default: false | |
description: 'Whether to run infracost on the Terraform Plan (secrets.infracost-api-key must be set if enabled)' | |
required: false | |
type: boolean | |
terraform-log-level: | |
default: '' | |
description: 'The log level of terraform' | |
required: false | |
type: string | |
runs-on: | |
default: "ubuntu-latest" | |
description: 'Single label value for the GitHub runner to use (custom value only applies to Terraform Plan and Apply steps)' | |
required: false | |
type: string | |
env: | |
TF_LOG: ${{ inputs.terraform-log-level }} | |
permissions: | |
id-token: write | |
contents: read | |
pull-requests: write | |
jobs: | |
terraform-format: | |
name: "Terraform Format" | |
runs-on: ubuntu-latest | |
outputs: | |
result: ${{ steps.format.outcome }} | |
steps: | |
- name: Checkout Repository | |
uses: actions/checkout@v3 | |
- name: Setup Terraform | |
uses: hashicorp/setup-terraform@v2 | |
with: | |
terraform_version: ${{ inputs.terraform-version }} | |
- name: Terraform Format | |
id: format | |
uses: dflook/terraform-fmt-check@v1 | |
terraform-lint: | |
name: "Terraform Lint" | |
runs-on: ubuntu-latest | |
outputs: | |
result: ${{ steps.lint.outcome }} | |
steps: | |
- name: Checkout Repository | |
uses: actions/checkout@v3 | |
- name: Setup Linter | |
uses: terraform-linters/setup-tflint@v3 | |
- name: Linter Initialize | |
run: tflint --init | |
- name: Linting Code | |
id: lint | |
run: tflint -f compact | |
terraform-plan: | |
name: "Terraform Plan" | |
if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') | |
runs-on: ${{ inputs.runs-on }} | |
environment: ${{ inputs.environment }} | |
env: | |
ARM_CLIENT_ID: ${{ inputs.AZURE_CLIENT_ID }} | |
ARM_SUBSCRIPTION_ID: ${{ inputs.terraform-subscription-id }} | |
ARM_TENANT_ID: ${{ inputs.AZURE_TENANT_ID }} | |
outputs: | |
result-auth: ${{ steps.auth.outcome }} | |
result-init: ${{ steps.init.outcome }} | |
result-validate: ${{ steps.validate.outcome }} | |
result-s3-backend-check: ${{ steps.s3-backend-check.outcome }} | |
result-plan: ${{ steps.plan.outcome }} | |
plan-stdout: ${{ steps.plan.outputs.stdout }} | |
steps: | |
- name: Checkout Repository | |
uses: actions/checkout@v3 | |
- name: Setup node | |
uses: actions/setup-node@v3 | |
with: | |
node-version: 16 | |
- name: Setup Terraform | |
uses: hashicorp/setup-terraform@v2 | |
with: | |
terraform_version: ${{ inputs.terraform-version }} | |
- uses: de-vri-es/setup-git-credentials@v2 | |
with: | |
credentials: https://whoisit:${{ secrets.GH_TOKEN }}@github.com | |
- name: Login via Az module | |
uses: azure/login@v1 | |
with: | |
client-id: ${{ inputs.AZURE_CLIENT_ID }} | |
tenant-id: ${{ inputs.AZURE_TENANT_ID }} | |
subscription-id: ${{ inputs.terraform-subscription-id }} | |
- name: Run Terraform init | |
run: | | |
terraform init -var use_oidc=true -var state_resource_group_name=${{ inputs.state-resource-group-name }} -lock=false \ | |
-backend-config="subscription_id=${{ inputs.terraform-backend-subscription-id }}" \ | |
-backend-config="storage_account_name=${{ inputs.terraform-backend-storage-account-name }}" \ | |
-backend-config="container_name=${{ inputs.terraform-backend-container-name }}" \ | |
-backend-config="key=${{ inputs.terraform-state-key }}" \ | |
-backend-config="tenant_id=${{ inputs.AZURE_TENANT_ID }}" \ | |
-backend-config="client_id=${{ inputs.AZURE_CLIENT_ID }}" | |
working-directory: ${{ inputs.terraform-working-directory }} | |
- name: Terraform Validate | |
id: validate | |
run: terraform validate -no-color | |
# - name: Terraform Storage Account Backend Check | |
# id: storage-account-backend-check | |
# run: | | |
# if grep -E '^[^#]*backend\s+"s3"' terraform.tf; then | |
# echo "Terraform configuration references an S3 backend." | |
# else | |
# echo "Terraform configuration does not reference an S3 backend." | |
# exit 1 | |
# fi | |
- name: Terraform Plan | |
id: plan | |
run: | | |
terraform plan -var-file=${{ inputs.terraform-values-file }} -no-color -input=false -out=tfplan | |
- name: Terraform Plan JSON Output | |
run: | | |
terraform show -json tfplan > tfplan.json | |
- name: Upload tfplan | |
uses: actions/upload-artifact@v3 | |
with: | |
name: tfplan | |
path: "tfplan*" | |
retention-days: 1 | |
get-cost-estimate: | |
name: "Get Cost Estimate" | |
if: github.event_name == 'pull_request' && inputs.enable-infracost | |
runs-on: ubuntu-latest | |
needs: | |
- terraform-plan | |
steps: | |
- name: Setup Infracost | |
uses: infracost/actions/setup@v2 | |
with: | |
api-key: ${{ secrets.infracost-api-key }} | |
currency: GBP | |
- name: Download tfplan | |
uses: actions/download-artifact@v3 | |
with: | |
name: tfplan | |
- name: Generate Infracost Cost Estimate | |
run: | | |
infracost breakdown --path=tfplan.json \ | |
--format=json \ | |
--out-file=/tmp/infracost.json | |
- name: Post Infracost comment | |
run: | | |
infracost comment github --path=/tmp/infracost.json \ | |
--repo=$GITHUB_REPOSITORY \ | |
--github-token=${{github.token}} \ | |
--pull-request=${{github.event.pull_request.number}} \ | |
--behavior=update | |
update-pr: | |
name: "Update PR" | |
if: github.event_name == 'pull_request' && (success() || failure()) | |
runs-on: ubuntu-latest | |
needs: | |
- terraform-format | |
- terraform-lint | |
- terraform-plan | |
steps: | |
- name: Add PR Comment | |
uses: actions/github-script@v6 | |
env: | |
PLAN: "${{ needs.terraform-plan.outputs.plan-stdout }}" | |
with: | |
script: | | |
// 1. Retrieve existing bot comments for the PR | |
const { data: comments } = await github.rest.issues.listComments({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: context.issue.number, | |
}) | |
const botComment = comments.find(comment => { | |
return comment.user.type === 'Bot' && comment.body.includes('Pull Request Review Status') | |
}) | |
// 2. Check output length | |
const PLAN = process.env.PLAN || ''; | |
const excludedStrings = ["Refreshing state...", "Reading...", "Read complete after"]; | |
const filteredLines = PLAN.split('\n').filter(line => | |
!excludedStrings.some(excludedStr => line.includes(excludedStr)) | |
); | |
var planOutput = filteredLines.join('\n').trim(); | |
if (planOutput.length < 1 || planOutput.length > 65000) { | |
planOutput = "Terraform Plan output is too large, please view the workflow run logs directly." | |
} | |
// 3. Prepare format of the comment | |
const output = `### Pull Request Review Status | |
* π <b>Terraform Format and Style:</b> \`${{ needs.terraform-format.outputs.result }}\` | |
* π <b>Terraform Linting:</b> \`${{ needs.terraform-lint.outputs.result }}\` | |
* π§ <b>Terraform Initialisation:</b> \`${{ needs.terraform-plan.outputs.result-init }}\` | |
* π€ <b>Terraform Validation:</b> \`${{ needs.terraform-plan.outputs.result-validate }}\` | |
* π <b>Terraform S3 Backend:</b> \`${{ needs.terraform-plan.outputs.result-s3-backend-check }}\` | |
* π <b>Terraform Plan:</b> \`${{ needs.terraform-plan.outputs.result-plan }}\` | |
<details><summary><b>Output: π Terraform Plan</b></summary> | |
\`\`\` | |
${planOutput} | |
\`\`\` | |
</details> | |
*<b>Pusher:</b> @${{ github.actor }}, <b>Action:</b> \`${{ github.event_name }}\`* | |
*<b>Workflow Run Link:</b> ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}*`; | |
// 4. If we have a comment, update it, otherwise create a new one | |
if (botComment) { | |
github.rest.issues.updateComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
comment_id: botComment.id, | |
body: output | |
}) | |
} else { | |
github.rest.issues.createComment({ | |
issue_number: context.issue.number, | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
body: output | |
}) | |
} | |
terraform-apply: | |
name: "Terraform Apply" | |
if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
runs-on: ${{ inputs.runs-on }} | |
environment: ${{ inputs.environment }} | |
env: | |
ARM_CLIENT_ID: ${{ inputs.AZURE_CLIENT_ID }} | |
ARM_SUBSCRIPTION_ID: ${{ inputs.terraform-subscription-id }} | |
ARM_TENANT_ID: ${{ inputs.AZURE_TENANT_ID }} | |
needs: | |
- terraform-format | |
- terraform-lint | |
- terraform-plan | |
steps: | |
- name: Checkout Repository | |
uses: actions/checkout@v3 | |
- name: Setup node | |
uses: actions/setup-node@v3 | |
with: | |
node-version: 16 | |
- name: Setup Terraform | |
uses: hashicorp/setup-terraform@v2 | |
with: | |
terraform_version: ${{ inputs.terraform-version }} | |
- uses: de-vri-es/setup-git-credentials@v2 | |
with: | |
credentials: https://whoisit:${{ secrets.GH_TOKEN }}@github.com | |
- name: Login via Az module | |
uses: azure/login@v1 | |
with: | |
client-id: ${{ inputs.AZURE_CLIENT_ID }} | |
tenant-id: ${{ inputs.AZURE_TENANT_ID }} | |
subscription-id: ${{ inputs.terraform-subscription-id }} | |
- name: Run Terraform init | |
working-directory: ${{ inputs.terraform-working-directory }} | |
run: | | |
terraform init -lock=false \ | |
-backend-config="subscription_id=${{ inputs.terraform-backend-subscription-id }}" \ | |
-backend-config="storage_account_name=${{ inputs.terraform-backend-storage-account-name }}" \ | |
-backend-config="container_name=${{ inputs.terraform-backend-container-name }}" \ | |
-backend-config="key=${{ inputs.terraform-state-key }}" \ | |
-backend-config="tenant_id=${{ inputs.AZURE_TENANT_ID }}" \ | |
-backend-config="client_id=${{ inputs.AZURE_CLIENT_ID }}" | |
- name: Download tfplan | |
uses: actions/download-artifact@v3 | |
with: | |
name: tfplan | |
- name: Terraform Apply | |
run: terraform apply -auto-approve -input=false tfplan |