Skip to content

Commit

Permalink
refactor pipeline for share source code sequentially for each stage (#48
Browse files Browse the repository at this point in the history
)

* refactor pipeline for share source code sequentially for each stage

* update the bucket role policy and clean source bucket step

* add constant for bastion bucket

* modify cloud front distribution origin path

* remove the dryrun

* refactor pipeline for share source code sequentially for each stage

* update the bucket role policy and clean source bucket step

* add constant for bastion bucket

* modify cloud front distribution origin path

* remove the dryrun

* update deploy logic

* remove synth

* add role for deploy  pipeline

* add change for commit

* add more specific permission
  • Loading branch information
raylrui authored Oct 17, 2024
1 parent a66d7e2 commit 62a7837
Show file tree
Hide file tree
Showing 19 changed files with 3,010 additions and 4,945 deletions.
9 changes: 9 additions & 0 deletions deploy/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,18 @@ export const cloudFrontBucketNameConfig: Record<AppStage, string> = {
[AppStage.PROD]: 'orcaui-cloudfront-472057503814',
};

export const configLambdaNameConfig: Record<AppStage, string> = {
[AppStage.BETA]: 'TriggerCodeBuildLambdaBeta',
[AppStage.GAMMA]: 'TriggerCodeBuildLambdaGamma',
[AppStage.PROD]: 'TriggerCodeBuildLambdaProd',
};

