Sidecar — Deploy and execute AWS Lambda functions from your Laravel application.

Handlers and Packages

To create a new Sidecar function, you must create a new PHP class that extends the abstract Hammerstone\Sidecar\LambdaFunction.

Every Lambda function requires at least two things:

  • the name of handler function
  • the file or files needed to execute that handler function

Because these two things are required, these are the two abstract methods that you must implement in your Sidecar function class.

1use Hammerstone\Sidecar\LambdaFunction;
2 
3class ExampleFunction extends LambdaFunction
4{
5 public function handler()
6 {
7 // TODO: Implement handler() method.
8 }
9 
10 public function package()
11 {
12 // TODO: Implement package() method.
13 }
14}

Function Handler

The handler function should return a string that points to the function that is your entry point for the function.

The format of your handler depends on the runtime that you're using, but in general it follows the format of filename.function.

For example, when using the Node runtimes your handler is always path/to/file.named-export. If you had the following file:

image.js

1exports.handle = async function() {
2 //
3}
Code highlighting powered by torchlight.dev, a Hammerstone product.

Then your handler would be image.handle:

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 // "image" is the filename, "handle" is the function.
6 return 'image.handle';
7 }
8 
9 public function package()
10 {
11 // TODO: Implement package() method.
12 }
13}

If you were deploying a Python function:

example.py

1def handler_name(event, context):
2 return some_value

Then your handler would be changed to example.handler_name

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 return 'example.handler_name';
6 }
7 
8 public function package()
9 {
10 // TODO: Implement package() method.
11 }
12}

If your handler file is nested in a subdirectory, you can prepend the path:

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 return 'resources/lambda/image.handle';
6 }
7 
8 public function package()
9 {
10 // TODO: Implement package() method.
11 }
12}

By default, all paths are relative to your application's base_path. Continue reading for tips on how to customize that.

To read more about what the handler should be based on your runtime, see the following pages in the AWS documentation:

Deployment Package

In order for your Lambda to run, you'll need at least the one file that contains your handler. In reality, you'll likely have many dependencies that support your handler. Sidecar will gather all of those files and zip them up so that they can be delivered to Lambda.

As the developer your only job is to define what files should be included, and optionally which ones should be excluded.

In its simplest use, you can return an array from the package method.

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 return 'image.handle';
6 }
7 
8 public function package()
9 {
10 return [
11 'resources/lambda/image.js'
12 ];
13 }
14}

You can include entire directories. You can also exclude files by prepending an exclamation mark !.

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 return 'image.handle';
6 }
7 
8 public function package()
9 {
10 return [
11 // Include the whole directory
12 'resources/lambda',
13 
14 // But not this file
15 '!resources/lambda/ignore.js'
16 ];
17 }
18}
Code highlighting powered by torchlight.dev, a Hammerstone product.

The Package Class

If you need a little more fine-grained control over the packaging process, you can use the Package class instead of the simpler array format.

Continuing on our example above, you can pass a set of paths into the constructor or the static make method.

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 return 'image.handle';
6 }
7 
8 public function package()
9 {
10 return Package::make([
11 // Include the whole directory
12 'resources/lambda',
13 
14 // But not this file
15 '!resources/lambda/ignore.js'
16 ]);
17 }
18}

By default, all paths are relative to your application's base_path(). If you'd like to change that, you can use the setBasePath method.

Note that you'll need to set the base path before you pass in any paths, so you'll need to use the include and exclude methods instead of passing paths through the constructor.

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 return 'image.handle';
6 }
7 
8 public function package()
9 {
10 return Package::make()
11 // Set the base path to the application's resource path.
12 ->setBasePath(resource_path())
13 ->include([
14 // Include the whole directory
15 'lambda',
16 ])
17 ->exclude([
18 // But not this file
19 'lambda/ignore.js'
20 ]);
21 }
22}

If you'd like to include the every file in your configured basePath, you can pass in * (asterisk) as a special path.

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 return 'image.handle';
6 }
7 
8 public function package()
9 {
10 return Package::make()
11 // Set the base path to the a folder named `lambda`
12 // in the application's resource path.
13 ->setBasePath(resource_path('lambda'))
14 // Include that whole directory.
15 ->include('*');
16 }
17}

Package Reuse

As Sidecar is building your package to be uploaded to S3, it creates a hash of the contents of every file included in the package. If Sidecar determines that this exact package has already been built, it will reuse the zip file that exists on S3 instead of creating a new one.

