12 December 2022

Boost Performance and Reduce Costs Using Lambda Extensions

Intro

Iโ€™m going to show you how & why you should use the Lambda Extension to cache your secrets and parameters.

What are Lambda Extensions?

In short: Lambda Extensions allow the user to make changes to the Lambda execution environment as a separate process that runs parallel to your Lambda functions runtime.

To explain Lambda Extensions, I always use telemetry as an example. Most monitoring solutions use an agent to collect and send information about your application. However, before Lambda Extensions, you could not install these agents to collect data when the Lambda processes weren’t processing any events. Only AWS could perform background tasks since they own and maintain the execution environment of Lambda. With Lambda Extension, you can run a script parallel to your Lambda and receive updates about your function via a poll-based API. This script can collect metrics like CPU and network usage and expose this to a third-party program.

For more information on Lambda Extensions, please refer to Lambda Extension Documentation

LAMBDA-EXECUTION-ENVIRONMENT

Pre Lambda Extensions

Currently, if you want to retrieve secrets from Secrets Manager or parameters from Parameter Store within your Lambda function, you need to make the correct API call to those services to retrieve your secret(s) and parameter(s).

For example, your Lambda gets triggered, retrieves its secret(s) and parameter(s) via an API call to those services, does its thing, and shuts down. Then, a couple of minutes later, it gets triggered again, and it does the same API calls to those services again and again and again… you see where I’m going with this ๐Ÿ˜„.

Where Do Lambda Extensions Fit In This Picture?

By utilizing the built-in Lambda Extension for Secrets and Parameters provided by AWS, a local cache is created and made available for your Lambda function. Secrets and parameters accessed by this function will get cached, reducing the number of API calls significantly. This will result in better performance since accessing a cache is much faster than making API calls, and it also reduces a small portion of your costs since you pay for API calls to retrieve secrets and parameters.

Note: As long as the same execution environment serves the requests and this environment has an active cache, the Lambda function will utilize this cache. If you have enabled concurrent executions for your Lambda function, every time a different execution environment is used, this cache must be built again.

For more information about the Lambda execution environment: Lambda Runtime Environment Documentation

How To Implement the Lambda Extension Layer

To demonstrate and test this function quickly, we’ve created the following resources:

  • A secret called /secret/lambda-extension.
  • An IAM Role for the Lambda function with the AWS Managed Policies: SecretsManagerReadWrite & CloudWatchLogsFullAccess.
  • A Lambda function with the Aws-Parameters-and-Secrets-Lambda-Extension layer was added. You can add this by going to the Lambda Console > choose your function > below the ‘code’ tab, you can add layers.

LAMBDA-LAYERS

Note: In production environments, you want to tighten the IAM role of your Lambda to something like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow Lambda to retrieve secrets",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
        "secretsmanager:ListSecretVersionIds"
      ],
      "Resource": [
        "<your-secret-arn>"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "logs:CreateLogGroup",
      "Resource": "arn:aws:logs:region:accountID:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:<region>:<accountID>:log-group:/aws/lambda/<functionName>:*"
      ]
    }
  ]
}

The Lambda code:

export const handler = async () => {
  const secretName = "/secret/lambda-extension";
  const url = `http://localhost:2773/secretsmanager/get?secretId=${secretName}`;
  const startTime = performance.now();
  const response = await fetch(url, {
    method: "GET",
    headers: {
      "X-Aws-Parameters-Secrets-Token": process.env.AWS_SESSION_TOKEN!,
    },
  });

  if (!response.ok) {
    throw new Error(
      `Error occured while requesting secret ${secretName}. Response status was ${response.status}`
    );
  }
  const endTime = performance.now();
  const milliseconds = endTime - startTime;
  console.log(
    `Total time of fetching the secret: ${milliseconds} milliseconds`
  );

  const secretContent = (await response.json()) as { SecretString: string };
  return secretContent.SecretString;
};

Explanation of the code:

  • There are a variety of environment variables that you can set within your Lambda runtime to customize this layer. See: Lambda Extension Documentation
  • To retrieve secret(s) from the cache, we need to provide a session token (AWS_SESSION_TOKEN, which is by default an environment variable available within your Lambda runtime) and pass this token to our HTTP GET header: X-Aws-Parameters-Secrets-Token.
  • To retrieve secrets within your Lambda function, you need to make an HTTP GET call to the localhost URL containing:
get?secretId=<secretName/secretArn>
  • We’ve added performance.now() to calculate/estimate the time of fetching a secret. It should be expected that this value will decrease after our first execution because of the caching.

Results

The first three runs with the Lambda extension enabled.

First run: Fetching time: 1442,58 ms | Billed duration: 1754 ms WITH-EXTENSION-RUN-1 Second run: Fetching time: 1,55 ms | Billed duration: 16 ms WITH-EXTENSION-RUN-2 Third run: Fetching time: 1,60 ms | Billed duration: 14 ms WITH-EXTENSION-RUN-3

As you can see, the first time the Lambda function got triggered, it still needed to build its cache. First, the function will fetch the secret from Secrets Manager and create the cache afterward, hence the long duration. Then, after the first run, the runs later use this cache, and you see the time to fetch the secret drop enormously.

To show the difference, we will do three runs without using the Lambda extension enabled and the code adjusted so that it makes ‘GetSecretValue’ calls to Secrets Manager.

First run: Fetching time: 356,34 ms | Billed duration: 536 ms WITHOUT-EXTENSION-RUN-1 Second run: Fetching time: 140,51 ms | Billed duration: 143 ms WITHOUT-EXTENSION-RUN-2 Third run: Fetching time: 118,77 ms | Billed duration: 125 ms WITHOUT-EXTENSION-RUN-3

The result speaks for itself ๐Ÿ˜„

Summary

To keep our Lambda functions lightweight and fast, you want to minimize the number of static API calls to services like Parameter Store and Secrets Manager to avoid reloading the same parameter(s) and secret(s). Therefore, the Lambda Extension for Parameter and Secrets is a simple add-on to reduce the number of static API calls your Lambda makes to these services.

Lambda Extensions provides your functions, within the same execution environment, a cache where parameters and secrets can be stored and rapidly accessed. Besides boosting performance, this will also impact your AWS bills because:

  • Accessing the local cache will reduce the number of API calls to Parameter Store and Secrets Manager.
  • Accessing the local cache will also result in a lower Lambda duration.

Some may ask: “But Iman, why should I use Lambda Extension? I can just fetch my secrets and parameters outside my handler, which will also get cached”.
That’s true! Both techniques will provide caching capabilities to cache your secrets/parameters. However, there are some slight differences:

  • With the Lambda Extension, you have some environment variables where you can finetune the cache settings.
  • In situations where your containers stay warm for a long time, and your parameters/secrets are rapidly changing value, you better use the extension layer to avoid having stale secret/parameter values with static initialization.
  • With the Lambda Extension, you call your localhost to fetch the value of a secret/parameter. If the cache is empty, this will automatically perform the corresponding API call to Parameter Store/Secrets Manager to fetch and store this in the cache. With static initialization, you need to implement a logic where you check if you have a cache enabled. Suppose that’s not the case, you need to make the API call to Parameter Store/Secrets Manager for fetching the parameter/secret.

For more information on static initialization, please refer to Lambda Static Initialization

If you enjoyed this post, don’t hesitate to share it with your friends and colleagues ๐Ÿ˜‰

Enjoy and until next time!

Subscribe to our newsletter

We'll keep you updated with more interesting articles from our team.

(about once a month)