Skip to content

Commit fe9f750

Browse files
authored
feat(aws-s3objectlambda): add L2 construct for S3 Object Lambda (#15833)
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*
1 parent 364b0ce commit fe9f750

File tree

7 files changed

+974
-17
lines changed

7 files changed

+974
-17
lines changed

packages/@aws-cdk/aws-s3objectlambda/README.md

+77-8
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,92 @@
99
>
1010
> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib
1111
12+
![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge)
13+
14+
> The APIs of higher level constructs in this module are experimental and under active development.
15+
> They are subject to non-backward compatible changes or removal in any future version. These are
16+
> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be
17+
> announced in the release notes. This means that while you may use them, you may need to update
18+
> your source code when upgrading to a newer version of this package.
19+
1220
---
1321

1422
<!--END STABILITY BANNER-->
1523

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

18-
```ts nofixture
26+
```ts
27+
import * as lambda from '@aws-cdk/aws-lambda';
28+
import * as s3 from '@aws-cdk/aws-s3';
1929
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda';
30+
import * as cdk from '@aws-cdk/core';
31+
32+
const stack = new cdk.Stack();
33+
const bucket = new s3.Bucket(stack, 'MyBucket');
34+
const handler = new lambda.Function(stack, 'MyFunction', {
35+
runtime: lambda.Runtime.NODEJS_14_X,
36+
handler: 'index.handler',
37+
code: lambda.Code.fromAsset('lambda.zip'),
38+
});
39+
new s3objectlambda.AccessPoint(stack, 'MyObjectLambda', {
40+
bucket,
41+
handler,
42+
accessPointName: 'my-access-point',
43+
payload: {
44+
prop: "value",
45+
},
46+
});
2047
```
2148

22-
<!--BEGIN CFNONLY DISCLAIMER-->
49+
## Handling range and part number requests
50+
51+
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:
52+
53+
```ts
54+
import * as lambda from '@aws-cdk/aws-lambda';
55+
import * as s3 from '@aws-cdk/aws-s3';
56+
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda';
57+
import * as cdk from '@aws-cdk/core';
2358

24-
There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet.
25-
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.
59+
const stack = new cdk.Stack();
60+
const bucket = new s3.Bucket(stack, 'MyBucket');
61+
const handler = new lambda.Function(stack, 'MyFunction', {
62+
runtime: lambda.Runtime.NODEJS_14_X,
63+
handler: 'index.handler',
64+
code: lambda.Code.fromAsset('lambda.zip'),
65+
});
66+
new s3objectlambda.AccessPoint(stack, 'MyObjectLambda', {
67+
bucket,
68+
handler,
69+
accessPointName: 'my-access-point',
70+
supportsGetObjectRange: true,
71+
supportsGetObjectPartNumber: true,
72+
});
73+
```
2674

27-
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).
75+
## Pass additional data to Lambda function
2876

29-
(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.)
77+
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:
3078

31-
<!--END CFNONLY DISCLAIMER-->
79+
```ts
80+
import * as lambda from '@aws-cdk/aws-lambda';
81+
import * as s3 from '@aws-cdk/aws-s3';
82+
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda';
83+
import * as cdk from '@aws-cdk/core';
84+
85+
const stack = new cdk.Stack();
86+
const bucket = new s3.Bucket(stack, 'MyBucket');
87+
const handler = new lambda.Function(stack, 'MyFunction', {
88+
runtime: lambda.Runtime.NODEJS_14_X,
89+
handler: 'index.handler',
90+
code: lambda.Code.fromAsset('lambda.zip'),
91+
});
92+
new s3objectlambda.AccessPoint(stack, 'MyObjectLambda', {
93+
bucket,
94+
handler,
95+
accessPointName: 'my-access-point',
96+
payload: {
97+
prop: "value",
98+
},
99+
});
100+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as lambda from '@aws-cdk/aws-lambda';
3+
import * as s3 from '@aws-cdk/aws-s3';
4+
import * as core from '@aws-cdk/core';
5+
import { Construct } from 'constructs';
6+
import { CfnAccessPoint } from './s3objectlambda.generated';
7+
8+
/**
9+
* The interface that represents the AccessPoint resource.
10+
*/
11+
export interface IAccessPoint extends core.IResource {
12+
/**
13+
* The ARN of the access point.
14+
* @attribute
15+
*/
16+
readonly accessPointArn: string;
17+
18+
/**
19+
* The creation data of the access point.
20+
* @attribute
21+
*/
22+
readonly accessPointCreationDate: string;
23+
24+
/**
25+
* The IPv4 DNS name of the access point.
26+
*/
27+
readonly domainName: string;
28+
29+
/**
30+
* The regional domain name of the access point.
31+
*/
32+
readonly regionalDomainName: string;
33+
34+
/**
35+
* The virtual hosted-style URL of an S3 object through this access point.
36+
* Specify `regional: false` at the options for non-regional URL.
37+
* @param key The S3 key of the object. If not specified, the URL of the
38+
* bucket is returned.
39+
* @param options Options for generating URL.
40+
* @returns an ObjectS3Url token
41+
*/
42+
virtualHostedUrlForObject(key?: string, options?: s3.VirtualHostedStyleUrlOptions): string;
43+
}
44+
45+
/**
46+
* The S3 object lambda access point configuration.
47+
*/
48+
export interface AccessPointProps {
49+
/**
50+
* The bucket to which this access point belongs.
51+
*/
52+
readonly bucket: s3.IBucket;
53+
54+
/**
55+
* The Lambda function used to transform objects.
56+
*/
57+
readonly handler: lambda.IFunction;
58+
59+
/**
60+
* The name of the S3 object lambda access point.
61+
*
62+
* @default a unique name will be generated
63+
*/
64+
readonly accessPointName?: string;
65+
66+
/**
67+
* Whether CloudWatch metrics are enabled for the access point.
68+
*
69+
* @default false
70+
*/
71+
readonly cloudWatchMetricsEnabled?: boolean;
72+
73+
/**
74+
* Whether the Lambda function can process `GetObject-Range` requests.
75+
*
76+
* @default false
77+
*/
78+
readonly supportsGetObjectRange?: boolean;
79+
80+
/**
81+
* Whether the Lambda function can process `GetObject-PartNumber` requests.
82+
*
83+
* @default false
84+
*/
85+
readonly supportsGetObjectPartNumber?: boolean;
86+
87+
/**
88+
* Additional JSON that provides supplemental data passed to the
89+
* Lambda function on every request.
90+
*
91+
* @default - No data.
92+
*/
93+
readonly payload?: { [key: string]: any };
94+
}
95+
96+
abstract class AccessPointBase extends core.Resource implements IAccessPoint {
97+
public abstract readonly accessPointArn: string;
98+
public abstract readonly accessPointCreationDate: string;
99+
public abstract readonly accessPointName: string;
100+
101+
/** Implement the {@link IAccessPoint.domainName} field. */
102+
get domainName(): string {
103+
const urlSuffix = this.stack.urlSuffix;
104+
return `${this.accessPointName}-${this.stack.account}.s3-object-lambda.${urlSuffix}`;
105+
}
106+
107+
/** Implement the {@link IAccessPoint.regionalDomainName} field. */
108+
get regionalDomainName(): string {
109+
const urlSuffix = this.stack.urlSuffix;
110+
const region = this.stack.region;
111+
return `${this.accessPointName}-${this.stack.account}.s3-object-lambda.${region}.${urlSuffix}`;
112+
}
113+
114+
/** Implement the {@link IAccessPoint.virtualHostedUrlForObject} method. */
115+
public virtualHostedUrlForObject(key?: string, options?: s3.VirtualHostedStyleUrlOptions): string {
116+
const domainName = options?.regional ?? true ? this.regionalDomainName : this.domainName;
117+
const prefix = `https://${domainName}`;
118+
if (!key) {
119+
return prefix;
120+
}
121+
if (key.startsWith('/')) {
122+
key = key.slice(1);
123+
}
124+
if (key.endsWith('/')) {
125+
key = key.slice(0, -1);
126+
}
127+
return `${prefix}/${key}`;
128+
}
129+
}
130+
131+
/**
132+
* The access point resource attributes.
133+
*/
134+
export interface AccessPointAttributes {
135+
/**
136+
* The ARN of the access point.
137+
*/
138+
readonly accessPointArn: string
139+
140+
/**
141+
* The creation data of the access point.
142+
*/
143+
readonly accessPointCreationDate: string;
144+
}
145+
146+
/**
147+
* Checks the access point name against the rules in https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-access-points.html#access-points-names
148+
* @param name The name of the access point
149+
*/
150+
function validateAccessPointName(name: string): void {
151+
if (name.length < 3 || name.length > 50) {
152+
throw new Error('Access point name must be between 3 and 50 characters long');
153+
}
154+
if (name.endsWith('-s3alias')) {
155+
throw new Error('Access point name cannot end with the suffix -s3alias');
156+
}
157+
if (name[0] === '-' || name[name.length - 1] === '-') {
158+
throw new Error('Access point name cannot begin or end with a dash');
159+
}
160+
if (!/^[0-9a-z](.(?![\.A-Z_]))+[0-9a-z]$/.test(name)) {
161+
throw new Error('Access point name must begin with a number or lowercase letter and not contain underscores, uppercase letters, or periods');
162+
}
163+
}
164+
165+
/**
166+
* An S3 object lambda access point for intercepting and
167+
* transforming `GetObject` requests.
168+
*/
169+
export class AccessPoint extends AccessPointBase {
170+
/**
171+
* Reference an existing AccessPoint defined outside of the CDK code.
172+
*/
173+
public static fromAccessPointAttributes(scope: Construct, id: string, attrs: AccessPointAttributes): IAccessPoint {
174+
const arn = core.Arn.split(attrs.accessPointArn, core.ArnFormat.SLASH_RESOURCE_NAME);
175+
if (!arn.resourceName) {
176+
throw new Error('Unable to parse acess point name');
177+
}
178+
const name = arn.resourceName;
179+
class Import extends AccessPointBase {
180+
public readonly accessPointArn: string = attrs.accessPointArn;
181+
public readonly accessPointCreationDate: string = attrs.accessPointCreationDate;
182+
public readonly accessPointName: string = name;
183+
}
184+
return new Import(scope, id);
185+
}
186+
187+
/**
188+
* The ARN of the access point.
189+
*/
190+
public readonly accessPointName: string
191+
192+
/**
193+
* The ARN of the access point.
194+
* @attribute
195+
*/
196+
public readonly accessPointArn: string
197+
198+
/**
199+
* The creation data of the access point.
200+
* @attribute
201+
*/
202+
public readonly accessPointCreationDate: string
203+
204+
constructor(scope: Construct, id: string, props: AccessPointProps) {
205+
super(scope, id, {
206+
physicalName: props.accessPointName,
207+
});
208+
209+
if (props.accessPointName) {
210+
validateAccessPointName(props.accessPointName);
211+
}
212+
213+
const supporting = new s3.CfnAccessPoint(this, 'SupportingAccessPoint', {
214+
bucket: props.bucket.bucketName,
215+
});
216+
217+
const allowedFeatures = [];
218+
if (props.supportsGetObjectPartNumber) {
219+
allowedFeatures.push('GetObject-PartNumber');
220+
}
221+
if (props.supportsGetObjectRange) {
222+
allowedFeatures.push('GetObject-Range');
223+
}
224+
225+
const accessPoint = new CfnAccessPoint(this, id, {
226+
name: this.physicalName,
227+
objectLambdaConfiguration: {
228+
allowedFeatures,
229+
cloudWatchMetricsEnabled: props.cloudWatchMetricsEnabled,
230+
supportingAccessPoint: supporting.attrArn,
231+
transformationConfigurations: [
232+
{
233+
actions: ['GetObject'],
234+
contentTransformation: {
235+
AwsLambda: {
236+
FunctionArn: props.handler.functionArn,
237+
FunctionPayload: props.payload ? JSON.stringify(props.payload) : undefined,
238+
},
239+
},
240+
},
241+
],
242+
},
243+
});
244+
this.accessPointName = accessPoint.ref;
245+
this.accessPointArn = accessPoint.attrArn;
246+
this.accessPointCreationDate = accessPoint.attrCreationDate;
247+
248+
props.handler.addToRolePolicy(
249+
new iam.PolicyStatement({
250+
actions: ['s3-object-lambda:WriteGetObjectResponse'],
251+
resources: ['*'],
252+
}),
253+
);
254+
}
255+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
export * from './access-point';
2+
13
// AWS::S3ObjectLambda CloudFormation Resources:
24
export * from './s3objectlambda.generated';

0 commit comments

Comments
 (0)