Skip to content

Deploy Terraform account #2

Deploy Terraform account

Deploy Terraform account #2

name: Deploy Terraform account
on:
workflow_dispatch:
inputs:
account:
description: "Account to deploy"
required: true
type: choice
options:
- nonprod
- prod
ref:
description: "The branch or tag ref to checkout"
type: string
required: false
apply:
type: boolean
required: true
description: "Apply the terraform?"
default: false
terraform-args:
type: string
required: false
description: "Additional arguments to pass to terraform"
workflow_call:
inputs:
ref:
description: "The branch or tag ref to checkout"
type: string
required: false
account:
description: "Environment to deploy"
type: string
required: true
apply:
type: boolean
default: false
terraform-args:
type: string
required: false
description: "Additional arguments to pass to terraform"
outputs:
terraform-output:
description: "Terraform output"
value: ${{ jobs.deploy.outputs.terraform-output }}
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-account-${{ inputs.account }}
jobs:
deploy:
name: ${{ inputs.apply && 'Apply' || 'Plan' }}
runs-on: ubuntu-latest
# As a workaround for: https://github.com/actions/runner/issues/2120
# Environment will not be defined for non-apply jobs to ensure that deployments are kept accurate in the GitHub UI.
# It is still possible to overwrite variables/secrets in this workflow by using `format('ACCOUNT_{0}_SOME_VAR', inputs.environment)` - e.g. ACCOUNT_nonprod_VAR
environment: ${{ inputs.apply && format('account-{0}', inputs.account) || null }}
outputs:
terraform-output: ${{ steps.terraform-output.outputs.json }}
env:
WORKING_DIR: infra/terraform/accounts/${{ inputs.account }}
AWS_OIDC_ROLE: ${{ vars[inputs.account == 'prod' && 'ACCOUNT_PROD_TF_OIDC_ROLE' || 'ACCOUNT_NONPROD_TF_OIDC_ROLE'] }}
AWS_REGION: ${{ vars.DVSA_AWS_REGION }}
defaults:
run:
shell: bash
working-directory: ${{ env.WORKING_DIR }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || null }}
sparse-checkout: infra/terraform
fetch-depth: ${{ !inputs.ref && 1 || 0 }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_OIDC_ROLE }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform init
id: init
run: terraform init -no-color -input=false -upgrade
- name: Validate
id: validate
run: terraform validate -no-color
- name: Plan
if: ${{ !inputs.apply }}
id: plan
run: terraform plan -no-color -input=false -out=tfplan ${{ inputs.terraform-args || '' }}
- name: Get plan changes
if: ${{ !inputs.apply }}
id: show
run: |
echo "changes=$(terraform-bin show -json -no-color tfplan | jq -r -c '[.resource_changes[] | select(.change.actions[0] != "no-op") | {action: .change.actions[0], address: .address}] | group_by(.action) | map({(.[0].action): map(.address)}) | add')" >> $GITHUB_OUTPUT
# The maximum input size is ~64KB.
# The maximum PR comment size is ~64KB.
# The plan can be larger than this, so we need to truncate it.
# Saving the plan to a file allows JavaScript to truncate this and avoid it being too large for the inputs and the PR comment.
- name: Save plan to file
if: ${{ !inputs.apply }}
run: terraform show -no-color tfplan > tfplan.txt
- uses: actions/github-script@v7
if: ${{ always() && !cancelled() && !failure() && !inputs.apply && github.event_name == 'pull_request' }}
env:
PLAN: "${{ steps.plan.outputs.stdout }}"
CHANGES: "${{ steps.show.outputs.changes }}"
with:
retries: 3
script: |
const fs = require('node:fs');
const plan = fs.readFileSync('${{ env.WORKING_DIR }}/tfplan.txt');
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('data-gh-workflow="${{ inputs.account }}-account-plan"')
})
let summary = "";
const actionIcons = {
create: "πŸ†•",
read: "πŸ“–",
update: "πŸ”„",
delete: "πŸ—‘οΈ",
"no-op": "🚫"
};
let changes = {};
if (process.env.CHANGES) {
changes = JSON.parse(process.env.CHANGES) || {};
}
Object.keys(changes).forEach(action => {
summary += `**${actionIcons[action]} ${action.charAt(0).toUpperCase() + action.slice(1)}s**\n\n\`\`\`tf\n`;
changes[action].forEach(change => {
summary += `${change}\n`;
});
summary += "\`\`\`\n";
});
const output = `
## Terraform plan for account: \`${{ inputs.account }}\`
**Commit:** ${{ github.event.pull_request.head.sha }}
### Plan summary
\`${changes.create?.length || 0} to add, ${changes.update?.length || 0} to change, ${changes.delete?.length || 0} to destroy\`
${summary}
----
<details data-gh-workflow="${{ inputs.account }}-account-plan"><summary>Show full plan</summary>
\`\`\`tf\n
${plan.length > 65000 ? plan.slice(0, 65000) + '... (truncated, see full plan in the workflow run logs)' : plan}
\`\`\`
</details>`;
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
})
}
- name: Apply
id: apply
if: ${{ inputs.apply }}
run: terraform apply -no-color -input=false -auto-approve ${{ inputs.terraform-args || '' }}
- name: Set outputs
if: ${{ always() && !cancelled() && !failure() }}
id: terraform-output
run: |
echo "json=$(terraform-bin output -json -no-color | jq -r -c 'to_entries | map({(.key): .value.value}) | add')" >> $GITHUB_OUTPUT