in src/installer/cdk/src/index.ts [67:686]
constructor(scope: cdk.Construct, id: string, props: Installer.Props) {
super(scope, id, props);
const { repoSource, acceleratorVersion } = props;
const acceleratorPrefixParam = new cdk.CfnParameter(this, 'AcceleratorPrefix', {
default: 'ASEA-',
description: 'Accelerator prefix used for deployment.',
allowedPattern: '[a-zA-Z][a-zA-Z0-9-]{0,8}-',
});
const acceleratorNameParam = new cdk.CfnParameter(this, 'AcceleratorName', {
default: 'ASEA',
description: 'Accelerator Name used for deployment.',
allowedPattern: '[a-zA-Z][a-zA-Z0-9]{0,3}',
});
const acceleratorName = acceleratorNameParam.valueAsString;
const acceleratorPrefix = acceleratorPrefixParam.valueAsString;
const acceleratorConfigS3Bucket = new cdk.CfnParameter(this, 'ConfigS3Bucket', {
default: 'AWSDOC-EXAMPLE-BUCKET',
description: 'The S3 bucket name that contains the initial Accelerator configuration.',
});
const configRepositoryName = new cdk.CfnParameter(this, 'ConfigRepositoryName', {
default: 'ASEA-Config-Repo',
description: 'The AWS CodeCommit repository name that contains the Accelerator configuration.',
});
const configBranchName = new cdk.CfnParameter(this, 'ConfigBranchName', {
default: 'main',
description: 'The AWS CodeCommit branch name that contains the Accelerator configuration',
});
const notificationEmail = new cdk.CfnParameter(this, 'Notification Email', {
description: 'The notification email that will get Accelerator State Machine execution notifications.',
});
const codebuildComputeType = new cdk.CfnParameter(this, 'CodeBuild Compute Type', {
description: 'The compute type of the build server for the Accelerator deployments.',
default: codebuild.ComputeType.LARGE,
allowedValues: [codebuild.ComputeType.MEDIUM, codebuild.ComputeType.LARGE, codebuild.ComputeType.X2_LARGE],
});
const stackDeployPageSize = new cdk.CfnParameter(this, 'Deployment Page Size', {
description: 'The number of stacks to deploy in parallel. This value SHOULD NOT normally be changed.',
default: 680,
});
const stateMachineName = `${acceleratorPrefix}MainStateMachine_sm`;
// The state machine name has to match the name of the state machine in initial setup
const stateMachineArn = `arn:aws:states:${this.region}:${this.account}:stateMachine:${stateMachineName}`;
// Use the `start-execution.js` script in the assets folder
const stateMachineStartExecutionCode = fs.readFileSync(path.join(__dirname, '..', 'assets', 'start-execution.js'));
// Use the `save-application-version.js` script in the assets folder
const saveApplicationVersionCode = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'save-application-version.js'),
);
// Use the `validate-parameters.js` script in the assets folder
const validateParametersCode = fs.readFileSync(path.join(__dirname, '..', 'assets', 'validate-parameters.js'));
// Role that is used by the CodeBuild project
const installerProjectRole = new iam.Role(this, 'InstallerProjectRole', {
roleName: `${acceleratorPrefix}CB-Installer`,
assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
});
// Allow creation of ECR repositories
installerProjectRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['ecr:*'],
resources: [`arn:aws:ecr:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:repository/aws-cdk/*`],
}),
);
// Allow getting authorization tokens for ECR
installerProjectRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['ecr:GetAuthorizationToken'],
resources: [`*`],
}),
);
installerProjectRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['sts:AssumeRole'],
resources: [`arn:aws:iam::${cdk.Aws.ACCOUNT_ID}:role/cdk-*`],
}),
);
// Allow all CloudFormation permissions
installerProjectRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['cloudformation:*'],
resources: [`arn:aws:cloudformation:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:stack/*`],
}),
);
// Allow the role to access the CDK asset bucket
installerProjectRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['s3:*'],
resources: [`arn:aws:s3:::cdk-*`],
}),
);
// Allow the role to create anything through CloudFormation
installerProjectRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['*'],
resources: ['*'],
conditions: {
'ForAnyValue:StringEquals': {
'aws:CalledVia': ['cloudformation.amazonaws.com'],
},
},
}),
);
const cfnInstallerProjectRole = installerProjectRole.node.defaultChild as iam.CfnRole;
cfnInstallerProjectRole.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W28', // Resource found with an explicit name, this disallows updates that require replacement of this resource
reason: 'Using explicit name for installer',
},
],
},
};
const cfnInstallerProjectRoleDefaultPolicy = installerProjectRole.node.findChild('DefaultPolicy').node
.defaultChild as iam.CfnPolicy;
cfnInstallerProjectRoleDefaultPolicy.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'F4', // IAM policy should not allow * action
reason: 'Allows cloudformation to generate resources, needs full access',
},
{
id: 'F39', // IAM policy should not allow * resource with PassRole action
reason: 'False error: assumeRole using cdk-*',
},
{
id: 'W12', // IAM policy should not allow * resource
reason: 'Allows cloudformation to generate resources, needs full access',
},
{
id: 'W76', // SPCM for IAM policy document is higher than 25
reason: 'IAM policy is generated by CDK',
},
],
},
};
// Create a CMK that can be used for the CodePipeline artifacts bucket
const installerCmk = new kms.Key(this, 'ArtifactsBucketCmk', {
enableKeyRotation: true,
description: 'ArtifactsBucketCmk',
alias: `alias/${acceleratorPrefix}Installer-Key`,
});
installerCmk.grantEncryptDecrypt(new iam.AccountRootPrincipal());
// Define a build specification to build the initial setup templates
const installerProject = new codebuild.PipelineProject(this, 'InstallerProject', {
projectName: `${acceleratorPrefix}InstallerProject_pl`,
role: installerProjectRole,
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
install: {
'runtime-versions': {
nodejs: 14,
},
// The flag '--unsafe-perm' is necessary to run pnpm scripts in Docker
commands: [
'npm install --global pnpm@6.2.3',
'pnpm install --unsafe-perm --frozen-lockfile',
'pnpm recursive run build --unsafe-perm',
],
},
pre_build: {
// The flag '--unsafe-perm' is necessary to run pnpm scripts in Docker
commands: ['pnpm recursive run build --unsafe-perm'],
},
build: {
commands: [
'cd src/core/cdk',
'export CDK_NEW_BOOTSTRAP=1',
`pnpx cdk bootstrap aws://${cdk.Aws.ACCOUNT_ID}/${cdk.Aws.REGION} --require-approval never --toolkit-stack-name=${acceleratorPrefix}CDKToolkit --cloudformation-execution-policies=arn:${cdk.Aws.PARTITION}:iam::aws:policy/AdministratorAccess`,
`pnpx cdk deploy --require-approval never --toolkit-stack-name=${acceleratorPrefix}CDKToolkit`,
],
},
},
}),
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
privileged: true, // Allow access to the Docker daemon
computeType: codebuild.ComputeType.MEDIUM,
environmentVariables: {
ACCELERATOR_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: acceleratorName,
},
ACCELERATOR_PREFIX: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: acceleratorPrefix,
},
ACCELERATOR_STATE_MACHINE_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: stateMachineName,
},
CONFIG_REPOSITORY_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: configRepositoryName.valueAsString,
},
CONFIG_BRANCH_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: configBranchName.valueAsString,
},
CONFIG_S3_BUCKET: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: acceleratorConfigS3Bucket.valueAsString,
},
ENABLE_PREBUILT_PROJECT: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: 'true', // Enable Docker prebuilt project
},
NOTIFICATION_EMAIL: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: notificationEmail.valueAsString,
},
INSTALLER_CMK: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: `alias/${acceleratorPrefix}Installer-Key`,
},
BUILD_COMPUTE_TYPE: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: codebuildComputeType.valueAsString,
},
DEPLOY_STACK_PAGE_SIZE: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: stackDeployPageSize.valueAsString,
},
},
},
cache: codebuild.Cache.local(codebuild.LocalCacheMode.SOURCE),
});
// This artifact is used as output for the Github code and as input for the build step
const sourceArtifact = new codepipeline.Artifact();
const repoName = new cdk.CfnParameter(this, 'RepositoryName', {
default: 'aws-secure-environment-accelerator',
description: 'The name of the git repository containing the Accelerator code.',
});
const repoBranch = new cdk.CfnParameter(this, 'RepositoryBranch', {
// Github release action sets GITHUB_DEFAULT_BRANCH
// Otherwise fall back to 'release'
default: process.env.GITHUB_DEFAULT_BRANCH || 'release',
description: 'The branch of the git repository containing the Accelerator code.',
});
let sourceAction: actions.GitHubSourceAction | actions.CodeCommitSourceAction; // Generic action for Source
let repoOwner: string;
if (repoSource === RepositorySources.CODECOMMIT) {
// Create the CodeCommit source action
sourceAction = new actions.CodeCommitSourceAction({
actionName: 'CodeCommitSource',
repository: codecommit.Repository.fromRepositoryName(this, 'CodeCommitRepo', repoName.valueAsString),
branch: repoBranch.valueAsString,
output: sourceArtifact,
trigger: actions.CodeCommitTrigger.NONE,
});
// Save off values for UpdateVersion action
repoOwner = 'CodeCommit';
} else {
// Default to GitHub
// Additional parameter needed for the GitHub secret
const githubOauthSecretId = new cdk.CfnParameter(this, 'GithubSecretId', {
default: 'accelerator/github-token',
description: 'The token to use to access the Github repository.',
});
const githubOwner = new cdk.CfnParameter(this, 'GithubOwner', {
default: 'aws-samples',
description: 'The owner of the Github repository containing the Accelerator code.',
});
// Create the GitHub source action
sourceAction = new actions.GitHubSourceAction({
actionName: 'GithubSource',
owner: githubOwner.valueAsString,
repo: repoName.valueAsString,
branch: repoBranch.valueAsString,
oauthToken: cdk.SecretValue.secretsManager(githubOauthSecretId.valueAsString),
output: sourceArtifact,
trigger: actions.GitHubTrigger.NONE,
});
// Save off values for UpdateVersion action
repoOwner = githubOwner.valueAsString;
}
// The role that will be used to start the state machine
const stateMachineExecutionRole = new iam.Role(this, 'ExecutionRoleName', {
roleName: `${acceleratorPrefix}L-SFN-Execution`,
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
// Grant permissions to write logs
stateMachineExecutionRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
resources: ['*'],
}),
);
stateMachineExecutionRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['ssm:PutParameter', 'ssm:GetParameter', 'ssm:GetParameterHistory'],
resources: ['*'],
}),
);
stateMachineExecutionRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['cloudformation:DescribeStacks'],
resources: ['*'],
}),
);
// Grant permissions to start the state machine
stateMachineExecutionRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['states:StartExecution'],
resources: [stateMachineArn],
}),
);
const cfnStateMachineExecutionRole = stateMachineExecutionRole.node.defaultChild as iam.CfnRole;
cfnStateMachineExecutionRole.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W28', // Resource found with an explicit name, this disallows updates that require replacement of this resource
reason: 'Using explicit name for installer',
},
],
},
};
const cfnStateMachineExecutionRoleDefaultPolicy = stateMachineExecutionRole.node.findChild('DefaultPolicy').node
.defaultChild as iam.CfnPolicy;
cfnStateMachineExecutionRoleDefaultPolicy.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W12', // IAM policy should not allow * resource
reason: 'Allows stateMachine to generate resources, needs full access',
},
{
id: 'W76', // SPCM for IAM policy document is higher than 25
reason: 'IAM policy is generated by CDK',
},
],
},
};
// Create the Lambda function that is responsible for launching the state machine
const stateMachineStartExecutionLambda = new lambda.Function(this, 'ExecutionLambda', {
functionName: `${acceleratorPrefix}Installer-StartExecution`,
role: stateMachineExecutionRole,
// Inline code is only allowed for Node.js version 12
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline(stateMachineStartExecutionCode.toString()),
handler: 'index.handler',
});
const cfnStateMachineStartExecutionLambda = stateMachineStartExecutionLambda.node
.defaultChild as lambda.CfnFunction;
cfnStateMachineStartExecutionLambda.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W58', // Lambda functions require permission to write CloudWatch Logs
reason: 'CloudWatch Logs not required for installer',
},
{
id: 'W89', // Lambda functions should be deployed inside a VPC
reason: 'Lambda inside VPC not required for installer',
},
{
id: 'W92', // Lambda functions should define ReservedConcurrentExecutions to reserve simultaneous executions
reason: 'ReservedConcurrentExecutions not required for installer',
},
],
},
};
// Create the Lambda function that is responsible for launching the state machine
const saveApplicationVersionLambda = new lambda.Function(this, 'SaveApplicationVersionLambda', {
functionName: `${acceleratorPrefix}Installer-SaveApplicationVersion`,
role: stateMachineExecutionRole,
// Inline code is only allowed for Node.js version 12
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline(saveApplicationVersionCode.toString()),
handler: 'index.handler',
});
const cfnSaveApplicationVersionLambda = saveApplicationVersionLambda.node.defaultChild as lambda.CfnFunction;
cfnSaveApplicationVersionLambda.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W58', // Lambda functions require permission to write CloudWatch Logs
reason: 'CloudWatch Logs not required for installer',
},
{
id: 'W89', // Lambda functions should be deployed inside a VPC
reason: 'Lambda inside VPC not required for installer',
},
{
id: 'W92', // Lambda functions should define ReservedConcurrentExecutions to reserve simultaneous executions
reason: 'ReservedConcurrentExecutions not required for installer',
},
],
},
};
// Create the Lambda function that is responsible for validating previous parameters
const validateParametersLambda = new lambda.Function(this, 'ValidateParametersLambda', {
functionName: `${acceleratorPrefix}Installer-ValidateParameters`,
role: stateMachineExecutionRole,
// Inline code is only allowed for Node.js version 12
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline(validateParametersCode.toString()),
handler: 'index.handler',
});
const cfnValidateParametersLambda = validateParametersLambda.node.defaultChild as lambda.CfnFunction;
cfnValidateParametersLambda.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W58', // Lambda functions require permission to write CloudWatch Logs
reason: 'CloudWatch Logs not required for installer',
},
{
id: 'W89', // Lambda functions should be deployed inside a VPC
reason: 'Lambda inside VPC not required for installer',
},
{
id: 'W92', // Lambda functions should define ReservedConcurrentExecutions to reserve simultaneous executions
reason: 'ReservedConcurrentExecutions not required for installer',
},
],
},
};
// Role that is used by the CodePipeline
// Permissions for
// - accessing the artifacts bucket
// - publishing to the manual approval SNS topic
// - running the CodeBuild project
// - running the state machine execution Lambda function
// will be added automatically by the CDK Pipeline construct
const installerPipelineRole = new iam.Role(this, 'InstallerPipelineRole', {
roleName: `${acceleratorPrefix}CP-Installer`,
assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
});
// This bucket will be used to store the CodePipeline source
const installerArtifactsBucket = new s3.Bucket(this, 'ArtifactsBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
encryption: s3.BucketEncryption.KMS,
encryptionKey: installerCmk,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
});
const cfnInstallerArtifactsBucket = installerArtifactsBucket.node.defaultChild as s3.CfnBucket;
cfnInstallerArtifactsBucket.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W35', // S3 Bucket should have access logging configured
reason: 'Access logs not required for installer',
},
],
},
};
// Allow only https requests
installerArtifactsBucket.addToResourcePolicy(
new iam.PolicyStatement({
actions: ['s3:*'],
resources: [installerArtifactsBucket.bucketArn, installerArtifactsBucket.arnForObjects('*')],
principals: [new iam.AnyPrincipal()],
conditions: {
Bool: {
'aws:SecureTransport': 'false',
},
},
effect: iam.Effect.DENY,
}),
);
const installerPipeline = new codepipeline.Pipeline(this, 'Pipeline', {
role: installerPipelineRole,
pipelineName: `${acceleratorPrefix}InstallerPipeline`,
artifactBucket: installerArtifactsBucket,
stages: [
{
stageName: 'Source',
actions: [sourceAction],
},
{
stageName: 'ValidateParameters',
actions: [
new actions.LambdaInvokeAction({
actionName: 'ValidateParameters',
lambda: validateParametersLambda,
role: installerPipelineRole,
userParameters: {
acceleratorName,
acceleratorPrefix,
},
}),
],
},
{
stageName: 'Deploy',
actions: [
new actions.CodeBuildAction({
actionName: 'DeployAccelerator',
project: installerProject,
input: sourceArtifact,
role: installerPipelineRole,
}),
],
},
{
stageName: 'UpdateVersion',
actions: [
new actions.LambdaInvokeAction({
actionName: 'UpdateVersion',
lambda: saveApplicationVersionLambda,
role: installerPipelineRole,
userParameters: {
commitId: sourceAction.variables.commitId,
repository: repoName,
owner: repoOwner,
branch: repoBranch,
acceleratorVersion,
acceleratorName,
acceleratorPrefix,
},
}),
],
},
{
stageName: 'Execute',
actions: [
new actions.LambdaInvokeAction({
actionName: 'ExecuteAcceleratorStateMachine',
lambda: stateMachineStartExecutionLambda,
role: installerPipelineRole,
userParameters: {
stateMachineArn,
},
}),
],
},
],
});
cdk.Aspects.of(this).add(new cdk.Tag('Accelerator', `${acceleratorName}1`));
const cfnInstallerPipelineRole = installerPipelineRole.node.defaultChild as iam.CfnRole;
cfnInstallerPipelineRole.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W28', // Resource found with an explicit name, this disallows updates that require replacement of this resource
reason: 'Using explicit name for installer',
},
],
},
};
const cfnInstallerPipelineRoleDefaultPolicy = installerPipeline.role.node.findChild('DefaultPolicy').node
.defaultChild as iam.CfnPolicy;
cfnInstallerPipelineRoleDefaultPolicy.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W12', // IAM policy should not allow * resource
reason: 'Allows CodePipeline to generate resources, needs full access',
},
{
id: 'W76', // SPCM for IAM policy document is higher than 25
reason: 'IAM policy is generated by CDK',
},
],
},
};
}