export const getAppStackConfig = (appStage: AppStage): ApplicationStackProps => {
switch (appStage) {
case AppStage.BETA:
return {
cloudFrontBucketName: cloudFrontBucketNameConfig[appStage],
configLambdaName: configLambdaNameConfig[appStage],
aliasDomainName: ['orcaui.dev.umccr.org'],
reactBuildEnvVariables: {
VITE_METADATA_URL: `https://metadata.dev.umccr.org`,
Expand All @@ -39,6 +46,7 @@ export const getAppStackConfig = (appStage: AppStage): ApplicationStackProps =>
case AppStage.GAMMA:
return {
cloudFrontBucketName: cloudFrontBucketNameConfig[appStage],
configLambdaName: configLambdaNameConfig[appStage],
aliasDomainName: ['orcaui.stg.umccr.org'],
reactBuildEnvVariables: {
VITE_METADATA_URL: `https://metadata.stg.umccr.org`,
Expand All @@ -51,6 +59,7 @@ export const getAppStackConfig = (appStage: AppStage): ApplicationStackProps =>
case AppStage.PROD:
return {
cloudFrontBucketName: cloudFrontBucketNameConfig[appStage],
configLambdaName: configLambdaNameConfig[appStage],
aliasDomainName: ['orcaui.umccr.org', 'orcaui.prod.umccr.org'],
reactBuildEnvVariables: {
VITE_METADATA_URL: `https://metadata.prod.umccr.org`,
Expand Down
64 changes: 64 additions & 0 deletions deploy/lambda/env_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

import os
import boto3
import json

def handler(event, context):
ssm = boto3.client('ssm')
s3 = boto3.client('s3')
cloudfront = boto3.client('cloudfront')

bucket_name = os.environ['BUCKET_NAME']
cloudfront_distribution_id = os.environ['CLOUDFRONT_DISTRIBUTION_ID']
# List of SSM parameters to fetch


env_params = [
'VITE_METADATA_URL',
'VITE_WORKFLOW_URL',
'VITE_SEQUENCE_RUN_URL',
'VITE_FILE_URL',

'VITE_REGION',
'VITE_COG_APP_CLIENT_ID',
'VITE_OAUTH_REDIRECT_IN',
'VITE_OAUTH_REDIRECT_OUT',
'VITE_COG_USER_POOL_ID',
'VITE_COG_IDENTITY_POOL_ID',
'VITE_OAUTH_DOMAIN',
'VITE_UNSPLASH_CLIENT_ID',
]

env_variables = {}
for param in env_params:
env_variables[param] = os.environ[param]

env_js_content = f"window.config = {json.dumps(env_variables, indent=2)}"


try:
s3.put_object(Bucket=bucket_name, Key='env.js', Body=env_js_content, ContentType='text/javascript')

# invalidate cloudfront distribution for all files (clear cache)
cloudfront.create_invalidation(
DistributionId=cloudfront_distribution_id,
InvalidationBatch={
'Paths': {
'Quantity': 1,
'Items': ['/*']
},
}
)

return {
'statusCode': 200,
'body': f" env.js uploaded to {bucket_name}, and CloudFront cache invalidated for {cloudfront_distribution_id}"
}
except Exception as e:
# Log the error and return a failure response
print("Error:")
print(e)
return {
'statusCode': 500,
'body': f"Failed to upload env.js to {bucket_name}. {e}"
}
24 changes: 0 additions & 24 deletions deploy/lambda/start_build.py

This file was deleted.

162 changes: 37 additions & 125 deletions deploy/lib/application-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,15 @@ import { BlockPublicAccess, Bucket, IBucket } from 'aws-cdk-lib/aws-s3';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { Construct } from 'constructs';
import { Architecture, Code, Runtime } from 'aws-cdk-lib/aws-lambda';
import {
BuildEnvironmentVariableType,
BuildSpec,
LinuxArmBuildImage,
PipelineProject,
} from 'aws-cdk-lib/aws-codebuild';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Pipeline, Artifact } from 'aws-cdk-lib/aws-codepipeline';
import {
CodeStarConnectionsSourceAction,
CodeBuildAction,
} from 'aws-cdk-lib/aws-codepipeline-actions';
import { Trigger } from 'aws-cdk-lib/triggers';
import { BuildEnvironmentVariableType } from 'aws-cdk-lib/aws-codebuild';
import { AccountPrincipal, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Function } from 'aws-cdk-lib/aws-lambda';
import { TOOLCHAIN_ACCOUNT_ID } from '../config';

export type ApplicationStackProps = {
cloudFrontBucketName: string;
configLambdaName: string;
aliasDomainName: string[];
reactBuildEnvVariables: Record<string, string>;
};
Expand All @@ -52,133 +44,53 @@ export class ApplicationStack extends Stack {
});

const distribution = this.setupS3CloudFrontIntegration(clientBucket, props.aliasDomainName);
this.buildReactApp(clientBucket, props.reactBuildEnvVariables, distribution);
}

private buildReactApp(
bucket: IBucket,
reactBuildEnvVariables: Record<string, string>,
distribution: Distribution
) {
const codeStarArn = StringParameter.valueForStringParameter(this, 'codestar_github_arn');

const sourceOutput = new Artifact();
const buildOutput = new Artifact();

const buildProject = new PipelineProject(this, 'ReactBuildCodeBuildProject', {
buildSpec: BuildSpec.fromObject({
version: 0.2,
phases: {
install: {
'runtime-versions': {
nodejs: 20,
},
commands: ['node -v', 'corepack enable', 'yarn --version', 'yarn install --immutable'],
},
build: {
commands: [
'set -eu',
'env | grep VITE',
'yarn build',
// deploy the react app to s3
'aws s3 rm s3://${VITE_BUCKET_NAME}/ --recursive && aws s3 sync ./dist s3://${VITE_BUCKET_NAME}',
// invalidate the cloudfront cache
'aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths "/*"',
],
},
},
}),
description: 'Build react app and publish assets to S3',
environment: { buildImage: LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0 },
environmentVariables: {
VITE_BUCKET_NAME: {
value: bucket.bucketName,
type: BuildEnvironmentVariableType.PLAINTEXT,
},
CLOUDFRONT_DISTRIBUTION_ID: {
value: distribution.distributionId,
type: BuildEnvironmentVariableType.PLAINTEXT,
},
VITE_REGION: {
value: 'ap-southeast-2',
type: BuildEnvironmentVariableType.PLAINTEXT,
},
// CodeBuild is smart enough to give permission to these ssm parameters
...this.getSSMEnvironmentVariables(),
// spread the reactBuildEnvironmentVariables
...Object.entries(reactBuildEnvVariables).reduce(
(acc, [key, value]) => {
acc[key] = {
value: value,
type: BuildEnvironmentVariableType.PLAINTEXT,
};
return acc;
},
{} as Record<string, Record<string, string>>
),
},
});

const pipeline = new Pipeline(this, 'ReactBuildPipeline', {
pipelineName: 'ReactBuildPipeline',
crossAccountKeys: false,
stages: [
{
stageName: 'Source',
actions: [
new CodeStarConnectionsSourceAction({
actionName: 'Source',
owner: 'umccr',
repo: 'orca-ui',
branch: 'main',
connectionArn: codeStarArn,
output: sourceOutput,
triggerOnPush: false, // disable trigger on push as we need to trigger the pipeline from the lambda
}),
],
},
{
stageName: 'BuildAndDeploy',
actions: [
new CodeBuildAction({
actionName: 'BuildAndDeploy',
project: buildProject,
input: sourceOutput,
outputs: [buildOutput],
}),
],
},
],
});

bucket.grantReadWrite(buildProject);
distribution.grantCreateInvalidation(buildProject);

const triggerFunction = new Function(this, 'TriggerCodeBuildLambda', {
/*
Trigger CodeBuild pipeline to build and deploy the react app
This lambda function has 3 stages:
1. read all env variables from os environment variables
2. write into env.js, upload to the S3 bucket
3. invalidate the CloudFront cache
*/
const configLambda = new Function(this, 'EnvConfigLambda', {
functionName: props.configLambdaName,
code: Code.fromAsset(path.join(__dirname, '..', 'lambda')),
timeout: Duration.minutes(10),
handler: 'start_build.handler',
handler: 'env_config.handler',
logRetention: RetentionDays.ONE_WEEK,
runtime: Runtime.PYTHON_3_12,
architecture: Architecture.ARM_64,
memorySize: 1024,
environment: {
CODEPIPELINE_NAME: pipeline.pipelineName,
VITE_REGION: 'ap-southeast-2',
BUCKET_NAME: clientBucket.bucketName,
CLOUDFRONT_DISTRIBUTION_ID: distribution.distributionId,
...props.reactBuildEnvVariables,
...Object.fromEntries(
Object.entries(this.getSSMEnvironmentVariables()).map(([key, value]) => [
key,
StringParameter.valueForStringParameter(this, value.value),
])
),
},

initialPolicy: [
new PolicyStatement({
actions: ['codepipeline:StartPipelineExecution'],
resources: [pipeline.pipelineArn],
actions: ['ssm:GetParameter'],
resources: Object.values(this.getSSMEnvironmentVariables()).map((value) => value.value),
}),
],
});

// Ths Lambda function trigger the CodeBuild project
new Trigger(this, 'TriggerCodeBuildTrigger', {
handler: triggerFunction,
executeAfter: [pipeline],
});
clientBucket.grantReadWrite(configLambda);
distribution.grantCreateInvalidation(configLambda);

/*
Grant the toolchain account access to the S3 bucket and lambda function
so that it can invoke the lambda function to trigger the CodeBuild pipeline
*/
clientBucket.grantDelete(new AccountPrincipal(TOOLCHAIN_ACCOUNT_ID));
clientBucket.grantReadWrite(new AccountPrincipal(TOOLCHAIN_ACCOUNT_ID));
configLambda.grantInvoke(new AccountPrincipal(TOOLCHAIN_ACCOUNT_ID));
}

private setupS3CloudFrontIntegration(s3Bucket: IBucket, aliasDomainName: string[]): Distribution {
Expand Down
Loading

0 comments on commit 62a7837

Please sign in to comment.