Search code examples
amazon-web-servicesaws-cdkaws-lambda-edge

CDK: Possible to put the stack created for EdgeFunction resource in another (cross-region) stack?


Problem statement

I have my main CDK stack in eu-west-1, but an EdgeFunction is created in us-east-1.

I have noticed two weird things with EdgeFunction:

  1. Even if declared in a NestedStack they appear in cdk ls as edge-lambda-stack-nnnnnnnnn (unless given an explicit name).
  2. When removing the main stack, let's call it primary, it does not remove the lambda stack. Probably because (1) above tells me it's not a part of the NestedStack.

I have tried putting the EdgeFunction in a separate stack created in us-east-1 explicitly and then cross-referencing it from primary but that fails with "Cannot use resource in a cross-environment fashion" (amongst others).

Questions

  • Why doesn't cloudfront.experimental.EdgeFunction respect the NestedStack boundary?
  • Can I somehow build a Stack that exists in us-east-1 and cross-reference the lambda into my main stack in eu-west-1?
  • Can I at least make it so that deleting the primary stack also automatically deletes the lambda stack.

The reason I'm asking is because I have a number of environments and a number of lambdas and the combinatorial explosion of stacks makes things a bit unwieldly.

Example

CDK version 1.116

import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as lambda from "@aws-cdk/aws-lambda";
import * as path from "path";

class PrimaryStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
        new SecondaryStack(this, "secondary-stack");
    }
}

class SecondaryStack extends cdk.NestedStack {
    constructor(scope: cdk.Construct, id: string) {
        super(scope, id);

        new cloudfront.experimental.EdgeFunction(this, "my-lambda", {
            runtime: lambda.Runtime.NODEJS_14_X,
            functionName: "my-lambda",
            handler: "index.handler",
            code: lambda.Code.fromAsset(
                path.join(__dirname, "..", "lib", "my-lambda"),
            ),
        });
    }
}


const app = new cdk.App();

new PrimaryStack(app, "primary", {
    env: { account: "123", region: "eu-west-1" },
});

cdk ls output:

edge-lambda-stack-cnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
primary

Solution

  • I talked to AWS premium support and the answer is that it doesn't seem like CDK can handle this by using nested stacks.

    Instead, create a separate stack for the EdgeLambdas in us-east-1 and then export the ARNs of the lambdas to SSM.

    In the other stack, import the ARNs from SSM and use those.

    This will result in the minimum amount of stacks to handle. You will still need to deploy the two stacks separately.

    The code I ended up using looks like this:

    export function exportLambdaSsm(
        func: cloudfront.experimental.EdgeFunction,
    ): ssm.StringParameter {
        const funcName = // ...
        return new ssm.StringParameter(this, `${funcName}-param`, {
            // Parameter with slash must start with a slash
            parameterName: `/${funcName}`,
            stringValue: func.functionArn,
        });
    }
    
    export function importLambdaFromSsm(stack: Stack): lambda.IVersion {
        const funcName = // ...
        const funcResource = new cr.AwsCustomResource(stack, `${funcName}-param`, {
            policy: cr.AwsCustomResourcePolicy.fromStatements([
                new iam.PolicyStatement({
                    effect: iam.Effect.ALLOW,
                    actions: ["ssm:GetParameter*"],
                    resources: [
                        stack.formatArn({
                            service: "ssm",
                            region: "us-east-1",
                            resource: "parameter",
                            resourceName: funcName,
                        }),
                    ],
                }),
            ]),
            onUpdate: {
                // will also be called for a CREATE event
                service: "SSM",
                action: "getParameter",
                parameters: {
                    Name: `/${funcName}`,
                },
                region: "us-east-1",
                // Update physical id to always fetch the latest version
                physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()),
            },
        });
    
        return lambda.Version.fromVersionArn(stack, funcName, funcResource.getResponseField("Parameter.Value"));
    }
    

    Please be aware that when fetching and storing SSM values there are a lot of rules around where slashes go. I tried to keep the code above correct.