diff --git a/.github/workflows/review_apps_on_pr_change.yml b/.github/workflows/review_apps_on_pr_change.yml new file mode 100644 index 000000000..441b12c16 --- /dev/null +++ b/.github/workflows/review_apps_on_pr_change.yml @@ -0,0 +1,110 @@ +name: "Review apps: on PR change" +on: + pull_request: + # being explicit about what to trigger on. + # matches the docs for the default types + # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request + types: [opened, reopened, synchronize] +jobs: + update-review-app: + # this references a codebuild project configured in forms-deploy + # see: https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner.html + runs-on: codebuild-review-forms-runner-gha-runner-${{github.run_id}}-${{github.run_attempt}} + + permissions: + pull-requests: write + + steps: + - name: Generate container image URI + run: | + echo "CONTAINER_IMAGE_URI=842676007477.dkr.ecr.eu-west-2.amazonaws.com/forms-runner:pr-${{github.event.pull_request.number}}-${{github.event.pull_request.head.sha}}-$(date +%s)" >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build container + run: | + # Docker credentials are configured in CodeBuild + # CodeBuild retrieves the credentials from ParameterStore + echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin + docker build \ + --tag "${{env.CONTAINER_IMAGE_URI}}" \ + . + + - name: Push container + id: build-container + run: | + aws ecr get-login-password --region eu-west-2 \ + | docker login --username AWS --password-stdin 842676007477.dkr.ecr.eu-west-2.amazonaws.com + + echo "Pushing container image" + echo "${{env.CONTAINER_IMAGE_URI}}" + + docker push "${CONTAINER_IMAGE_URI}" + + - name: Determine Terraform version + id: terraform-version + run: | + cat .review_apps/.terraform-version | xargs printf "TF_VERSION=%s" >> "$GITHUB_OUTPUT" + + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{steps.terraform-version.outputs.TF_VERSION}} + + - name: Deploy review app + id: deploy + run: | + cd .review_apps/ + + terraform init -backend-config="key=review-apps/forms-runner/pr-${{github.event.pull_request.number}}.tfstate" + + terraform apply \ + -var "pull_request_number=${{github.event.pull_request.number}}" \ + -var "forms_runner_container_image=${{env.CONTAINER_IMAGE_URI}}" \ + -no-color \ + -auto-approve + + echo "REVIEW_APP_URL=$(terraform output -raw review_app_url)" >> "$GITHUB_OUTPUT" + echo "ADMIN_APP_URL=$(terraform output -raw admin_app_url)" >> "$GITHUB_OUTPUT" + echo "ECS_CLUSTER_ID=$(terraform output -raw review_app_ecs_cluster_id)" >> "$GITHUB_OUTPUT" + echo "ECS_SERVICE_NAME=$(terraform output -raw review_app_ecs_service_name)" >> "$GITHUB_OUTPUT" + + - name: Wait for AWS ECS deployments to finish + run: | + aws ecs wait services-stable \ + --cluster "${{steps.deploy.outputs.ECS_CLUSTER_ID}}" \ + --services "${{steps.deploy.outputs.ECS_SERVICE_NAME}}" + + - name: Comment on PR + env: + COMMENT_MARKER: + GH_TOKEN: ${{ github.token }} + run: | + cat < "${{runner.temp}}/pr-comment.md" + :tada: A review copy of this PR has been deployed! It is made of up two components + + 1. [A review copy of forms-runner](${{steps.deploy.outputs.REVIEW_APP_URL}}) + 2. [A production copy of forms-admin](${{steps.deploy.outputs.ADMIN_APP_URL}}) + + > [!IMPORTANT] + > Not all of the functionality of forms-runner is present in review apps. + > Functionality such as sending emails, file upload, and S3 submission types are + > deliberately disabled for the sake of simplifying review apps. + > + > You should use the full dev environment to test the functionality which is disabled here. + + It may take 5 minutes or so for the application to be fully deployed and working. If it still isn't ready + after 5 minutes, there may be something wrong with the ECS task. You will need to go to the integration AWS account + to debug, or otherwise ask an infrastructure person. + + For the sign in details and more information, [see the review apps wiki page](https://github.com/alphagov/forms-team/wiki/Review-apps). + + $COMMENT_MARKER + EOF + + old_comment_ids=$(gh api "repos/{owner}/{repo}/issues/${{github.event.pull_request.number}}/comments" --jq 'map(select((.user.login == "github-actions[bot]") and (.body | endswith($ENV.COMMENT_MARKER + "\n")))) | .[].id') + for comment_id in $old_comment_ids; do + gh api -X DELETE "repos/{owner}/{repo}/issues/comments/${comment_id}" + done + + gh pr comment "${{github.event.pull_request.html_url}}" --body-file "${{runner.temp}}/pr-comment.md" diff --git a/.github/workflows/review_apps_on_pr_close.yml b/.github/workflows/review_apps_on_pr_close.yml new file mode 100644 index 000000000..2fba91e0f --- /dev/null +++ b/.github/workflows/review_apps_on_pr_close.yml @@ -0,0 +1,36 @@ +name: "Review apps: on PR close" +on: + pull_request: + # only run when a PR is closed or merged + types: [closed] +env: + IMAGE_TAG: "842676007477.dkr.ecr.eu-west-2.amazonaws.com/forms-runner:pr-${{github.event.pull_request.number}}-${{github.event.pull_request.head.ref}}" +jobs: + delete-review-app: + # this references a codebuild project configured in forms-deploy + # see: https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner.html + runs-on: codebuild-review-forms-runner-gha-runner-${{github.run_id}}-${{github.run_attempt}} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Determine Terraform version + id: terraform-version + run: | + cat .review_apps/.terraform-version | xargs printf "TF_VERSION=%s" >> "$GITHUB_OUTPUT" + + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{steps.terraform-version.outputs.TF_VERSION}} + + - name: Delete review app + run: | + cd .review_apps/ + + terraform init -backend-config="key=review-apps/forms-runner/pr-${{github.event.pull_request.number}}.tfstate" + terraform destroy \ + -var "pull_request_number=${{github.event.pull_request.number}}" \ + -var "forms_runner_container_image=${{env.IMAGE_TAG}}" \ + -no-color \ + -auto-approve diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 000000000..8a9b731ac --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,37 @@ +name: "Terraform" +on: + pull_request: + branches: [main] + paths: + - ".review_apps/**" + merge_group: + types: [checks_requested] +env: + TERRAFORM_VERSION: "1.11.0" +jobs: + terraform: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{env.TERRAFORM_VERSION}} + + - name: Check Terraform style + id: tf_fmt + working-directory: ".review_apps/" + run: | + terraform fmt -write=false -diff=true -list=true -recursive -check + + - name: Lint Terraform + run: | + pip install -r .review_apps/requirements.txt + checkov -d .review_apps/ --framework terraform --quiet + + - name: Validate Terraform syntax + working-directory: ".review_apps/" + run : | + terraform init -backend=false || exit + terraform validate diff --git a/.gitignore b/.gitignore index 1005b2bca..eb6d026a7 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ config/settings/*.local.yml config/environments/*.local.yml .DS_Store + +# Terraform +.terraform/ diff --git a/.review_apps/.terraform-version b/.review_apps/.terraform-version new file mode 100644 index 000000000..169f19b49 --- /dev/null +++ b/.review_apps/.terraform-version @@ -0,0 +1 @@ +1.11.0 \ No newline at end of file diff --git a/.review_apps/.terraform.lock.hcl b/.review_apps/.terraform.lock.hcl new file mode 100644 index 000000000..d858d1200 --- /dev/null +++ b/.review_apps/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.72.1" + constraints = "~> 5.72.1" + hashes = [ + "h1:jhd5O5o0CfZCNEwwN0EiDAzb7ApuFrtxJqa6HXW4EKE=", + "zh:0dea6843836e926d33469b48b948744079023816d16a2ff7666bcfb6aa3522d4", + "zh:195fa9513f75800a0d62797ebec75ee73e9b8c28d713fe9b63d3b1d1eec129b3", + "zh:1ed92f3961715bf0e024bcde3c12dfbdc50b00c1f8a43cc00802cfc45a256208", + "zh:2ac687e3a52606466cae4a6813e81d923042488df88d2424e28d3f8530f091bb", + "zh:32e7ca75f9314557daada3c44628fe1f3bf964a4f833bfb4b2295d833fe64b6f", + "zh:374ee0e6b4327cc6ef666908ce5d6450a3a56e90cd2b785e83c2bcfc100021d2", + "zh:5500fd6fdac44f96411fcf9c6d01691159ec35455ed127eb4c3a498e1cc92a64", + "zh:723a2dc4b064c12e7ee62ad4fbfd72fa5e025206ea47b735994ef53f3c373152", + "zh:89d97b87605f1d734f27e642567cbecf785b521af8ea81dac55c77ccde876221", + "zh:951ee1e5731e8d65d521d71b95927e55055b3c4656eef6d46fa580a63328befc", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9b2b362470b64ec227b2da64762ab8bc4111c6b80365fd9d82fc5e1e33f44038", + "zh:aa6e57d0cb974ff0da5dee5d43ad2745cbbc4a2b507d4c799839b9fa96daf688", + "zh:ba0d14c4a6b7aa844a830d47c0bf995b632e37f0795394b5b60c638b62b7fc03", + "zh:c9764065a9c5d324db0b02bd201b9e3a2118e49c4960884acdeea377173302e9", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.11.1" + constraints = "0.11.1" + hashes = [ + "h1:pQGSL9mdgw4qsLndFYsEF93mbsIxyxNoAyIbBqhS3Xo=", + "zh:19a393db736ec4fd024d098d55aefaef07056c37a448ece3b55b3f5f4c2c7e4a", + "zh:227fa1e221de2907f37be78d40c06ca6a6f7b243a1ec33ade014dfaf6d92cd9c", + "zh:29970fecbf4a3ca23bacbb05d6b90cdd33dd379f90059fe39e08289951502d9f", + "zh:65024596f22f10e7dcb5e0e4a75277f275b529daa0bc0daf34ca7901c678ab88", + "zh:694d080cb5e3bf5ef08c7409208d061c135a4f5f4cdc93ea8607860995264b2e", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:b29d15d13e1b3412e6a4e1627d378dbd102659132f7488f64017dd6b6d5216d3", + "zh:bb79f4cae9f8c17c73998edc54aa16c2130a03227f7f4e71fc6ac87e230575ec", + "zh:ceccf80e95929d97f62dcf1bb3c7c7553d5757b2d9e7d222518722fc934f7ad5", + "zh:f40e638336527490e294d9c938ae55919069e6987e85a80506784ba90348792a", + "zh:f99ef33b1629a3b2278201142a3011a8489e66d92da832a5b99e442204de18fb", + "zh:fded14754ea46fdecc62a52cd970126420d4cd190e598cb61190b4724a727edb", + ] +} diff --git a/.review_apps/README.md b/.review_apps/README.md new file mode 100644 index 000000000..981e83a33 --- /dev/null +++ b/.review_apps/README.md @@ -0,0 +1,19 @@ +# Review apps + +The Terraform code in this directory is used to deploy a review copy of `forms-admin`. + +It constructs a minimal, ephemeral version of a GOV.UK Forms environment in AWS ECS that can be used for reviews, then freely destroyed. This includes: + +* a copy of `forms-admin` at the commit in question +* a copy of the version of `forms-api` currently in production +* a local PostgreSQL database with seed data for both `forms-api` and `forms-admin` + +Review apps rely on a set of underlying infrastructure managed and deployed in `forms-deploy`. The Terraform will require you to be targeting the `integration` AWS account (where the `review` environment lives), and you should not override this. + +### State files +Each review app uses its own Terraform state file, stored in an S3 bucket. The bucket itself is created and managed by `forms-deploy` and its name is safely assumed. + +### `forms-admin` container image +The `forms-admin` container image to deploy is supplied under the `forms_admin_container_image` variable. Terraform does not build the container. It is assumed to be built and stored ahead of time. + + diff --git a/.review_apps/app_autoscaling.tf b/.review_apps/app_autoscaling.tf new file mode 100644 index 000000000..69a9355c4 --- /dev/null +++ b/.review_apps/app_autoscaling.tf @@ -0,0 +1,38 @@ +resource "aws_appautoscaling_target" "review_app" { + service_namespace = "ecs" + resource_id = "service/${data.terraform_remote_state.review.outputs.ecs_cluster_id}/${aws_ecs_service.app.name}" + scalable_dimension = "ecs:service:DesiredCount" + + max_capacity = 1 + min_capacity = 1 +} + +resource "aws_appautoscaling_scheduled_action" "shutdown_at_night" { + name = "forms-runner-pr-${var.pull_request_number}-shutdown-at-night" + + service_namespace = aws_appautoscaling_target.review_app.service_namespace + resource_id = aws_appautoscaling_target.review_app.resource_id + scalable_dimension = aws_appautoscaling_target.review_app.scalable_dimension + + schedule = "cron(0 18 * * ? *)" # daily at 1800 + + scalable_target_action { + min_capacity = 0 + max_capacity = 0 + } +} + +resource "aws_appautoscaling_scheduled_action" "startup_weekday_mornings" { + name = "forms-runner-pr-${var.pull_request_number}-startup-weekday-mornings" + + service_namespace = aws_appautoscaling_target.review_app.service_namespace + resource_id = aws_appautoscaling_target.review_app.resource_id + scalable_dimension = aws_appautoscaling_target.review_app.scalable_dimension + + schedule = "cron(0 8 ? * MON-FRI *)" # Monday-Friday at 0800 + + scalable_target_action { + min_capacity = 1 + max_capacity = 1 + } +} diff --git a/.review_apps/dependencies.tf b/.review_apps/dependencies.tf new file mode 100644 index 000000000..4360223a6 --- /dev/null +++ b/.review_apps/dependencies.tf @@ -0,0 +1,26 @@ +## +# Terraform remote state data resources are +# used to read the content of a Terraform state +# file. +# +# This is common pattern in the `forms-deploy` +# codebase, and is used to share information +# between different Terraform roots without +# having to do any external wiring of outputs +# to inputs. +# +# In this instance, we will be sharing things +# like the subnet and security groups ids that +# are necessary for deploying to AWS ECS. +## +data "terraform_remote_state" "review" { + backend = "s3" + + config = { + key = "review.tfstate" + bucket = "gds-forms-integration-tfstate" + region = "eu-west-2" + + use_lockfile = true + } +} diff --git a/.review_apps/ecs_service.tf b/.review_apps/ecs_service.tf new file mode 100644 index 000000000..e3fdb3804 --- /dev/null +++ b/.review_apps/ecs_service.tf @@ -0,0 +1,24 @@ +resource "aws_ecs_service" "app" { + #checkov:skip=CKV_AWS_332:We don't want to target "LATEST" and get a surprise when a new version is released. + name = "forms-runner-pr-${var.pull_request_number}" + + cluster = data.terraform_remote_state.review.outputs.ecs_cluster_id + task_definition = aws_ecs_task_definition.task.arn + + desired_count = 1 + deployment_maximum_percent = "200" + deployment_minimum_healthy_percent = "100" + force_new_deployment = true + + + launch_type = "FARGATE" + platform_version = "1.4.0" + + network_configuration { + subnets = data.terraform_remote_state.review.outputs.private_subnet_ids + security_groups = [data.terraform_remote_state.review.outputs.review_apps_security_group_id] + assign_public_ip = false + } + + depends_on = [aws_ecs_task_definition.task] +} diff --git a/.review_apps/ecs_task_definition.tf b/.review_apps/ecs_task_definition.tf new file mode 100644 index 000000000..7eb32774c --- /dev/null +++ b/.review_apps/ecs_task_definition.tf @@ -0,0 +1,365 @@ +locals { + logs_stream_prefix = "${data.terraform_remote_state.review.outputs.review_apps_log_group_name}/forms-runner/pr-${var.pull_request_number}" + + runner_review_app_hostname = "pr-${var.pull_request_number}.submit.review.forms.service.gov.uk" + + # Admin has it's own hostname because it needs to be user facing too, + # but it cannot be on a subdomain of the runner review app's hostname + # because we use a wildcard certificate of the form "*.submit.review.forms.service.gov.uk". + # Subject Alternative Names on certificates can only contain one wildcard, + # and the wildcard can only match a single label (i.e. "y." but not "x.y.") + admin_app_hostname = "pr-${var.pull_request_number}-admin.submit.review.forms.service.gov.uk" + + forms_runner_env_vars = [ + { name = "DATABASE_URL", value = "postgres://postgres:postgres@127.0.0.1:5432/forms-runner" }, + { name = "GOVUK_APP_DOMAIN", value = "publishing.service.gov.uk" }, + { name = "PORT", value = "3001" }, + { name = "QUEUE_DATABASE_URL", value = "postgres://postgres:postgres@127.0.0.1:5432/forms-runner-queue" }, + { name = "RAILS_DEVELOPMENT_HOSTS", value = local.runner_review_app_hostname }, + { name = "RAILS_ENV", value = "production" }, + { name = "REDIS_URL", value = "redis://localhost:6379/" }, + { name = "SECRET_KEY_BASE", value = "unsecured_secret_key_material" }, + { name = "SETTINGS__ANALYTICS_ENABLED", value = "false" }, + { name = "SETTINGS__CLOUDWATCH_METRICS_ENABLED", value = "false" }, + { name = "SETTINGS__FORMS_ADMIN__BASE_URL", value = "https://${local.admin_app_hostname}" }, + { name = "SETTINGS__FORMS_API__AUTH_KEY", value = "unsecured_api_key_for_review_apps_only" }, + { name = "SETTINGS__FORMS_API__BASE_URL", value = "http://localhost:9292" }, + { name = "SETTINGS__FORMS_ENV", value = "review" }, + + ## + # Settings for AWS SES email sending, and S3 CSV submission and file upload + # are deliberately omitted here. + # + # We aren't enabling them for review apps for the time being. + ## + ] + + forms_api_env_vars = [ + { name = "DATABASE_URL", value = "postgres://postgres:postgres@127.0.0.1:5432" }, + { name = "EMAIL", value = "review-app-submissions@review.forms.service.gov.uk" }, + { name = "RAILS_DEVELOPMENT_HOSTS", value = "localhost:9292" }, + { name = "RAILS_ENV", value = "production" }, + { name = "SECRET_KEY_BASE", value = "unsecured_secret_key_material" }, + { name = "SETTINGS__FORMS_API__AUTH_KEY", value = "unsecured_api_key_for_review_apps_only" }, + { name = "SETTINGS__FORMS_ENV", value = "review" }, + ] + + forms_admin_env_vars = [ + { name = "DATABASE_URL", value = "postgres://postgres:postgres@127.0.0.1:5432" }, + { name = "GOVUK_APP_DOMAIN", value = "publishing.service.gov.uk" }, + { name = "PORT", value = "3000" }, + { name = "RAILS_DEVELOPMENT_HOSTS", value = local.admin_app_hostname }, + { name = "RAILS_ENV", value = "production" }, + { name = "SECRET_KEY_BASE", value = "unsecured_secret_key_material" }, + { name = "SETTINGS__ACT_AS_USER_ENABLED", value = "true" }, + { name = "SETTINGS__AUTH_PROVIDER", value = "developer" }, + { name = "SETTINGS__FORMS_API__AUTH_KEY", value = "unsecured_api_key_for_review_apps_only" }, + { name = "SETTINGS__FORMS_API__BASE_URL", value = "http://localhost:9292" }, + { name = "SETTINGS__FORMS_ENV", value = "review" }, + { name = "SETTINGS__FORMS_RUNNER__URL", value = "https://${local.runner_review_app_hostname}" }, + ] +} + +resource "aws_ecs_task_definition" "task" { + family = "forms-runner-pr-${var.pull_request_number}" + + network_mode = "awsvpc" + cpu = 256 + memory = 1024 + + requires_compatibilities = ["FARGATE"] + + runtime_platform { + operating_system_family = "LINUX" + cpu_architecture = "ARM64" + } + + execution_role_arn = data.terraform_remote_state.review.outputs.ecs_task_execution_role_arn + + container_definitions = jsonencode([ + + # forms-runner + { + name = "forms-runner" + image = var.forms_runner_container_image + command = [] + essential = true + environment = local.forms_runner_env_vars + + dockerLabels = { + "traefik.http.middlewares.forms-runner-pr-${var.pull_request_number}.basicauth.users" : data.terraform_remote_state.review.outputs.traefik_basic_auth_credentials + + "traefik.http.routers.forms-runner-pr-${var.pull_request_number}.rule" : "Host(`${local.runner_review_app_hostname}`)", + "traefik.http.routers.forms-runner-pr-${var.pull_request_number}.service" : "forms-runner-pr-${var.pull_request_number}", + "traefik.http.routers.forms-runner-pr-${var.pull_request_number}.middlewares" : "forms-runner-pr-${var.pull_request_number}@ecs" + + "traefik.http.services.forms-runner-pr-${var.pull_request_number}.loadbalancer.server.port" : "3001", + "traefik.http.services.forms-runner-pr-${var.pull_request_number}.loadbalancer.healthcheck.path" : "/up", + "traefik.enable" : "true", + }, + + portMappings = [ + { + containerPort = 3001 + protocol = "tcp" + appProtocol = "http" + } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/forms-runner" + } + } + + healthCheck = { + command = ["CMD-SHELL", "wget -O - 'http://localhost:3001/up' || exit 1"] + interval = 30 + retries = 5 + startPeriod = 180 + } + + dependsOn = [ + { + containerName = "postgres" + condition = "HEALTHY" + }, + { + containerName = "redis" + condition = "HEALTHY" + }, + { + containerName = "forms-runner-seeding", + condition = "SUCCESS" + }, + ] + }, + + # forms-api + { + name = "forms-api" + image = "711966560482.dkr.ecr.eu-west-2.amazonaws.com/forms-api-deploy:latest" + command = [] + essential = true + environment = local.forms_api_env_vars + + portMappings = [{ containerPort = 9292 }] + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/forms-api" + } + } + + healthCheck = { + command = ["CMD-SHELL", "wget -O - 'http://localhost:9292/up' || exit 1"] + interval = 30 + retries = 5 + startPeriod = 180 + } + + dependsOn = [ + { + containerName = "postgres" + condition = "HEALTHY" + }, + { + containerName = "forms-api-seeding", + condition = "SUCCESS" + } + ] + }, + + # forms-admin + { + name = "forms-admin" + image = "711966560482.dkr.ecr.eu-west-2.amazonaws.com/forms-admin-deploy:latest" + command = [] + essential = true + environment = local.forms_admin_env_vars + + dockerLabels = { + "traefik.http.middlewares.forms-runner-pr-${var.pull_request_number}-admin-app.basicauth.users" : data.terraform_remote_state.review.outputs.traefik_basic_auth_credentials + + "traefik.http.routers.forms-runner-pr-${var.pull_request_number}-admin-app.rule" : "Host(`${local.admin_app_hostname}`)", + "traefik.http.routers.forms-runner-pr-${var.pull_request_number}-admin-app.service" : "forms-runner-pr-${var.pull_request_number}-admin-app", + "traefik.http.routers.forms-runner-pr-${var.pull_request_number}-admin-app.middlewares" : "forms-runner-pr-${var.pull_request_number}-admin-app@ecs" + + "traefik.http.services.forms-runner-pr-${var.pull_request_number}-admin-app.loadbalancer.server.port" : "3000", + "traefik.http.services.forms-runner-pr-${var.pull_request_number}-admin-app.loadbalancer.healthcheck.path" : "/up", + "traefik.enable" : "true", + }, + + + portMappings = [ + { + containerPort = 3000 + protocol = "tcp" + appProtocol = "http" + } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/forms-admin" + } + } + + healthCheck = { + command = ["CMD-SHELL", "wget -O - 'http://localhost:3000/up' || exit 1"] + interval = 30 + retries = 5 + startPeriod = 180 + } + + dependsOn = [ + { + containerName = "postgres" + condition = "HEALTHY" + }, + { + containerName = "forms-admin-seeding", + condition = "SUCCESS" + } + ] + }, + + # postgres + { + name = "postgres" + image = "public.ecr.aws/docker/library/postgres:16.6" + command = [] + essential = true + + portMappings = [{ containerPort = 5432 }] + + environment = [ + { name = "POSTGRES_PASSWORD", value = "postgres" } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/postgres" + } + } + + healthCheck = { + command = ["CMD-SHELL", "psql -h localhost -p 5432 -U postgres -c \"SELECT current_timestamp - pg_postmaster_start_time();\""] + } + }, + + # redis + { + name = "redis", + image = "public.ecr.aws/docker/library/redis:latest", + command = [ + "redis-server", + "--appendonly", + "yes" + ], + essential = true + + portMappings = [{ containerPort = 6379 }] + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/redis" + } + } + + healthCheck = { + command = ["CMD-SHELL", "redis-cli", "ping"] + } + }, + + # forms-runner-seeding + { + name = "forms-runner-seeding" + image = var.forms_runner_container_image + command = ["rake", "db:setup"] + essential = false + environment = local.forms_runner_env_vars + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/forms-runner-seeding" + } + } + + dependsOn = [ + { + containerName = "postgres" + condition = "HEALTHY" + } + ] + }, + + # forms-api-seeding + { + name = "forms-api-seeding" + image = "711966560482.dkr.ecr.eu-west-2.amazonaws.com/forms-api-deploy:latest" + command = ["rake", "db:setup"] + essential = false + environment = local.forms_api_env_vars + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/forms-api-seeding" + } + } + + dependsOn = [ + { + containerName = "postgres" + condition = "HEALTHY" + } + ] + }, + + # forms-admin-seeding + { + name = "forms-admin-seeding" + image = "711966560482.dkr.ecr.eu-west-2.amazonaws.com/forms-admin-deploy:latest" + command = ["rake", "db:setup"] + essential = false + environment = local.forms_admin_env_vars + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = data.terraform_remote_state.review.outputs.review_apps_log_group_name + awslogs-region = "eu-west-2" + awslogs-stream-prefix = "${local.logs_stream_prefix}/forms-admin-seeding" + } + } + + dependsOn = [ + { + containerName = "postgres" + condition = "HEALTHY" + } + ] + }, + ]) +} diff --git a/.review_apps/outputs.tf b/.review_apps/outputs.tf new file mode 100644 index 000000000..46b1de364 --- /dev/null +++ b/.review_apps/outputs.tf @@ -0,0 +1,19 @@ +output "review_app_url" { + description = "The full URL of the review app" + value = "https://${local.runner_review_app_hostname}/" +} + +output "admin_app_url" { + description = "The full URL of the admin app that accompanies the review version of forms-runner" + value = "https://${local.admin_app_hostname}" +} + +output "review_app_ecs_cluster_id" { + description = "The id of the AWS ECS cluster into which the review app is deployed " + value = data.terraform_remote_state.review.outputs.ecs_cluster_id +} + +output "review_app_ecs_service_name" { + description = "The name of the AWS ECS service for this review app" + value = aws_ecs_service.app.name +} diff --git a/.review_apps/requirements.txt b/.review_apps/requirements.txt new file mode 100644 index 000000000..6965d4ee9 --- /dev/null +++ b/.review_apps/requirements.txt @@ -0,0 +1,3 @@ +# This is here exclusively to inform the version of Checkov we use. +# We do not write anything in Python. +checkov==3.2.369 diff --git a/.review_apps/site.tf b/.review_apps/site.tf new file mode 100644 index 000000000..140503cd8 --- /dev/null +++ b/.review_apps/site.tf @@ -0,0 +1,35 @@ +terraform { + required_version = "~> 1.11.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.72.1" + } + + time = { + source = "hashicorp/time" + version = "0.11.1" + } + } + + backend "s3" { + bucket = "gds-forms-integration-tfstate" + region = "eu-west-2" + # key is set when initializing Terraform + # e.g. `terraform init -backend-config="key=review-apps/forms-admin/pr-123.tfstate"` + } +} + +provider "aws" { + allowed_account_ids = ["842676007477"] + region = "eu-west-2" + + default_tags { + tags = { + Environment = "review" + Deployment = "github.com/alphagov/forms-admin/.review_apps" + PullRequest = "https://github.com/alphagov/forms-admin/pull/${var.pull_request_number}" + } + } +} diff --git a/.review_apps/variables.tf b/.review_apps/variables.tf new file mode 100644 index 000000000..cf78bcda6 --- /dev/null +++ b/.review_apps/variables.tf @@ -0,0 +1,10 @@ +variable "pull_request_number" { + type = number + description = "The id of the GitHub pull request on which the review app is based" +} + + +variable "forms_runner_container_image" { + type = string + description = "The forms-admin container image which should be deployed" +}