From 2bfef798877c51d8a2a14060df1c57c33b338843 Mon Sep 17 00:00:00 2001 From: Andy Price Date: Wed, 24 Apr 2024 17:01:58 +0100 Subject: [PATCH] SP-2061 - Update terraform pipeline to plan on PR changes and include a permanent development environment #minor (#262) * SP-2061 - Update terraform pipeline to plan on PR changes and include a permanent development environment #minor * SP-2061 - Update AWS, fix deprecations, add tflint #minor * SP-2061 - Remove db_subnet_group as actually managed in opg-shared-infrastructure #minor * SP-2061 - Fix ALB Bucket S3 Permissions, wait for ECS Service to be stable #minor --- .github/workflows/deploy.yml | 76 ----------- .github/workflows/terraform.yml | 132 +++++++++++++++++++ .github/workflows/terraform_pull_request.yml | 39 ------ terraform/.terraform.lock.hcl | 29 ++-- terraform/access_logs.tf | 28 +++- terraform/aurora.tf | 5 - terraform/data_sources.tf | 41 +++--- terraform/dns.tf | 10 +- terraform/ecs-task-definition.tf | 6 +- terraform/ecs.tf | 17 +-- terraform/loadbalancer.tf | 2 +- terraform/variables.tf | 22 ++-- terraform/versions.tf | 7 +- 13 files changed, 222 insertions(+), 192 deletions(-) delete mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/terraform.yml delete mode 100644 .github/workflows/terraform_pull_request.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index cd844d0..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Deploy - -on: - push: - branches: - - main - -defaults: - run: - shell: bash - -jobs: - pull_tag: - name: Pull latest tag from parameter store. - runs-on: ubuntu-latest - outputs: - latest-tag: ${{ steps.output_tag.outputs.tag }} - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - steps: - - uses: actions/checkout@v4 - - - name: Wait for build - uses: lewagon/wait-on-check-action@v1.3.1 - with: - ref: main - repo-token: ${{ secrets.GITHUB_TOKEN }} - wait-interval: 20 - running-workflow-name: 'Pull latest tag from parameter store.' - - - 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::997462338508:role/incident-response-ci - role-duration-seconds: 3600 - role-session-name: GitHubActions - - name: Install AWS CLI - id: install-aws-cli - uses: unfor19/install-aws-cli-action@v1 - - name: Pull Tag from Parameter Store - run: | - echo 'TAG_NAME='$(aws ssm get-parameter --region "eu-west-1" --name "incident-response-production-tag" --query Parameter.Value) >> $GITHUB_ENV - - name: Output Tag - id: output_tag - run: echo "::set-output name=tag::${{ env.TAG_NAME }}" - - - terraform: - name: 'Terraform' - needs: pull_tag - runs-on: ubuntu-latest - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - TF_WORKSPACE: production - TF_VAR_response_tag: ${{ needs.pull_tag.outputs.latest-tag }} - TF_VAR_nginx_tag: ${{ needs.pull_tag.outputs.latest-tag }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 - with: - terraform_version: 1.0.0 - - - name: Terraform Init - working-directory: ./terraform - run: terraform init - - - name: Terraform Apply - working-directory: ./terraform - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: terraform apply --auto-approve diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 0000000..03cb3c9 --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,132 @@ +name: Terraform Lint, Plan, Apply + +on: + pull_request: + branches: + - main + paths: + - 'terraform/*' + push: + branches: + - main + workflow_dispatch: + +defaults: + run: + shell: bash + working-directory: terraform + +jobs: + pull-tag: + name: Pull latest tag from parameter store. + runs-on: ubuntu-latest + outputs: + latest-tag: ${{ steps.output_tag.outputs.tag }} + steps: + - uses: actions/checkout@v4 + - 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::997462338508:role/incident-response-ci + role-duration-seconds: 3600 + role-session-name: GitHubActions + - name: Install AWS CLI + id: install-aws-cli + uses: unfor19/install-aws-cli-action@v1 + - name: Pull Tag from Parameter Store + run: | + echo 'TAG_NAME='$(aws ssm get-parameter --region "eu-west-1" --name "incident-response-production-tag" --query Parameter.Value) >> $GITHUB_ENV + - name: Output Tag + id: output_tag + run: echo "::set-output name=tag::${{ env.TAG_NAME }}" + + lint-and-validate: + name: Terraform Lint & Validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 1.8.1 + terraform_wrapper: false + - uses: terraform-linters/setup-tflint@v4 + name: Setup TFLint + with: + tflint_version: v0.50.1 + + - name: Configure AWS Credentials For Terraform + uses: aws-actions/configure-aws-credentials@v1 + 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-session-name: GitHubActionsTerraform + + - name: Terraform Format + run: terraform fmt --check --recursive + + - name: TF Lint + run: tflint --recursive + + - name: Terraform Init + run: terraform init + + - name: Terraform Validate + run: terraform validate + + + plan-and-apply: + name: Plan ${{ matrix.environment }} + runs-on: ubuntu-latest + needs: + - lint-and-validate + - pull-tag + env: + TF_VAR_response_tag: ${{ needs.pull-tag.outputs.latest-tag }} + TF_VAR_nginx_tag: ${{ needs.pull-tag.outputs.latest-tag }} + strategy: + max-parallel: 1 + matrix: + include: + - environment: "Development" + workspace_environment: "development" + + - environment: "Production" + workspace_environment: "production" + + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 1.8.1 + terraform_wrapper: false + + - name: Configure AWS Credentials For Terraform + uses: aws-actions/configure-aws-credentials@v1 + 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-session-name: GitHubActionsTerraform + + - name: Setup + run: echo TF_WORKSPACE=${{ matrix.workspace_environment }} >> $GITHUB_ENV + + - name: Init + run: terraform init + + - name: Plan + run: terraform plan --lock-timeout=300s --parallelism=200 --out=${{ env.TF_WORKSPACE }}.plan > ${{ env.TF_WORKSPACE }}.log + + - name: Output Plan + run: cat ${{ env.TF_WORKSPACE }}.log + + - name: Output ConcisePlan + run: cat ${{ env.TF_WORKSPACE }}.log | grep '\.' | grep '#' || true + + - name: Apply ${{ matrix.environment }} + if: github.ref == 'refs/heads/main' + run: terraform apply -parallelism=200 -lock-timeout=300s ${{ env.TF_WORKSPACE }}.plan diff --git a/.github/workflows/terraform_pull_request.yml b/.github/workflows/terraform_pull_request.yml deleted file mode 100644 index 9268981..0000000 --- a/.github/workflows/terraform_pull_request.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Terraform Plan & Validate - -on: - pull_request: - branches: - - main - paths: - - 'terraform/*' - -defaults: - run: - shell: bash - -jobs: - terraform: - name: 'Terraform' - runs-on: ubuntu-latest - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - TF_WORKSPACE: production - - steps: - - name: Checkout - uses: actions/checkout@main - - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 - with: - terraform_version: 1.0.0 - - - name: Terraform Init - run: cd terraform && terraform init - - - name: Terraform Validate - run: cd terraform && terraform validate - - - name: Terraform Plan - run: cd terraform && terraform plan diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl index 34bbb93..29fe58e 100644 --- a/terraform/.terraform.lock.hcl +++ b/terraform/.terraform.lock.hcl @@ -2,20 +2,23 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.8.0" + version = "5.46.0" hashes = [ - "h1:W2cPGKmqkPbTc91lu42QeC3RFBqB5TnRnS3IxNME2FM=", - "zh:16cbdbc03ad13358d12433e645e2ab5a615e3a3662a74e3c317267c9377713d8", - "zh:1d813c5e6c21fe370652495e29f783db4e65037f913ff0d53d28515c36fbb70a", - "zh:31ad8282e31d0fac62e96fc2321a68ad4b92ab90f560be5f875d1b01a493e491", - "zh:5099a9e699784cabb5686d2cb52ca910f9c697e977c654ecedd196e838387623", - "zh:5758cbb813091db8573f27bba37c48f63ba95f2104f3bc49f13131e3c305b848", - "zh:67ea77fb00bf0a09e712f5259a7acb494ce503a34809b7919996744fd92e3312", - "zh:72c87be5d1f7917d4281c14a3335a9ec3cd57bf63d95a440faa7035248083dcd", - "zh:79005154b9f5eccc1580e0eb803f0dfee68ba856703ef6489719cb014a3c2b18", + "h1:d0Mf33mbbQujZ/JaYkqmH5gZGvP+iEIWf9yBSiOwimE=", + "zh:05ae6180a7f23071435f6e5e59c19af0b6c5da42ee600c6c1568c8660214d548", + "zh:0d878d1565d5e57ce6b34ec5f04b28662044a50c999ec5770c374aa1f1020de2", + "zh:25ef1467af2514d8011c44759307445f7057836ff87dfe4503c3e1c9776d5c1a", + "zh:26c006df6200f0063b827aab05bec94f9f3f77848e82ed72e48a51d1170d1961", + "zh:37cdf4292649a10f12858622826925e18ad4eca354c31f61d02c66895eb91274", + "zh:4315b0433c2fc512666c74e989e2d95240934ef370bea1c690d36cb02d30c4ce", + "zh:75df0b3f631b78aeff1832cc77d99b527c2a5e79d40f7aac40bdc4a66124dac2", + "zh:90693d936c9a556d2bf945de4920ff82052002eb73139bd7164fafd02920f0ef", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:d27f9a8b5b30883a3e45f77506391524df0c66a76c3bc71f7236c3fc81d0597d", - "zh:e2985563dc652cf9b10420bc62f0a710308ef5c31e46b94c8ea10b8f27fa1ef3", - "zh:f11bb34ee0dad4bc865db51e7e299a4f030c5e9f6b6080d611797cc99deeb40a", + "zh:c9177ad09804c60fd2ed25950570407b6bdcdf0fcc309e1673b584f06a827fae", + "zh:ca8e8db24a4d62d92afd8d3d383b81a08693acac191a2e0a110fb46deeff56a3", + "zh:d5fa3a36e13957d63bfe9bbd6df0426a2422214403aac9f20b60c36f8d9ebec6", + "zh:e4ede44a112296c9cc77b15e439e41ee15c0e8b3a0dec94ae34df5ebba840e8b", + "zh:f2d4de8d8cde69caffede1544ebea74e69fcc4552e1b79ae053519a05c060706", + "zh:fc19e9266b1841d4a3aeefa8a5b5ad6988baed6540f85a373b6c2d0dc1ca5830", ] } diff --git a/terraform/access_logs.tf b/terraform/access_logs.tf index 45e2c32..9223158 100644 --- a/terraform/access_logs.tf +++ b/terraform/access_logs.tf @@ -27,9 +27,24 @@ resource "aws_s3_bucket" "access_log" { force_destroy = true } -resource "aws_s3_bucket_acl" "access_log" { +resource "aws_s3_bucket_ownership_controls" "bucket_object_ownership" { bucket = aws_s3_bucket.access_log.id - acl = "private" + rule { + object_ownership = "BucketOwnerEnforced" + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "bucket" { + bucket = aws_s3_bucket.access_log.id + + rule { + id = "ExpireObjectsAfter13Months" + status = "Enabled" + + expiration { + days = 400 + } + } } resource "aws_s3_bucket_server_side_encryption_configuration" "access_log" { @@ -42,6 +57,15 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "access_log" { } } +resource "aws_s3_bucket_public_access_block" "public_access_policy" { + bucket = aws_s3_bucket.access_log.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + resource "aws_s3_bucket_policy" "access_log" { bucket = aws_s3_bucket.access_log.id policy = data.aws_iam_policy_document.loadbalancer.json diff --git a/terraform/aurora.tf b/terraform/aurora.tf index 0780aff..bd8b38f 100644 --- a/terraform/aurora.tf +++ b/terraform/aurora.tf @@ -42,8 +42,3 @@ resource "aws_security_group_rule" "response_rds_ecs_task" { source_security_group_id = aws_security_group.ecs_service.id description = "Response RDS inbound from Response ECS tasks" } - -resource "aws_db_subnet_group" "data_persitance_subnet_group" { - name = "data-persitance-subnet-${terraform.workspace}" - subnet_ids = data.aws_subnet_ids.data_persitance.ids -} diff --git a/terraform/data_sources.tf b/terraform/data_sources.tf index 74847cc..0db74a7 100644 --- a/terraform/data_sources.tf +++ b/terraform/data_sources.tf @@ -2,38 +2,27 @@ data "aws_vpc" "default" { default = true } -data "aws_availability_zones" "available" { - state = "available" -} - -data "aws_subnet_ids" "public" { - vpc_id = data.aws_vpc.default.id - tags = { Name = "*public*" } -} - -data "aws_subnet" "public" { - count = length(tolist(data.aws_subnet_ids.public.ids)) - availability_zone = data.aws_availability_zones.available.names[count.index] - tags = { Name = "*public*" } -} - -data "aws_subnet_ids" "private" { - vpc_id = data.aws_vpc.default.id - tags = { Name = "private" } -} +data "aws_subnets" "public" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } -data "aws_subnet" "private" { - count = length(tolist(data.aws_subnet_ids.private.ids)) - availability_zone = data.aws_availability_zones.available.names[count.index] - tags = { Name = "private" } + filter { + name = "tag:Name" + values = ["public"] + } } -data "aws_subnet_ids" "data_persitance" { - vpc_id = data.aws_vpc.default.id +data "aws_subnets" "private" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } filter { name = "tag:Name" - values = ["persistence"] + values = ["private"] } } diff --git a/terraform/dns.tf b/terraform/dns.tf index b71b288..2372a2c 100644 --- a/terraform/dns.tf +++ b/terraform/dns.tf @@ -1,6 +1,11 @@ +locals { + dns_prefix = lookup(local.dns_prefixes, terraform.workspace, "incident") + dns_suffix = "opg.service.justice.gov.uk" + dns_name = "${local.dns_prefix}.${local.dns_suffix}" +} data "aws_route53_zone" "opg_service_justice_gov_uk" { provider = aws.management - name = "opg.service.justice.gov.uk" + name = local.dns_suffix } resource "aws_route53_record" "response" { @@ -21,7 +26,7 @@ resource "aws_route53_record" "response" { } resource "aws_acm_certificate" "response" { - domain_name = aws_route53_record.response.fqdn + domain_name = local.dns_name validation_method = "DNS" lifecycle { @@ -44,7 +49,6 @@ resource "aws_route53_record" "validation" { type = each.value.type provider = aws.management zone_id = data.aws_route53_zone.opg_service_justice_gov_uk.id - depends_on = [aws_acm_certificate.response] } resource "aws_acm_certificate_validation" "response" { diff --git a/terraform/ecs-task-definition.tf b/terraform/ecs-task-definition.tf index c1cb54c..f51318e 100644 --- a/terraform/ecs-task-definition.tf +++ b/terraform/ecs-task-definition.tf @@ -22,11 +22,13 @@ data "aws_ecr_repository" "nginx" { } variable "nginx_tag" { - default = "v1.4.0" + default = "v1.117.0" + type = string } variable "response_tag" { - default = "v1.4.0" + default = "v1.117.0" + type = string } locals { diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 4e9575a..3d68a63 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -3,13 +3,14 @@ resource "aws_ecs_cluster" "cluster" { } resource "aws_ecs_service" "service" { - name = "response" - cluster = aws_ecs_cluster.cluster.id - task_definition = aws_ecs_task_definition.response.arn - desired_count = 1 - launch_type = "FARGATE" - platform_version = "1.4.0" - depends_on = [aws_lb.loadbalancer] + name = "response" + cluster = aws_ecs_cluster.cluster.id + task_definition = aws_ecs_task_definition.response.arn + desired_count = 1 + launch_type = "FARGATE" + platform_version = "1.4.0" + depends_on = [aws_lb.loadbalancer] + wait_for_steady_state = true deployment_controller { type = "ECS" @@ -17,7 +18,7 @@ resource "aws_ecs_service" "service" { network_configuration { security_groups = [aws_security_group.ecs_service.id] - subnets = data.aws_subnet.private.*.id + subnets = data.aws_subnets.private.ids assign_public_ip = false } diff --git a/terraform/loadbalancer.tf b/terraform/loadbalancer.tf index 8e3bb8e..401190e 100644 --- a/terraform/loadbalancer.tf +++ b/terraform/loadbalancer.tf @@ -2,7 +2,7 @@ resource "aws_lb" "loadbalancer" { name = "incident-response-${terraform.workspace}" internal = false load_balancer_type = "application" - subnets = data.aws_subnet.public.*.id + subnets = data.aws_subnets.public.ids security_groups = [ aws_security_group.incident_response_loadbalancer.id, diff --git a/terraform/variables.tf b/terraform/variables.tf index 8404424..a5eee4a 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -1,20 +1,19 @@ locals { + accounts = { + development = 679638075911 + production = 997462338508 + } + dns_prefixes = { "development" = "dev.incident" "production" = "incident" } - is_production = { - "development" = "false" - "production" = "true" - } - - dns_prefix = "incident" - mandatory_moj_tags = { business-unit = "OPG" application = "opg-incident-response" environment-name = terraform.workspace + is-production = tostring(terraform.workspace == "production" ? true : false) owner = "OPG Webops: opgteam@digital.justice.gov.uk" } @@ -28,15 +27,10 @@ locals { variable "default_role" { default = "incident-response-ci" + type = string } variable "management_role" { default = "incident-response-ci" -} - -variable "accounts" { - default = { - "development" = "679638075911" - "production" = "997462338508" - } + type = string } diff --git a/terraform/versions.tf b/terraform/versions.tf index aae90fe..61ba4b2 100644 --- a/terraform/versions.tf +++ b/terraform/versions.tf @@ -10,17 +10,18 @@ terraform { required_providers { aws = { - source = "hashicorp/aws" + source = "hashicorp/aws" + version = ">= 5.42.0" } } - required_version = ">= 1.0.0" + required_version = ">= 1.8.1" } provider "aws" { region = "eu-west-1" assume_role { - role_arn = "arn:aws:iam::${lookup(var.accounts, terraform.workspace)}:role/${var.default_role}" + role_arn = "arn:aws:iam::${lookup(local.accounts, terraform.workspace, local.accounts["development"])}:role/${var.default_role}" session_name = "terraform-session" }