-
Notifications
You must be signed in to change notification settings - Fork 4k
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(cloudfront): function URL
origin access control L2 construct
#31339
Changes from 2 commits
bc5f929
80f07f7
a6386c1
da605ea
b2baafa
5a7352a
8e8e2ac
7fcbea3
12f13f9
b257227
8f24a4a
81c0785
47775c8
df0c972
a43bf40
3731370
f660a83
2ad9170
989141b
7226680
7f9ba73
a577a65
beeb0f4
c129a19
64c92a7
ad5778d
5802d54
258e464
8ada469
d8b0b16
1e4983b
16eb227
693abfe
92f7e71
adc12d3
e4817ff
fd9f9b4
fea5a32
873777b
fcf8db0
2375bcd
a6f5726
fce4e73
3475884
f958621
baddb09
8be1369
1b41408
86f60d8
ada7bee
d838678
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import { Construct } from 'constructs'; | ||
import { validateSecondsInRangeOrUndefined } from './private/utils'; | ||
import * as cloudfront from '../../aws-cloudfront'; | ||
import * as iam from '../../aws-iam'; | ||
import * as lambda from '../../aws-lambda'; | ||
import * as cdk from '../../core'; | ||
|
||
|
@@ -30,10 +32,35 @@ export interface FunctionUrlOriginProps extends cloudfront.OriginProps { | |
readonly keepaliveTimeout?: cdk.Duration; | ||
} | ||
|
||
/** | ||
* Properties for configuring a origin using a standard Lambda Functions URLs. | ||
*/ | ||
export interface FunctionUrlOriginBaseProps extends cloudfront.OriginProps { } | ||
|
||
/** | ||
* Properties for configuring a Lambda Functions URLs with OAC. | ||
*/ | ||
export interface FunctionUrlOriginWithOACProps extends FunctionUrlOriginProps { | ||
/** | ||
* An optional Origin Access Control | ||
* | ||
* @default - an Origin Access Control will be created. | ||
*/ | ||
readonly originAccessControl?: cloudfront.IOriginAccessControl; | ||
|
||
} | ||
|
||
/** | ||
* An Origin for a Lambda Function URL. | ||
*/ | ||
export class FunctionUrlOrigin extends cloudfront.OriginBase { | ||
/** | ||
* Create a Functions URL Origin with Origin Access Control (OAC) configured | ||
*/ | ||
public static withOriginAccessControl(url: lambda.IFunctionUrl, props?: FunctionUrlOriginWithOACProps): cloudfront.IOrigin { | ||
return new FunctionUrlOriginWithOAC(url, props); | ||
} | ||
|
||
constructor(lambdaFunctionUrl: lambda.IFunctionUrl, private readonly props: FunctionUrlOriginProps = {}) { | ||
// Lambda Function URL is of the form 'https://<lambda-id>.lambda-url.<region>.on.aws/' | ||
// No need to split URL as we do with REST API, the entire URL is needed | ||
|
@@ -52,4 +79,66 @@ export class FunctionUrlOrigin extends cloudfront.OriginBase { | |
originKeepaliveTimeout: this.props.keepaliveTimeout?.toSeconds(), | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* An Origin for a Lambda Function URL with OAC. | ||
*/ | ||
export class FunctionUrlOriginWithOAC extends cloudfront.OriginBase { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep this class private as it's an implementation detail and not intended for public usage There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed. |
||
private originAccessControl?: cloudfront.IOriginAccessControl; | ||
private functionArn: string | ||
|
||
constructor(lambdaFunctionUrl: lambda.IFunctionUrl, props: FunctionUrlOriginWithOACProps = {}) { | ||
const domainName = cdk.Fn.select(2, cdk.Fn.split('/', lambdaFunctionUrl.url)); | ||
super(domainName, props); | ||
|
||
this.functionArn = lambdaFunctionUrl.functionArn; | ||
this.originAccessControl = props.originAccessControl; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since props can be empty object, do we need optional chaining operator ( ?.) while accessing the prop?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed. 8ada469 |
||
|
||
validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout); | ||
validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout); | ||
} | ||
|
||
protected renderCustomOriginConfig(): cloudfront.CfnDistribution.CustomOriginConfigProperty | undefined { | ||
return { | ||
originSslProtocols: [cloudfront.OriginSslPolicy.TLS_V1_2], | ||
originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY, | ||
}; | ||
gracelu0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { | ||
const originBindConfig = super.bind(scope, options); | ||
const distributionId = options.distributionId; | ||
|
||
if (!this.originAccessControl) { | ||
const cfnOriginAccessControl = new cloudfront.CfnOriginAccessControl(scope, 'LambdaOriginAccessControl', { | ||
originAccessControlConfig: { | ||
name: 'OAC for Lambda Function URL', | ||
originAccessControlOriginType: 'lambda', | ||
signingBehavior: 'always', | ||
signingProtocol: 'sigv4', | ||
}, | ||
}); | ||
|
||
this.originAccessControl = { | ||
originAccessControlId: cfnOriginAccessControl.attrId, | ||
} as cloudfront.IOriginAccessControl; | ||
|
||
const lambdaFunction = lambda.Function.fromFunctionArn(scope, 'ReferencedLambdaFunction', this.functionArn); | ||
|
||
lambdaFunction.addPermission('AllowCloudFrontServicePrincipal', { | ||
principal: new iam.ServicePrincipal('cloudfront.amazonaws.com'), | ||
action: 'lambda:InvokeFunctionUrl', | ||
sourceArn: `arn:${cdk.Aws}:cloudfront::${cdk.Stack.of(scope).account}:distribution/${distributionId}`, | ||
}); | ||
} | ||
|
||
return { | ||
...originBindConfig, | ||
originProperty: { | ||
...originBindConfig.originProperty!, | ||
originAccessControlId: this.originAccessControl?.originAccessControlId, | ||
}, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
import { Template, Match } from '../../assertions'; | ||
import * as cloudfront from '../../aws-cloudfront'; | ||
import * as lambda from '../../aws-lambda'; | ||
import { Stack } from '../../core'; | ||
import * as cdk from '../../core'; | ||
import { FunctionUrlOrigin } from '../lib'; | ||
|
||
let stack: Stack; | ||
|
@@ -41,3 +44,174 @@ test('Correctly renders the origin for a Lambda Function URL', () => { | |
}, | ||
}); | ||
}); | ||
|
||
test('Correctly sets readTimeout and keepaliveTimeout', () => { | ||
const fn = new lambda.Function(stack, 'MyFunction', { | ||
code: lambda.Code.fromInline('exports.handler = async () => {};'), | ||
handler: 'index.handler', | ||
runtime: lambda.Runtime.NODEJS_20_X, | ||
}); | ||
|
||
const fnUrl = fn.addFunctionUrl({ | ||
authType: lambda.FunctionUrlAuthType.NONE, | ||
}); | ||
|
||
const origin = new FunctionUrlOrigin(fnUrl, { | ||
readTimeout: cdk.Duration.seconds(120), | ||
keepaliveTimeout: cdk.Duration.seconds(60), | ||
}); | ||
|
||
const originBindConfig = origin.bind(stack, { originId: 'StackOriginLambdaFunctionURL' }); | ||
|
||
expect(stack.resolve(originBindConfig.originProperty)).toMatchObject({ | ||
customOriginConfig: { | ||
originReadTimeout: 120, | ||
originKeepaliveTimeout: 60, | ||
}, | ||
}); | ||
}); | ||
|
||
test('Correctly adds permission to Lambda for CloudFront', () => { | ||
const fn = new lambda.Function(stack, 'MyFunction', { | ||
code: lambda.Code.fromInline('exports.handler = async () => {};'), | ||
handler: 'index.handler', | ||
runtime: lambda.Runtime.NODEJS_20_X, | ||
}); | ||
|
||
const fnUrl = fn.addFunctionUrl({ | ||
authType: lambda.FunctionUrlAuthType.NONE, | ||
}); | ||
|
||
const distribution = new cloudfront.Distribution(stack, 'MyDistribution', { | ||
defaultBehavior: { | ||
origin: FunctionUrlOrigin.withOriginAccessControl(fnUrl, { | ||
originAccessControl: undefined, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed. |
||
}), | ||
}, | ||
}); | ||
|
||
const template = Template.fromStack(stack); | ||
|
||
template.hasResourceProperties('AWS::CloudFront::Distribution', { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since this test is checking for the correct permissions, can we also check the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed. |
||
DistributionConfig: { | ||
Origins: Match.arrayWith([ | ||
Match.objectLike({ | ||
DomainName: { | ||
'Fn::Select': [ | ||
2, | ||
{ | ||
'Fn::Split': [ | ||
'/', | ||
{ | ||
'Fn::GetAtt': ['MyFunctionFunctionUrlFF6DE78C', 'FunctionUrl'], | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
OriginAccessControlId: Match.objectLike({ | ||
'Fn::GetAtt': [ | ||
Match.stringLikeRegexp('MyDistributionOrigin.*LambdaOriginAccessControl.*'), | ||
'Id', | ||
], | ||
}), | ||
}), | ||
]), | ||
}, | ||
}); | ||
}); | ||
|
||
test('Correctly configures CloudFront Distribution with Origin Access Control', () => { | ||
const fn = new lambda.Function(stack, 'MyFunction', { | ||
code: lambda.Code.fromInline('exports.handler = async () => {};'), | ||
handler: 'index.handler', | ||
runtime: lambda.Runtime.NODEJS_20_X, | ||
}); | ||
|
||
const fnUrl = fn.addFunctionUrl({ | ||
authType: lambda.FunctionUrlAuthType.NONE, | ||
}); | ||
|
||
new cloudfront.Distribution(stack, 'MyDistribution', { | ||
defaultBehavior: { | ||
origin: FunctionUrlOrigin.withOriginAccessControl(fnUrl, { | ||
originAccessControl: undefined, | ||
}), | ||
}, | ||
}); | ||
|
||
const template = Template.fromStack(stack); | ||
|
||
template.hasResourceProperties('AWS::CloudFront::Distribution', { | ||
DistributionConfig: { | ||
Origins: Match.arrayWith([ | ||
Match.objectLike({ | ||
DomainName: { | ||
'Fn::Select': [ | ||
2, | ||
{ | ||
'Fn::Split': [ | ||
'/', | ||
{ | ||
'Fn::GetAtt': ['MyFunctionFunctionUrlFF6DE78C', 'FunctionUrl'], | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
OriginAccessControlId: Match.objectLike({ | ||
'Fn::GetAtt': [ | ||
Match.stringLikeRegexp('MyDistributionOrigin.*LambdaOriginAccessControl.*'), | ||
'Id', | ||
], | ||
}), | ||
}), | ||
]), | ||
}, | ||
}); | ||
|
||
template.hasResourceProperties('AWS::CloudFront::OriginAccessControl', { | ||
OriginAccessControlConfig: { | ||
OriginAccessControlOriginType: 'lambda', | ||
SigningBehavior: 'always', | ||
SigningProtocol: 'sigv4', | ||
}, | ||
}); | ||
}); | ||
|
||
test('Correctly configures CloudFront Distribution with a custom Origin Access Control', () => { | ||
const fn = new lambda.Function(stack, 'MyFunction', { | ||
code: lambda.Code.fromInline('exports.handler = async () => {};'), | ||
handler: 'index.handler', | ||
runtime: lambda.Runtime.NODEJS_20_X, | ||
}); | ||
|
||
const fnUrl = fn.addFunctionUrl({ | ||
authType: lambda.FunctionUrlAuthType.NONE, | ||
}); | ||
|
||
// Custom OAC configuration | ||
const oac = new cloudfront.FunctionUrlOriginAccessControl(stack, 'CustomOAC', { | ||
originAccessControlName: 'CustomLambdaOAC', | ||
signing: cloudfront.Signing.SIGV4_ALWAYS, | ||
}); | ||
|
||
new cloudfront.Distribution(stack, 'MyDistribution', { | ||
defaultBehavior: { | ||
origin: FunctionUrlOrigin.withOriginAccessControl(fnUrl, { | ||
originAccessControl: oac, | ||
}), | ||
}, | ||
}); | ||
|
||
const template = Template.fromStack(stack); | ||
|
||
template.hasResourceProperties('AWS::CloudFront::OriginAccessControl', { | ||
OriginAccessControlConfig: { | ||
Name: 'CustomLambdaOAC', | ||
OriginAccessControlOriginType: 'lambda', | ||
SigningBehavior: 'always', | ||
SigningProtocol: 'sigv4', | ||
}, | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we update the
url
tolambdaFunctionUrl
to be consistent across the code and also readable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed. 258e464