0%
September 6, 2024

Lambda Function Running Nodejs Docker Image

aws

docker

lambda

Scenario

  • My teamate used my template to run an express application in zipped fashion.

  • One day we are asked to create pdf using react-pdf and sharp for resizing images.

  • The sharp dependency must be built by linux machine, luckily we can find a sharp-layer.zip online and simply upload it to our lambda layer registry.

  • Zip-based lambda function will combine lambda layer when finalizing the total size, unfortunately however hard we try we still exceed the 250MB limit by 6MB.

The next comes to the rescue!

Running Docker Image by Lambda Function

This approach solve our problem in two-fold:

  • For one it bypasses the 250MB limit constriant;
  • For second it helps solve the complicated dependencies problem because we install everything we need when building the image.
Serverless Framework Version
package.json
{
    "scripts": {
        "build": "tsc",
        ...
    },
    ...
}
serverless.yml

Be careful the highlighted lights must be the same:

service: some-pdf-generator

provider:
  name: aws
  runtime: nodejs18.x
  stage: prod
  region: ap-southeast-2
  timeout: 900
  iam:
    role:
      name: ${self:service}-${self:provider.stage}-role
  environment:
    FILE_STORAGE: /tmp/files
    PORT: 3033
    FRONTEND_URL: some-url
  ecr:
    # This allows Serverless to automatically handle image pushing
    images:
      nodejs-pdf-generator:
        path: ./

functions:
  api:
    image:
      name: nodejs-pdf-generator
      command:
        - dist/server.handler
    timeout: 900
    events:
      - http: ANY /
      - http: ANY /{proxy+}
Dockerfile at the Project Root Directory
FROM public.ecr.aws/lambda/nodejs:18

RUN npm install --arch=x64 --platform=linux sharp

COPY package*.json ./
RUN cat package.json | sed "/^ *\"sharp/d" > package.json.tmp && mv package.json.tmp package.json
RUN npm install
RUN npm install --arch=x64 --platform=linux sharp
COPY . .
# Compile TypeScript to JavaScript
RUN npm run build

# Set the CMD to your handler
CMD [ "dist/server.handler" ]
Dependencies
{
    ...
    "dependencies": {
        "@react-pdf/renderer": "^3.4.4",
        "@types/express": "^4.17.21",
        "@types/probe-image-size": "^7.2.4",
        "aws-sdk": "^2.1534.0",
        "axios": "^1.6.4",
        "express": "^4.18.2",
        "serverless": "^3.38.0",
        "serverless-http": "^3.2.0",
        "ts-node": "^10.9.2",
        "typescript": "^5.3.3"
    },
    "devDependencies": {
        "@babel/core": "^7.25.2",
        "@types/node": "^20.11.0",
    }
}
Entry Files
app.ts
// app.ts
import express, { Request, Response } from "express";

export const app = express();

app.get("/", (req, res) => {
  res.json({
    success: true,
    version: "0.3",
  });
});

app.post("/some-endpoint", some.controller);

const PORT = Number(process.env?.PORT || "3000");
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

process.on("uncaughtException", function (err) {
  console.log(err.stack);
  console.log("Node NOT Exiting...");
});
server.ts
// server.ts
import { app } from "./app";
import * as serverless from "serverless-http";

export const handler = serverless.default(app);