Skip to content

Commit fae25cf

Browse files
authored
Merge pull request #1958 from alphagov/whi-tw/allow-github-hosted-runner-deploy-review-apps
Add GitHub OIDC authentication for review app deployments
2 parents 513b849 + 04a92d3 commit fae25cf

12 files changed

Lines changed: 690 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
name: "Review apps: on PR change"
2+
on:
3+
workflow_call:
4+
inputs:
5+
aws-account-number:
6+
type: string
7+
default: "842676007477"
8+
aws-region:
9+
type: string
10+
default: "eu-west-2"
11+
app-name:
12+
type: string
13+
description: "The name of the application, used for the ECR repository and CodeBuild project. eg. forms-product-page"
14+
15+
jobs:
16+
update-review-app:
17+
runs-on: ubuntu-24.04-arm
18+
permissions:
19+
id-token: write
20+
contents: read
21+
pull-requests: write
22+
23+
steps:
24+
- name: Configure AWS credentials
25+
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
26+
with:
27+
role-to-assume: arn:aws:iam::${{ inputs.aws-account-number }}:role/review-github-actions-${{ inputs.app-name }}
28+
aws-region: ${{ inputs.aws-region }}
29+
30+
- name: Checkout code
31+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
32+
33+
- name: Generate container image URI
34+
id: generate_image_uri
35+
env:
36+
ECR_REPO: ${{ inputs.aws-account-number }}.dkr.ecr.${{ inputs.aws-region }}.amazonaws.com/${{ inputs.app-name }}
37+
PR_NUMBER: ${{ github.event.pull_request.number }}
38+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
39+
run: |
40+
echo "ECR_REPO=${ECR_REPO}" >> "$GITHUB_OUTPUT"
41+
BASE_URI="${ECR_REPO}:pr-${PR_NUMBER}"
42+
echo "BASE_URI=${BASE_URI}" >> "$GITHUB_OUTPUT"
43+
echo "URI=${BASE_URI}-${HEAD_SHA}-$(date +%s)" >> "$GITHUB_OUTPUT"
44+
45+
- name: Log in to Amazon ECR
46+
uses: aws-actions/amazon-ecr-login@5a88a04c91d5c6f97aae0d9be790e64d9b1d47b7 # v1.7.1
47+
48+
- name: Set up Docker Buildx
49+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
50+
51+
- name: Build
52+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
53+
with:
54+
push: true
55+
tags: ${{ steps.generate_image_uri.outputs.URI }}
56+
cache-from: type=gha
57+
cache-to: type=gha,mode=max
58+
59+
- name: Deploy review app via CodeBuild
60+
id: codebuild
61+
uses: aws-actions/aws-codebuild-run-build@4d15a47425739ac2296ba5e7eee3bdd4bfbdd767 # v1.0.18
62+
with:
63+
project-name: review-${{ inputs.app-name }}-deploy
64+
env-vars-for-codebuild: |
65+
PR_NUMBER,
66+
CONTAINER_IMAGE
67+
env:
68+
PR_NUMBER: ${{ github.event.pull_request.number }}
69+
CONTAINER_IMAGE: ${{ steps.generate_image_uri.outputs.URI }}
70+
71+
- name: Fetch terraform outputs
72+
id: outputs
73+
env:
74+
BUILD_ID: ${{ steps.codebuild.outputs.aws-build-id }}
75+
run: |
76+
# Extract build UUID from ARN (format: arn:aws:codebuild:region:account:build/project:uuid)
77+
# shellcheck disable=SC2153 # BUILD_ID is set in env, but shellcheck doesn't recognize it
78+
BUILD_UUID="${BUILD_ID##*:}"
79+
80+
# Download artifact
81+
aws s3 cp "s3://forms-review-codebuild-artifacts/${BUILD_UUID}/review-${{ inputs.app-name }}-deploy/outputs.json" outputs.json
82+
83+
# Parse outputs
84+
{
85+
echo "REVIEW_APP_URL=$(jq -r '.review_app_url.value' outputs.json)"
86+
echo "ECS_CLUSTER_ID=$(jq -r '.review_app_ecs_cluster_id.value' outputs.json)"
87+
echo "ECS_SERVICE_NAME=$(jq -r '.review_app_ecs_service_name.value' outputs.json)"
88+
} >> "$GITHUB_OUTPUT"
89+
90+
# Clean up artifact
91+
aws s3 rm "s3://forms-review-codebuild-artifacts/${BUILD_UUID}/review-${{ inputs.app-name }}-deploy/outputs.json"
92+
93+
- name: Wait for AWS ECS deployments to finish
94+
run: |
95+
aws ecs wait services-stable \
96+
--cluster "${{ steps.outputs.outputs.ECS_CLUSTER_ID }}" \
97+
--services "${{ steps.outputs.outputs.ECS_SERVICE_NAME }}"
98+
99+
- name: Comment on PR
100+
env:
101+
COMMENT_MARKER: <!-- review apps on pr change -->
102+
GH_TOKEN: ${{ github.token }}
103+
run: |
104+
cat <<EOF > "${{runner.temp}}/pr-comment.md"
105+
:tada: A review copy of this PR has been deployed! You can reach it at: ${{steps.outputs.outputs.REVIEW_APP_URL}}
106+
107+
It may take 5 minutes or so for the application to be fully deployed and working. If it still isn't ready
108+
after 5 minutes, there may be something wrong with the ECS task. You will need to go to the integration AWS account
109+
to debug, or otherwise ask an infrastructure person.
110+
111+
For the sign in details and more information, [see the review apps wiki page](https://github.com/alphagov/forms-team/wiki/Review-apps).
112+
113+
$COMMENT_MARKER
114+
EOF
115+
116+
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")
117+
for comment_id in $old_comment_ids; do
118+
gh api -X DELETE "repos/{owner}/{repo}/issues/comments/${comment_id}"
119+
done
120+
121+
gh pr comment "${{github.event.pull_request.html_url}}" --body-file "${{runner.temp}}/pr-comment.md"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: "Review apps: on PR close"
2+
on:
3+
workflow_call:
4+
inputs:
5+
aws-account-number:
6+
type: string
7+
default: "842676007477"
8+
aws-region:
9+
type: string
10+
default: "eu-west-2"
11+
app-name:
12+
type: string
13+
description: "The name of the application, used for the ECR repository and CodeBuild project. eg. forms-product-page"
14+
15+
jobs:
16+
delete-review-app:
17+
runs-on: ubuntu-24.04-arm
18+
permissions:
19+
id-token: write
20+
contents: read
21+
22+
steps:
23+
- name: Configure AWS credentials
24+
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
25+
with:
26+
role-to-assume: arn:aws:iam::${{ inputs.aws-account-number }}:role/review-github-actions-${{ inputs.app-name }}
27+
aws-region: ${{ inputs.aws-region }}
28+
29+
- name: Destroy review app via CodeBuild
30+
uses: aws-actions/aws-codebuild-run-build@4d15a47425739ac2296ba5e7eee3bdd4bfbdd767 # v1.0.18
31+
env:
32+
PR_NUMBER: ${{ github.event.pull_request.number }}
33+
with:
34+
project-name: review-${{ inputs.app-name }}-destroy
35+
env-vars-for-codebuild: |
36+
PR_NUMBER
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
resource "aws_iam_openid_connect_provider" "github" {
2+
url = "https://token.actions.githubusercontent.com"
3+
client_id_list = ["sts.amazonaws.com"]
4+
# AWS does not use any provided `thumbprint_list` values for GitHub OIDC providers.
5+
# See: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider.html#thumbprint_list-1
6+
}

infra/deployments/integration/account/outputs.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ output "codeconnection_arn" {
1616
output "kinesis_subscription_role_arn" {
1717
value = aws_iam_role.kinesis_subscription_role.arn
1818
}
19+
20+
output "github_oidc_provider_arn" {
21+
description = "The ARN of the GitHub OIDC provider"
22+
value = aws_iam_openid_connect_provider.github.arn
23+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
data "aws_region" "current" {}
2+
3+
locals {
4+
github_actions_apps = {
5+
"forms-admin" = {
6+
ecr_repository_arn = module.forms_admin_container_repo.arn
7+
github_repository = "alphagov/forms-admin"
8+
}
9+
"forms-runner" = {
10+
ecr_repository_arn = module.forms_runner_container_repo.arn
11+
github_repository = "alphagov/forms-runner"
12+
}
13+
"forms-product-page" = {
14+
ecr_repository_arn = module.forms_product_page_container_repo.arn
15+
github_repository = "govuk-forms/forms-product-page"
16+
}
17+
}
18+
}
19+
20+
# S3 bucket for CodeBuild artifacts (terraform outputs)
21+
module "codebuild_artifacts" {
22+
source = "../../../modules/secure-bucket"
23+
24+
name = "forms-review-codebuild-artifacts"
25+
versioning_enabled = false
26+
access_logging_enabled = false
27+
}
28+
29+
resource "aws_s3_bucket_lifecycle_configuration" "codebuild_artifacts" {
30+
bucket = module.codebuild_artifacts.name
31+
32+
rule {
33+
id = "expire-old-artifacts"
34+
status = "Enabled"
35+
36+
filter {}
37+
38+
expiration {
39+
days = 1
40+
}
41+
42+
abort_incomplete_multipart_upload {
43+
days_after_initiation = 1
44+
}
45+
}
46+
}
47+
48+
# CodeBuild projects for each app
49+
module "codebuild_deploy" {
50+
for_each = local.github_actions_apps
51+
source = "./review-app-codebuild"
52+
53+
application_name = each.key
54+
action = "deploy"
55+
github_repository = "https://github.com/${each.value.github_repository}"
56+
codeconnection_arn = data.terraform_remote_state.account.outputs.codeconnection_arn
57+
artifacts_bucket_name = module.codebuild_artifacts.name
58+
ecs_cluster_arn = aws_ecs_cluster.review.arn
59+
ecs_cluster_name = aws_ecs_cluster.review.name
60+
ecr_repository_arn = each.value.ecr_repository_arn
61+
task_execution_role_arn = aws_iam_role.ecs_execution.arn
62+
autoscaling_role_arn = aws_iam_service_linked_role.app_autoscaling.arn
63+
deploy_account_id = var.deploy_account_id
64+
}
65+
66+
module "codebuild_destroy" {
67+
for_each = local.github_actions_apps
68+
source = "./review-app-codebuild"
69+
70+
application_name = each.key
71+
action = "destroy"
72+
github_repository = "https://github.com/${each.value.github_repository}"
73+
codeconnection_arn = data.terraform_remote_state.account.outputs.codeconnection_arn
74+
artifacts_bucket_name = module.codebuild_artifacts.name
75+
ecs_cluster_arn = aws_ecs_cluster.review.arn
76+
ecs_cluster_name = aws_ecs_cluster.review.name
77+
ecr_repository_arn = each.value.ecr_repository_arn
78+
task_execution_role_arn = aws_iam_role.ecs_execution.arn
79+
autoscaling_role_arn = aws_iam_service_linked_role.app_autoscaling.arn
80+
deploy_account_id = var.deploy_account_id
81+
}
82+
83+
# OIDC roles with minimal permissions (trigger CodeBuild + push to ECR)
84+
data "aws_iam_policy_document" "github_actions_assume_role" {
85+
for_each = local.github_actions_apps
86+
87+
statement {
88+
effect = "Allow"
89+
actions = ["sts:AssumeRoleWithWebIdentity"]
90+
91+
principals {
92+
type = "Federated"
93+
identifiers = [data.terraform_remote_state.account.outputs.github_oidc_provider_arn]
94+
}
95+
96+
condition {
97+
test = "StringEquals"
98+
variable = "token.actions.githubusercontent.com:aud"
99+
values = ["sts.amazonaws.com"]
100+
}
101+
102+
condition {
103+
test = "StringLike"
104+
variable = "token.actions.githubusercontent.com:sub"
105+
values = [
106+
"repo:${each.value.github_repository}:pull_request"
107+
]
108+
}
109+
}
110+
}
111+
112+
resource "aws_iam_role" "github_actions" {
113+
for_each = local.github_actions_apps
114+
115+
name = "review-github-actions-${each.key}"
116+
description = "Role assumed by GitHub Actions workflows for ${each.key} review apps"
117+
118+
assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role[each.key].json
119+
}
120+
121+
resource "aws_iam_role_policy" "github_actions" {
122+
for_each = local.github_actions_apps
123+
124+
role = aws_iam_role.github_actions[each.key].name
125+
policy = data.aws_iam_policy_document.github_actions[each.key].json
126+
}
127+
128+
data "aws_iam_policy_document" "github_actions" {
129+
for_each = local.github_actions_apps
130+
131+
# Trigger CodeBuild projects
132+
statement {
133+
sid = "TriggerCodeBuild"
134+
actions = ["codebuild:StartBuild", "codebuild:BatchGetBuilds"]
135+
resources = [
136+
module.codebuild_deploy[each.key].project_arn,
137+
module.codebuild_destroy[each.key].project_arn
138+
]
139+
}
140+
141+
# Read CodeBuild logs
142+
statement {
143+
sid = "ReadCodeBuildLogs"
144+
actions = ["logs:GetLogEvents"]
145+
resources = [
146+
module.codebuild_deploy[each.key].log_group_arn,
147+
module.codebuild_destroy[each.key].log_group_arn
148+
]
149+
}
150+
151+
# Push container images to ECR
152+
statement {
153+
sid = "PushToECR"
154+
actions = [
155+
"ecr:BatchCheckLayerAvailability",
156+
"ecr:CompleteLayerUpload",
157+
"ecr:InitiateLayerUpload",
158+
"ecr:PutImage",
159+
"ecr:UploadLayerPart"
160+
]
161+
resources = [each.value.ecr_repository_arn]
162+
}
163+
164+
statement {
165+
sid = "ECRLogin"
166+
actions = ["ecr:GetAuthorizationToken"]
167+
resources = ["*"]
168+
}
169+
170+
# Wait for ECS service stability (read-only)
171+
statement {
172+
sid = "WaitForECSStability"
173+
actions = ["ecs:DescribeServices"]
174+
resources = [
175+
"arn:aws:ecs:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:service/${aws_ecs_cluster.review.name}/${each.key}-pr-*"
176+
]
177+
}
178+
179+
# Read and delete CodeBuild artifacts (terraform outputs)
180+
statement {
181+
sid = "ReadArtifacts"
182+
actions = ["s3:GetObject", "s3:DeleteObject"]
183+
resources = [
184+
"${module.codebuild_artifacts.arn}/*/review-${each.key}-deploy/outputs.json"
185+
]
186+
}
187+
}

infra/deployments/integration/review/outputs.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,10 @@ output "traefik_basic_auth_credentials" {
4848
value = data.aws_ssm_parameter.traefik_basic_auth_credentials.value
4949
sensitive = true
5050
}
51+
52+
output "github_actions_role_arns" {
53+
description = "IAM role ARNs for GitHub Actions review app deployments"
54+
value = {
55+
for app, role in aws_iam_role.github_actions : app => role.arn
56+
}
57+
}

0 commit comments

Comments
 (0)