Skip to content

Commit 0912db7

Browse files
committed
feat: add GitHub Actions OIDC + IAM role to Terraform layer 2
Moves CI/CD setup from imperative AWS CLI commands to declarative Terraform. Layer 2 now creates: - OIDC identity provider for GitHub Actions - IAM role scoped to repo:coder/usgov-deploy-aws:* - Inline policy granting ECR push to project repos only Outputs github_actions_role_arn and ecr_registry for easy GitHub secret setup. Runbook updated to match.
1 parent a90e7f3 commit 0912db7

4 files changed

Lines changed: 154 additions & 124 deletions

File tree

docs/DEPLOYMENT_RUNBOOK.md

Lines changed: 32 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,13 @@ gp3, 50–200 GiB autoscaling, 7-day backups, deletion protection),
113113
3 ECR repositories (coder, base-fips, desktop-fips — all
114114
KMS-encrypted with scan-on-push), KMS CMK (shared key for RDS/EBS/
115115
ECR/Secrets Manager), Secrets Manager secrets (RDS password
116-
auto-generated, Coder license placeholder).
116+
auto-generated, Coder license placeholder), GitHub Actions OIDC
117+
provider + IAM role for CI/CD (scoped to this repo, ECR push only).
117118

118119
**Why:** Coder needs a PostgreSQL database. ECR stores the FIPS
119120
container images. Secrets Manager holds credentials that
120-
ExternalSecrets syncs into Kubernetes.
121+
ExternalSecrets syncs into Kubernetes. The OIDC + IAM role lets
122+
GitHub Actions push images to ECR without static access keys.
121123

