Skip to content

Deploy Terraform environment #19

Deploy Terraform environment

Deploy Terraform environment #19

name: Deploy Terraform environment
on:
workflow_dispatch:
inputs:
ref:
description: "The branch or tag ref to checkout"
type: string
required: false
environment:
description: "Environment to deploy"
type: choice
options:
- dev
- int
- prep
- prod
required: true
api-image-tag:
description: "API image tag"
type: string
required: true
cli-image-tag:
description: "CLI image tag"
type: string
required: true
selfserve-image-tag:
description: "Selfserve image tag"
type: string
required: true
internal-image-tag:
description: "Internal image tag"
type: string
required: true
search-image-tag:
description: "Search image tag"
type: string
required: true
assets-version:
description: "Assets version"
type: string
required: true
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
environment:
type: string
required: true
workspace:
type: string
required: false
api-image-tag:
type: string
required: true
cli-image-tag:
description: "CLI image tag"
type: string
required: true
selfserve-image-tag:
type: string
required: true
internal-image-tag:
type: string
required: true
search-image-tag:
type: string
required: true
assets-version:
type: string
required: true
apply:
type: boolean
default: false
destroy:
type: boolean
required: false
default: false
rollback:
type: boolean
required: false
default: false
outputs:
terraform-output:
description: "Terraform output"
value: ${{ jobs.deploy.outputs.terraform-output }}
previous_api_image_tag:
description: "Previous API image tag"
value: ${{ jobs.deploy.outputs.previous_api_image_tag }}
previous_cli_image_tag:
description: "Previous CLI image tag"
value: ${{ jobs.deploy.outputs.previous_cli_image_tag }}
previous_selfserve_image_tag:
description: "Previous Selfserve image tag"
value: ${{ jobs.deploy.outputs.previous_selfserve_image_tag }}
previous_internal_image_tag:
description: "Previous Internal image tag"
value: ${{ jobs.deploy.outputs.previous_internal_image_tag }}
previous_search_image_tag:
description: "Previous Search image tag"
value: ${{ jobs.deploy.outputs.previous_search_image_tag }}
previous_assets_version:
description: "Previous Assets version"
value: ${{ jobs.deploy.outputs.previous_assets_version }}
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-environment-${{ inputs.environment }}
jobs:
deploy:
name: ${{ inputs.rollback && 'Rollback' || '' }}${{ inputs.destroy && 'Destroy ' || '' }}${{ 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 by using `format('ENV_{0}_SOME_VAR', inputs.environment)` - e.g. ENV_dev_VAR
environment:
name: ${{ (inputs.apply && !inputs.destroy) && (inputs.workspace || inputs.environment) || null }}
outputs:
terraform-output: ${{ steps.terraform-output.outputs.json }}
previous_api_image_tag: ${{ steps.get_current_versions.outputs.api_image_tag }}
previous_cli_image_tag: ${{ steps.get_current_versions.outputs.cli_image_tag }}
previous_selfserve_image_tag: ${{ steps.get_current_versions.outputs.selfserve_image_tag }}
previous_internal_image_tag: ${{ steps.get_current_versions.outputs.internal_image_tag }}
previous_search_image_tag: ${{ steps.get_current_versions.outputs.search_image_tag }}
previous_assets_version: ${{ steps.get_current_versions.outputs.assets_version }}
env:
WORKING_DIR: infra/terraform/environments/${{ inputs.environment }}
AWS_OIDC_ROLE: ${{ vars[format('ENV_{0}_TF_OIDC_ROLE', inputs.environment)] || vars[(inputs.environment == 'prep' || inputs.environment == '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: Get Current Versions from Terraform State
id: get_current_versions
run: |
terraform output -json > outputs.json || true
if [ -s outputs.json ] && [ "$(jq -r 'length' outputs.json)" -gt 0 ]; then
jq -r 'to_entries[] | select(.key | startswith("deployed_")) | "\(.key | ltrimstr("deployed_"))=\(.value.value // "")"' outputs.json >> $GITHUB_OUTPUT
else
echo "No outputs found, could be first run"
fi
- name: Select workspace
if: ${{ inputs.workspace }}
run: terraform workspace select -or-create ${{ inputs.workspace }}
- name: Validate
id: validate
run: terraform validate -no-color
- name: Plan
if: ${{ !inputs.apply }}
id: plan
env:
TF_VAR_api_image_tag: ${{ inputs.api-image-tag }}
TF_VAR_cli_image_tag: ${{ inputs.cli-image-tag }}
TF_VAR_selfserve_image_tag: ${{ inputs.selfserve-image-tag }}
TF_VAR_internal_image_tag: ${{ inputs.internal-image-tag }}
TF_VAR_search_image_tag: ${{ inputs.search-image-tag }}
TF_VAR_assets_version: ${{ inputs.assets-version }}
run: terraform plan -parallelism=80 ${{ inputs.destroy && '-destroy ' || '' }} -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:
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.environment }}-environment-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 environment: \`${{ inputs.workspace || inputs.environment }}\`
**Commit:** ${{ github.event.pull_request.head.sha }}
\n
**API version:** ${{ inputs.api-image-tag }}
**CLI version:** ${{ inputs.cli-image-tag }}
**Selfserve version:** ${{ inputs.selfserve-image-tag }}
**Internal version:** ${{ inputs.internal-image-tag }}
**search version:** ${{ inputs.search-image-tag }}
### 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.environment }}-environment-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 }}
env:
TF_VAR_api_image_tag: ${{ inputs.api-image-tag }}
TF_VAR_cli_image_tag: ${{ inputs.cli-image-tag }}
TF_VAR_selfserve_image_tag: ${{ inputs.selfserve-image-tag }}
TF_VAR_internal_image_tag: ${{ inputs.internal-image-tag }}
TF_VAR_search_image_tag: ${{ inputs.search-image-tag }}
TF_VAR_assets_version: ${{ inputs.assets-version }}
run: terraform apply -parallelism=80 ${{ inputs.destroy && '-destroy ' || '' }} -no-color -input=false -auto-approve ${{ inputs.terraform-args || '' }}
- name: Delete workspace
if: ${{ inputs.apply && inputs.destroy && inputs.workspace }}
run: terraform workspace select default && terraform workspace delete ${{ inputs.workspace }}
- name: Add Rollback Notice
if: ${{ inputs.rollback && inputs.apply }}
run: |
echo "## ⚠️ Environment Rollback Performed" >> $GITHUB_STEP_SUMMARY
echo "Environment: ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY
echo "Rolled back to:" >> $GITHUB_STEP_SUMMARY
echo "- API: ${{ inputs.api-image-tag }}" >> $GITHUB_STEP_SUMMARY
echo "- CLI: ${{ inputs.cli-image-tag }}" >> $GITHUB_STEP_SUMMARY
echo "- Selfserve: ${{ inputs.selfserve-image-tag }}" >> $GITHUB_STEP_SUMMARY
echo "- Internal: ${{ inputs.internal-image-tag }}" >> $GITHUB_STEP_SUMMARY
echo "- Search: ${{ inputs.search-image-tag }}" >> $GITHUB_STEP_SUMMARY
echo "- Assets: ${{ inputs.assets-version }}" >> $GITHUB_STEP_SUMMARY
- name: Set outputs
if: ${{ always() && !cancelled() && !failure() && !inputs.destroy }}
id: terraform-output
run: |
echo "json=$(terraform-bin output -json -no-color | jq -r -c 'to_entries | map({(.key): .value.value}) | add')" >> $GITHUB_OUTPUT