Skip to content

WIP: initial files to add review app to product pages #748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/review_apps_on_pr_change.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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-product-page-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-product-page: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-product-page/pr-${{github.event.pull_request.number}}.tfstate"

terraform apply \
-var "pull_request_number=${{github.event.pull_request.number}}" \
-var "forms_product_page_container_image=${{env.CONTAINER_IMAGE_URI}}" \
-no-color \
-auto-approve

echo "REVIEW_APP_URL=$(terraform output -raw review_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: <!-- review apps on pr change -->
GH_TOKEN: ${{ github.token }}
run: |
cat <<EOF > "${{runner.temp}}/pr-comment.md"
:tada: A review copy of this PR has been deployed! You can reach it at: ${{steps.deploy.outputs.REVIEW_APP_URL}}

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"
36 changes: 36 additions & 0 deletions .github/workflows/review_apps_on_pr_close.yml
Original file line number Diff line number Diff line change
@@ -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-product-page: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-product-page-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-product-page/pr-${{github.event.pull_request.number}}.tfstate"
terraform destroy \
-var "pull_request_number=${{github.event.pull_request.number}}" \
-var "forms_product_page_container_image=${{env.IMAGE_TAG}}" \
-no-color \
-auto-approve
1 change: 1 addition & 0 deletions .review_apps/.terraform-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.11.4
17 changes: 17 additions & 0 deletions .review_apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Review apps

The Terraform code in this directory is used to deploy a review copy of `forms-product-page`.

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-product-page` at the commit in question

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-product-page` container image
The `forms-product-page` container image to deploy is supplied under the `forms_product_page_container_image` variable. Terraform does not build the container. It is assumed to be built and stored ahead of time.


38 changes: 38 additions & 0 deletions .review_apps/app_autoscaling.tf
Original file line number Diff line number Diff line change
@@ -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 = "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 = "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
}
}
26 changes: 26 additions & 0 deletions .review_apps/dependencies.tf
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions .review_apps/ecs_service.tf
Original file line number Diff line number Diff line change
@@ -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-product-page-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]
}
95 changes: 95 additions & 0 deletions .review_apps/ecs_task_definition.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
locals {
logs_stream_prefix = "${data.terraform_remote_state.review.outputs.review_apps_log_group_name}/pr-${var.pull_request_number}"

review_app_hostname = "pr-${var.pull_request_number}.review.forms.service.gov.uk"

forms_product_page_startup_commands = [
"echo $PATH",
"bundle install",
"echo $PATH",
"bin/rails s -b 0.0.0.0"
]

forms_product_page_shell_script = join(" && ", local.forms_product_page_startup_commands)

forms_product_page_env_vars = [
{ name = "RACK_ENV", value = "production" },
{ name = "RAILS_ENV", value = "production" },
{ name = "SETTINGS__SENTRY__ENVIRONMENT", value = "aws-review" }, # todo: is this review?
{ name = "SETTINGS__FORMS_ENV", value = "review" },
{ name = "SETTINGS__ZENDESK__SUBDOMAIN", value = var.zendesk_subdomain }, #todo: get subdomain
{ name = "SETTINGS__FORMS_ADMIN__BASE_URL", value = var.admin_base_url }, #todo: get url
]
}

resource "aws_ecs_task_definition" "task" {
family = "forms-product-page-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-product-page
{
name = "forms-product-page"
image = var.forms_product_page_container_image
command = []
essential = true
environment = local.forms_product_page_env_vars

dockerLabels = {
"traefik.http.middlewares.forms-product-page-pr-${var.pull_request_number}.basicauth.users" : data.terraform_remote_state.review.outputs.traefik_basic_auth_credentials

"traefik.http.routers.forms-product-page-pr-${var.pull_request_number}.rule" : "Host(`${local.review_app_hostname}`)",
"traefik.http.routers.forms-product-page-pr-${var.pull_request_number}.service" : "forms-product-page-pr-${var.pull_request_number}",
"traefik.http.routers.forms-product-page-pr-${var.pull_request_number}.middlewares" : "forms-product-page-pr-${var.pull_request_number}@ecs"

"traefik.http.services.forms-product-page-pr-${var.pull_request_number}.loadbalancer.server.port" : "3000",
"traefik.http.services.forms-product-page-pr-${var.pull_request_number}.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-product-page"
}
}

healthCheck = {
command = ["CMD-SHELL", "wget -O - 'http://localhost:3000/up' || exit 1"]
interval = 30
retries = 5
startPeriod = 180
}

dependsOn = [
{
containerName = "postgres"
condition = "HEALTHY"
}
]
},
])
}
14 changes: 14 additions & 0 deletions .review_apps/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
output "review_app_url" {
description = "The full URL of the review app"
value = "https://${local.review_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
}
3 changes: 3 additions & 0 deletions .review_apps/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions .review_apps/site.tf
Original file line number Diff line number Diff line change
@@ -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-product-page/pr-123.tfstate"`
}
}

provider "aws" {
allowed_account_ids = ["842676007477"]
region = "eu-west-2"

default_tags {
tags = {
Environment = "review"
Deployment = "github.com/alphagov/forms-product-page/.review_apps"
PullRequest = "https://github.com/alphagov/forms-product-page/pull/${var.pull_request_number}"
}
}
}
Loading
Loading