122124
```bash
123125
cd infra/terraform/2-data
@@ -289,141 +291,37 @@ this depending on your setup):
289291

290292
## Phase D — Set Up CI/CD (GitHub Actions → ECR)
291293

292-
ECR repos now exist (created by layer 2). This phase wires GitHub
293-
Actions to push FIPS images into them.
294+
The OIDC provider, IAM role, and ECR permissions were all created
295+
by Terraform layer 2 (`infra/terraform/2-data/ci.tf`). No manual
296+
AWS CLI commands needed.
294297

295-
### D1. Get your AWS account ID
298+
### D1. Get the CI values from Terraform outputs
296299

297300
```bash
298-
aws sts get-caller-identity --query Account --output text
299-
# e.g. 123456789012
300-
```
301-
302-
### D2. Create the OIDC identity provider
303-
304-
**What this does:** Tells AWS to trust GitHub Actions as an identity
305-
provider. When a GitHub Actions workflow runs, GitHub issues a JWT.
306-
This OIDC provider lets AWS validate that JWT so the workflow can
307-
assume an IAM role — no static access keys needed.
308-
309-
**One-time setup. Skip if you already have this from another repo.**
310-
311-
Check first:
312-
```bash
313-
aws iam list-open-id-connect-providers \
314-
| grep token.actions.githubusercontent.com
315-
# If it prints something, skip to D3
316-
```
317-
318-
If nothing returned:
319-
```bash
320-
aws iam create-open-id-connect-provider \
321-
--url https://token.actions.githubusercontent.com \
322-
--client-id-list sts.amazonaws.com \
323-
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
324-
```
325-
326-
### D3. Create the IAM role for CI
327-
328-
**What this does:** Creates an IAM role that only GitHub Actions
329-
workflows running in the `coder/usgov-deploy-aws` repo can assume.
330-
The trust policy uses the OIDC provider from D2 and restricts
331-
access to this specific repo. The permissions allow pushing and
332-
pulling container images to/from ECR.
333-
334-
Save this as `/tmp/trust-policy.json` (replace `ACCOUNT_ID`):
335-
336-
```json
337-
{
338-
"Version": "2012-10-17",
339-
"Statement": [
340-
{
341-
"Effect": "Allow",
342-
"Principal": {
343-
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
344-
},
345-
"Action": "sts:AssumeRoleWithWebIdentity",
346-
"Condition": {
347-
"StringEquals": {
348-
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
349-
},
350-
"StringLike": {
351-
"token.actions.githubusercontent.com:sub": "repo:coder/usgov-deploy-aws:*"
352-
}
353-
}
354-
}
355-
]
356-
}
357-
```
358-
359-
```bash
360-
# Replace ACCOUNT_ID in the file first, then:
361-
aws iam create-role \
362-
--role-name usgov-deploy-aws-ci \
363-
--assume-role-policy-document file:///tmp/trust-policy.json
364-
```
365-
366-
### D4. Attach ECR permissions to the role
367-
368-
**What this does:** Grants the CI role permission to authenticate
369-
with ECR (GetAuthorizationToken works globally) and push/pull
370-
images to the three repos created by Terraform layer 2. Scoped
371-
to `coder4gov/*` repos only — can't touch anything else in ECR.
372-
373-
Save as `/tmp/ecr-policy.json` (replace `ACCOUNT_ID`):
374-
375-
```json
376-
{
377-
"Version": "2012-10-17",
378-
"Statement": [
379-
{
380-
"Sid": "ECRAuth",
381-
"Effect": "Allow",
382-
"Action": "ecr:GetAuthorizationToken",
383-
"Resource": "*"
384-
},
385-
{
386-
"Sid": "ECRPush",
387-
"Effect": "Allow",
388-
"Action": [
389-
"ecr:BatchCheckLayerAvailability",
390-
"ecr:GetDownloadUrlForLayer",
391-
"ecr:BatchGetImage",
392-
"ecr:PutImage",
393-
"ecr:InitiateLayerUpload",
394-
"ecr:UploadLayerPart",
395-
"ecr:CompleteLayerUpload",
396-
"ecr:CreateRepository",
397-
"ecr:DescribeRepositories"
398-
],
399-
"Resource": "arn:aws:ecr:us-west-2:ACCOUNT_ID:repository/coder4gov/*"
400-
}
401-
]
402-
}
403-
```
301+
cd infra/terraform/2-data
302+
terraform output github_actions_role_arn
303+
# e.g. arn:aws:iam::123456789012:role/coder4gov-github-actions-ci
404304

405-
```bash
406-
aws iam put-role-policy \
407-
--role-name usgov-deploy-aws-ci \
408-
--policy-name ecr-push \
409-
--policy-document file:///tmp/ecr-policy.json
305+
terraform output ecr_registry
306+
# e.g. 123456789012.dkr.ecr.us-west-2.amazonaws.com
307+
cd ../../..
410308
```
411309

412-
### D5. Add GitHub repo secrets
310+
### D2. Add GitHub repo secrets
413311

414312
**Where:** https://github.com/coder/usgov-deploy-aws/settings/secrets/actions
415313

416314
**Why:** The GitHub Actions workflows reference these secrets to
417315
authenticate with AWS and know which ECR registry to push to. No
418316
AWS access keys are stored — the workflow uses OIDC federation to
419-
get short-lived credentials via the role from D3.
317+
get short-lived credentials via the IAM role Terraform created.
420318

421-
| Secret name | Value | Example |
319+
| Secret name | Value | Source |
422320
|---|---|---|
423-
| `ECR_REGISTRY` | `<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com` | `123456789012.dkr.ecr.us-west-2.amazonaws.com` |
424-
| `AWS_ROLE_ARN` | `arn:aws:iam::<ACCOUNT_ID>:role/usgov-deploy-aws-ci` | `arn:aws:iam::123456789012:role/usgov-deploy-aws-ci` |
321+
| `AWS_ROLE_ARN` | The role ARN from D1 | `terraform output github_actions_role_arn` |
322+
| `ECR_REGISTRY` | The registry URL from D1 | `terraform output ecr_registry` |
425323

426-
### D6. Test the CI pipeline
324+
### D3. Test the CI pipeline
427325

428326
**What this does:** Manually triggers the Coder FIPS build workflow.
429327
It clones the Coder source, compiles a FIPS-enabled binary with
@@ -445,14 +343,24 @@ Successfully pushed <REGISTRY>/coder4gov/coder:latest-fips
445343
Then test **Workspace FIPS Images** the same way. This builds the
446344
RHEL 9 base-fips and desktop-fips images.
447345

448-
### D7. Verify images in ECR
346+
### D4. Verify images in ECR
449347

450348
```bash
451349
aws ecr list-images --repository-name coder4gov/coder
452350
aws ecr list-images --repository-name coder4gov/base-fips
453351
aws ecr list-images --repository-name coder4gov/desktop-fips
454352
```
455353

354+
> **Note:** If the OIDC provider already exists in your account from
355+
> another repo, Terraform will fail on `aws_iam_openid_connect_provider.github`.
356+
> Import it instead:
357+
> ```bash
358+
> cd infra/terraform/2-data
359+
> terraform import aws_iam_openid_connect_provider.github \
360+
> arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com
361+
> ```
362+
363+
456364
---
457365
458366
## Phase E — Seed API Keys (for usgov-env-demo only)
@@ -478,7 +386,7 @@ aws secretsmanager create-secret \
478386
A1–A2 Clone + configure
479387
A3 Layer 0: S3 state bucket (~1 min)
480388
A4 Layer 1: VPC, DNS, ACM (~3 min) → update NS records
481-
A5 Layer 2: RDS, ECR, KMS, Secrets (~10 min)
389+
A5 Layer 2: RDS, ECR, KMS, Secrets, CI (~10 min)
482390
A6 Layer 3: EKS cluster (~15 min)
483391
A7 Layer 4: Karpenter, ALB, ESO (~5 min)
484392
B1–B2 Seed Coder license

infra/terraform/2-data/ci.tf

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
################################################################################
2+
# GitHub Actions OIDC → AWS IAM (CI/CD for ECR image pushes)
3+
#
4+
# Creates:
5+
# - OIDC identity provider for GitHub Actions (if not already present)
6+
# - IAM role assumable only by this repo's GitHub Actions workflows
7+
# - Inline policy granting ECR push/pull to project repos only
8+
#
9+
# The role ARN and ECR registry URL should be stored as GitHub repo secrets:
10+
# AWS_ROLE_ARN = aws_iam_role.github_actions.arn
11+
# ECR_REGISTRY = <account_id>.dkr.ecr.<region>.amazonaws.com
12+
################################################################################
13+
14+
# ---------------------------------------------------------------------------
15+
# OIDC Identity Provider — trust GitHub Actions JWTs
16+
# One per AWS account. If you already have this from another repo, import it:
17+
# terraform import aws_iam_openid_connect_provider.github \
18+
# arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com
19+
# ---------------------------------------------------------------------------
20+
21+
resource "aws_iam_openid_connect_provider" "github" {
22+
url = "https://token.actions.githubusercontent.com"
23+
client_id_list = ["sts.amazonaws.com"]
24+
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
25+
26+
tags = {
27+
Name = "github-actions-oidc"
28+
}
29+
}
30+
31+
# ---------------------------------------------------------------------------
32+
# IAM Role — assumable only by workflows in this specific GitHub repo
33+
# ---------------------------------------------------------------------------
34+
35+
resource "aws_iam_role" "github_actions" {
36+
name = "${var.project_name}-github-actions-ci"
37+
38+
assume_role_policy = jsonencode({
39+
Version = "2012-10-17"
40+
Statement = [
41+
{
42+
Effect = "Allow"
43+
Principal = {
44+
Federated = aws_iam_openid_connect_provider.github.arn
45+
}
46+
Action = "sts:AssumeRoleWithWebIdentity"
47+
Condition = {
48+
StringEquals = {
49+
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
50+
}
51+
StringLike = {
52+
"token.actions.githubusercontent.com:sub" = "repo:${var.github_repo}:*"
53+
}
54+
}
55+
},
56+
]
57+
})
58+
59+
tags = {
60+
Name = "${var.project_name}-github-actions-ci"
61+
}
62+
}
63+
64+
# ---------------------------------------------------------------------------
65+
# ECR push/pull permissions — scoped to this project's repos only
66+
# ---------------------------------------------------------------------------
67+
68+
resource "aws_iam_role_policy" "github_actions_ecr" {
69+
name = "ecr-push"
70+
role = aws_iam_role.github_actions.id
71+
72+
policy = jsonencode({
73+
Version = "2012-10-17"
74+
Statement = [
75+
{
76+
Sid = "ECRAuth"
77+
Effect = "Allow"
78+
Action = "ecr:GetAuthorizationToken"
79+
Resource = "*"
80+
},
81+
{
82+
Sid = "ECRPush"
83+
Effect = "Allow"
84+
Action = [
85+
"ecr:BatchCheckLayerAvailability",
86+
"ecr:GetDownloadUrlForLayer",
87+
"ecr:BatchGetImage",
88+
"ecr:PutImage",
89+
"ecr:InitiateLayerUpload",
90+
"ecr:UploadLayerPart",
91+
"ecr:CompleteLayerUpload",
92+
"ecr:CreateRepository",
93+
"ecr:DescribeRepositories",
94+
]
95+
Resource = [
96+
for repo in aws_ecr_repository.repos :
97+
repo.arn
98+
]
99+
},
100+
]
101+
})
102+
}

infra/terraform/2-data/outputs.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,14 @@ output "secret_arns" {
5353
coder_license = aws_secretsmanager_secret.coder_license.arn
5454
}
5555
}
56+
57+
# CI/CD — store these as GitHub repo secrets
58+
output "github_actions_role_arn" {
59+
description = "IAM role ARN for GitHub Actions CI (set as AWS_ROLE_ARN secret)."
60+
value = aws_iam_role.github_actions.arn
61+
}
62+
63+
output "ecr_registry" {
64+
description = "ECR registry URL (set as ECR_REGISTRY secret)."
65+
value = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.aws_region}.amazonaws.com"
66+
}

infra/terraform/2-data/variables.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ variable "db_backup_retention_period" {
5353
default = 7
5454
}
5555

56+
# ---------------------------------------------------------------------------
57+
# CI/CD
58+
# ---------------------------------------------------------------------------
59+
variable "github_repo" {
60+
description = "GitHub repo (org/name) allowed to assume the CI role via OIDC."
61+
type = string
62+
default = "coder/usgov-deploy-aws"
63+
}
64+
5665
# ---------------------------------------------------------------------------
5766
# Tags
5867
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)