Skip to content

Commit

Permalink
feat(aws-s3objectlambda): add L2 construct for S3 Object Lambda (#15833)
Browse files Browse the repository at this point in the history
This PR adds an L2 construct for the S3 Object Lambda.

To avoid a circular dependency, the construct lives outside of the aws-s3 package.

Fixes #13675

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
duarten authored Mar 4, 2022
1 parent 364b0ce commit fe9f750
Show file tree
Hide file tree
Showing 7 changed files with 974 additions and 17 deletions.
85 changes: 77 additions & 8 deletions packages/@aws-cdk/aws-s3objectlambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,92 @@
>
> [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.
---

<!--END STABILITY BANNER-->

This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.
This construct library allows you to define S3 object lambda access points.

```ts nofixture
```ts
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda';
import * as cdk from '@aws-cdk/core';

const stack = new cdk.Stack();
const bucket = new s3.Bucket(stack, 'MyBucket');
const handler = new lambda.Function(stack, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda.zip'),
});
new s3objectlambda.AccessPoint(stack, 'MyObjectLambda', {
bucket,
handler,
accessPointName: 'my-access-point',
payload: {
prop: "value",
},
});
```

<!--BEGIN CFNONLY DISCLAIMER-->
## Handling range and part number requests

Lambdas are currently limited to only transforming `GetObject` requests. However, they can additionally support `GetObject-Range` and `GetObject-PartNumber` requests, which needs to be specified in the access point configuration:

```ts
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda';
import * as cdk from '@aws-cdk/core';

There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet.
However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly.
const stack = new cdk.Stack();
const bucket = new s3.Bucket(stack, 'MyBucket');
const handler = new lambda.Function(stack, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda.zip'),
});
new s3objectlambda.AccessPoint(stack, 'MyObjectLambda', {
bucket,
handler,
accessPointName: 'my-access-point',
supportsGetObjectRange: true,
supportsGetObjectPartNumber: true,
});
```

For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::S3ObjectLambda](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_S3ObjectLambda.html).
## Pass additional data to Lambda function

(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.)
You can specify an additional object that provides supplemental data to the Lambda function used to transform objects. The data is delivered as a JSON payload to the Lambda:

<!--END CFNONLY DISCLAIMER-->
```ts
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda';
import * as cdk from '@aws-cdk/core';

const stack = new cdk.Stack();
const bucket = new s3.Bucket(stack, 'MyBucket');
const handler = new lambda.Function(stack, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda.zip'),
});
new s3objectlambda.AccessPoint(stack, 'MyObjectLambda', {
bucket,
handler,
accessPointName: 'my-access-point',
payload: {
prop: "value",
},
});
```
255 changes: 255 additions & 0 deletions packages/@aws-cdk/aws-s3objectlambda/lib/access-point.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
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 * as core from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnAccessPoint } from './s3objectlambda.generated';

/**
* The interface that represents the AccessPoint resource.
*/
export interface IAccessPoint extends core.IResource {
/**
* The ARN of the access point.
* @attribute
*/
readonly accessPointArn: string;

/**
* The creation data of the access point.
* @attribute
*/
readonly accessPointCreationDate: string;

/**
* The IPv4 DNS name of the access point.
*/
readonly domainName: string;

/**
* The regional domain name of the access point.
*/
readonly regionalDomainName: string;

/**
* The virtual hosted-style URL of an S3 object through this access point.
* Specify `regional: false` at the options for non-regional URL.
* @param key The S3 key of the object. If not specified, the URL of the
* bucket is returned.
* @param options Options for generating URL.
* @returns an ObjectS3Url token
*/
virtualHostedUrlForObject(key?: string, options?: s3.VirtualHostedStyleUrlOptions): string;
}

/**
* The S3 object lambda access point configuration.
*/
export interface AccessPointProps {
/**
* The bucket to which this access point belongs.
*/
readonly bucket: s3.IBucket;

/**
* The Lambda function used to transform objects.
*/
readonly handler: lambda.IFunction;

/**
* The name of the S3 object lambda access point.
*
* @default a unique name will be generated
*/
readonly accessPointName?: string;

/**
* Whether CloudWatch metrics are enabled for the access point.
*
* @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?: { [key: string]: any };
}

abstract class AccessPointBase extends core.Resource implements IAccessPoint {
public abstract readonly accessPointArn: string;
public abstract readonly accessPointCreationDate: string;
public abstract readonly accessPointName: string;

/** Implement the {@link IAccessPoint.domainName} field. */
get domainName(): string {
const urlSuffix = this.stack.urlSuffix;
return `${this.accessPointName}-${this.stack.account}.s3-object-lambda.${urlSuffix}`;
}

/** Implement the {@link IAccessPoint.regionalDomainName} field. */
get regionalDomainName(): string {
const urlSuffix = this.stack.urlSuffix;
const region = this.stack.region;
return `${this.accessPointName}-${this.stack.account}.s3-object-lambda.${region}.${urlSuffix}`;
}

/** Implement the {@link IAccessPoint.virtualHostedUrlForObject} method. */
public virtualHostedUrlForObject(key?: string, options?: s3.VirtualHostedStyleUrlOptions): string {
const domainName = options?.regional ?? true ? this.regionalDomainName : this.domainName;
const prefix = `https://${domainName}`;
if (!key) {
return prefix;
}
if (key.startsWith('/')) {
key = key.slice(1);
}
if (key.endsWith('/')) {
key = key.slice(0, -1);
}
return `${prefix}/${key}`;
}
}

