Skip to content

Commit

Permalink
ci: extend workflows with Terraform functionality (#24)
Browse files Browse the repository at this point in the history
* ci: extend workflows with Terraform functionality

* fixup! ci: extend workflows with Terraform functionality

* feat(terraform): add GitHub OIDC role

* ci: fix orchestrator patterns

* ci: add support for readonly OIDC role during plan

* ci: add Terraform lock files

* ci: fix the GitHub OIDC subjects

* fixup! ci: add support for readonly OIDC role during plan

* ci: fix steps running on failure

* ci: fix orchestrator job

* fixup! ci: fix steps running on failure

* fixup! ci: fix steps running on failure

* fix(terraform): add Dynamodb and S3 permissions to RO role

* fix(terraform): import remote state

* ci: disable production workflows

* ci: improve orchestrator to pickup module changes

* fix(terraform): add environment state lock permissions to RO OIDC

* feat(terraform): add dev environment state

* ci: support empty TF state

* fix(terraform): remove `hashicorp/http`

* fixup! ci: support empty TF state

* ci: allow Terraform env and account to plan in parallel

* fix(terraform): remove `prod` related files

* fix(terraform): run `fmt`

* ci: fix variable name

* ci: add image tags to TF plan comment
  • Loading branch information
JoshuaLicense authored Mar 11, 2024
1 parent c50dd0e commit a1d4494
Show file tree
Hide file tree
Showing 26 changed files with 1,085 additions and 10 deletions.
110 changes: 102 additions & 8 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,27 @@ jobs:
name: Orchestrator
runs-on: ubuntu-latest
outputs:
# App
should-build-app: ${{ steps.changed-api-files.outputs.any_changed == 'true' || steps.changed-selfserve-files.outputs.any_changed == 'true' || steps.changed-internal-files.outputs.any_changed == 'true' || null }}
should-build-docker: ${{ steps.changed-api-docker-files.outputs.any_changed == 'true' || steps.changed-selfserve-docker-files.outputs.any_changed == 'true' || steps.changed-internal-docker-files.outputs.any_changed == 'true' || null }}
should-build-api: ${{ steps.changed-api-files.outputs.any_changed == 'true' || null }}
should-build-selfserve: ${{ steps.changed-selfserve-files.outputs.any_changed == 'true' || null }}
should-build-internal: ${{ steps.changed-internal-files.outputs.any_changed == 'true' || null }}
# Docker
should-build-docker: ${{ steps.changed-api-docker-files.outputs.any_changed == 'true' || steps.changed-selfserve-docker-files.outputs.any_changed == 'true' || steps.changed-internal-docker-files.outputs.any_changed == 'true' || null }}
should-build-api-docker: ${{ steps.changed-api-docker-files.outputs.any_changed == 'true' || steps.changed-api-files.outputs.any_changed == 'true' || null }}
should-build-selfserve-docker: ${{ steps.changed-selfserve-docker-files.outputs.any_changed == 'true' || steps.changed-selfserve-files.outputs.any_changed == 'true' || null }}
should-build-internal-docker: ${{ steps.changed-internal-docker-files.outputs.any_changed == 'true' || steps.changed-internal-files.outputs.any_changed == 'true' || null }}
should-build-docs: ${{ steps.changed-website-files.outputs.any_changed == 'true' || null }}
# Terraform accounts
should-plan-terraform-accounts: ${{ steps.changed-accounts-terraform-files.outputs.any_changed == 'true' || null }}
should-plan-nonprod-account-terraform: ${{ contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/modules') || contains(steps.changed-accounts-terraform-files.outputs.all_changed_files, 'infra/terraform/accounts/nonprod') || null }}
should-plan-prod-account-terraform: ${{ contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/modules') || contains(steps.changed-accounts-terraform-files.outputs.all_changed_files, 'infra/terraform/accounts/prod') || null }}
# Terraform environments
should-plan-terraform-environments: ${{ steps.changed-environments-terraform-files.outputs.any_changed == 'true' || null }}
should-plan-dev-environment-terraform: ${{ contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/modules') || contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/environments/dev') || null }}
should-plan-int-environment-terraform: ${{ contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/modules') || contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/environments/int') || null }}
should-plan-prep-environment-terraform: ${{ contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/modules') || contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/environments/prep') || null }}
should-plan-prod-environment-terraform: ${{ contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/modules') || contains(steps.changed-environments-terraform-files.outputs.all_changed_files, 'infra/terraform/environments/prod') || null }}
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -59,6 +71,26 @@ jobs:
files: |
infra/docker/internal/**
# since_last_remote_commit: true
- uses: tj-actions/changed-files@v42
id: changed-accounts-terraform-files
with:
files: |
infra/terraform/accounts/**
infra/terraform/modules/**
files_ignore: |
infra/terraform/modules/service/**
# since_last_remote_commit: true
- uses: tj-actions/changed-files@v42
id: changed-environments-terraform-files
with:
files: |
infra/terraform/environments/{dev,int,prep,prod}/**
infra/terraform/modules/**
files_ignore: |
infra/terraform/modules/account/**
infra/terraform/modules/github/**
infra/terraform/modules/remote-state/**
# since_last_remote_commit: true
- uses: tj-actions/changed-files@v42
id: changed-website-files
with:
Expand All @@ -78,7 +110,7 @@ jobs:
permissions:
contents: write

get-app-versions:
get-version:
name: Get latest app version
needs:
- orchestrator
Expand Down Expand Up @@ -114,10 +146,10 @@ jobs:
app:
name: App
concurrency:
group: app-${{ matrix.project }}-${{ needs.get-app-versions.outputs[matrix.project] }}
group: app-${{ matrix.project }}-${{ needs.get-version.outputs[matrix.project] }}
needs:
- orchestrator
- get-app-versions
- get-version
if: ${{ needs.orchestrator.outputs.should-build-app || needs.orchestrator.outputs.should-build-docker }}
strategy:
fail-fast: false
Expand All @@ -134,18 +166,18 @@ jobs:
with:
project: ${{ matrix.project }}
should-upload-artefact: ${{ !!needs.orchestrator.outputs[format('should-build-{0}-docker', matrix.project)] }}
artefact-name: app-${{ matrix.project}}-${{ needs.get-app-versions.outputs[matrix.project] }}
artefact-name: app-${{ matrix.project}}-${{ needs.get-version.outputs[matrix.project] }}
retention-days: 1
permissions:
contents: read

docker:
name: Docker
concurrency:
group: docker-${{ matrix.project }}-${{ needs.get-app-versions.outputs[matrix.project] }}
group: docker-${{ matrix.project }}-${{ needs.get-version.outputs[matrix.project] }}
needs:
- orchestrator
- get-app-versions
- get-version
- app
if: ${{ always() && !cancelled() && !failure() && needs.orchestrator.outputs.should-build-docker }}
strategy:
Expand All @@ -162,8 +194,70 @@ jobs:
uses: ./.github/workflows/docker.yaml
with:
project: ${{ matrix.project }}
app-artefact-name: app-${{ matrix.project}}-${{ needs.get-app-versions.outputs[matrix.project] }}
app-artefact-name: app-${{ matrix.project}}-${{ needs.get-version.outputs[matrix.project] }}
should-upload-artefact-to-ecr: false
permissions:
contents: read
id-token: write

terraform-account:
name: Terraform Account
concurrency:
group: terraform-account-${{ matrix.account }}
if: ${{ needs.orchestrator.outputs.should-plan-terraform-accounts }}
needs:
- orchestrator
strategy:
fail-fast: false
matrix:
account:
- nonprod
#- prod
exclude:
- account: ${{ needs.orchestrator.outputs.should-plan-nonprod-account-terraform && 'ignored' || 'nonprod' }}
- account: ${{ needs.orchestrator.outputs.should-plan-prod-account-terraform && 'ignored' || 'prod' }}
uses: ./.github/workflows/deploy-account.yaml
with:
account: ${{ matrix.account }}
apply: false
permissions:
contents: read
id-token: write
pull-requests: write
deployments: write
secrets: inherit

terraform-env:
name: Terraform Environment
concurrency:
group: terraform-environment-${{ matrix.environment }}
needs:
- get-version
- orchestrator
if: ${{ needs.orchestrator.outputs.should-plan-terraform-environments }}
strategy:
fail-fast: false
matrix:
environment:
- dev
- int
#- prep
#- prod
exclude:
- environment: ${{ needs.orchestrator.outputs.should-plan-dev-environment-terraform && 'ignored' || 'dev' }}
- environment: ${{ needs.orchestrator.outputs.should-plan-int-environment-terraform && 'ignored' || 'int' }}
- environment: ${{ needs.orchestrator.outputs.should-plan-prep-environment-terraform && 'ignored' || 'prep' }}
- environment: ${{ needs.orchestrator.outputs.should-plan-prod-environment-terraform && 'ignored' || 'prod' }}
uses: ./.github/workflows/deploy-environment.yaml
with:
environment: ${{ matrix.environment }}
api-image-tag: ${{ needs.get-version.outputs.api }}
selfserve-image-tag: ${{ needs.get-version.outputs.selfserve }}
internal-image-tag: ${{ needs.get-version.outputs.internal }}
apply: false
permissions:
contents: read
id-token: write
pull-requests: write
deployments: write
secrets: inherit
197 changes: 197 additions & 0 deletions .github/workflows/deploy-account.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
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
required: true
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[format('ACCOUNT_{0}_TF_OIDC{1}_ROLE', inputs.account, (inputs.apply && '' || '_READONLY'))] || vars[format('TF_OIDC{0}_ROLE', (inputs.apply && '' || '_READONLY'))] }}
AWS_REGION: ${{ vars[format('ACCOUNT_{0}_TF_AWS_REGION', inputs.account)] || vars.TF_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

- 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

- 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 || '' }} && terraform show -no-color tfplan

- 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
- 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 { 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
${process.env.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
Loading

0 comments on commit a1d4494

Please sign in to comment.