Skip to content
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