From 173d7bd06f5d5ec0af0674e3d82a1202e7cd2aa6 Mon Sep 17 00:00:00 2001 From: Greg Tyler Date: Fri, 29 Sep 2023 15:56:33 +0100 Subject: [PATCH 1/2] Build PR environments To test the current configuration #major --- .github/workflows/build-push-images.yml | 90 +++++++++++++++++++++++++ .github/workflows/build.yml | 27 -------- .github/workflows/validate-api.yaml | 2 +- .github/workflows/workflow-pr.yml | 83 +++++++++++++++++++++++ 4 files changed, 174 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/build-push-images.yml delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/workflow-pr.yml diff --git a/.github/workflows/build-push-images.yml b/.github/workflows/build-push-images.yml new file mode 100644 index 00000000..988634b8 --- /dev/null +++ b/.github/workflows/build-push-images.yml @@ -0,0 +1,90 @@ +name: "[Job] Docker Build, Scan and Push to ECR" + +on: + workflow_call: + inputs: + docker_tag: + description: "Tag for docker image" + required: true + type: string + checkout_tag: + description: "Ref or tag to checkout" + default: ${{ github.ref }} + required: false + 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 + +defaults: + run: + shell: bash + +permissions: + id-token: write + contents: write + security-events: write + pull-requests: read + +jobs: + docker_build_scan_push: + strategy: + matrix: + include: + - ecr_repository: lpa-store/lambda/api-create + dir: create + runs-on: ubuntu-latest + name: ${{ matrix.ecr_repository }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.checkout_tag }} + - name: Build ${{ matrix.ecr_repository }} Image + id: build_image + run: | + docker build -f ./lambda/Dockerfile -t ${{ matrix.ecr_repository }} --build-arg DIR=${{ matrix.dir }} . + - name: Trivy Image Vulnerability Scanner for ${{ matrix.ecr_repository }} + id: trivy_scan + uses: aquasecurity/trivy-action@0.12.0 + with: + image-ref: ${{ matrix.ecr_repository }}:latest + severity: "HIGH,CRITICAL" + format: "sarif" + output: "trivy-results.sarif" + - name: Upload Trivy scan results to GitHub Security tab for ${{ matrix.ecr_repository }} + id: trivy_upload_sarif + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: "trivy-results.sarif" + - uses: unfor19/install-aws-cli-action@v1 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + 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-to-assume: arn:aws:iam::311462405659:role/lpa-store-ci + role-duration-seconds: 3600 + role-session-name: GitHubActions + - name: ECR Login + id: login_ecr + uses: aws-actions/amazon-ecr-login@v1.7.0 + with: + mask-password: true + registries: 311462405659 + - name: Push ${{ matrix.ecr_repository }} Image to ECR + env: + ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} + ECR_REPOSITORY: ${{ matrix.ecr_repository }} + run: | + docker tag ${{ matrix.ecr_repository }}:latest $ECR_REGISTRY/$ECR_REPOSITORY:${{ inputs.docker_tag }} + if ${{ github.workflow == 'Path To Live' }}; then + docker tag ${{ matrix.ecr_repository }}:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest + docker tag ${{ matrix.ecr_repository }}:latest $ECR_REGISTRY/$ECR_REPOSITORY:main-${{ inputs.docker_tag }} + fi + docker push --all-tags $ECR_REGISTRY/$ECR_REPOSITORY diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index f970d5d4..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Test & Build - -on: - pull_request: - branches: - - main - push: - branches: - - main - workflow_dispatch: - -defaults: - run: - shell: bash - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: "0" - - name: Build Images - run: make build - - name: Test API - run: make up test-api diff --git a/.github/workflows/validate-api.yaml b/.github/workflows/validate-api.yaml index 8a7265f7..41fd0be8 100644 --- a/.github/workflows/validate-api.yaml +++ b/.github/workflows/validate-api.yaml @@ -13,7 +13,7 @@ jobs: name: Validate runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v3 diff --git a/.github/workflows/workflow-pr.yml b/.github/workflows/workflow-pr.yml new file mode 100644 index 00000000..c32a7be8 --- /dev/null +++ b/.github/workflows/workflow-pr.yml @@ -0,0 +1,83 @@ +name: Test & Build + +on: + pull_request: + branches: + - main + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + generate-tags: + name: Generate tags + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: "0" + - name: Extract branch name + id: extract_branch + run: | + if [ "$GITHUB_EVENT_NAME" == "push" ]; then + echo BRANCH_NAME=main >> $GITHUB_ENV + else + branch=${{ github.head_ref }} + branch=${branch//-} + branch=${branch//_} + branch=${branch//\/} + echo BRANCH_NAME=${branch} >> $GITHUB_ENV + fi + - name: Bump version + id: bump_version + uses: anothrNick/github-tag-action@1.67.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INITIAL_VERSION: 0.0.0 + DEFAULT_BUMP: minor + PRERELEASE: true + PRERELEASE_SUFFIX: ${{ env.BRANCH_NAME }} + RELEASE_BRANCHES: main + WITH_V: true + outputs: + docker_tag: ${{ steps.bump_version.outputs.tag }} + + 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 }} + + build: + name: Build, Scan & Push Images + needs: [generate-tags] + uses: ./.github/workflows/build-push-images.yml + with: + docker_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/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 }} From b866e0382a8ae01f9d326942b0d150d6e96e2699 Mon Sep 17 00:00:00 2001 From: Greg Tyler Date: Mon, 2 Oct 2023 12:07:13 +0100 Subject: [PATCH 2/2] 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..9e582bea --- /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/environment + continue-on-error: true + + - name: Terraform Init + run: terraform init -input=false + working-directory: ./terraform/environment + + - 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/environment 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" +}