packages/constructs/L3/ai/sm-notebook-l3-construct/lib/sm-notebook-l3-construct.ts (240 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { MdaaParamAndOutput } from '@aws-mdaa/construct'; //NOSONAR
import { MdaaSecurityGroup, MdaaSecurityGroupProps, MdaaSecurityGroupRuleProps } from '@aws-mdaa/ec2-constructs';
import { MdaaResolvableRole, MdaaRoleRef } from '@aws-mdaa/iam-role-helper';
import { MdaaKmsKey, DECRYPT_ACTIONS, ENCRYPT_ACTIONS } from '@aws-mdaa/kms-constructs';
import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct';
import { MdaaNoteBook, MdaaNoteBookProps } from '@aws-mdaa/sagemaker-constructs';
import { AssetDeploymentProps, AssetProps, LifeCycleConfigHelper, LifecycleScriptProps } from '@aws-mdaa/sm-shared';
import { Stack } from 'aws-cdk-lib';
import { SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
import { Effect, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam';
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { CfnNotebookInstanceLifecycleConfig, CfnNotebookInstanceLifecycleConfigProps } from 'aws-cdk-lib/aws-sagemaker';
export interface NotebookLifeCycleConfigProps {
readonly onCreate?: LifecycleScriptProps;
readonly onStart?: LifecycleScriptProps;
}
export interface NotebookAssetDeploymentConfig {
readonly assetBucketName: string;
readonly assetDeploymentRoleArn: string;
readonly assetPrefix?: string;
readonly memoryLimitMB?: number;
}
export interface NamedAssetProps {
/** @jsii ignore */
readonly [name: string]: AssetProps;
}
export interface SagemakerNotebookL3ConstructProps extends MdaaL3ConstructProps {
readonly assetDeployment?: NotebookAssetDeploymentConfig;
readonly lifecycleConfigs?: NamedLifecycleConfigProps;
/**
* List of sagemaker notebook instances to be launched.
*/
readonly notebooks?: NotebookWithIdProps;
/**
* Optional KMS key to encrypt the notebooks. If not specified, one will be created.
*/
readonly kmsKeyArn?: string;
}
export interface InstanceMetadataServiceConfiguration {
readonly minimumInstanceMetadataServiceVersion: string;
}
export interface NamedLifecycleConfigProps {
/**
* Lifecycle config scripts
*/
/** @jsii ignore */
readonly [name: string]: NotebookLifeCycleConfigProps;
}
export interface NotebookWithIdProps {
/** @jsii ignore */
readonly [name: string]: NotebookProps;
}
export interface NotebookProps {
readonly notebookName?: string;
readonly vpcId: string;
readonly subnetId: string;
readonly instanceType: string;
readonly securityGroupId?: string;
readonly securityGroupIngress?: MdaaSecurityGroupRuleProps;
readonly securityGroupEgress?: MdaaSecurityGroupRuleProps;
readonly notebookRole: MdaaRoleRef;
readonly acceleratorTypes?: string[];
readonly additionalCodeRepositories?: string[];
readonly defaultCodeRepository?: string;
readonly instanceMetadataServiceConfiguration?: InstanceMetadataServiceConfiguration;
readonly platformIdentifier?: string;
readonly volumeSizeInGb?: number;
readonly rootAccess?: boolean;
readonly lifecycleConfigName?: string;
}
//This stack creates and manages a SageMaker Studio Domain
export class SagemakerNotebookL3Construct extends MdaaL3Construct {
protected readonly props: SagemakerNotebookL3ConstructProps;
constructor(stack: Stack, id: string, props: SagemakerNotebookL3ConstructProps) {
super(stack, id, props);
this.props = props;
const lifecycleConfigsMap = props.lifecycleConfigs ? this.createLifecycleConfigs(props.lifecycleConfigs) : {};
if (this.props.notebooks) this.createNotebooks(this.props.notebooks, lifecycleConfigsMap);
}
private createLifecycleConfigs(lifecycleConfigs: NamedLifecycleConfigProps): {
[k: string]: CfnNotebookInstanceLifecycleConfig;
} {
return Object.fromEntries(
Object.entries(lifecycleConfigs).map(entry => {
const lifecycleName = entry[0];
const lifecycleProps = entry[1];
const lifecycleConfig = this.createLifecycleConfig(lifecycleName, lifecycleProps);
return [lifecycleName, lifecycleConfig];
}),
);
}
private createNotebooks(
notebooks: NotebookWithIdProps,
lifecycleConfigsMap: { [k: string]: CfnNotebookInstanceLifecycleConfig },
) {
if (Object.keys(notebooks).length > 0) {
const resolvedRoles = Object.fromEntries(
Object.entries(notebooks).map(entry => {
const resolved = this.props.roleHelper.resolveRoleRefWithRefId(entry[1].notebookRole, entry[0]);
return [entry[0], resolved];
}) || [],
);
const kmsKey = this.props.kmsKeyArn
? Key.fromKeyArn(this, `imported-key`, this.props.kmsKeyArn)
: this.createKMSKey(
'notebooks',
Object.entries(resolvedRoles).map(x => x[1].arn()),
);
Object.entries(notebooks).forEach(entry => {
const notebookId = entry[0];
const notebookProps = entry[1];
this.createNotebook(notebookId, notebookProps, kmsKey, lifecycleConfigsMap, resolvedRoles);
});
}
}
private createNotebook(
notebookId: string,
notebookProps: NotebookProps,
kmsKey: IKey,
lifecycleConfigsMap: { [k: string]: CfnNotebookInstanceLifecycleConfig },
resolvedRoles: { [k: string]: MdaaResolvableRole },
) {
const securityGroup = notebookProps.securityGroupId
? SecurityGroup.fromSecurityGroupId(this, `${notebookId}-sg`, notebookProps.securityGroupId)
: this.createSecurityGroup(notebookId, notebookProps);
const lifecycleConfigName: string | undefined = notebookProps.lifecycleConfigName
? this.resolveLifecycleConfigName(notebookProps.lifecycleConfigName, lifecycleConfigsMap)
: undefined;
// Create notebook instance
const createNotebookProps: MdaaNoteBookProps = {
notebookInstanceId: notebookId,
naming: this.props.naming,
notebookInstanceName: notebookProps.notebookName ?? notebookId,
instanceType: notebookProps.instanceType,
roleArn: resolvedRoles[notebookId].arn(),
kmsKeyId: kmsKey.keyArn,
acceleratorTypes: notebookProps.acceleratorTypes,
additionalCodeRepositories: notebookProps.additionalCodeRepositories,
defaultCodeRepository: notebookProps.defaultCodeRepository,
instanceMetadataServiceConfiguration: notebookProps.instanceMetadataServiceConfiguration,
lifecycleConfigName: lifecycleConfigName,
platformIdentifier: notebookProps.platformIdentifier,
volumeSizeInGb: notebookProps.volumeSizeInGb,
securityGroupIds: [securityGroup.securityGroupId],
subnetId: notebookProps.subnetId,
rootAccess: notebookProps.rootAccess != undefined && notebookProps.rootAccess ? 'Enabled' : undefined,
};
new MdaaNoteBook(this, notebookId, createNotebookProps);
}
/** @jsii ignore */
private resolveLifecycleConfigName(
lifecycleConfigName: string,
lifecycleConfigsMap: { [name: string]: CfnNotebookInstanceLifecycleConfig },
): string {
if (lifecycleConfigName.startsWith('external:')) {
return lifecycleConfigName.replace(/^external:/, '');
} else {
const nameRef = lifecycleConfigsMap[lifecycleConfigName]?.notebookInstanceLifecycleConfigName;
if (!nameRef) {
throw new Error(`Non-existant lifecycle config referenced: ${lifecycleConfigName}`);
}
return nameRef;
}
}
private createSecurityGroup(notebookId: string, notebookProps: NotebookProps): SecurityGroup {
const notebookVpc = Vpc.fromVpcAttributes(this, 'vpc of' + notebookId, {
availabilityZones: ['dummy'],
vpcId: notebookProps.vpcId,
});
const customEgress: boolean =
(notebookProps.securityGroupEgress?.ipv4 && notebookProps.securityGroupEgress?.ipv4.length > 0) ||
(notebookProps.securityGroupEgress?.prefixList && notebookProps.securityGroupEgress?.prefixList.length > 0) ||
(notebookProps.securityGroupEgress?.sg && notebookProps.securityGroupEgress?.sg.length > 0) ||
false;
const securityGroupProps: MdaaSecurityGroupProps = {
securityGroupName: notebookId,
vpc: notebookVpc,
naming: this.props.naming,
ingressRules: notebookProps.securityGroupIngress,
egressRules: notebookProps.securityGroupEgress,
allowAllOutbound: !customEgress,
addSelfReferenceRule: false,
};
const securityGroup = new MdaaSecurityGroup(this, `${notebookId}-sg`, securityGroupProps);
return securityGroup;
}
private createKMSKey(notebookName: string, roleArns: string[]): IKey {
const kmsKey = new MdaaKmsKey(this, `kmskey-${notebookName}`, {
alias: `kmskey-${notebookName}`,
naming: this.props.naming,
});
// Allow execution role to use the key
const kmsEncryptDecryptPolicy = new PolicyStatement({
effect: Effect.ALLOW,
// Use of * mirrors what is done in the CDK methods for adding policy helpers.
resources: ['*'],
actions: [
...DECRYPT_ACTIONS,
...ENCRYPT_ACTIONS,
'kms:GenerateDataKeyWithoutPlaintext',
'kms:CreateGrant',
'kms:DescribeKey',
'kms:ListAliases',
],
});
roleArns.forEach(roleArn => kmsEncryptDecryptPolicy.addArnPrincipal(roleArn));
kmsKey.addToResourcePolicy(kmsEncryptDecryptPolicy);
return kmsKey;
}
private createLifecycleConfig(
lifecycleName: string,
lifecycleConfigProps: NotebookLifeCycleConfigProps,
): CfnNotebookInstanceLifecycleConfig {
const assetDeployment: AssetDeploymentProps | undefined = this.props.assetDeployment
? {
scope: this,
assetBucket: Bucket.fromBucketName(
this,
`asset-bucket-${lifecycleName}`,
this.props.assetDeployment.assetBucketName,
),
assetDeploymentRole: Role.fromRoleArn(
this,
`asset-role-${lifecycleName}`,
this.props.assetDeployment.assetDeploymentRoleArn,
),
assetPrefix: this.props.assetDeployment?.assetPrefix || `sagemaker-lifecycle-assets/notebooks`,
memoryLimitMB: this.props.assetDeployment?.memoryLimitMB,
}
: undefined;
const onStartContent = lifecycleConfigProps.onStart
? LifeCycleConfigHelper.createLifecycleConfigContents(lifecycleConfigProps.onStart, 'onStart', assetDeployment)
: undefined;
const onCreateContent = lifecycleConfigProps.onCreate
? LifeCycleConfigHelper.createLifecycleConfigContents(lifecycleConfigProps.onCreate, 'onCreate', assetDeployment)
: undefined;
const cfnLifecycleConfigProps: CfnNotebookInstanceLifecycleConfigProps = {
notebookInstanceLifecycleConfigName: this.props.naming.resourceName(lifecycleName),
onStart: onStartContent ? [{ content: onStartContent }] : undefined,
onCreate: onCreateContent ? [{ content: onCreateContent }] : undefined,
};
const lifecycleConfig = new CfnNotebookInstanceLifecycleConfig(
this,
`${lifecycleName}-lifecycle`,
cfnLifecycleConfigProps,
);
new MdaaParamAndOutput(this, {
naming: this.props.naming,
resourceId: lifecycleName,
resourceType: 'lifecycle-config',
name: 'name',
value: lifecycleConfig.getAtt('NotebookInstanceLifecycleConfigName').toString(),
});
return lifecycleConfig;
}
}