From 07fee9ebc02d60307e9d7af1a1eb91ed6accb5cd Mon Sep 17 00:00:00 2001 From: Greg Tyler Date: Mon, 2 Oct 2023 11:43:41 +0100 Subject: [PATCH] Add initial Terraform, deploy and destroy workflows When a PR is opened/reopened/updated, build the code and deploy to a popup env When a PR is closed, delete the popup env #minor --- .github/workflows/env-deploy.yml | 83 +++++++++++++++++++++ .github/workflows/env-destroy.yml | 54 ++++++++++++++ .github/workflows/workflow-pr-close.yml | 41 ++++++++++ .github/workflows/workflow-pr.yml | 23 +++--- terraform/environment/.envrc | 6 ++ terraform/environment/.terraform.lock.hcl | 25 +++++++ terraform/environment/dynamodb.tf | 32 ++++++++ terraform/environment/terraform.tf | 74 ++++++++++++++++++ terraform/environment/terraform.tfvars.json | 14 ++++ terraform/environment/variables.tf | 41 ++++++++++ 10 files changed, 382 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/env-deploy.yml create mode 100644 .github/workflows/env-destroy.yml create mode 100644 .github/workflows/workflow-pr-close.yml create mode 100644 terraform/environment/.envrc create mode 100644 terraform/environment/.terraform.lock.hcl create mode 100644 terraform/environment/dynamodb.tf create mode 100644 terraform/environment/terraform.tf create mode 100644 terraform/environment/terraform.tfvars.json create mode 100644 terraform/environment/variables.tf diff --git a/.github/workflows/env-deploy.yml b/.github/workflows/env-deploy.yml new file mode 100644 index 00000000..324cf007 --- /dev/null +++ b/.github/workflows/env-deploy.yml @@ -0,0 +1,83 @@ +name: "[Job] Deploy to Environment" + +on: + workflow_call: + inputs: + workspace_name: + description: "The terraform workspace to target for environment actions" + required: true + type: string + version_tag: + description: "The docker image tag to deploy in the environment" + required: true + type: string + secrets: + aws_access_key_id: + description: "AWS Access Key ID" + required: true + aws_secret_access_key: + description: "AWS Secret Access Key" + required: true + github_access_token: + description: 'Github Token' + required: true + +jobs: + terraform_environment_workflow: + runs-on: ubuntu-latest + # environment: + # name: ${{ inputs.workspace_name }} popup environment + # url: ${{ steps.terraform_outputs.outputs.url }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: "0" + - uses: unfor19/install-aws-cli-action@v1 + - uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.4.6 + terraform_wrapper: false + - name: Configure AWS Credentials For Terraform + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.aws_access_key_id }} + aws-secret-access-key: ${{ secrets.aws_secret_access_key }} + aws-region: eu-west-1 + role-duration-seconds: 3600 + role-session-name: OPGLpaStoreGithubAction + + - name: Lint Terraform + run: terraform fmt -check -recursive + working-directory: ./terraform/environment + continue-on-error: true + + - name: Terraform Init + run: terraform init -input=false + working-directory: ./terraform/environment + + - name: Terraform Plan + env: + TF_WORKSPACE: ${{ inputs.workspace_name }} + TF_VAR_app_version: ${{ inputs.version_tag }} + run: | + terraform workspace show + echo "plan_summary=$(terraform plan -no-color -lock-timeout=300s -input=false -parallelism=30 | grep -ioE 'Plan: [[:digit:]]+ to add, [[:digit:]]+ to change, [[:digit:]]+ to destroy|No changes. Your infrastructure matches the configuration.')" >> $GITHUB_OUTPUT + terraform plan -lock-timeout=300s -input=false -parallelism=30 + working-directory: ./terraform/environment + + - name: Terraform Apply + env: + TF_WORKSPACE: ${{ inputs.workspace_name }} + TF_VAR_app_version: ${{ inputs.version_tag }} + run: | + terraform apply -lock-timeout=300s -input=false -auto-approve -parallelism=30 + working-directory: ./terraform/environment + + # - name: Terraform Outputs + # id: terraform_outputs + # env: + # TF_WORKSPACE: ${{ inputs.workspace_name }} + # TF_VAR_app_version: ${{ inputs.version_tag }} + # run: | + # echo "url=$(terraform output -raw app_fqdn)" >> $GITHUB_OUTPUT + # working-directory: ./terraform/environment diff --git a/.github/workflows/env-destroy.yml b/.github/workflows/env-destroy.yml new file mode 100644 index 00000000..966e507d --- /dev/null +++ b/.github/workflows/env-destroy.yml @@ -0,0 +1,54 @@ +name: "[Job] Destroy Environment" + +on: + workflow_call: + inputs: + workspace_name: + description: "The terraform workspace to target for environment actions" + required: true + type: string + secrets: + aws_access_key_id: + description: "AWS Access Key ID" + required: true + aws_secret_access_key: + description: "AWS Secret Access Key" + required: true + +jobs: + terraform_environment_workflow: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: "0" + - uses: unfor19/install-aws-cli-action@v1 + - uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.4.6 + terraform_wrapper: false + - name: Configure AWS Credentials For Terraform + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.aws_access_key_id }} + aws-secret-access-key: ${{ secrets.aws_secret_access_key }} + aws-region: eu-west-1 + role-duration-seconds: 3600 + role-session-name: OPGLpaStoreGithubAction + + - name: Lint Terraform + run: terraform fmt -check -recursive + working-directory: ./terraform/aws + continue-on-error: true + + - name: Terraform Init + run: terraform init -input=false + working-directory: ./terraform/aws + + - name: Terraform Destroy + run: | + terraform workspace select ${{ inputs.workspace_name }} + terraform destroy -auto-approve + terraform workspace select default + terraform workspace delete ${{ inputs.workspace_name }} + working-directory: ./terraform/aws diff --git a/.github/workflows/workflow-pr-close.yml b/.github/workflows/workflow-pr-close.yml new file mode 100644 index 00000000..b95b43ad --- /dev/null +++ b/.github/workflows/workflow-pr-close.yml @@ -0,0 +1,41 @@ +name: PR Workflow + +on: + pull_request: + types: + - closed + branches: + - main + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + generate-environment-workspace-name: + runs-on: ubuntu-latest + steps: + - name: Generate workspace name + id: name_workspace + run: | + workspace=${{ github.event.number }}${{ github.head_ref }} + workspace=${workspace//-} + workspace=${workspace//_} + workspace=${workspace//\/} + workspace=${workspace:0:11} + workspace=$(echo ${workspace} | tr '[:upper:]' '[:lower:]') + echo "name=${workspace}" >> $GITHUB_OUTPUT + echo ${workspace} + outputs: + environment_workspace_name: ${{ steps.name_workspace.outputs.name }} + + destroy-pr-env: + name: Destroy PR Environment + needs: [generate-environment-workspace-name] + uses: ./.github/workflows/env-destroy.yml + with: + workspace_name: ${{ needs.generate-environment-workspace-name.outputs.environment_workspace_name }} + secrets: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/workflow-pr.yml b/.github/workflows/workflow-pr.yml index c32a7be8..fbf35673 100644 --- a/.github/workflows/workflow-pr.yml +++ b/.github/workflows/workflow-pr.yml @@ -1,4 +1,4 @@ -name: Test & Build +name: PR Workflow on: pull_request: @@ -71,13 +71,14 @@ jobs: aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - # deploy-pr-env: - # name: Deploy PR Environment - # needs: [build, generate-environment-workspace-name] - # uses: ./.github/workflows/deploy.yml - # with: - # workspace_name: ${{ needs.generate-environment-workspace-name.outputs.environment_workspace_name }} - # version_tag: ${{ needs.generate-tags.outputs.docker_tag }} - # secrets: - # aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - # aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + deploy-pr-env: + name: Deploy PR Environment + needs: [build, generate-environment-workspace-name] + uses: ./.github/workflows/env-deploy.yml + with: + workspace_name: ${{ needs.generate-environment-workspace-name.outputs.environment_workspace_name }} + version_tag: ${{ needs.generate-tags.outputs.docker_tag }} + secrets: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + github_access_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/terraform/environment/.envrc b/terraform/environment/.envrc new file mode 100644 index 00000000..1ce1470c --- /dev/null +++ b/terraform/environment/.envrc @@ -0,0 +1,6 @@ +# Terraform +export TF_WORKSPACE=development +export TF_VAR_default_role=operator +export TF_VAR_management_role=operator + +export TF_CLI_ARGS_init="-backend-config=role_arn=arn:aws:iam::311462405659:role/operator" diff --git a/terraform/environment/.terraform.lock.hcl b/terraform/environment/.terraform.lock.hcl new file mode 100644 index 00000000..188e9868 --- /dev/null +++ b/terraform/environment/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.19.0" + constraints = ">= 5.8.0" + hashes = [ + "h1:rgsqMIwX/2b2Ghrfd3lPasPoHupkWsEA+fcXod60+v8=", + "zh:03aa0f857c6dfce5f46c9bf3aad45534b9421e68983994b6f9dd9812beaece9c", + "zh:0639818c5bf9f9943667f39ec38bb945c9786983025dff407390133fa1ca5041", + "zh:0b82ad42ced8fb4a138eaf2fd37cf6059ca0bb482114b35fb84f22fc1500324a", + "zh:173e8c19a9f1d8f6457c80f4a73a92f420a81d650fc4ad0f97a5dc4b9485bba8", + "zh:42913a40ddfe9b4f3c78ad2e3cdc1dcfd48151bc132dc6b49fc32cd6da79db21", + "zh:452db5caca2e53d5f7090979d518e77aa5fd98385514b11ee2ce76a46e89cb53", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a12377ade89ee18d9be116436e411e8396898bd70b21ab027c161c785e86238d", + "zh:aa9e4746ba49044ad5b4dda57fcdba7bc16fe65f696766fb2c55c30a27abf844", + "zh:adfaee76d283f1c321fad2e4154be88d57da8c2ecfdca9516c8920bd2ece36ed", + "zh:bf6fbc6d60661c03ed2214173c1deced908dc62480dd41e67ac399fa4abd7467", + "zh:cb685da03ad00d1a27891f3d366d75e8795ac81f1b427888b434e6832ca40633", + "zh:e0432c78dfaf2baebe2bf5c0ad8087f547c69c2c5a00e4c1dcd5a6344ce726df", + "zh:e0ec9ccb8d34d6d0d8bf7f8628c223951832b4d50ea8887fc711fa854b3a28b4", + "zh:f274397ada4ef3c1dce2f70e719c8ccf19fc4e7a2e3f45d018764c6267fd7157", + ] +} diff --git a/terraform/environment/dynamodb.tf b/terraform/environment/dynamodb.tf new file mode 100644 index 00000000..339d81a8 --- /dev/null +++ b/terraform/environment/dynamodb.tf @@ -0,0 +1,32 @@ +resource "aws_dynamodb_table" "deeds_table" { + name = "deeds-${local.environment_name}" + billing_mode = "PAY_PER_REQUEST" + deletion_protection_enabled = local.environment.is_production + stream_enabled = false + hash_key = "uid" + + server_side_encryption { + enabled = true + } + + attribute { + name = "uid" + type = "S" + } + + point_in_time_recovery { + enabled = true + } + + lifecycle { + ignore_changes = [replica] + } + + provider = aws.eu_west_1 +} + +resource "aws_dynamodb_table_replica" "deeds_table" { + global_table_arn = aws_dynamodb_table.deeds_table.arn + point_in_time_recovery = true + provider = aws.eu_west_2 +} diff --git a/terraform/environment/terraform.tf b/terraform/environment/terraform.tf new file mode 100644 index 00000000..de16922a --- /dev/null +++ b/terraform/environment/terraform.tf @@ -0,0 +1,74 @@ +terraform { + backend "s3" { + bucket = "opg.terraform.state" + key = "opg-data-lpa-deed/terraform.tfstate" + encrypt = true + region = "eu-west-1" + role_arn = "arn:aws:iam::311462405659:role/lpa-store-ci" + dynamodb_table = "remote_lock" + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.8.0" + } + } + required_version = ">= 1.4.0" +} + +provider "aws" { + alias = "global" + region = "us-east-1" + + assume_role { + role_arn = "arn:aws:iam::${local.environment.account_id}:role/${var.default_role}" + session_name = "terraform-session" + } + + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + alias = "eu_west_1" + region = "eu-west-1" + + assume_role { + role_arn = "arn:aws:iam::${local.environment.account_id}:role/${var.default_role}" + session_name = "terraform-session" + } + + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + alias = "eu_west_2" + region = "eu-west-2" + + assume_role { + role_arn = "arn:aws:iam::${local.environment.account_id}:role/${var.default_role}" + session_name = "terraform-session" + } + + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + alias = "management" + region = "eu-west-1" + + assume_role { + role_arn = "arn:aws:iam::311462405659:role/${var.management_role}" + session_name = "terraform-session" + } + + default_tags { + tags = local.default_tags + } +} diff --git a/terraform/environment/terraform.tfvars.json b/terraform/environment/terraform.tfvars.json new file mode 100644 index 00000000..be484434 --- /dev/null +++ b/terraform/environment/terraform.tfvars.json @@ -0,0 +1,14 @@ +{ + "environments": { + "default": { + "account_id": "493907465011", + "account_name": "development", + "is_production": false + }, + "development": { + "account_id": "493907465011", + "account_name": "development", + "is_production": false + } + } +} diff --git a/terraform/environment/variables.tf b/terraform/environment/variables.tf new file mode 100644 index 00000000..b4d7be11 --- /dev/null +++ b/terraform/environment/variables.tf @@ -0,0 +1,41 @@ +locals { + environment_name = lower(replace(terraform.workspace, "_", "-")) + environment = contains(keys(var.environments), local.environment_name) ? var.environments[local.environment_name] : var.environments["default"] + + default_tags = merge(local.mandatory_moj_tags, local.optional_tags) + mandatory_moj_tags = { + business-unit = "OPG" + application = "opg-data-lpa-deed" + environment-name = local.environment_name + account = local.environment.account_name + is-production = local.environment.is_production + owner = "opgteam@digital.justice.gov.uk" + } + + optional_tags = { + source-code = "https://github.com/ministryofjustice/opg-data-lpa-deed" + infrastructure-support = "opgteam@digital.justice.gov.uk" + } +} + +variable "environments" { + type = map( + object({ + account_id = string + account_name = string + is_production = bool + }) + ) +} + +variable "default_role" { + description = "Role to assume in LPA Store account" + type = string + default = "lpa-store-ci" +} + +variable "management_role" { + description = "Role to assume in Management account" + type = string + default = "lpa-store-ci" +}