Getting the most of AWS Lambda free compute - wrapper scripts
Tags: serverless, lambda, aws, cdk
Published: 2021-11-14 16:43:07

---
title: Getting the most of AWS Lambda free compute - wrapper scripts
published: true
description: How one might abuse AWS Lambda wrapper scripts for cost savings.
tags: serverless, lambda, aws, cdk
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6l0ebh7vfm1931lrlph.jpeg
---

Since the service's inception, the [_AWS Lambda_](https://aws.amazon.com/lambda/) quickly rose to the status of the go-to tool in many developers' toolbelt. This course of events is entirely understandable. A versatile compute platform with [per millisecond billing](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-1ms-billing-granularity-adds-cost-savings/) is a dream come true for developers alike.

This blog post will show you how to squeeze even more value from the _AWS Lambda_ service. By utilizing a particular part of the invocation lifecycle, we will get away with up to **ten seconds of free _AWS Lambda_ compute**.

Let us dive in.

> All the code from this article is available on [my GitHub](https://github.com/WojciechMatuszewski/lambda-wrapper-scripts-abuse).

## AWS Lambda invocation lifecycle

Here is an image representing various stages your function goes through during an invocation. [The source of the image, the AWS documentation page](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html) is a great resource to learn more about nitty-gritty details about _AWS Lambda_ service itself.

![AWSLambda execution environment stages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g0kp7fzoijw8lpddkjx3.png)

I will not be spending much time here – describing each phase is out of scope for this blog post. The most critical information I would like you to keep in mind is that **the `Init` phase is entirely free. You do not pay for it**. The **`Init` phase can last up to ten seconds**.

Keep all of this in mind as our entire master plan of using _AWS Lambda_ as free compute is based on this idea.

## You are most likely already utilizing the `Init` phase

Since the `Init` phase is all the code executed **outside of _AWS Lambda_ primary handler**, by leveraging some of the best practices – like initializing third-party libraries outside the main _AWS Lambda_ handler, you are already taking advantage of the free compute time.

Here is an example of initializing third-party dependencies outside the primary _AWS Lambda_ handler written in [Node.js](https://nodejs.org/en/).

```js
// The next two lines are executed in the `Init` phase.
import S3 from "aws-sdk/clients/s3";
const s3Client = new S3();

export const handler = async () => {...}
```

And here is an example written in Golang.

```go
package main

import (
    "context"
    "github.com/aws/aws-lambda-go/lambda"
)

// The whole `main` function is executed in the `Init` phase.
func main() {
    s3Client := s3.NewFromConfig(getConfig());
    // I could easily perform a network request here if I wanted to.
    lambda.Start(handler)
}

func handler(ctx context.Context) error {...}
```

To achieve our objective of free compute, all we have to do is pack as much code as possible before our handler executes.

## The Node.js problem

The `main` function in languages like Golang and Rust executes in the `Init` phase allowing developers to perform all kinds of operations, including waiting for HTTP responses, for free.

Sadly this is not the case with the Node.js language in the context of _AWS Lambda_ handlers. Since, to my best knowledge, _AWS Lambda_ Node.js runtime does not support the top-level `await` feature (despite supporting Node.js 14.x), developers writing lambda functions are constrained to only _synchronous_ actions in their `Init` phase code.

```js
import S3 from "aws-sdk/clients/s3";

const s3Client = new S3();

// The following will not work, even when deployed as Node.js 14.x ESM handler.
const s3Result = await s3Client.getObject({Bucket: "bucketName", Key: "file.txt"}).promise()

// The following will work, but the result will not be available to you in the handler.
// This is because Node.js will NOT wait for the promise inside the IFFIE to resolve before running the handler.
let otherS3Result;
(async () => {
    otherS3Result = await s3Client.getObject({Bucket: "bucketName", Key: "file.txt"}).promise()
})()

export const handler = async () => {...}
```

Inability to wait for _asynchronous_ code in the `Init` phase of _AWS Lambda_ execution environment greatly hinders our options. **From my experience, most lambda functions spend time idling - waiting for HTTP responses for requests fired in the main handler code**.

Would not it be nice to move some of that idling to the `Init` phase, so we do not have to pay for it?

It turns out there is – enter _AWS Lambda wrapper scripts_.

## Wrapper scripts to the rescue

I first learned about _AWS Lambda wrapper scripts_ while reading about [_AWS Lambda extensions_](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-extensions-in-preview/) and topics regarding _AWS Lambda_ custom runtimes.

Wrapper scripts are, well, scripts designed to allow you to augment the runtime environment. Think of passing special flags to the Node.js runtime like [`--enable-source-maps`](https://nodejs.org/docs/latest-v14.x/api/cli.html#cli_enable_source_maps).

In addition to passing command line arguments to your runtime of choice, **_AWS Lambda wrapper scripts_ allow you to perform arbitrary shell scripts or invoke binaries – all of them executed in the `Init` phase of _AWS Lambda_ execution environment**.

Let us explore how one might leverage this fact to circumvent the inability to reliably wait for _asynchronous_ operations in the `Init` phase in the context of Node.js _AWS Lambda_ runtime.

## Writing the scripts

Let us say we want to fetch a piece of data from S3 needed for our handler to operate correctly – think configuration file. We do not want to do that inside the handler definition since we would be paying for idle time.

Instead, armed with the knowledge about _wrapper scripts_, let us write a Node.js script to do that for us. We could then execute that script as a part of the _wrapper script_ logic giving us confidence that the S3 data will be there for the handler to consume it before its invocation.

The **first step** is to write _s3_downloader_ the script. The following is a sample and self-contained Node.js script whose role is to fetch a configuration file from S3.

```js
// wrapper-script/index.js

const S3 = require("aws-sdk/clients/s3");
const fs = require("fs-extra");

const s3Client = new S3();

async function main() {
  const result = await s3Client
    .getObject({
      Key: process.env.CONFIG_FILE_KEY,
      Bucket: process.env.BUCKET_NAME
    })
    .promise();

  await fs.writeJson(
    process.env.CONFIG_FILE_PATH,
    JSON.parse(result.Body.toString()),
    {
      encoding: "utf-8"
    }
  );
}

process.on("unhandledRejection", () => {
  console.log("unhandled error", e);

  process.exit(1);
});

main();
```

Since Node.js waits for the event loop to be empty before exiting, we are guaranteed to have our `main` function fully executed before the program exits.

Take note of the environment variables. Since the environment variables specified for the _AWS Lambda_ function are available in the `Init` phase, they are also available to us in the _wrapper scripts_ and every binary or script they invoke.

The **second step** is to write the _wrapper script_ itself. As I eluded earlier, its primary purpose is to provide additional parameters to the runtime of your choice, but in our case, the script will be invoking the Node.js script from above.

Here is an example of the _wrapper script_ that invokes the `index.js` script depicted earlier.

```sh
#wrapper-script/wrapper-script

#!/bin/bash

args=("$@")

script_full_path=$(dirname $(readlink -f "$0"))

node ${script_full_path}/index.js

s3_downloader_script_result=$?

if [ $s3_downloader_script_result -ne 0 ]; then
    echo "Error: 's3_downloader_script' script returned with non-zero status code: $s3_downloader_script_result"
    exit $s3_downloader_script_result
fi

exec "${args[@]}"
```

Here, it is **crucial to handle the errors correctly**. Remember – unless we explicitly exit from the script, your handler will be invoked (the `exec "${args[@]}"` part). There would be no point in invoking our _AWS Lambda_ handler if we did not manage to download the config file from S3.

Of course, I'm not that smart to write such scripts on my own. The _wrapper script_ logic was heavily inspired by [this great article](https://aws.amazon.com/blogs/compute/creating-aws-lambda-environmental-variables-from-aws-secrets-manager/).

## Writing it up together

With the scripts written, all that is left is to wire the pieces together and deploy the infrastructure. I'm going to use [_AWS CDK_](https://aws.amazon.com/cdk/) for that.

**Firstly** let us tackle the _wrapper script_ deployment. _Wrapper scripts_ are deployed as layers. The _wrapper script_ and an _AWS Lambda_ handler connect through a particular environment variable. More on that when during the handler configuration.

The following is a declaration of _AWS Lambda_ layer containing our scripts.

```ts
import * as lambda from "@aws-cdk/aws-lambda";

// Other AWS-CDK code...

const wrapperScriptLayer = new lambda.LayerVersion(this, "wrapperScriptLayer", {
  code: lambda.Code.fromAsset(join(__dirname, "../wrapper-script"))
});
```

**Next** up on our TODO list is the _AWS Lambda_ handler and the _S3_ bucket. Luckily for us, _AWS CDK_ makes it easy to define those resources.

```ts
import * as lambda from "@aws-cdk/aws-lambda";
import * as s3 from "@aws-cdk/aws-s3";

// Other AWS-CDK code...

const assetsBucket = new s3.Bucket(this, "assets-bucket");

const handler = new lambda.Function(this, "handler", {
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: "index.handler",
  code: lambda.Code.fromInline(`
    const fs = require("fs");
    module.exports.handler = async () => {
      const configFile = fs.readFileSync(process.env.CONFIG_FILE_PATH, "utf8");
      console.log(configFile.toString());
    }
  `),
  layers: [wrapperScriptLayer],
  environment: {
    CONFIG_FILE_PATH: "/tmp/config.json",
    AWS_LAMBDA_EXEC_WRAPPER: "/opt/wrapper-script",
    BUCKET_NAME: assetsBucket.bucketName,
    CONFIG_FILE_KEY: "config.json"
  }
});

assetsBucket.grantRead(handler);
```

There are three crucial things to keep in mind:

- By specifying the `AWS_LAMBDA_EXEC_WRAPPER` environment variable, you tell the _AWS Lambda_ runtime where the _wrapper script_ location. Please note that **contents of _AWS Lambda_ layers are unpacked into the `/opt` directory**.

- The _wrapper script_ shares IAM permissions with your _AWS Lambda_ handler.

- The _wrapper script_ shares the environment variables with your _AWS Lambda_ handler.

## Running the handler

With the infrastructure and the code in place, let us run the _AWS Lambda_ function and see if the configuration file is available to our handler.

![Test invocation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/olgig6g7btnbfkqmnara.png)

Notice the massive difference between the _Init duration_ and the _Billed duration_. **Keep in mind that we are only paying for the _Billed duration_**. The more compute you push into the `Init` phase, the less you will pay for **some** (more on that later) of your _AWS Lambda_ invocations.

In my test scenario, the configuration file size is trivial – the cost gain from using the _wrapper script_ to fetch the data is negligible. But **the more time you spend in the `Init` phase, the more significant your cost savings will be**.

## Too good to be true

If something sounds too good to be true, it probably is. The topic this blog post touches on is no exception to this rule.

As good as _wrapper scripts_ are, there are some **significant limitations** that you must consider before incorporating this solution into your codebase.

Apart from the limit of having a **maximum of ten seconds for the `Init` phase**, the most notable thing to consider is that **the _wrapper script_ is only invoked on _AWS Lambda_ cold start**. This makes it an ideal solution for fetching configuration data or performing static compute tasks, but **do not expect to build a free API with this method** (If you manage to do that, please let me know).

## Closing words

I hope that you found this journey through _AWS Lambda wrapper scripts_ helpful and insightful.

For more serverless content, head to my Twitter - [@wm_matuszewski](https://twitter.com/wm_matuszewski).

Thank you for your time.