Skip to content

Commit fca64e8

Browse files
committed
feat: implement continuous deployment
1 parent ce9fc9a commit fca64e8

File tree

13 files changed

+1211
-345
lines changed

13 files changed

+1211
-345
lines changed

.github/workflows/cd.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Continuous Deploy solution
2+
3+
on:
4+
push:
5+
branches:
6+
- saga
7+
workflow_dispatch:
8+
9+
permissions:
10+
id-token: write
11+
contents: write
12+
issues: write
13+
14+
env:
15+
CI: 1
16+
FORCE_COLOR: 3
17+
18+
jobs:
19+
deploy:
20+
runs-on: ubuntu-22.04
21+
22+
timeout-minutes: 10
23+
24+
environment:
25+
name: production
26+
url: ${{ steps.endpoint.outputs.url }}
27+
28+
steps:
29+
- uses: actions/checkout@v3
30+
31+
- uses: actions/setup-node@v3
32+
with:
33+
node-version: "18.x"
34+
35+
- name: Keep npm cache around to speed up installs
36+
uses: actions/cache@v3
37+
with:
38+
path: ~/.npm
39+
key: build-${{ hashFiles('**/package-lock.json') }}
40+
41+
- name: Install dependencies
42+
run: npm ci --no-audit
43+
44+
- name: Check TypeScript
45+
run: npx tsc
46+
47+
- name: Run Unit Tests
48+
run: npm test
49+
50+
- name: Configure AWS credentials
51+
uses: aws-actions/configure-aws-credentials@v2
52+
with:
53+
role-to-assume: ${{ secrets.AWS_ROLE }}
54+
role-session-name: github-action-public-parameter-registry-cd
55+
aws-region: ${{ vars.AWS_REGION }}
56+
57+
- name: Deploy solution stack
58+
run: npx cdk deploy
59+
60+
- name: Get endpoint URL
61+
id: endpoint
62+
run: |
63+
ENDPOINT=`aws cloudformation describe-stacks --stack-name ${STACK_NAME:-public-parameter-registry} | jq -r '.Stacks[0].Outputs[] | select(.OutputKey == "registryEndpoint") | .OutputValue' | sed -E 's/\/$//g'`
64+
echo "url=${ENDPOINT}" >> $GITHUB_OUTPUT

.github/workflows/test-and-release.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ jobs:
1919

2020
timeout-minutes: 30
2121

22+
environment: ci
23+
2224
steps:
2325
- uses: actions/checkout@v3
2426

@@ -46,7 +48,7 @@ jobs:
4648
with:
4749
role-to-assume: ${{ secrets.AWS_ROLE }}
4850
role-session-name: github-action-public-parameter-registry
49-
aws-region: ${{ secrets.AWS_REGION }}
51+
aws-region: ${{ vars.AWS_REGION }}
5052

5153
- name: Generate Stack ID
5254
run: |

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ aws ssm put-parameter --name /${STACK_NAME:-public-parameter-registry}/public/so
5353
For parameters to be published, they must be below the path
5454
`/<stack name>/public/`.
5555

56+
## CD with GitHub Actions
57+
58+
Create a GitHub environment `production`.
59+
60+
<!-- FIXME: add CLI comment -->
61+
62+
Store the role name from the output as a GitHub Action secret:
63+
64+
```bash
65+
CD_ROLE_ARN=`aws cloudformation describe-stacks --stack-name ${STACK_NAME:-public-parameter-registry} | jq -r '.Stacks[0].Outputs[] | select(.OutputKey == "cdRoleArn") | .OutputValue' | sed -E 's/\/$//g'`
66+
gh variable set AWS_REGION --env production --body "${AWS_REGION}"
67+
gh secret set AWS_ROLE --env production --body "${CD_ROLE_ARN}"
68+
```
69+
5670
## CI with GitHub Actions
5771

5872
Configure the AWS credentials for an account used for CI, then run
@@ -64,10 +78,14 @@ npx cdk --app 'npx tsx cdk/ci.ts' deploy
6478
This creates a role with Administrator privileges in that account, and allows
6579
the GitHub repository of this repo to assume it.
6680

81+
Create a GitHub environment `ci`.
82+
83+
<!-- FIXME: add CLI comment -->
84+
6785
Store the role name from the output as a GitHub Action secret:
6886

6987
```bash
70-
ROLE_ARN=`aws cloudformation describe-stacks --stack-name ${STACK_NAME:-public-parameter-registry}-ci | jq -r '.Stacks[0].Outputs[] | select(.OutputKey == "roleArn") | .OutputValue' | sed -E 's/\/$//g'`
71-
gh secret set AWS_REGION --body "${AWS_REGION}"
72-
gh secret set AWS_ROLE --body "${ROLE_ARN}"
88+
CI_ROLE_ARN=`aws cloudformation describe-stacks --stack-name ${STACK_NAME:-public-parameter-registry}-ci | jq -r '.Stacks[0].Outputs[] | select(.OutputKey == "roleArn") | .OutputValue' | sed -E 's/\/$//g'`
89+
gh variable set AWS_REGION --env ci --body "${AWS_REGION}"
90+
gh secret set AWS_ROLE --env ci --body "${CI_ROLE_ARN}"
7391
```

