It would be great to have a L2 construct providing Lambda@Edge support. Though it's not clear to me what the best way to add this would be, since whereas most other event sources have a method on the resource (i.e. such as topic.subscribeLambda(...)
), Lambda@Edge associations are made between a lambda function and a specific behavior of a distribution, and the individual behaviors are not exposed by the L2 construct (you end up with a CloudFrontDistribution object, but no way to reference individual behaviors of that distribution).
Adding support for Lambda@Edge would probably also require adding better support for Lambda function versions (it would be great to just be able to use something like AutoPublishAlias from SAM).
Hello, is there any workaround we can use to deploy lambda@edge with aws-cdk?
Can it use sam module with https://github.com/awslabs/serverless-application-model/tree/master/examples/2016-10-31/lambda_edge ?
Or can we trigger shell command after regular lambda deployment to deploy it to the edge and get the version?
I'm not sure if you can use the AutoPublishAlias feature of SAM from CDK, but recently I did discover it's pretty easy to create a cfn custom resource that implements that functionality (it's really just making a call to PublishVersion and UpdateAlias, if you care about that) - perhaps something like this would be worth adding to the construct library?
To actually setup the Lambda@Edge function associations, I haven't tried it yet, but perhaps we can use https://github.com/awslabs/aws-cdk/blob/521570a7c3a3788ce313f310ccb35bd1484ad2f5/docs/src/aws-construct-lib.rst#access-the-aws-cloudformation-layer for this, at least until proper support is added to the L2 construct.
@LordPython Thanks a lot for your answer. Do you have any examples with custom resource and described PublishVersion
and UpdateAlias
calls?
These are just the Lambda API calls, documented here https://docs.aws.amazon.com/lambda/latest/dg/API_PublishVersion.html and https://docs.aws.amazon.com/lambda/latest/dg/API_UpdateAlias.html (and you should pretty easily be able to find documentation for them in each languages SDK, eg boto3 docs for PublishVersion are here https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.publish_version)
So I was able to solve my task with this way. My requirements were mainly that I want to have bucket in a different region than us-east-1, so I need to pass the lambda version somehow to another region
First definitions:
const cdk = require('@aws-cdk/cdk');
const lambda = require('@aws-cdk/aws-lambda');
const s3 = require('@aws-cdk/aws-s3');
const cfr = require('@aws-cdk/aws-cloudfront');
const iam = require('@aws-cdk/aws-iam');
const cf = require('@aws-cdk/aws-cloudformation');
const r53 = require('@aws-cdk/aws-route53');
const sha256 = require('sha256-file');
const CF_HOSTED_ZONE_ID = 'Z2FDTNDATAQYW2';
const LAMBDA_OUTPUT_NAME = 'LambdaOutput';
const LAMBDA_EDGE_STACK_NAME = 'stack-name';
const DOMAIN_NAME = 'example.com';
const CERTIFICATE_ARN = 'arn:aws:acm:us-east-1:<aid>:certificate/<cert>';
const app = new cdk.App();
Then the edge lambda stack itself:
class LambdaStack extends cdk.Stack {
constructor(parent, id, props) {
super(parent, id, props);
const override = new lambda.Function(this, 'your-lambda', {
runtime: lambda.Runtime.NodeJS810,
handler: 'index.handler',
code: lambda.Code.asset('./lambda'),
role: new iam.Role(this, 'AllowLambdaServiceToAssumeRole', {
assumedBy: new iam.CompositePrincipal(
new iam.ServicePrincipal('lambda.amazonaws.com'),
new iam.ServicePrincipal('edgelambda.amazonaws.com'),
),
managedPolicyArns: ['arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']
})
});
// this way it updates version only in case lambda code changes
const version = override.addVersion(':sha256:' + sha256('./lambda/index.js'));
// the main magic to easily pass the lambda version to stack in another region
new cdk.CfnOutput(this, LAMBDA_OUTPUT_NAME, {
value: cdk.Fn.join(":", [
override.functionArn,
version.functionVersion
])
});
}
}
Then cloud front definition:
class StaticSiteStack extends cdk.Stack {
constructor(parent, id, props) {
super(parent, id, props);
const lambdaProvider = new lambda.SingletonFunction(this, 'Provider', {
uuid: 'f7d4f730-4ee1-11e8-9c2d-fa7ae01bbebc',
code: lambda.Code.asset('./cfn'),
handler: 'stack.handler',
timeout: 60,
runtime: lambda.Runtime.NodeJS810,
});
// to allow aws sdk call inside the lambda
lambdaProvider.addToRolePolicy(
new iam.PolicyStatement()
.allow()
.addAction('cloudformation:DescribeStacks')
.addResource(`arn:aws:cloudformation:*:*:stack/${LAMBDA_EDGE_STACK_NAME}/*`)
);
// This basically goes to another region to edge stack and grabs the version output
const stackOutput = new cf.CustomResource(this, 'StackOutput', {
lambdaProvider,
properties: {
StackName: LAMBDA_EDGE_STACK_NAME,
OutputKey: LAMBDA_OUTPUT_NAME,
// just to change custom resource on code update
LambdaHash: sha256('./lambda/index.js')
}
});
const bucket = new s3.Bucket(this, 'bucket', {
publicReadAccess: true // not really sure I need this permission actually
});
const origin = {
domainName: bucket.domainName,
id: 'origin1',
s3OriginConfig: {}
};
// CloudFrontWebDistribution will simplify a lot,
// but it doesn't support lambdaFunctionAssociations in any way :(
const distribution = new cfr.CfnDistribution(this, 'WebSiteDistribution', {
distributionConfig: {
aliases: ['site.example.com', '*.site.example.com'],
defaultCacheBehavior: {
allowedMethods: ['GET', 'HEAD'],
cachedMethods: ['GET', 'HEAD'],
defaultTtl: 60,
maxTtl: 60,
targetOriginId: origin.id,
viewerProtocolPolicy: cfr.ViewerProtocolPolicy.RedirectToHTTPS,
forwardedValues: {
cookies: {
forward: 'none'
},
queryString: false
},
lambdaFunctionAssociations: [
{
eventType: 'viewer-request',
lambdaFunctionArn: stackOutput.getAtt('Output')
}
]
},
defaultRootObject: 'index.html',
enabled: true,
httpVersion: cfr.HttpVersion.HTTP2,
origins: [
origin
],
priceClass: cfr.PriceClass.PriceClass100,
viewerCertificate: {
acmCertificateArn: CERTIFICATE_ARN,
sslSupportMethod: cfr.SSLMethod.SNI
}
},
tags: [{
key: 'stack',
value: this.name
}]
});
const zone = new r53.HostedZoneProvider(this, {
domainName: DOMAIN_NAME
}).findAndImport(this, 'MyPublicZone');
new r53.AliasRecord(this, 'BaseRecord', {
recordName: 'site',
zone: zone,
target: {
asAliasRecordTarget: () => ({
hostedZoneId: CF_HOSTED_ZONE_ID,
dnsName: distribution.distributionDomainName
})
}
});
new r53.AliasRecord(this, 'StarRecord', {
recordName: '*.site',
zone: zone,
target: {
asAliasRecordTarget: () => ({
hostedZoneId: CF_HOSTED_ZONE_ID,
dnsName: distribution.distributionDomainName
})
}
});
new cdk.CfnOutput(this, 'Bucket', {
value: `s3://${bucket.bucketName}`
});
new cdk.CfnOutput(this, 'CfDomain', {
value: distribution.distributionDomainName
});
new cdk.CfnOutput(this, 'CfId', {
value: distribution.distributionId
});
// to reverify it was really updated to a proper version
new cdk.CfnOutput(this, 'LambdaEdge', {
value: stackOutput.getAtt('Output')
});
}
}
Then stack creation
const ls = new LambdaStack(app, LAMBDA_EDGE_STACK_NAME, {
env: {
region: 'us-east-1'
}
});
new StaticSiteStack(app, 'cf-stack').addDependency(ls);
app.run();
To test that it works:
/lambda/index.js
(edge lambda)
exports.handler = (event, context, callback) => {
console.log("REQUEST", JSON.stringify(event));
const status = '200';
const headers = {
'content-type': [{
key: 'Content-Type',
value: 'application/json'
}]
};
const body = JSON.stringify(event, null, 2);
return callback(null, {status, headers, body});
};
/cfn/stack.js
exports.handler = (event, context) => {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
const aws = require("aws-sdk");
const response = require('cfn-response');
const {RequestType, ResourceProperties: {StackName, OutputKey}} = event;
if (RequestType === 'Delete') {
return response.send(event, context, response.SUCCESS);
}
const cfn = new aws.CloudFormation({region: 'us-east-1'});
cfn.describeStacks({StackName}, (err, {Stacks}) => {
if (err) {
console.log("Error during stack describe:\n", err);
return response.send(event, context, response.FAILED, err);
}
const Output = Stacks[0].Outputs
.filter(out => out.OutputKey === OutputKey)
.map(out => out.OutputValue)
.join();
response.send(event, context, response.SUCCESS, {Output});
});
};
don't forget to add /cfn/cfn-response.js
file with a content listed here:
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html
published an article https://lanwen.ru/posts/aws-cdk-edge-lambda/
I would give this a try to integrate into CDK (I mean the lambdaFunctionAssociations
).
@rix0rrr @RomainMuller
I've referenced you, because I didn't know whom to reference here but given the contributions guide one should talk about it before creating the PR. :smile:
Hi @KnisterPeter, thanks for your work, since the 2 PRs are already merged, is there a guide or doc to indicate how to create a lambda@edge function effectively with CDK?
@PinkyJie Not really, the two PRs are basic work to get it going.
The main complication with lambda@edge are that edge functions need to be deployed in the region us-east-1
. If you stack is in the same region its easy, otherwise you need to follow the path of https://github.com/aws/aws-cdk/issues/1575#issuecomment-480738659
Basicly export a concrete version of the edge function from the us stack and import that version in your regional stack. Then put it in front of cloudfront.
The work I've done was to add all typings so a lambda function could be connected with cloudfront.
@KnisterPeter Thanks for the explanation, I'm using the solution from https://github.com/aws/aws-cdk/issues/1575#issuecomment-480738659 now, and it works like a charm, just wondering if there's more efficient solution after the 2 PRs.
@PinkyJie Just no need to use the basic CfnDistribution
class but WebDistribution
instead.
I'm having a super hard time using CDK with Lambda@Edge.
I have one stack that deploys a lambda. I have another stack that deploys a Cloudfront distribution where I want to use the Lambda as a Viewer Request event lambda.
I can get everything to deploy once. However, I run into issues when I've change my lambda's code and want to redeploy. The problem is the same issue that is affecting Lambda layer redeployments: https://github.com/aws/aws-cdk/issues/1972#issuecomment-521628844
Hi all.
I used SSM to store the Function's arn/version, but the workaround above works fine.
The "resource must be in us-east-1" issue has already been "solved" in CDK: ACM certificates for CloudFront also need to be in us-east-1.
The certificatemanager.DnsValidatedCertificate
construct deals with it perfectly: it takes a region property, and the custom resource it creates takes care of creating the certificate in the correct region, returning just the ARN.
I suggest an Edge function construct that behaves the same way.
No need for a "secondary" stack or CFn output, and it would be consistent with the ACM construct (and could become the de-facto way of handling this situation).
Thank you.
Using SSM seems like a simpler solution indeed:
when you define your lambda@茅dge
new ssm.StringParameter(this, "ssm-value", {
parameterName: props.outputName,
stringValue: `${lambdaEdge.functionArn}:${lambdaEdgeVersion.version}`
})
And to get it in a stack in another region:
const getParameter = new customResource.AwsCustomResource(
this,
"GetParameter",
{
onUpdate: {
// will also be called for a CREATE event
service: "SSM",
action: "getParameter",
parameters: {
Name: props.outputName
},
region: "us-east-1",
physicalResourceId: Date.now().toString() // Update physical id to always fetch the latest version
}
}
)
Related to #572
P.S. CloudFrontWebDistribution
does support lambdaFunctionAssociations now! So you can do the cloudfront now just like that:
const distribution = new cloudfront.CloudFrontWebDistribution(
this,
'MyWebsiteCloudFront',
{
originConfigs: [
{
s3OriginSource: {
s3BucketSource: bucket,
},
behaviors: [
{
isDefaultBehavior: true,
lambdaFunctionAssociations: [
{
eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
lambdaFunction: authLambdaVersion,
},
],
},
],
},
],
loggingConfig: {},
viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(
acmCertificate,
{
aliases: [DOMAIN],
}
),
}
);
Hi all.
I also faced a problem using cdk destroy
.
The deletion of the Lambda@Edge function is not properly handled.
CDK tries to remove the function without waiting for the deletion of the replicas (~20min on my side).
It causes this error :
Lambda was unable to delete arn:aws:lambda:us-east-1:234295088632:function:IDCDP-ResourceIndexesFunction-dev:9 because it is a replicated function. Please see our documentation for Deleting Lambda@Edge Functions and Replicas. (Service: AWSLambdaInternal; Status Code: 400; Error Code: InvalidParameterValueException; Request ID: e51c0b46-f848-4d36-b625-8343e4db0ce9)
Regards
@kinbald I'm not sure there's a workaround for that behavior. In my experience I've had to wait 24 hours, and then I can destroy the stack. The behavior's the same with any Lambda@Edge function, even when created via the console.
@blimmer Sure, more a product issue. Just thinking that as we wait for WebDistribution deployment, could be interesting to wait for lambda replicas deletion (e.g. creating a proper type of lambda for edge).
@jtomaszewski how to get authLambdaVersion when this function was created inanother stack ?
@st-quando
In your lambda stack you store the version ARN in SSM Parameter store:
[...]
// Export ARN with version
new StringParameter(this, 'edge-lambda-arn', {
parameterName: '/exampleproject/example-parameter',
description: 'CDK parameter stored for cross region Edge Lambda',
stringValue: yourFunction.currentVersion.functionArn
})
And in your CloudFront stack you get the parameter and instantiate a Version object with the ARN (note the region in AwsCustomResource):
[...]
const lambdaParameter = new AwsCustomResource(this, 'GetParameter', {
policy: AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['ssm:GetParameter*'],
resources: [
this.formatArn({
service: 'ssm',
region: 'us-east-1',
resource: `parameter/exampleproject/example-parameter'`
})
]
})
]),
onUpdate: {
// will also be called for a CREATE event
service: 'SSM',
action: 'getParameter',
parameters: {
Name: '/exampleproject/example-parameter'
},
region: 'us-east-1',
physicalResourceId: PhysicalResourceId.of(Date.now().toString()) // Update physical id to always fetch the latest version
}
})
[...]
lambdaFunctionAssociations: [
{
eventType: LambdaEdgeEventType.VIEWER_REQUEST,
lambdaFunction: lambda.Version.fromVersionArn(
this,
'cf-lambda',
lambdaParameter.getResponseField('Parameter.Value')
)
}
]
[...]
We have an ongoing effort of redesigning the CloudFront module. To that end, we are currently in the middle of an RFC and we would love feedback from all interested parties.
Specifically, checkout the Lambda@Edge
section.
@iliapolo is there any ongoing effort on Lambda@Edge
(couldn't find much in the linked issues/RFCs)?
IMO the CDK should take care of deploying the Lambda@Edge functions to us-east-1
. This should be transparent to CDK users.
I think this is a prime example of how a CDK construct can abstract from technical details and make our lives easier.
In your lambda stack you store the version ARN in SSM Parameter store:
[...]
And in your CloudFront stack you get the parameter and instantiate a Version object with the ARN (note the region in AwsCustomResource):
I might miss something, but why not simply rading back the version ARN in the CloudFront stack using SSM:
const versionArn = ssm.StringParameter.fromStringParameterAttributes(this, 'MyValue', {
parameterName: '/exampleproject/example-parameter',
}).stringValue;
EDIT: I missed something: SSM can only access parameters in the same region. For cross-region/account, an IAM user (or group) that can assume a role in the target region/account with permission to access the parameter in that region/account is required. Hence the custom resource.
@iliapolo is there any ongoing effort on Lambda@Edge (couldn't find much in the linked issues/RFCs)?
@njlynch Got some insight?
Yes, the Lambda@Edge support for the redesign of CloudFront (the Distribution
construct) has been released: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-cloudfront-readme.html#lambda-edge
Simple example:
const myFunc = new lambda.Function(...);
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: {
origin: new origins.S3Origin(myBucket),
edgeLambdas: [
{
functionVersion: myFunc.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
}
],
},
});
@njlynch thanks.
It is quite tedious to deploy Lambda edge functions to regions other than us-east-1
(see all the conversations in this thread).
My findings are that you need:
us-east
),lambda.amazonaws.com
is not sufficient, it needs edgelambda.amazonaws.com
too, see CloudFront docs), andimport * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as iam from '@aws-cdk/aws-iam';
import * as ssm from '@aws-cdk/aws-ssm';
import { ServicePrincipals, ManagedPolicies } from 'cdk-constants';
interface EdgeLambdaStackProps extends cdk.StackProps {
lambdaFunctionArnParameterName: string;
}
export class EdgeLambdaStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: EdgeLambdaStackProps) {
super(scope, id, props);
if (props.env?.region !== 'us-east-1') {
throw new Error("The stack contains Lambda@Edge functions and must be deployed in 'us-east-1'");
}
const { lambdaFunctionArnParameterName } = props
const { managedPolicyArn } = iam.ManagedPolicy.fromAwsManagedPolicyName(
ManagedPolicies.AWS_LAMBDA_BASIC_EXECUTION_ROLE
);
const ssrLambda = new lambda.Function(this, 'EdgeLambdaFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: '../some-dir/my-handler',
handler: 'index.handler',
role: new iam.Role(this, 'EdgeLambdaServiceRole', {
assumedBy: new iam.CompositePrincipal(
new iam.ServicePrincipal(ServicePrincipals.LAMBDA),
new iam.ServicePrincipal(ServicePrincipals.EDGE_LAMBDA)
),
managedPolicies: [
{
managedPolicyArn,
},
],
}),
});
const { functionArn } = ssrLambda.currentVersion;
new ssm.StringParameter(this, 'LambdaFunctionArnParameter', {
parameterName: lambdaFunctionArnParameterName,
stringValue: functionArn,
});
}
}
In the stack that contains the CloudFront distribution, S3 buckets etc. (to be deployed in another region):
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3deploy from '@aws-cdk/aws-s3-deployment';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as cr from '@aws-cdk/custom-resources';
import * as iam from '@aws-cdk/aws-iam';
interface WebStackProps extends cdk.StackProps {
lambdaFunctionArnParameterName: string;
domainName: string;
certificate: acm.ICertificate;
}
export class WebStack extends cdk.Stack {
public readonly siteBucket: s3.IBucket;
constructor(scope: cdk.Construct, id: string, props: WebStackProps) {
super(scope, id, props);
const { lambdaFunctionArnParameterName, domainName, certificate } = props;
const bucket = new s3.Bucket(this, 'Bucket');
const bucketOai = new cloudfront.OriginAccessIdentity(this, 'BucketOai');
bucket.grantRead(bucketOai);
const egdeLambdaFunctionArn = new SsmParameterReader(this, 'LambdaFunctionArnReader', {
parameterName: lambdaFunctionArnParameterName,
region: 'us-east-1',
}).stringValue;
const distribution = new cloudfront.CloudFrontWebDistribution(this, 'Distribution', {
originConfigs: [
{
s3OriginSource: {
s3BucketSource: bucket,
originAccessIdentity: bucketOai,
},
behaviors: [
{
defaultTtl: cdk.Duration.seconds(0),
minTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.seconds(0),
forwardedValues: {
cookies: {
forward: 'all',
},
queryString: true,
},
lambdaFunctionAssociations: [
{
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
lambdaFunction: lambda.Version.fromVersionArn(this, 'EdgeLambdaFunctionArn', egdeLambdaFunctionArn),
},
],
isDefaultBehavior: true,
},
],
},
],
viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(certificate, {
securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2018,
aliases: [domainName, `www.${domainName}`],
}),
});
new s3deploy.BucketDeployment(this, 'DeployNextJsStaticAssets', {
sources: [
s3deploy.Source.asset('../some-dir', {
exclude: ['my-handler'], // don't upload handler code to S3
}),
],
destinationBucket: bucket,
});
}
}
interface SsmParameterReaderProps {
parameterName: string;
region: string;
}
// https://stackoverflow.com/a/59774628/6058505
export class SsmParameterReader extends cdk.Construct {
private reader: cr.AwsCustomResource;
get stringValue(): string {
return this.getParameterValue();
}
constructor(scope: cdk.Construct, name: string, props: SsmParameterReaderProps) {
super(scope, name);
const { parameterName, region } = props;
const customResource = new cr.AwsCustomResource(scope, `${name}CustomResource`, {
policy: cr.AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['ssm:GetParameter*'],
resources: [
cdk.Stack.of(this).formatArn({
service: 'ssm',
region,
resource: 'parameter',
resourceName: parameterName.replace(/^\/+/, ''), // remove leading '/', since formatArn() will add one
}),
],
}),
]),
onUpdate: {
service: 'SSM',
action: 'getParameter',
parameters: {
Name: parameterName,
},
region,
physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()), // Update physical id to always fetch the latest version
},
});
this.reader = customResource;
}
private getParameterValue(): string {
return this.reader.getResponseField('Parameter.Value');
}
}
Ideally, we could drop an instance of an (imaginary) EdgeLambda
construct in any region, and the construct would take care of deploying the backing lambda to us-east-1
, assigning the required role to it, and reading back the ARN.
Thanks @asterikx (and all others who have contributed work-arounds and solutions to this so far)!
I think the request for a construct that enables requesting Lambda functions cross-region is reasonable given the complexity of the above work-arounds. I have created #9862 to track this request. Given the long history of this issue, and the multiple side-threads that have since come up, I am going to close this issue out in favor of the above to track the cross-region-specific piece.
If you have been following this issue and have a use case or need for requesting Lambda functions cross-region, please go :+1: #9862 so we can track priority of this request.
Most helpful comment
So I was able to solve my task with this way. My requirements were mainly that I want to have bucket in a different region than us-east-1, so I need to pass the lambda version somehow to another region
First definitions:
Then the edge lambda stack itself:
Then cloud front definition:
Then stack creation
To test that it works:
/lambda/index.js
(edge lambda)/cfn/stack.js
don't forget to add
/cfn/cfn-response.js
file with a content listed here:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html
published an article https://lanwen.ru/posts/aws-cdk-edge-lambda/