Skip to content

Commit 1436189

Browse files
committed
Refactor OIDC review apps deployment w/ CodeBuild
Instead of giving GitHub Actions direct access to deploy review apps to ECS, we set up CodeBuild projects that GitHub Actions can trigger via OIDC. This reduces the permissions granted to GitHub Actions, as they no longer need direct access to ECS, ECR, and other resources.
1 parent 9efd131 commit 1436189

File tree

7 files changed

+438
-149
lines changed

7 files changed

+438
-149
lines changed
Lines changed: 118 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
data "aws_region" "current" {}
2+
13
locals {
24
github_actions_apps = {
35
"forms-admin" = {
@@ -12,32 +14,103 @@ locals {
1214
}
1315
}
1416

17+
# S3 bucket for CodeBuild artifacts (terraform outputs)
18+
module "codebuild_artifacts" {
19+
source = "../../../modules/secure-bucket"
20+
21+
name = "forms-review-codebuild-artifacts"
22+
versioning_enabled = false
23+
access_logging_enabled = false
24+
}
25+
26+
resource "aws_s3_bucket_lifecycle_configuration" "codebuild_artifacts" {
27+
bucket = module.codebuild_artifacts.name
28+
29+
rule {
30+
id = "expire-old-artifacts"
31+
status = "Enabled"
32+
33+
filter {}
34+
35+
expiration {
36+
days = 1
37+
}
38+
39+
abort_incomplete_multipart_upload {
40+
days_after_initiation = 1
41+
}
42+
}
43+
}
44+
45+
# CodeBuild projects for each app
46+
module "codebuild_deploy" {
47+
for_each = local.github_actions_apps
48+
source = "./review-app-codebuild"
49+
50+
application_name = each.key
51+
action = "deploy"
52+
github_repository = "https://github.com/alphagov/${each.key}"
53+
codeconnection_arn = data.terraform_remote_state.account.outputs.codeconnection_arn
54+
artifacts_bucket_name = module.codebuild_artifacts.name
55+
ecs_cluster_arn = aws_ecs_cluster.review.arn
56+
ecs_cluster_name = aws_ecs_cluster.review.name
57+
ecr_repository_arn = each.value.ecr_repository_arn
58+
task_execution_role_arn = aws_iam_role.ecs_execution.arn
59+
autoscaling_role_arn = aws_iam_service_linked_role.app_autoscaling.arn
60+
deploy_account_id = var.deploy_account_id
61+
}
62+
63+
module "codebuild_destroy" {
64+
for_each = local.github_actions_apps
65+
source = "./review-app-codebuild"
66+
67+
application_name = each.key
68+
action = "destroy"
69+
github_repository = "https://github.com/alphagov/${each.key}"
70+
codeconnection_arn = data.terraform_remote_state.account.outputs.codeconnection_arn
71+
artifacts_bucket_name = module.codebuild_artifacts.name
72+
ecs_cluster_arn = aws_ecs_cluster.review.arn
73+
ecs_cluster_name = aws_ecs_cluster.review.name
74+
ecr_repository_arn = each.value.ecr_repository_arn
75+
task_execution_role_arn = aws_iam_role.ecs_execution.arn
76+
autoscaling_role_arn = aws_iam_service_linked_role.app_autoscaling.arn
77+
deploy_account_id = var.deploy_account_id
78+
}
79+
80+
# OIDC roles with minimal permissions (trigger CodeBuild + push to ECR)
81+
data "aws_iam_policy_document" "github_actions_assume_role" {
82+
for_each = local.github_actions_apps
83+
84+
statement {
85+
effect = "Allow"
86+
actions = ["sts:AssumeRoleWithWebIdentity"]
87+
88+
principals {
89+
type = "Federated"
90+
identifiers = [data.terraform_remote_state.account.outputs.github_oidc_provider_arn]
91+
}
92+
93+
condition {
94+
test = "StringEquals"
95+
variable = "token.actions.githubusercontent.com:aud"
96+
values = ["sts.amazonaws.com"]
97+
}
98+
99+
condition {
100+
test = "StringLike"
101+
variable = "token.actions.githubusercontent.com:sub"
102+
values = ["repo:alphagov/${each.key}:pull_request"]
103+
}
104+
}
105+
}
106+
15107
resource "aws_iam_role" "github_actions" {
16108
for_each = local.github_actions_apps
17109

18110
name = "review-github-actions-${each.key}"
19111
description = "Role assumed by GitHub Actions workflows for ${each.key} review apps"
20112

21-
assume_role_policy = jsonencode({
22-
Version = "2012-10-17"
23-
Statement = [
24-
{
25-
Effect = "Allow"
26-
Action = "sts:AssumeRoleWithWebIdentity"
27-
Principal = {
28-
Federated = data.terraform_remote_state.account.outputs.github_oidc_provider_arn
29-
}
30-
Condition = {
31-
StringEquals = {
32-
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
33-
}
34-
StringLike = {
35-
"token.actions.githubusercontent.com:sub" = "repo:alphagov/${each.key}:pull_request"
36-
}
37-
}
38-
}
39-
]
40-
})
113+
assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role[each.key].json
41114
}
42115

43116
resource "aws_iam_role_policy" "github_actions" {
@@ -50,164 +123,60 @@ resource "aws_iam_role_policy" "github_actions" {
50123
data "aws_iam_policy_document" "github_actions" {
51124
for_each = local.github_actions_apps
52125

126+
# Trigger CodeBuild projects
53127
statement {
54-
sid = "UseECSServices"
55-
effect = "Allow"
56-
actions = [
57-
"ecs:*Service",
58-
"ecs:*Services",
59-
"ecs:TagResource"
60-
]
128+
sid = "TriggerCodeBuild"
129+
actions = ["codebuild:StartBuild", "codebuild:BatchGetBuilds"]
61130
resources = [
62-
aws_ecs_cluster.review.arn,
63-
"arn:aws:ecs:eu-west-2:${data.aws_caller_identity.current.account_id}:service/${aws_ecs_cluster.review.name}/${each.key}-pr-*"
131+
module.codebuild_deploy[each.key].project_arn,
132+
module.codebuild_destroy[each.key].project_arn
64133
]
65134
}
66135

136+
# Read CodeBuild logs
67137
statement {
68-
sid = "UseECSTaskDefinitions"
69-
effect = "Allow"
70-
actions = [
71-
"ecs:*TaskDefinition",
72-
"ecs:*TaskDefinitions",
73-
"ecs:TagResource"
74-
]
138+
sid = "ReadCodeBuildLogs"
139+
actions = ["logs:GetLogEvents"]
75140
resources = [
76-
"arn:aws:ecs:eu-west-2:${data.aws_caller_identity.current.account_id}:task-definition/${each.key}-pr-*"
141+
module.codebuild_deploy[each.key].log_group_arn,
142+
module.codebuild_destroy[each.key].log_group_arn
77143
]
78144
}
79145

146+
# Push container images to ECR
80147
statement {
81-
sid = "AllowTaskDefinitionsNeedingStar"
82-
effect = "Allow"
83-
actions = [
84-
"ecs:DeregisterTaskDefinition",
85-
"ecs:DescribeTaskDefinition"
86-
]
87-
resources = ["*"]
88-
}
89-
90-
statement {
91-
sid = "ReadTerraformStateFiles"
92-
effect = "Allow"
93-
actions = [
94-
"s3:GetObject",
95-
"s3:GetObjectVersion",
96-
"s3:HeadObject",
97-
"s3:ListBucket"
98-
]
99-
resources = [
100-
"arn:aws:s3:::gds-forms-integration-tfstate",
101-
"arn:aws:s3:::gds-forms-integration-tfstate/review.tfstate",
102-
"arn:aws:s3:::gds-forms-integration-tfstate/review-apps/${each.key}/pr-*.tfstate"
103-
]
104-
}
105-
106-
statement {
107-
sid = "WriteTerraformStateFiles"
108-
effect = "Allow"
109-
actions = [
110-
"s3:PutObject",
111-
"s3:PutObjectVersion"
112-
]
113-
resources = [
114-
"arn:aws:s3:::gds-forms-integration-tfstate/review-apps/${each.key}/pr-*.tfstate"
115-
]
116-
}
117-
118-
statement {
119-
sid = "ReleaseTerraformStateLock"
120-
actions = [
121-
"s3:DeleteObject",
122-
]
123-
resources = [
124-
"arn:aws:s3:::gds-forms-integration-tfstate/review-apps/${each.key}/pr-*.tflock"
125-
]
126-
effect = "Allow"
127-
}
128-
129-
statement {
130-
sid = "UseLocalECR"
131-
effect = "Allow"
148+
sid = "PushToECR"
132149
actions = [
133150
"ecr:BatchCheckLayerAvailability",
134-
"ecr:BatchGetImage",
135151
"ecr:CompleteLayerUpload",
136-
"ecr:GetDownloadUrlForLayer",
137152
"ecr:InitiateLayerUpload",
138153
"ecr:PutImage",
139-
"ecr:UploadLayerPart",
140-
]
141-
resources = [
142-
each.value.ecr_repository_arn
143-
]
144-
}
145-
146-
statement {
147-
sid = "LogIntoLocalECR"
148-
effect = "Allow"
149-
actions = [
150-
"ecr:GetAuthorizationToken",
151-
]
152-
resources = [
153-
"*"
154-
]
155-
}
156-
157-
statement {
158-
sid = "UseDeployECR"
159-
effect = "Allow"
160-
actions = [
161-
"ecr:BatchCheckLayerAvailability",
162-
"ecr:BatchGetImage",
163-
"ecr:GetDownloadUrlForLayer"
164-
]
165-
resources = [
166-
"arn:aws:ecr:eu-west-2:${var.deploy_account_id}:repository/*"
154+
"ecr:UploadLayerPart"
167155
]
156+
resources = [each.value.ecr_repository_arn]
168157
}
169158

170159
statement {
171-
sid = "AllowIAMPassRole"
172-
effect = "Allow"
173-
actions = [
174-
"iam:PassRole"
175-
]
176-
resources = [
177-
aws_iam_role.ecs_execution.arn,
178-
aws_iam_service_linked_role.app_autoscaling.arn,
179-
]
160+
sid = "ECRLogin"
161+
actions = ["ecr:GetAuthorizationToken"]
162+
resources = ["*"]
180163
}
181164

165+
# Wait for ECS service stability (read-only)
182166
statement {
183-
sid = "UseAppAutoscaling"
184-
effect = "Allow"
185-
actions = [
186-
"application-autoscaling:*ScalableTarget",
187-
"application-autoscaling:*ScalableTargets",
188-
"application-autoscaling:*ScheduledAction",
189-
"application-autoscaling:*ScheduledActions",
190-
"application-autoscaling:*ScalingPolicy",
191-
"application-autoscaling:*ScalingPolicies",
192-
"application-autoscaling:ListTagsForResource",
193-
"application-autoscaling:TagResource",
194-
"application-autoscaling:UntagResource"
195-
]
167+
sid = "WaitForECSStability"
168+
actions = ["ecs:DescribeServices"]
196169
resources = [
197-
"arn:aws:application-autoscaling:eu-west-2:${data.aws_caller_identity.current.account_id}:scalable-target/*"
170+
"arn:aws:ecs:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:service/${aws_ecs_cluster.review.name}/${each.key}-pr-*"
198171
]
199172
}
200173

174+
# Read and delete CodeBuild artifacts (terraform outputs)
201175
statement {
202-
sid = "UseAppAutoscalingNeedingStar"
203-
effect = "Allow"
204-
actions = [
205-
"application-autoscaling:DescribeScalableTargets",
206-
"application-autoscaling:DescribeScalingPolicies",
207-
"application-autoscaling:DescribeScheduledActions",
208-
]
176+
sid = "ReadArtifacts"
177+
actions = ["s3:GetObject", "s3:DeleteObject"]
209178
resources = [
210-
"*"
179+
"${module.codebuild_artifacts.arn}/*/review-${each.key}-deploy/outputs.json"
211180
]
212181
}
213182
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
version: 0.2
2+
3+
phases:
4+
install:
5+
commands:
6+
- set -e
7+
- export TERRAFORM_VERSION=$(< .review_apps/.terraform-version)
8+
- curl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_arm64.zip" -o "terraform-${TERRAFORM_VERSION}.zip"
9+
- unzip "terraform-${TERRAFORM_VERSION}.zip"
10+
- mv terraform /usr/local/bin/terraform
11+
finally:
12+
- terraform --version
13+
14+
pre_build:
15+
commands:
16+
- cd .review_apps
17+
- terraform init -backend-config="key=review-apps/${APPLICATION_NAME}/pr-${PR_NUMBER}.tfstate"
18+
19+
build:
20+
commands:
21+
- |
22+
terraform apply \
23+
-var "pull_request_number=${PR_NUMBER}" \
24+
-var "${APPLICATION_NAME//-/_}_container_image=${CONTAINER_IMAGE}" \
25+
-no-color \
26+
-auto-approve
27+
- terraform output -json > $CODEBUILD_SRC_DIR/outputs.json
28+
29+
artifacts:
30+
files:
31+
- outputs.json
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: 0.2
2+
3+
phases:
4+
install:
5+
commands:
6+
- set -e
7+
- export TERRAFORM_VERSION=$(< .review_apps/.terraform-version)
8+
- curl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_arm64.zip" -o "terraform-${TERRAFORM_VERSION}.zip"
9+
- unzip "terraform-${TERRAFORM_VERSION}.zip"
10+
- mv terraform /usr/local/bin/terraform
11+
finally:
12+
- terraform --version
13+
14+
pre_build:
15+
commands:
16+
- cd .review_apps
17+
- terraform init -backend-config="key=review-apps/${APPLICATION_NAME}/pr-${PR_NUMBER}.tfstate"
18+
19+
build:
20+
commands:
21+
- |
22+
terraform destroy \
23+
-var "pull_request_number=${PR_NUMBER}" \
24+
-var "${APPLICATION_NAME//-/_}_container_image=placeholder" \
25+
-no-color \
26+
-auto-approve

0 commit comments

Comments
 (0)