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

Closed
jpmartin2 opened this issue Jan 19, 2019 · 27 comments
Closed

[lambda] Lambda@Edge support #1575

jpmartin2 opened this issue Jan 19, 2019 · 27 comments
Assignees
Labels
@aws-cdk/aws-cloudfront Related to Amazon CloudFront effort/large Large work item – several weeks of effort feature-request A feature should be added or improved.

Comments

@jpmartin2
Copy link

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

@rix0rrr rix0rrr added feature-request A feature should be added or improved. @aws-cdk/aws-lambda Related to AWS Lambda gap labels Jan 21, 2019
@lanwen
Copy link
Contributor

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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Contributor

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 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 pushed 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
Copy link

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
Copy link
Contributor

@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
Copy link

@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
Copy link
Contributor

@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
Copy link

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 added @aws-cdk/aws-cloudfront Related to Amazon CloudFront @aws-cdk/aws-lambda Related to AWS Lambda and removed @aws-cdk/aws-lambda Related to AWS Lambda labels Jan 14, 2020
@rootkc
Copy link

rootkc commented Feb 21, 2020

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

@SomayaB SomayaB removed the gap label Feb 25, 2020
@iliapolo
Copy link
Contributor

iliapolo commented Mar 9, 2020

Related to #572

@iliapolo iliapolo added the effort/large Large work item – several weeks of effort label Mar 9, 2020
@jtomaszewski
Copy link

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],
          }
        ),
      }
    );

@kinbald
Copy link

kinbald commented Mar 18, 2020

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

@blimmer
Copy link
Contributor

blimmer commented Mar 18, 2020

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

@kinbald
Copy link

kinbald commented Mar 18, 2020

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

elhedran pushed a commit to ElhedranOrg/cdkStaticSite that referenced this issue Apr 22, 2020
Looking at the CDK code this is not well demonstrated in a way that the
sub-stack is hidden from the user
of the resource.

See aws/aws-cdk#1575 for
better tracking of CDK support for
lambda at edge cross regions.
@st-quando
Copy link

@jtomaszewski how to get authLambdaVersion when this function was created inanother stack ?

@KurtMar
Copy link

KurtMar commented Jul 1, 2020

@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')
                  )
                }
              ]

[...]

@iliapolo
Copy link
Contributor

iliapolo commented Jul 5, 2020

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.

@asterikx
Copy link
Contributor

asterikx commented Aug 13, 2020

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

@asterikx
Copy link
Contributor

asterikx commented Aug 13, 2020

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
Copy link
Contributor

@iliapolo is there any ongoing effort on Lambda@Edge (couldn't find much in the linked issues/RFCs)?

@njlynch Got some insight?

@njlynch
Copy link
Contributor

njlynch commented Aug 17, 2020

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,
      }
    ],
  },
});

@asterikx
Copy link
Contributor

asterikx commented Aug 17, 2020

@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:

  • Create a separate stack with the lambda function (to be deployed in us-east),
  • assign a custom execution role to the lambda (lambda.amazonaws.com is not sufficient, it needs edgelambda.amazonaws.com too, see CloudFront docs), and
  • upload the function ARN to SSM parameter store
import * 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):

  • create a custom resource that reads back the SSM parameter.
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.

@njlynch
Copy link
Contributor

njlynch commented Aug 20, 2020

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 👍 #9862 so we can track priority of this request.

@njlynch njlynch closed this as completed Aug 20, 2020
njlynch added a commit that referenced this issue Sep 23, 2020
DRAFT PR - Looking for early-stages feedback

This PR creates a construct (`EdgeFunction`) for Lambda@Edge functions.
CloudFront requires that a function be in us-east-1 to be used with Lambda@Edge,
even if the logical distribution is created via another region. The initial goal
of this construct is to make it easier to request and work with a function in
us-east-1 when the primary stack is in another region.

In the future, this can be extended to validate other Lambda@Edge restrictions.
See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
and #9833 for more information on those limitations.

DRAFT -- What exists does work and has been successfully deployed with a
CloudFront distribution based in eu-west-1, but all of the normal Lambda
functionality (e.g., permissions) has not been verified to work yet.