Package Limitations

There is a 250mb hard upper limit of imposed by Amazon on the size of your Lambda package. That means that when your package is uncompressed it must be smaller that 250mb, including all of your layers.

You can read it the official docs here or in a good real-world boundary-testing article here.

Note that because Sidecar uses the S3-to-Lambda method, the 250mb limit applies, not the 50mb direct upload limit.

Strategies for Dealing With node_modules

With the 250mb limitation in mind, you may need to consider some alternate strategies regarding your node_modules directory, as it can easily grow beyond 250mb with very little effort on your part.

If shipping your entire node_modules folder is viable, that is certainly the easiest route.

Separate Modules

Since your Lambda functions live inside your main Laravel application, it may be tempting to npm install [something] from your app's base directory and add the new dependency to your root package.json.

However, this means that you'll have to ship your application's node_modules directory, which will undoubtedly contain lots of modules you don't need.

Consider instead making a new package.json solely for your Lambda functions. If you store all of your Lambda-specific code in resources/lambda, consider having a package.json and node_modules in that directory, containing only the modules needed for your functions to run, not the modules required by your Laravel application.

  • app
  • bootstrap
  • config
  • database
  • node_modules ← Modules for your main application
  • public
  • resources
    • css
    • lambda
      • image.js
      • node_modules ← Modules just for your Lambda
      • package.json
    • js
  • routes
  • storage
  • tests
  • vendor
  • package.json

This will give you a much smaller node_modules directory, that you may be able to ship directly to Lambda.

Compiling Your Handler with NCC

Sometimes even when you separate your node_modules, you may still end up with hundreds of megabytes of module code that you don't necessarily need. If that's the case, your best option is going to be to use a tool called NCC to compile your entire handler + supporting code down to a single file.

NCC is a tool developed by Vercel for the explicit purposes of compiling Node.js modules into a single file. From their readme:

[A] simple CLI for compiling a Node.js module into a single file, together with all its dependencies, gcc-style.

NCC is a wrapper around webpack that finds all the code needed to execute your handler and rolls it up into a single file.

Continuing with the example from earlier, you would develop your image.js handler the same as you have been, and then run NCC over it:

1ncc build resources/lambda/image.js -o resources/lambda/dist

This will produce a dist folder in your lambda folder.

  • app
  • bootstrap
  • config
  • database
  • node_modules
  • public
  • resources
    • css
    • lambda
      • dist ← Built by NCC
        • index.js ← Your handler code + all supporting modules
      • image.js
      • node_modules
      • package.json
    • js
  • routes
  • storage
  • tests
  • vendor
  • package.json

And you'd now update your Sidecar class to point to the dist folder:

1class ExampleFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 // NCC outputs a single file named "index", not "image".
6 return 'index.handle';
7 }
8 
9 public function package()
10 {
11 return Package::make()
12 // Set the base path to the NCC built folder
13 ->setBasePath(resource_path('lambda/dist'))
14 // Include the whole directory.
15 ->include('*');
16 }
17}

Because NCC takes a single file as input and generates a single file as an output, you will need to put all of your handlers in one file if you have multiple functions.

If you had three functions named image, screenshot, and thumbnail, you might have a file called handlers.js that only exists to export all of your handlers:

lambda/handlers.js

1const Image = require('./handlers/image');
2const Screenshot = require('./handlers/screenshot');
3const Thumbnail = require('./handlers/thumbnail');
4 
5exports.image = Image.handle;
6exports.screenshot = Screenshot.handle;
7exports.thumbnail = Thumbnail.handle;

Now you can run NCC on the handlers file:

1ncc build resources/lambda/handlers.js -o resources/lambda/dist

And update your Sidecar function to point to the specific named export:

1class ScreenshotFunction extends LambdaFunction
2{
3 public function handler()
4 {
5 // The export named "screenshot"
6 return 'index.screenshot';
7 }
8 
9 public function package()
10 {
11 return Package::make()
12 // Set the base path to the NCC built folder
13 ->setBasePath(resource_path('lambda/dist'))
14 // Include the whole directory.
15 ->include('*');
16 }
17}
Code highlighting powered by torchlight.dev, a Hammerstone product.

Further Reading

To read more about what you can and should include in your package, based on your runtime, see the following pages in Amazon's documentation: