Skip to content

Commit

Permalink
GitHub OIDC CDK deploy
Browse files Browse the repository at this point in the history
  • Loading branch information
ScriptSmith committed Sep 14, 2023
1 parent 5fb3070 commit 18c06a7
Show file tree
Hide file tree
Showing 16 changed files with 4,693 additions and 5 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Deploy CDK Stack
on:
push:
branches:
- cdk
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: test-app
- name: Install CDK
run: npm install -g aws-cdk
- name: Install dependencies
working-directory: test-app
run: npm install
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-southeast-2
role-to-assume: arn:aws:iam::854640616043:role/GitHubStack-deployroleC69923DD-BV0NXT8YQN38
role-duration-seconds: 900
output-credentials: true
- name: Deploy CDK Stack
working-directory: test-app
run: cdk deploy TestAppStack
2 changes: 1 addition & 1 deletion buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ phases:
build:
commands:
- sam build -t edge.yaml
- sam deploy --config-env $ENV --s3-bucket $LAMBDA_BUCKET_NAME --s3-prefix $ENV-$STACK_NAME --stack-name $STACK_NAME-edge --capabilities CAPABILITY_IAM --profile account-role --region us-east-1 -t edge.yaml
- sam deploy --config-env $ENV --s3-bucket qut-lambda-code-us-east-1-$ENV --s3-prefix $ENV-$STACK_NAME-edge --stack-name $STACK_NAME-edge --capabilities CAPABILITY_IAM --profile account-role --region us-east-1 -t edge.yaml

- (cd api; npm install)
- (cd api; npm run build)
Expand Down
3 changes: 3 additions & 0 deletions samconfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ capabilities = "CAPABILITY_IAM"
parameter_overrides = [
"Environment=dev",

"EdgeStackName=dev-transcription-edge",
"UserPoolStackName=qut-user-pool",

"LogBucketSuffix=qut-cloudfront-logs",
Expand All @@ -34,6 +35,7 @@ capabilities = "CAPABILITY_IAM"
parameter_overrides = [
"Environment=qa",

"EdgeStackName=qa-transcription-edge",
"UserPoolStackName=qut-user-pool",

"LogBucketSuffix=qut-cloudfront-logs",
Expand All @@ -60,6 +62,7 @@ capabilities = "CAPABILITY_IAM"
parameter_overrides = [
"Environment=prod",

"EdgeStackName=prod-transcription-edge",
"UserPoolStackName=qut-user-pool",

"LogBucketSuffix=qut-cloudfront-logs",
Expand Down
8 changes: 4 additions & 4 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Parameters:
Type: String
Default: Z2FDTNDATAQYW2
Description: CloudFront resources HostedZoneId
EdgeStackName:
Type: String
Description: Name of the edge stack containing the routing lambda in us-east-1
UserPoolStackName:
Type: String
Description: The user pool stack name
Expand Down Expand Up @@ -58,9 +61,6 @@ Parameters:
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: VPC subnets
EdgeStackName:
Type: String
Description: Name of the edge stack containing the routing lambda in us-east-1
Conditions:
CreateARecords: !Not [ !Equals [ !Ref HostedZoneName, "" ] ]
IsDev: !Equals [ !Ref Environment, "dev" ]
Expand Down Expand Up @@ -164,7 +164,7 @@ Resources:
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN:
Fn::ImportValue: !Sub "EdgeStackName-EdgeLambdaVersion"
Fn::ImportValue: !Sub "${EdgeStackName}-EdgeLambdaVersion"
PriceClass: PriceClass_All
Logging:
IncludeCookies: false
Expand Down
8 changes: 8 additions & 0 deletions test-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions test-app/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
14 changes: 14 additions & 0 deletions test-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
30 changes: 30 additions & 0 deletions test-app/bin/test-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { TestAppStack } from "../lib/test-app-stack";
import { GitHubStack } from "../lib/test-app-github-stack";

type Environment = "dev" | "qa" | "prod";

const envs = {
"dev": { account: "854640616043", region: "ap-southeast-2" },
"qa": { account: "488710887118", region: "ap-southeast-2" },
"prod": { account: "827511434808", region: "ap-southeast-2" }
};

const owner = "eresearchqut";
const repo = "transcription";
const stackName = "TestAppStack";
const env = envs[process.env.GITHUB_REF_NAME as Environment] ?? envs["dev"];

const app = new cdk.App();
const githubActionsSynthesizer = new cdk.CliCredentialsStackSynthesizer({
fileAssetsBucketName: `${env.account}-${owner}-${repo}-asset-bucket`
});

new TestAppStack(app, "TestAppStack", {
synthesizer: process.env.GITHUB_ACTIONS ? githubActionsSynthesizer : undefined,
stackName,
env
});
new GitHubStack(app, "GitHubStack", { owner, repo, stacks: [stackName], env });
61 changes: 61 additions & 0 deletions test-app/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"app": "npx ts-node --prefer-ts-exts bin/test-app.ts",
"requireApproval": "never",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true
}
}
8 changes: 8 additions & 0 deletions test-app/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
}
};
112 changes: 112 additions & 0 deletions test-app/lib/test-app-github-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";