cdk/CD.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Duration, aws_iam as IAM, Stack } from 'aws-cdk-lib'
2+
import { Construct } from 'constructs'
3+
4+
export class CD extends Construct {
5+
public readonly role: IAM.IRole
6+
constructor(
7+
parent: Stack,
8+
{
9+
repository: r,
10+
gitHubOIDC,
11+
}: {
12+
repository: Repository
13+
gitHubOIDC: IAM.IOpenIdConnectProvider
14+
},
15+
) {
16+
super(parent, 'cd')
17+
18+
this.role = new IAM.Role(this, 'ghRole', {
19+
roleName: `${parent.stackName}-github-actions`,
20+
assumedBy: new IAM.WebIdentityPrincipal(
21+
gitHubOIDC.openIdConnectProviderArn,
22+
{
23+
StringEquals: {
24+
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
25+
'token.actions.githubusercontent.com:sub': `repo:${r.owner}/${r.repo}:environment:production`,
26+
},
27+
},
28+
),
29+
description: `This role is used by GitHub Actions for CI of ${r.owner}/${r.repo}`,
30+
maxSessionDuration: Duration.hours(1),
31+
managedPolicies: [
32+
IAM.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'),
33+
],
34+
})
35+
}
36+
}

cdk/CIStack.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,31 @@ export class CIStack extends Stack {
66
parent: App,
77
{
88
repository: r,
9+
gitHubOICDProviderArn,
910
}: {
1011
repository: {
1112
owner: string
1213
repo: string
1314
}
15+
gitHubOICDProviderArn: string
1416
},
1517
) {
1618
super(parent, CI_STACK_NAME)
1719

18-
const githubDomain = 'token.actions.githubusercontent.com'
19-
const ghProvider = new IAM.OpenIdConnectProvider(this, 'githubProvider', {
20-
url: `https://${githubDomain}`,
21-
clientIds: ['sts.amazonaws.com'],
22-
thumbprints: ['6938fd4d98bab03faadb97b34396831e3780aea1'],
23-
})
20+
const gitHubOIDC = IAM.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
21+
this,
22+
'gitHubOICDProvider',
23+
gitHubOICDProviderArn,
24+
)
2425

