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

[lambda] Lambda@Edge support #1575

Open
jpmartin2 opened this issue Jan 19, 2019 · 12 comments
Open

[lambda] Lambda@Edge support #1575

jpmartin2 opened this issue Jan 19, 2019 · 12 comments

Comments

@jpmartin2
Copy link

@jpmartin2 jpmartin2 commented Jan 19, 2019

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).

@lanwen

This comment has been minimized.

Copy link

@lanwen lanwen commented Apr 3, 2019

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?

@jpmartin2

This comment has been minimized.

Copy link
Author

@jpmartin2 jpmartin2 commented Apr 3, 2019

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.

@lanwen

This comment has been minimized.

Copy link

@lanwen lanwen commented Apr 3, 2019

@LordPython Thanks a lot for your answer. Do you have any examples with custom resource and described PublishVersion and UpdateAlias calls?

@jpmartin2

This comment has been minimized.

Copy link
Author

@jpmartin2 jpmartin2 commented Apr 3, 2019

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)

@lanwen

This comment has been minimized.

Copy link

@lanwen lanwen commented Apr 8, 2019

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/

@KnisterPeter

This comment has been minimized.

Copy link
Contributor

@KnisterPeter KnisterPeter commented Jun 5, 2019

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. 😄

@KnisterPeter KnisterPeter mentioned this issue Jun 5, 2019
3 of 4 tasks complete
KnisterPeter added a commit to KnisterPeter/aws-cdk that referenced this issue Jun 13, 2019
This declaration is required for deploying custom resources as lamdba
association, which by itself is required to deploy a lambda@edge for a
stack which is in a different region as 'us-east-1'.

Relates to aws#1575
KnisterPeter added a commit to KnisterPeter/aws-cdk that referenced this issue Jun 17, 2019
This declaration is required for deploying custom resources as lamdba
association, which by itself is required to deploy a lambda@edge for a
stack which is in a different region as 'us-east-1'.

Relates to aws#1575
KnisterPeter added a commit to KnisterPeter/aws-cdk that referenced this issue Jun 18, 2019
This declaration is required for deploying custom resources as lamdba
association, which by itself is required to deploy a lambda@edge for a
stack which is in a different region as 'us-east-1'.

Relates to aws#1575
KnisterPeter added a commit to KnisterPeter/aws-cdk that referenced this issue Jun 20, 2019
This declaration is required for deploying custom resources as lamdba
association, which by itself is required to deploy a lambda@edge for a
stack which is in a different region as 'us-east-1'.

Relates to aws#1575
KnisterPeter added a commit to KnisterPeter/aws-cdk that referenced this issue Jun 21, 2019
This declaration is required for deploying custom resources as lamdba
association, which by itself is required to deploy a lambda@edge for a
stack which is in a different region as 'us-east-1'.

Relates to aws#1575
@eladb eladb self-assigned this Aug 12, 2019
mergify bot added a commit that referenced this issue Aug 21, 2019
* feat(cloudfront): define lambda@edge as resolvable resource

This declaration is required for deploying custom resources as lamdba
association, which by itself is required to deploy a lambda@edge for a
stack which is in a different region as 'us-east-1'.

Relates to #1575

* test: simplified test case by just test resources required for the case

* feat: allow to create a version from an arn

This commit allows to reference a lambda from a different region and use
that as function association.

* refactor: resolve conflicts

* refactor: update based on review

* refactor: use version arn as function arn

* Fixing package.json

* Fix tests
@PinkyJie

This comment has been minimized.

Copy link

@PinkyJie PinkyJie commented Aug 23, 2019

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?

@KnisterPeter

This comment has been minimized.

Copy link
Contributor

@KnisterPeter KnisterPeter commented Aug 23, 2019

@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 #1575 (comment)

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.

@PinkyJie

This comment has been minimized.

Copy link

@PinkyJie PinkyJie commented Aug 23, 2019

@KnisterPeter Thanks for the explanation, I'm using the solution from #1575 (comment) now, and it works like a charm, just wondering if there's more efficient solution after the 2 PRs.

@KnisterPeter

This comment has been minimized.

Copy link
Contributor

@KnisterPeter KnisterPeter commented Aug 23, 2019

@PinkyJie Just no need to use the basic CfnDistribution class but WebDistribution instead.

@eladb eladb assigned nija-at and unassigned eladb Sep 3, 2019
@charlesswanson

This comment has been minimized.

Copy link

@charlesswanson charlesswanson commented Sep 11, 2019

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: #1972 (comment)

@nija-at nija-at changed the title Lambda@Edge support [lambda] Lambda@Edge support Oct 21, 2019
@nija-at nija-at removed their assignment Jan 14, 2020
@nija-at nija-at removed the package/lambda label Jan 14, 2020
@ralovely

This comment has been minimized.

Copy link

@ralovely ralovely commented Jan 23, 2020

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.

@eladb eladb assigned iliapolo and unassigned eladb Jan 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
10 participants
You can’t perform that action at this time.