export interface GitHubStackProps extends cdk.StackProps {
readonly stacks: string[];
readonly owner: string;
readonly repo: string;
readonly filters?: string[];
}

const appResourcePolicies = (stacks: string[]) => {
stacks = stacks.map(stack => stack.toLowerCase());
return [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"s3:*"
],
resources: stacks.map(stack => `arn:aws:s3:::${cdk.Aws.ACCOUNT_ID}-${stack}-*`)
}),
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"dynamodb:*"
],
resources: stacks.flatMap(stack => [
`arn:aws:dynamodb:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:table/${cdk.Aws.ACCOUNT_ID}-${stack}-*`,
`arn:aws:dynamodb:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:table/${cdk.Aws.ACCOUNT_ID}-${stack}-*/index/*`,
`arn:aws:dynamodb:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:table/${cdk.Aws.ACCOUNT_ID}-${stack}-*/stream/*`,
`arn:aws:dynamodb:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:table/${cdk.Aws.ACCOUNT_ID}-${stack}-*/backup/*`,
`arn:aws:dynamodb:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:table/${cdk.Aws.ACCOUNT_ID}-${stack}-*/import/*`,
`arn:aws:dynamodb:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:table/${cdk.Aws.ACCOUNT_ID}-${stack}-*/export/*`
])
}),
];
};

export class GitHubStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: GitHubStackProps) {
super(scope, id, props);
const filters = props.filters ?? ["*"];

const provider = new iam.OpenIdConnectProvider(this, "oidc-provider-github", {
url: `https://token.actions.githubusercontent.com`,
clientIds: ["sts.amazonaws.com"]
});

const principal = new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, {
StringLike: {
[`token.actions.githubusercontent.com:sub`]: filters.map(filter => `repo:${props.owner}/${props.repo}:${filter}`)
}
});

const assetBucket = new s3.Bucket(this, "asset-bucket", {
bucketName: `${this.account}-${props.owner}-${props.repo}-asset-bucket`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});

const readAccountPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["sts:GetCallerIdentity"],
resources: ["*"]
});

const cloudformationPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"cloudformation:UpdateTerminationProtection",
"cloudformation:CreateChangeSet",
"cloudformation:DeleteChangeSet",
"cloudformation:GetTemplateSummary",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:CreateStack",
"cloudformation:GetTemplate",
"cloudformation:DeleteStack",
"cloudformation:UpdateStack",
"sts:GetCallerIdentity",
"cloudformation:DescribeChangeSet",
"cloudformation:ExecuteChangeSet"
],
resources: props.stacks.map(stack => `arn:aws:cloudformation:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:stack/${stack}/*`)
});

const deployRole = new iam.Role(this, "deploy-role", {
assumedBy: principal,
description: "This role is used via GitHub Actions to deploy with AWS CDK on the target AWS account",
inlinePolicies: {
"read-account": new iam.PolicyDocument({
statements: [
readAccountPolicy,
cloudformationPolicy,
...appResourcePolicies(props.stacks)
]
})
},
maxSessionDuration: cdk.Duration.hours(1)
});

assetBucket.grantReadWrite(deployRole);

new cdk.CfnOutput(this, "deployRoleArn", {
value: deployRole.roleArn,
description: "The role used via GitHub Actions to deploy with AWS CDK on the target AWS account",
exportName: "deployRoleArn"
});
}
}
22 changes: 22 additions & 0 deletions test-app/lib/test-app-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as ddb from "aws-cdk-lib/aws-dynamodb";

export class TestAppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const resourcePrefix = `${this.account}-${this.stackName.toLowerCase()}`;

new s3.Bucket(this, "MyFirstBucket", {
bucketName: `${resourcePrefix}-bucket`,
versioned: true,
});

new ddb.Table(this, "MyFirstTable", {
tableName: `${resourcePrefix}-table`,
partitionKey: { name: "pk", type: ddb.AttributeType.STRING },
sortKey: { name: "sk", type: ddb.AttributeType.STRING },
billingMode: ddb.BillingMode.PAY_PER_REQUEST
});
}
}
Loading

0 comments on commit 18c06a7

Please sign in to comment.