installer/lib/mdaa-installer-stack.ts (281 lines of code) (raw):
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
export enum RepositorySources {
GITHUB = 'github',
S3 = 's3',
}
export class MdaaInstallerStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, {
...props,
synthesizer: new cdk.DefaultStackSynthesizer({
generateBootstrapVersionRule: false,
}),
});
// Parameters
const repositorySource = new cdk.CfnParameter(this, 'RepositorySource', {
type: 'String',
description: 'Repository source for the code.',
allowedValues: [RepositorySources.GITHUB, RepositorySources.S3],
default: RepositorySources.GITHUB,
});
// GitHub parameters
const repositoryOwner = new cdk.CfnParameter(this, 'RepositoryOwner', {
type: 'String',
description: 'The owner of the GitHub repository containing the code.',
default: 'aws',
});
const repositoryName = new cdk.CfnParameter(this, 'RepositoryName', {
type: 'String',
description: 'The name of the repository containing the code.',
default: 'modern-data-architecture-accelerator',
});
const repositoryBranchName = new cdk.CfnParameter(this, 'RepositoryBranchName', {
type: 'String',
description: 'The name of the branch to use.',
default: 'main',
});
const githubTokenSecretsManagerId = new cdk.CfnParameter(this, 'GithubTokenSecretsManagerId', {
type: 'String',
description: 'Secrets Manager secrets Id containing the GitHub oauth token',
});
// S3 parameters
const repositoryBucketName = new cdk.CfnParameter(this, 'RepositoryBucketName', {
type: 'String',
description: 'The S3 bucket containing the code. This bucket must be in the same region as the stack.',
});
const repositoryBucketObject = new cdk.CfnParameter(this, 'RepositoryBucketObject', {
type: 'String',
description: 'The S3 object key for the code zip file.',
default: 'release/latest.zip',
});
// Conditions
const useGitHubCondition = new cdk.CfnCondition(this, 'UseGitHubCondition', {
expression: cdk.Fn.conditionEquals(repositorySource.valueAsString, RepositorySources.GITHUB),
});
const useS3Condition = new cdk.CfnCondition(this, 'UseS3Condition', {
expression: cdk.Fn.conditionEquals(repositorySource.valueAsString, RepositorySources.S3),
});
// which sample to deploy
const sampleNameParam = new cdk.CfnParameter(this, 'SampleName', {
type: 'String',
description: 'MDAA Sample you want to deploy',
allowedValues: ['basic_datalake', 'basic_datascience_platform'],
default: 'basic_datalake',
});
// org name
const orgNameParam = new cdk.CfnParameter(this, 'OrgName', {
type: 'String',
description:
'An MDAA deployment requires an Org Name (must start with a letter, contain only alphanumeric characters and hyphens, and be 100 characters or less)',
allowedPattern: '^[a-zA-Z][a-zA-Z0-9-]{0,99}$',
constraintDescription:
'Org Name must start with a letter and contain only alphanumeric characters (case-sensitive) and hyphens. Maximum length is 100 characters.',
});
// network info
const vpcIdParam = new cdk.CfnParameter(this, 'VpcId', {
type: 'String',
description: 'The ID of the VPC to use for deployment',
});
const subnetIdParam = new cdk.CfnParameter(this, 'SubnetId', {
type: 'String',
description: 'The ID of the subnet to use for deployment',
});
// KMS Key for encryption
const installerKey = new kms.Key(this, 'InstallerKey', {
enableKeyRotation: true,
description: 'Key used for encryption of pipeline artifacts',
});
// S3 Buckets
const accessLogsBucket = new s3.Bucket(this, 'AccessLogsBucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
});
const artifactBucket = new s3.Bucket(this, 'ArtifactBucket', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: installerKey,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
serverAccessLogsBucket: accessLogsBucket,
serverAccessLogsPrefix: 'artifact-bucket-logs/',
});
// IAM Role for CodeBuild
const buildRole = new iam.Role(this, 'BuildRole', {
assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')],
});
// CodeBuild Project
const buildProject = new codebuild.PipelineProject(this, 'BuildProject', {
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
privileged: true,
},
encryptionKey: installerKey,
role: buildRole,
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
install: {
'runtime-versions': {
nodejs: 22,
},
commands: ['ls -lt', 'npm ci', 'npm install -g aws-cdk'],
},
build: {
commands: [
'set -e',
'echo "Starting build phase..."',
"export CDK_DEFAULT_REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].RegionName')",
"export CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query 'Account' --output text)",
'export CDK_NEW_BOOTSTRAP=1 && aws cloudformation describe-stacks --stack-name CDKToolkit || npx cdk bootstrap aws://${CDK_DEFAULT_ACCOUNT}/${CDK_DEFAULT_REGION}',
'echo "org: ${ORG_NAME}"',
'echo "Replacing org-name place holder"',
'echo using sample: sample_configs/${SAMPLE_NAME}/mdaa.yaml',
"sed -i 's/<unique[- ]org[- ]name>/'\"$ORG_NAME\"'/g' sample_configs/${SAMPLE_NAME}/mdaa.yaml",
'find sample_configs/${SAMPLE_NAME}/ -type f \\( -name "*.yaml" -o -name "*.yml" \\) -exec sed -i \'s/<your vpc id>/\'"$VPC_ID"\'/g\' {} \\;',
'find sample_configs/${SAMPLE_NAME}/ -type f \\( -name "*.yaml" -o -name "*.yml" \\) -exec sed -i \'s/<your subnet id>/\'"$SUBNET_ID"\'/g\' {} \\;',
'find sample_configs/${SAMPLE_NAME}/ -type f \\( -name "*.yaml" -o -name "*.yml" \\) -exec sed -i \'s/<data scientist user id>/\'"$ORG_NAME"\'-datascientist/g\' {} \\;',
'./bin/mdaa -c sample_configs/${SAMPLE_NAME}/mdaa.yaml deploy',
'echo "Deployment completed successfully"',
],
},
},
cache: {
paths: ['node_modules/**/*'],
},
}),
environmentVariables: {
REPOSITORY_SOURCE: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: repositorySource.valueAsString,
},
REPOSITORY_OWNER: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: repositoryOwner.valueAsString,
},
REPOSITORY_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: repositoryName.valueAsString,
},
REPOSITORY_BRANCH_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: repositoryBranchName.valueAsString,
},
REPOSITORY_BUCKET_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: repositoryBucketName.valueAsString,
},
REPOSITORY_BUCKET_OBJECT: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: repositoryBucketObject.valueAsString,
},
SAMPLE_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: sampleNameParam.valueAsString,
},
ORG_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: orgNameParam.valueAsString,
},
VPC_ID: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: vpcIdParam.valueAsString,
},
SUBNET_ID: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: subnetIdParam.valueAsString,
},
},
});
// Pipeline artifact
const sourceOutput = new codepipeline.Artifact();
// GitHub Pipeline with explicit role
const githubPipelineRole = new iam.Role(this, 'GitHubPipelineRole', {
assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
});
// Add inline policy to the role for accessing the secret
githubPipelineRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],
resources: [
`arn:aws:secretsmanager:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:secret:${githubTokenSecretsManagerId.valueAsString}`,
],
}),
);
const githubPipeline = new codepipeline.Pipeline(this, 'GitHubPipeline', {
pipelineName: 'MDAA-GitHubPipeline',
artifactBucket,
role: githubPipelineRole,
stages: [
{
stageName: 'Source',
actions: [
new codepipeline_actions.GitHubSourceAction({
actionName: 'Source',
owner: repositoryOwner.valueAsString,
repo: repositoryName.valueAsString,
branch: repositoryBranchName.valueAsString,
oauthToken: cdk.SecretValue.secretsManager(githubTokenSecretsManagerId.valueAsString),
output: sourceOutput,
trigger: codepipeline_actions.GitHubTrigger.NONE,
}),
],
},
{
stageName: 'Build',
actions: [
new codepipeline_actions.CodeBuildAction({
actionName: 'Build',
project: buildProject,
input: sourceOutput,
role: githubPipelineRole,
}),
],
},
],
});
// S3 Pipeline with shared role for pipeline and source action
const s3PipelineRole = new iam.Role(this, 'S3PipelineRole', {
assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
});
// Add S3 permissions to the pipeline role
s3PipelineRole.addToPolicy(
new iam.PolicyStatement({
actions: ['s3:GetObject', 's3:GetObjectVersion'],
resources: [`arn:aws:s3:::${repositoryBucketName.valueAsString}/${repositoryBucketObject.valueAsString}`],
}),
);
const s3Pipeline = new codepipeline.Pipeline(this, 'S3Pipeline', {
pipelineName: 'MDAA-S3Pipeline',
artifactBucket,
role: s3PipelineRole,
stages: [
{
stageName: 'Source',
actions: [
new codepipeline_actions.S3SourceAction({
actionName: 'Source',
bucket: s3.Bucket.fromBucketName(this, 'SourceBucket', repositoryBucketName.valueAsString),
bucketKey: repositoryBucketObject.valueAsString,
output: sourceOutput,
role: s3PipelineRole, // Use the same role as the pipeline
}),
],
},
{
stageName: 'Build',
actions: [
new codepipeline_actions.CodeBuildAction({
actionName: 'Build',
project: buildProject,
input: sourceOutput,
role: s3PipelineRole,
}),
],
},
],
});
// Apply conditions to all resources related to GitHub pipeline
githubPipeline.node.findAll().forEach(child => {
if (child instanceof cdk.CfnResource) {
child.cfnOptions.condition = useGitHubCondition;
}
});
// Apply conditions to all resources related to S3 pipeline
s3Pipeline.node.findAll().forEach(child => {
if (child instanceof cdk.CfnResource) {
child.cfnOptions.condition = useS3Condition;
}
});
}
}