Finding the URL and ARN of AWS CDK Lambdas

Published

I spent the last few months creating Node.js Lambda functions that integrate various services for an e-commerce site. This was my first time working with a composable architecture, and I enjoyed the event-driven nature of the programming.

We had a strategy worked out to auto-deployment our Lambdas, but regarding configuration, we faced a chicken and the egg situation: how could we automatically point downstream services to a Lambda if the ARN and API Gateway URL aren’t available until after it is deployed?

Problem Background and CDK Usage

This problem arose when using AWS’s CDK to define the cloud application stacks that included our Lambda functions and API Gateway REST APIs.

Having used the Serverless Framework in the past, CDK seemed like a pretty powerful alternative that let you to dynamically define resources in an otherwise static ecosystem (e.g. Serverless, or straight CloudFormation Templates).

Here is a code snippet defining a Node.js Lambda accessible via API Gateway:

const { Stack } = require('aws-cdk-lib');
const lambda = require('aws-cdk-lib/aws-lambda');
const apigateway = require("aws-cdk-lib/aws-apigateway");

const { LAMBDA_FUNCTION_NAME, API_GATEWAY_REST_API_NAME } = require('./constants');

class AwsCdkNodejsLambdaStack extends Stack {

  constructor(scope, id, props) {
    super(scope, id, props);

    const getHikesLambdaFunction = new lambda.Function(this, LAMBDA_FUNCTION_NAME, {
      runtime: lambda.Runtime.NODEJS_14_X,    // execution environment
      code: lambda.Code.fromAsset('handler'), // code loaded from "handler" directory
      handler: 'index.main'                   // file is "index", function is "main"
    });

    const getHikesApi = new apigateway.RestApi(this, 'getHikesRestApiId', {
      restApiName: API_GATEWAY_REST_API_NAME,
      description: "This service returns a static list of hikes in JSON format."
    });

    const getHikesIntegration = new apigateway.LambdaIntegration(getHikesLambdaFunction, {
      requestTemplates: { "application/json": '{ "statusCode": "200" }' }
    });

    getHikesApi.root.addMethod("GET", getHikesIntegration);

  }
}

module.exports = { AwsCdkNodejsLambdaStack }

CDK provided a straightforward way to define and configure these resources, but did not provide the ability to retrieve the resource ARN or URL once it was deployed.

Solution with NPM Postdeploy Scripts

The problem was solved by introducing logic to query AWS for the deployed Lambda ARN and URLs in an NPM postdeploy script.

In our deployment pipeline, NPM was used to execute tests and perform deployments. NPM provides support for pre and post scripts that can be automatically executed with other named scripts.

// package.json - non pertinent contents removed for brevity
{
  "name": "aws-cdk-nodejs-lambda",
  "version": "0.1.0",
  "scripts": {
    "deploy": "cdk deploy",
    "postdeploy": "node ./postdeploy/getDeployedApiGatewayRestApiUrl.js && node ./postdeploy/getDeployedLambdaARN.js"
  },
}

In the example above, calling npm run deploy would automatically trigger the postdeploy script once the original deploy script had finished. This postdeploy script is where we introduced the logic to query AWS for the deployed Lambda ARN and URLs.

Finding the ARN of a Deployed Lambda Function

The strategy shown below for determining a Lambda’s ARN once it is deployed is as follows:

  1. Retrieve all Lambdas available in a given AWS region.
  2. Loop through the Lambdas and find the one with the matching Function Name (defined as LAMBDA_FUNCTION_NAME in the CDK configuration above).
  3. Return the ARN associated with that Lambda.
const AWS = require('aws-sdk');

/**
 * Queries AWS to find a Lambda function's ARN based on the supplied stack name and function name 
 * used when creating the Lambda with the CDK.
 *
 * @param {string} lambdaFunctionName the Lambda function's Name (defined in lib/aws-cdk-nodejs-lambda-stack.js).
 * @returns {string} Full AWS ARN for the Lambda function.
 * @throws {Error} Will throw an error if the Lambda function cannot be found.
 */
const getLambdaFunctionArn = async function(lambdaFunctionName) {

  const lambda = new AWS.Lambda({
    apiVersion: '2015-03-31',
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY,
    region: AWS_REGION
  });

  const params = {
    FunctionVersion: 'ALL'
  };

  const results = await lambda.listFunctions(params).promise();

  for (let i = 0; i < results.Functions.length; ++i) {
    const lambdaFunction = results.Functions[i];
    // match function by CDK defined lambda function name
    if (lambdaFunction.FunctionName.includes(lambdaFunctionName)) {
      return lambdaFunction.FunctionArn;
    }
  }

  // throw exception if ARN is not found
  throw new Error(`Unable to find AWS Lambda ARN for function name '${lambdaFunctionName}'`);

}

We can see that line 22 retrieves all available Lambdas, line 24 starts the iteration over these Lambdas, and line 27 makes the name comparison. Lambda function names may include other information like the CDK App or Stack, so .includes() is used for the name comparison.

Note that in this example Version 2 of the AWS SDK for JavaScript is used.

Determining an API Gateway REST API URL

A similar process is followed when determining the API Gateway URL for a CDK deployed Lambda:

  1. List all API Gateway REST APIs available in an AWS Region.
  2. Iterate over all REST APIs and match based on the name (specified as API_GATEWAY_REST_API_NAME in the CDK configuration above).
  3. Assemble the execute-api URL for the matched REST API.
const AWS = require('aws-sdk');

/**
 * Retrieves the URL for a REST API defined inside API Gateway. 
 *
 * @param {string} restApiName REST API name used in the CDK Stack when defining the API.
 * @returns {string} URL for the REST API.
 * @throws {Error} Will throw an error if the REST API cannot be found.
 */
const getApiGatewayRestApiUrl =  async function(restApiName) {

  const apigateway = new AWS.APIGateway({
    apiVersion: '2015-07-09',
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY,
    region: AWS_REGION
  });

  // find restApiId - uses older verion of AWS SDK
  //  https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html
  const restApiResults = await apigateway.getRestApis().promise();
  const restApi = restApiResults.items.find(api => api.name === restApiName );

  // get rest api resourceId
  if (restApi && restApi.id) {

    // return execute-api structured URL - no method but documented a few places
    //  - https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started.html
    //  - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getResources-property
    return `https://${restApi.id}.execute-api.${AWS_REGION}.amazonaws.com/prod`

  }

  // throw an error if no API is found
  throw new Error(`unable to find REST API named '${restApiName}' in API Gateway`);

}

In the snippet above, line 21 retrieves all API Gateway REST APIs, line 22 performs the name matching, and line 30 assembles the ugly execute-api URL we're all used to for API Gateway Lambdas (e.g. https://4ilrw8zgp3.execute-api.us-east-1.amazonaws.com/prod). This logic may need to be adjusted if using a stage other than the default prod.

Note that in this example Version 2 of the AWS SDK for JavaScript is used.

Example Project Posted to GitHub

A code repository has been assembled and posted to GitHub that contains an example Node.js Lambda that is accessible via API Gateway. The CDK configuration is included, as well as the postdeploy scripts detailed in this post.

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *