diff --git a/packages/@aws-cdk/aws-s3objectlambda/README.md b/packages/@aws-cdk/aws-s3objectlambda/README.md index ebb6f1b79c7e6..2c1db89330407 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/README.md +++ b/packages/@aws-cdk/aws-s3objectlambda/README.md @@ -9,6 +9,14 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- diff --git a/packages/@aws-cdk/aws-s3objectlambda/lib/index.ts b/packages/@aws-cdk/aws-s3objectlambda/lib/index.ts index 791ddcf126933..cfd57d6d01b0a 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/lib/index.ts +++ b/packages/@aws-cdk/aws-s3objectlambda/lib/index.ts @@ -1,2 +1,4 @@ +export * from './object-lambda'; + // AWS::S3ObjectLambda CloudFormation Resources: export * from './s3objectlambda.generated'; diff --git a/packages/@aws-cdk/aws-s3objectlambda/lib/object-lambda.ts b/packages/@aws-cdk/aws-s3objectlambda/lib/object-lambda.ts new file mode 100644 index 0000000000000..1ad3b975fe437 --- /dev/null +++ b/packages/@aws-cdk/aws-s3objectlambda/lib/object-lambda.ts @@ -0,0 +1,107 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Construct } from 'constructs'; +import { CfnAccessPoint } from './s3objectlambda.generated'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Creates an S3 Object Lambda, which can intercept and transform + * `GetObject` requests. + * + * @param fn The Lambda function + * @param props Configuration for this Object Lambda + */ +export interface ObjectLambdaProps { + /** + * The bucket to which this Object Lambda belongs + */ + readonly bucket: s3.IBucket + + /** + * The Lambda function used to transform objects. + */ + readonly fn: lambda.IFunction + + /** + * Whether CloudWatch metrics are enabled for the Object Lambda. + * + * @default false + */ + readonly cloudWatchMetricsEnabled?: boolean + + /** + * Whether the Lambda function can process `GetObject-Range` requests. + * + * @default false + */ + readonly supportsGetObjectRange?: boolean + + /** + * Whether the Lambda function can process `GetObject-PartNumber` requests. + * + * @default false + */ + readonly supportsGetObjectPartNumber?: boolean + + /** + * Additional JSON that provides supplemental data passed to the + * Lambda function on every request. + * + * @default - No data. + */ + readonly payload?: string +} + +/** + * An S3 Object Lambda for intercepting and transforming `GetObject` requests. + */ +export class ObjectLambda extends CoreConstruct { + constructor(scope: Construct, id: string, props: ObjectLambdaProps) { + super(scope, id); + + const supporting = new s3.CfnAccessPoint(this, 'access-point', { + bucket: props.bucket.bucketName, + // TODO: configure publicAccessBlockConfiguration? + }); + + const allowedFeatures = []; + if (props.supportsGetObjectPartNumber) { + allowedFeatures.push('GetObject-PartNumber'); + } + if (props.supportsGetObjectRange) { + allowedFeatures.push('GetObject-Range'); + } + + const objectLambda = new CfnAccessPoint(this, 'lambda-access-point', { + name: `${props.fn.functionName}-access-point`, + objectLambdaConfiguration: { + allowedFeatures, + cloudWatchMetricsEnabled: props.cloudWatchMetricsEnabled, + supportingAccessPoint: supporting.getAtt('Arn').toString(), + transformationConfigurations: [ + { + actions: ['GetObject'], + contentTransformation: { + AwsLambda: { + FunctionArn: props.fn.functionArn, + FunctionPayload: props.payload ?? '', + }, + }, + }, + ], + }, + }); + objectLambda.addDependsOn(supporting); + + props.fn.addToRolePolicy( + new iam.PolicyStatement({ + actions: ['s3-object-lambda:WriteGetObjectResponse'], + resources: ['*'], + }), + ); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3objectlambda/package.json b/packages/@aws-cdk/aws-s3objectlambda/package.json index 7818c261a21bc..f58bb799a09b0 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/package.json +++ b/packages/@aws-cdk/aws-s3objectlambda/package.json @@ -79,25 +79,35 @@ "devDependencies": { "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, "publishConfig": { "tag": "latest" } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.expected.json b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.expected.json new file mode 100644 index 0000000000000..9bfa4b20cf15a --- /dev/null +++ b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.expected.json @@ -0,0 +1,266 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyFunction1ServiceRole9852B06B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyFunction1ServiceRoleDefaultPolicy39556460": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3-object-lambda:WriteGetObjectResponse", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyFunction1ServiceRoleDefaultPolicy39556460", + "Roles": [ + { + "Ref": "MyFunction1ServiceRole9852B06B" + } + ] + } + }, + "MyFunction12A744C2E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Role": { + "Fn::GetAtt": [ + "MyFunction1ServiceRole9852B06B", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "MyFunction1ServiceRoleDefaultPolicy39556460", + "MyFunction1ServiceRole9852B06B" + ] + }, + "MyFunction2ServiceRole07E5BE0E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyFunction2ServiceRoleDefaultPolicyA79C693E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3-object-lambda:WriteGetObjectResponse", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyFunction2ServiceRoleDefaultPolicyA79C693E", + "Roles": [ + { + "Ref": "MyFunction2ServiceRole07E5BE0E" + } + ] + } + }, + "MyFunction2F2A964CA": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Role": { + "Fn::GetAtt": [ + "MyFunction2ServiceRole07E5BE0E", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "MyFunction2ServiceRoleDefaultPolicyA79C693E", + "MyFunction2ServiceRole07E5BE0E" + ] + }, + "MyObjectLambda1accesspoint2C407F9B": { + "Type": "AWS::S3::AccessPoint", + "Properties": { + "Bucket": { + "Ref": "MyBucketF68F3FF0" + } + } + }, + "MyObjectLambda1lambdaaccesspoint742618A6": { + "Type": "AWS::S3ObjectLambda::AccessPoint", + "Properties": { + "Name": { + "Fn::Join": [ + "", + [ + { + "Ref": "MyFunction12A744C2E" + }, + "-access-point" + ] + ] + }, + "ObjectLambdaConfiguration": { + "AllowedFeatures": [ + "GetObject-PartNumber" + ], + "CloudWatchMetricsEnabled": true, + "SupportingAccessPoint": { + "Fn::GetAtt": [ + "MyObjectLambda1accesspoint2C407F9B", + "Arn" + ] + }, + "TransformationConfigurations": [ + { + "Actions": [ + "GetObject" + ], + "ContentTransformation": { + "AwsLambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "MyFunction12A744C2E", + "Arn" + ] + }, + "FunctionPayload": "" + } + } + } + ] + } + }, + "DependsOn": [ + "MyObjectLambda1accesspoint2C407F9B" + ] + }, + "MyObjectLambda2accesspoint757C55B0": { + "Type": "AWS::S3::AccessPoint", + "Properties": { + "Bucket": { + "Ref": "MyBucketF68F3FF0" + } + } + }, + "MyObjectLambda2lambdaaccesspoint16890151": { + "Type": "AWS::S3ObjectLambda::AccessPoint", + "Properties": { + "Name": { + "Fn::Join": [ + "", + [ + { + "Ref": "MyFunction2F2A964CA" + }, + "-access-point" + ] + ] + }, + "ObjectLambdaConfiguration": { + "AllowedFeatures": [ + "GetObject-Range" + ], + "SupportingAccessPoint": { + "Fn::GetAtt": [ + "MyObjectLambda2accesspoint757C55B0", + "Arn" + ] + }, + "TransformationConfigurations": [ + { + "Actions": [ + "GetObject" + ], + "ContentTransformation": { + "AwsLambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "MyFunction2F2A964CA", + "Arn" + ] + }, + "FunctionPayload": "{foo: 10}" + } + } + } + ] + } + }, + "DependsOn": [ + "MyObjectLambda2accesspoint757C55B0" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.ts b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.ts new file mode 100644 index 0000000000000..97842b55b6c34 --- /dev/null +++ b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.ts @@ -0,0 +1,43 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { ObjectLambda } from '../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string) { + super(scope, id); + + const bucket = new s3.Bucket(this, 'MyBucket'); + const fn1 = new lambda.Function(this, 'MyFunction1', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline('foo'), + }); + + const fn2 = new lambda.Function(this, 'MyFunction2', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline('foo'), + }); + + new ObjectLambda(this, 'MyObjectLambda1', { + bucket, + fn: fn1, + cloudWatchMetricsEnabled: true, + supportsGetObjectPartNumber: true, + }); + + new ObjectLambda(this, 'MyObjectLambda2', { + bucket, + fn: fn2, + supportsGetObjectRange: true, + payload: '{foo: 10}', + }); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'aws-s3-object-lambda'); + +app.synth();