/**
* The access point resource attributes.
*/
export interface AccessPointAttributes {
/**
* The ARN of the access point.
*/
readonly accessPointArn: string

/**
* The creation data of the access point.
*/
readonly accessPointCreationDate: string;
}

/**
* Checks the access point name against the rules in https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-access-points.html#access-points-names
* @param name The name of the access point
*/
function validateAccessPointName(name: string): void {
if (name.length < 3 || name.length > 50) {
throw new Error('Access point name must be between 3 and 50 characters long');
}
if (name.endsWith('-s3alias')) {
throw new Error('Access point name cannot end with the suffix -s3alias');
}
if (name[0] === '-' || name[name.length - 1] === '-') {
throw new Error('Access point name cannot begin or end with a dash');
}
if (!/^[0-9a-z](.(?![\.A-Z_]))+[0-9a-z]$/.test(name)) {
throw new Error('Access point name must begin with a number or lowercase letter and not contain underscores, uppercase letters, or periods');
}
}

/**
* An S3 object lambda access point for intercepting and
* transforming `GetObject` requests.
*/
export class AccessPoint extends AccessPointBase {
/**
* Reference an existing AccessPoint defined outside of the CDK code.
*/
public static fromAccessPointAttributes(scope: Construct, id: string, attrs: AccessPointAttributes): IAccessPoint {
const arn = core.Arn.split(attrs.accessPointArn, core.ArnFormat.SLASH_RESOURCE_NAME);
if (!arn.resourceName) {
throw new Error('Unable to parse acess point name');
}
const name = arn.resourceName;
class Import extends AccessPointBase {
public readonly accessPointArn: string = attrs.accessPointArn;
public readonly accessPointCreationDate: string = attrs.accessPointCreationDate;
public readonly accessPointName: string = name;
}
return new Import(scope, id);
}

/**
* The ARN of the access point.
*/
public readonly accessPointName: string

/**
* The ARN of the access point.
* @attribute
*/
public readonly accessPointArn: string

/**
* The creation data of the access point.
* @attribute
*/
public readonly accessPointCreationDate: string

constructor(scope: Construct, id: string, props: AccessPointProps) {
super(scope, id, {
physicalName: props.accessPointName,
});

if (props.accessPointName) {
validateAccessPointName(props.accessPointName);
}

const supporting = new s3.CfnAccessPoint(this, 'SupportingAccessPoint', {
bucket: props.bucket.bucketName,
});

const allowedFeatures = [];
if (props.supportsGetObjectPartNumber) {
allowedFeatures.push('GetObject-PartNumber');
}
if (props.supportsGetObjectRange) {
allowedFeatures.push('GetObject-Range');
}

const accessPoint = new CfnAccessPoint(this, id, {
name: this.physicalName,
objectLambdaConfiguration: {
allowedFeatures,
cloudWatchMetricsEnabled: props.cloudWatchMetricsEnabled,
supportingAccessPoint: supporting.attrArn,
transformationConfigurations: [
{
actions: ['GetObject'],
contentTransformation: {
AwsLambda: {
FunctionArn: props.handler.functionArn,
FunctionPayload: props.payload ? JSON.stringify(props.payload) : undefined,
},
},
},
],
},
});
this.accessPointName = accessPoint.ref;
this.accessPointArn = accessPoint.attrArn;
this.accessPointCreationDate = accessPoint.attrCreationDate;

props.handler.addToRolePolicy(
new iam.PolicyStatement({
actions: ['s3-object-lambda:WriteGetObjectResponse'],
resources: ['*'],
}),
);
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-s3objectlambda/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './access-point';

// AWS::S3ObjectLambda CloudFormation Resources:
export * from './s3objectlambda.generated';
Loading

0 comments on commit fe9f750

Please sign in to comment.