Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-s3objectlambda): add L2 construct for S3 Object Lambda #15833

Merged
merged 5 commits into from
Mar 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

---
corymhall marked this conversation as resolved.
Show resolved Hide resolved

<!--END STABILITY BANNER-->

This module is part of the [AWS Cloud Development Kit](https:/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:/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;

corymhall marked this conversation as resolved.
Show resolved Hide resolved
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