in source/backend/lib/cdk-photosearch-backend-stack.ts [83:537]
constructor(scope: cdk.Construct, id: string, props: PhotoSearchBackendStackProps) {
super(scope, id);
const rekognitionCollectionId = 'photosearch';
// Define a deployment of Amazon VPC for Amazon Elasticsearch Service
const myvpc = defaults.buildVpc(this, {
defaultVpcProps: defaults.DefaultIsolatedVpcProps(), // eslint-disable-line new-cap
});
const rekvpceSg = defaults.buildSecurityGroup(
this,
'RekognitionSecurityGroup',
{
vpc: myvpc,
},
[{ peer: ec2.Peer.ipv4(myvpc.vpcCidrBlock), connection: ec2.Port.tcp(443) }],
[]
);
myvpc.addInterfaceEndpoint('RekognitionVPCE', {
service: ec2.InterfaceVpcEndpointAwsService.REKOGNITION,
securityGroups: [rekvpceSg],
});
// Define a deployment of Security Group in the VPC for Amazon Elasticsearch Service
const aesSg = defaults.buildSecurityGroup(
this,
'AESSecurityGroup',
{
vpc: myvpc,
},
[{ peer: ec2.Peer.ipv4(myvpc.vpcCidrBlock), connection: ec2.Port.tcp(443) }],
[]
);
const aesDomainName = 'photosearch-domain';
// Define a deployment of Amazon Elasticsearch Service Domain
const cfnDomainProps: es.CfnDomainProps = {
domainName: aesDomainName,
elasticsearchVersion: '7.10',
vpcOptions: {
subnetIds: [myvpc.isolatedSubnets[0].subnetId, myvpc.isolatedSubnets[1].subnetId],
securityGroupIds: [aesSg.securityGroupId],
},
elasticsearchClusterConfig: {
dedicatedMasterCount: 3,
dedicatedMasterEnabled: true,
dedicatedMasterType: props.aesInstanceType,
instanceCount: 2,
instanceType: props.aesInstanceType,
zoneAwarenessEnabled: true,
},
encryptionAtRestOptions: {
enabled: true,
},
nodeToNodeEncryptionOptions: {
enabled: true,
},
ebsOptions: {
ebsEnabled: true,
volumeSize: 100,
},
accessPolicies: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
principals: [new iam.AnyPrincipal()],
actions: ['es:ESHttp*'],
resources: [`arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${aesDomainName}/*`],
}),
],
}),
// `logPublishingOptions` is disabled due to the issue below. When you anable it, Custom Resource is required.
// https://github.com/aws/aws-cdk/issues/5343
};
const esDomain = new es.CfnDomain(this, 'ElasticsearchDomain', cfnDomainProps);
esDomain.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W28',
reason:
'The ES Domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific ES instance only.',
},
],
},
};
// Amazon S3 Bucket for registration of photos
const corsRule: [s3.CorsRule] = [
{
allowedOrigins: [props.bucketAllowOrigin],
allowedMethods: [s3.HttpMethods.PUT],
allowedHeaders: ['*'],
},
];
const [bucketForRegister] = defaults.buildS3Bucket(
this,
{
bucketProps: {
cors: corsRule,
},
},
'S3BucketForRegister'
);
const restApi = new privateapi.PrivateApi(this, 'PrivateApi', { vpc: myvpc }).restApi;
// securityGroup for lambda in VPC
const securityGroup = defaults.buildSecurityGroup(
this,
'LambdaSecurityGroup',
{
vpc: myvpc,
},
[{ peer: ec2.Peer.ipv4(myvpc.vpcCidrBlock), connection: ec2.Port.tcp(443) }],
[]
);
// Define a common properties setting for Lambda functions
const commonLambdaProps = {
handler: 'handler',
runtime: lambda.Runtime.NODEJS_14_X,
timeout: cdk.Duration.minutes(1),
memorySize: 256,
vpc: myvpc,
securityGroups: [securityGroup],
tracing: lambda.Tracing.ACTIVE,
environment: {
AES_NODE_ENDPOINT: esDomain.attrDomainEndpoint,
S3_BUCKET_NAME: bucketForRegister.bucketName,
AES_INDEX_NAME: 'photosearch',
CUSTOM_USER_AGENT: 'AwsSolution/SO0173/1.0.0',
CORS_ALLOWED_ORIGIN: props.apiGatewayAllowOrigin,
},
};
// Define Lambda functions to deploy
const webApiLambdas = [
{
// Web API to get a photo
functionName: 'GetPhotoWithId',
webApiPath: 'photos/{photo_id}',
httpMethod: 'GET',
codePath: 'lambda/photos/get-photo/index.ts',
grantingProcess: (lambdaFunction: lambda.Function) => {
bucketForRegister.grantRead(lambdaFunction);
},
},
{
// Web API to get photos
functionName: 'GetPhotos',
webApiPath: 'photos',
httpMethod: 'GET',
codePath: 'lambda/photos/get-photos/index.ts',
grantingProcess: (lambdaFunction: lambda.Function) => {
bucketForRegister.grantRead(lambdaFunction);
},
},
{
// Web API to get urls to upload photos
functionName: 'GetPhotoUploadURLs',
webApiPath: 'photos/upload_urls',
httpMethod: 'GET',
codePath: 'lambda/photos/get-upload-urls/index.ts',
grantingProcess: (lambdaFunction: lambda.Function) => {
bucketForRegister.grantPut(lambdaFunction);
},
},
{
// Web API to delete specific photo
functionName: 'DeletePhoto',
webApiPath: 'photos/{photo_id}',
httpMethod: 'DELETE',
codePath: 'lambda/photos/delete-photo/index.ts',
grantingProcess: (lambdaFunction: lambda.Function) => {
bucketForRegister.grantRead(lambdaFunction);
bucketForRegister.grantDelete(lambdaFunction);
lambdaFunction.role!.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['rekognition:DeleteFaces'],
resources: [
`arn:aws:rekognition:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:collection/${rekognitionCollectionId}`,
],
})
);
},
},
{
// Web API to set tags on specific photo
functionName: 'SetPhotoTags',
webApiPath: 'photos/{photo_id}/tags',
httpMethod: 'PUT',
codePath: 'lambda/photos/set-tags/index.ts',
},
{
// Web API to set faces from specific photo
functionName: 'GetPhotoFaces',
webApiPath: 'photos/{photo_id}/faces',
httpMethod: 'GET',
codePath: 'lambda/photos/get-faces/index.ts',
grantingProcess: (lambdaFunction: lambda.Function) => {
bucketForRegister.grantRead(lambdaFunction);
lambdaFunction.role!.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['rekognition:DetectFaces'],
resources: ['*'],
})
);
},
},
{
// Web API to set similar faces from specific photo
functionName: 'GetSimilars',
webApiPath: 'photos/{photo_id}/similars',
httpMethod: 'GET',
codePath: 'lambda/photos/get-similars/index.ts',
grantingProcess: (lambdaFunction: lambda.Function) => {
bucketForRegister.grantReadWrite(lambdaFunction);
lambdaFunction.role!.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['rekognition:SearchFaces', 'rekognition:SearchFacesByImage'],
resources: [
`arn:aws:rekognition:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:collection/${rekognitionCollectionId}`,
],
})
);
},
},
{
// Web API to register human name
functionName: 'RegisterHumanName',
webApiPath: 'names',
httpMethod: 'PATCH',
codePath: 'lambda/names/resigiter-name/index.ts',
},
];
for (const webApiLambda of webApiLambdas) {
const bundling = {
dockerImage: cdk.DockerImage.fromBuild(path.join(__dirname, 'bundling')),
forceDockerBundling: true,
nodeModules: webApiLambda.functionName === 'GetSimilars' ? ['sharp'] : undefined,
};
// Define an Lambda function
const lambdaFunction = new NodejsFunction(this, webApiLambda.functionName, {
entry: webApiLambda.codePath,
bundling,
role: this.makeLambdaRole(webApiLambda.functionName),
...commonLambdaProps,
});
// Define a function to build API path of API Gateway repeatedly
const allowOrigins = [props.apiGatewayAllowOrigin];
const getResource = (p: string, rs: apigateway.IResource) => {
let newResource = rs.getResource(p);
if (newResource == undefined) {
newResource = rs.addResource(p);
newResource.addCorsPreflight({ allowOrigins });
}
return newResource;
};
// Integrate among a Lambda function and the API Gateway
const paths = webApiLambda.webApiPath.split('/');
let resource = getResource(paths[0], restApi.root);
for (let i = 1; i < paths.length; i++) {
resource = getResource(paths[i], resource);
}
resource.addMethod(webApiLambda.httpMethod, new apigateway.LambdaIntegration(lambdaFunction));
// Grant access rights to Lambda function according to the definition
webApiLambda.grantingProcess && webApiLambda.grantingProcess(lambdaFunction);
const cfnLambdafunction: lambda.CfnFunction = lambdaFunction.node.findChild('Resource') as lambda.CfnFunction;
cfnLambdafunction.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W58',
reason:
'Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions.',
},
{
id: 'W92',
reason: 'reserved capacity depends on how much users use this system.',
},
],
},
};
}
restApi.methods.forEach((apiMethod) => {
const child = apiMethod.node.findChild('Resource') as apigateway.CfnMethod;
if (apiMethod.httpMethod === 'OPTIONS') {
child.addPropertyOverride('AuthorizationType', 'NONE');
} else {
child.addPropertyOverride('AuthorizationType', 'AWS_IAM');
}
});
// API authorization
const identityPool = new cognito.CfnIdentityPool(this, 'IdentityPool', {
allowUnauthenticatedIdentities: true,
});
identityPool.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W57',
reason: 'Proper restrictive IAM roles and permissions are established for unauthenticated users.',
},
],
},
};
const unauthPolicyDocument = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['execute-api:Invoke'],
resources: [`${restApi.arnForExecuteApi()}/*`],
effect: iam.Effect.ALLOW,
}),
],
});
const unauthRole = new iam.Role(this, 'PhotoSearchUnauthRole', {
assumedBy: new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com', {
StringEquals: { 'cognito-identity.amazonaws.com:aud': identityPool.ref },
'ForAnyValue:StringLike': { 'cognito-identity.amazonaws.com:amr': 'unauthenticated' },
}),
inlinePolicies: { policy: unauthPolicyDocument },
});
new cognito.CfnIdentityPoolRoleAttachment(this, 'IdentityPoolAttachment', {
identityPoolId: identityPool.ref,
roles: {
unauthenticated: unauthRole.roleArn,
},
});
// batch lambda function
const s3triggerFunctionRole = this.makeLambdaRole('RegisterPhoto');
const s3triggerFunction = new NodejsFunction(this, 'RegisterPhoto', {
entry: 'lambda/batches/register-photo/index.ts',
role: s3triggerFunctionRole,
...commonLambdaProps,
});
new s3lambda.S3ToLambda(this, 'S3ToLambda', {
existingBucketObj: bucketForRegister,
existingLambdaObj: s3triggerFunction,
});
s3triggerFunctionRole.addToPolicy(
new iam.PolicyStatement({
actions: ['rekognition:IndexFaces'],
resources: [
`arn:aws:rekognition:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:collection/${rekognitionCollectionId}`,
],
})
);
bucketForRegister.grantRead(s3triggerFunction);
const notificationHandler = cdk.Stack.of(this).node.tryFindChild(
'BucketNotificationsHandler050a0587b7544547bf325f094a3db834'
) as lambda.Function;
const s3ToLambdaFunction: lambda.CfnFunction = notificationHandler.node.findChild('Resource') as lambda.CfnFunction;
const s3CfntriggerFunction: lambda.CfnFunction = s3triggerFunction.node.findChild('Resource') as lambda.CfnFunction;
s3CfntriggerFunction.cfnOptions.metadata = s3ToLambdaFunction.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W58',
reason:
'Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions.',
},
{
id: 'W89',
reason: 'This is not a rule for the general case, just for specific use cases/industries',
},
{
id: 'W92',
reason: 'Reserved capacity depends on how much users use this system.',
},
],
},
};
// backend custom resource to create rekognition and AES index
const backendEventHandlerRole = this.makeLambdaRole('BackendEventHandler');
const backendEventHandler = new NodejsFunction(this, 'backend-event-handler', {
entry: 'lambda/custom-resources/backend-event-handler/index.ts',
role: backendEventHandlerRole,
...commonLambdaProps,
});
const provider = new Provider(this, 'provider', {
onEventHandler: backendEventHandler,
});
new cdk.CustomResource(this, 'cdk-event-handler', {
serviceToken: provider.serviceToken,
properties: {
AesNodeEndpoint: esDomain.attrDomainEndpoint,
AesIndexName: rekognitionCollectionId,
RekognitionCollectionId: rekognitionCollectionId,
},
});
backendEventHandlerRole.addToPolicy(
new iam.PolicyStatement({
actions: ['rekognition:CreateCollection'],
resources: [
`arn:aws:rekognition:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:collection/${rekognitionCollectionId}`,
],
})
);
const eventHandlerFunction: lambda.CfnFunction = backendEventHandler.node.findChild(
'Resource'
) as lambda.CfnFunction;
const providerFunction: lambda.CfnFunction = provider.node
.findChild('framework-onEvent')
.node.findChild('Resource') as lambda.CfnFunction;
const customResourceRules = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W58',
reason:
'Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions.',
},
{
id: 'W89',
reason: 'This is not a rule for the general case, just for specific use cases/industries',
},
{
id: 'W92',
reason: 'Reserved capacity depends on how much users use this system.',
},
],
},
};
myvpc.addGatewayEndpoint('S3GW', {
service: ec2.GatewayVpcEndpointAwsService.S3,
});
providerFunction.cfnOptions.metadata = eventHandlerFunction.cfnOptions.metadata = customResourceRules;
this.apiId = restApi.restApiId;
this.identityPoolId = identityPool.ref;
new cdk.CfnOutput(this, 'region', {
description: 'Region of the backend',
value: this.region,
});
new cdk.CfnOutput(this, 'apiId', {
description: 'Endpoint of Api Gateway',
value: restApi.restApiId,
});
}