Some open questions:
1. Where does this belong? There are circular dependency issues with
  `custom-resources` that prevents this from being in `aws-lambda`.
  `aws-cloudfront` seems possibly the next-best thing, but I can also see an
  argument for moving this into its own module.
2. Does this approach (creating a new stack and using SSM + an AWS custom
  resource) make sense, or should another approach be taken?
3. I intentionally didn't extend from `FunctionBase` in order to override most
  of the methods there to redirect to the delegate function; it's not clear (yet)
  that this will work and/or is a good idea. Feedback welcome.

Thanks to @asterikx for the inspiration and consolidated writeup in #1575.

fixes #9862
njlynch added a commit that referenced this issue Oct 6, 2020
DRAFT PR - Looking for early-stages feedback

This PR creates a construct (`EdgeFunction`) for Lambda@Edge functions.
CloudFront requires that a function be in us-east-1 to be used with Lambda@Edge,
even if the logical distribution is created via another region. The initial goal
of this construct is to make it easier to request and work with a function in
us-east-1 when the primary stack is in another region.

In the future, this can be extended to validate other Lambda@Edge restrictions.
See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
and #9833 for more information on those limitations.

Some open questions:
1. Is there a more clever way to "refresh" the SSM parameter reader when the
   underlying function changes?
2. How to make this and `edgeArn` play nicely together?

Thanks to @asterikx for the inspiration and consolidated writeup in #1575.

fixes #9862
njlynch added a commit that referenced this issue Oct 14, 2020
DRAFT PR - Looking for early-stages feedback

This PR creates a construct (`EdgeFunction`) for Lambda@Edge functions.
CloudFront requires that a function be in us-east-1 to be used with Lambda@Edge,
even if the logical distribution is created via another region. The initial goal
of this construct is to make it easier to request and work with a function in
us-east-1 when the primary stack is in another region.

In the future, this can be extended to validate other Lambda@Edge restrictions.
See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
and #9833 for more information on those limitations.

Some open questions:
1. Is there a more clever way to "refresh" the SSM parameter reader when the
   underlying function changes?
2. How to make this and `edgeArn` play nicely together?

Thanks to @asterikx for the inspiration and consolidated writeup in #1575.

fixes #9862
njlynch added a commit that referenced this issue Oct 14, 2020
DRAFT PR - Looking for early-stages feedback

This PR creates a construct (`EdgeFunction`) for Lambda@Edge functions.
CloudFront requires that a function be in us-east-1 to be used with Lambda@Edge,
even if the logical distribution is created via another region. The initial goal
of this construct is to make it easier to request and work with a function in
us-east-1 when the primary stack is in another region.

In the future, this can be extended to validate other Lambda@Edge restrictions.
See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
and #9833 for more information on those limitations.

Some open questions:
1. Is there a more clever way to "refresh" the SSM parameter reader when the
   underlying function changes?
2. How to make this and `edgeArn` play nicely together?

Thanks to @asterikx for the inspiration and consolidated writeup in #1575.

fixes #9862
mergify bot pushed a commit that referenced this issue Nov 25, 2020
This PR creates a construct (`EdgeFunction`) for Lambda@Edge functions.
CloudFront requires that a function be in us-east-1 to be used with Lambda@Edge,
even if the logical distribution is created via another region. The initial goal
of this construct is to make it easier to request and work with a function in
us-east-1 when the primary stack is in another region.

In the future, this can be extended to validate other Lambda@Edge restrictions.
See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
and #9833 for more information on those limitations.

Thanks to @asterikx for the inspiration and consolidated writeup in #1575.

Related changes:
* When updating the CloudFront README, I noticed that the `Distribution` sub-section hadn't been updated when it was flipped from Experimental to Dev Preview.
* A minor docstring fix for Lambda.

fixes #9862


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-cloudfront Related to Amazon CloudFront effort/large Large work item – several weeks of effort feature-request A feature should be added or improved.
Projects
None yet
Development

No branches or pull requests