2526
const ghRole = new IAM.Role(this, 'ghRole', {
2627
roleName: `${CI_STACK_NAME}-github-actions`,
2728
assumedBy: new IAM.WebIdentityPrincipal(
28-
ghProvider.openIdConnectProviderArn,
29+
gitHubOIDC.openIdConnectProviderArn,
2930
{
3031
StringEquals: {
3132
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
32-
},
33-
StringLike: {
34-
[`${githubDomain}:sub`]: `repo:${r.owner}/${r.repo}:ref:refs/heads/*`,
33+
'token.actions.githubusercontent.com:sub': `repo:${r.owner}/${r.repo}:environment:ci`,
3534
},
3635
},
3736
),

cdk/RegistryApp.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@ import type { RegistryLambdas } from './RegistryLambdas.js'
33
import { RegistryStack } from './RegistryStack.js'
44

55
export class RegistryApp extends App {
6-
public constructor({ lambdaSources }: { lambdaSources: RegistryLambdas }) {
6+
public constructor({
7+
lambdaSources,
8+
repository,
9+
gitHubOICDProviderArn,
10+
}: {
11+
lambdaSources: RegistryLambdas
12+
repository: Repository
13+
gitHubOICDProviderArn: string
14+
}) {
715
super()
816

917
new RegistryStack(this, {
1018
lambdaSources,
19+
repository,
20+
gitHubOICDProviderArn,
1121
})
1222
}
1323
}

cdk/RegistryStack.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
aws_s3 as S3,
1111
Stack,
1212
} from 'aws-cdk-lib'
13+
import { CD } from './CD.js'
1314
import type { RegistryLambdas } from './RegistryLambdas.js'
1415
import { STACK_NAME } from './stackConfig.js'
1516

@@ -18,8 +19,12 @@ export class RegistryStack extends Stack {
1819
parent: App,
1920
{
2021
lambdaSources,
22+
repository,
23+
gitHubOICDProviderArn,
2124
}: {
2225
lambdaSources: RegistryLambdas
26+
repository: Repository
27+
gitHubOICDProviderArn: string
2328
},
2429
) {
2530
super(parent, STACK_NAME)
@@ -79,6 +84,20 @@ export class RegistryStack extends Stack {
7984
sourceArn: publishToS3Rule.ruleArn,
8085
})
8186

87+
// Set up role for CD
88+
const gitHubOIDC = IAM.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
89+
this,
90+
'gitHubOICDProvider',
91+
gitHubOICDProviderArn,
92+
)
93+
const cd = new CD(this, { repository, gitHubOIDC })
94+
95+
new CfnOutput(this, 'cdRoleArn', {
96+
exportName: `${this.stackName}:cdRoleArn`,
97+
description: 'Role to use in GitHub Actions',
98+
value: cd.role.roleArn,
99+
})
100+
82101
new CfnOutput(this, 'registryEndpoint', {
83102
exportName: `${this.stackName}:registryEndpoint`,
84103
description: 'Endpoint used for fetch the parameters',

cdk/ci.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { IAMClient } from '@aws-sdk/client-iam'
12
import pJSON from '../package.json'
23
import { CIApp } from './CIApp.js'
4+
import { ensureGitHubOIDCProvider } from './ensureGitHubOIDCProvider'
35

46
const repoUrl = new URL(pJSON.repository.url)
57
const repository = {
@@ -8,4 +10,9 @@ const repository = {
810
repoUrl.pathname.split('/')[2]?.replace(/\.git$/, '') ??
911
'public-parameter-registry-aws-js',
1012
}
11-
new CIApp({ repository })
13+
new CIApp({
14+
repository,
15+
gitHubOICDProviderArn: await ensureGitHubOIDCProvider({
16+
iam: new IAMClient({}),
17+
}),
18+
})

cdk/ensureGitHubOIDCProvider.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
CreateOpenIDConnectProviderCommand,
3+
GetOpenIDConnectProviderCommand,
4+
IAMClient,
5+
ListOpenIDConnectProvidersCommand,
6+
} from '@aws-sdk/client-iam'
7+
import chalk from 'chalk'
8+
9+
/**
10+
* Returns the ARN of the OpenID Connect provider for GitHub of the account.
11+
*/
12+
export const ensureGitHubOIDCProvider = async ({
13+
iam,
14+
}: {
15+
iam: IAMClient
16+
}): Promise<string> => {
17+
const { OpenIDConnectProviderList } = await iam.send(
18+
new ListOpenIDConnectProvidersCommand({}),
19+
)
20+
21+
const maybeGithubProvider = (
22+
await Promise.all(
23+
OpenIDConnectProviderList?.map(async ({ Arn }) =>
24+
iam
25+
.send(
26+
new GetOpenIDConnectProviderCommand({
27+
OpenIDConnectProviderArn: Arn,
28+
}),
29+
)
30+
.then((provider) => ({ Arn, provider })),
31+
) ?? [],
32+
)
33+
).find(
34+
({ provider: { Url } }) => Url === 'token.actions.githubusercontent.com',
35+
)
36+
37+
if (maybeGithubProvider?.Arn !== undefined) {
38+
console.debug(
39+
chalk.green(
40+
`OIDC provider for GitHub exists: ${maybeGithubProvider.Arn}`,
41+
),
42+
)
43+
return maybeGithubProvider.Arn
44+
}
45+
46+
console.log(
47+
chalk.yellow(`OIDC provider for GitHub does not exist. Creating ...`),
48+
)
49+
50+
const provider = await iam.send(
51+
new CreateOpenIDConnectProviderCommand({
52+
Url: `https://token.actions.githubusercontent.com`,
53+
ClientIDList: ['sts.amazonaws.com'],
54+
ThumbprintList: ['6938fd4d98bab03faadb97b34396831e3780aea1'],
55+
}),
56+
)
57+
58+
if (provider.OpenIDConnectProviderArn === undefined)
59+
throw new Error(`Failed to create OpenID Connect Provider for GitHub!`)
60+
61+
return provider.OpenIDConnectProviderArn
62+
}

cdk/registry.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { IAMClient } from '@aws-sdk/client-iam'
12
import { mkdir } from 'node:fs/promises'
23
import path from 'node:path'
4+
import pJSON from '../package.json'
35
import { RegistryApp } from './RegistryApp.js'
6+
import { ensureGitHubOIDCProvider } from './ensureGitHubOIDCProvider.js'
47
import { packLambda } from './packLambda.js'
58

9+
const repoUrl = new URL(pJSON.repository.url)
10+
const repository = {
11+
owner: repoUrl.pathname.split('/')[1] ?? 'bifravst',
12+
repo:
13+
repoUrl.pathname.split('/')[2]?.replace(/\.git$/, '') ??
14+
'public-parameter-registry-aws-js',
15+
}
16+
617
export type PackedLambda = { lambdaZipFile: string; handler: string }
718

819
const pack = async (id: string, handler = 'handler'): Promise<PackedLambda> => {
@@ -28,4 +39,8 @@ new RegistryApp({
2839
lambdaSources: {
2940
publishToS3: await pack('publishToS3'),
3041
},
42+
repository,
43+
gitHubOICDProviderArn: await ensureGitHubOIDCProvider({
44+
iam: new IAMClient({}),
45+
}),
3146
})

0 commit comments

Comments
 (0)