Tal Rofe
Tal Rofe

Reputation: 1864

Run AWS Lambda function locally with Docker & Hot Reload in monorepo environment

Demo of the application use case is here for your convenience: https://github.com/tal-rofe/demo-lambda


I set up a monorepo environment, where I simply have apps folder with 2 applications, frontend and pixel-api.

I want to run these both locally with hot reload. Meaning- whenever I modify each project's source code, the artifacts refreshes. For example I run the frontend application with NextJS so it is easily supported by running next dev inside the docker container and using Bind Mount to the source code on host machine.

Problem is I try to do the same with Lambda function. AWS support the sam local start-api (https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-start-api.html) command which is helpful, but I rather not install the sam CLI to a developer machine.

So my intention is to run the SAM CLI inside Docker.

I found this comment describing how to do so: https://github.com/aws/aws-sam-cli/issues/55#issuecomment-1717711860 And also this: https://github.com/aws/aws-sam-cli/issues/55#issuecomment-1717819156

I copied the code and tried to adjust it to my project but didn't go well.

Root/docker-compose.dev.yaml:

version: '3.8'

services:
    pixel-api:
        build:
            context: .
            dockerfile: ./docker/Dockerfile.pixel-api-dev
        command: sam local start-api -t "${PWD}/apps/pixel-api/template.yaml" -v "${PWD}/.aws-sam/build" --host=0.0.0.0 --container-host=host.docker.internal --container-host-interface=127.0.0.1
        ports:
            - '3000:3000'
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock:ro
            - ${PWD}:${PWD}:ro
            - /dashboard/apps/pixel-api/node_modules
            - /dashboard/node_modules
        environment:
            SAM_CLI_TELEMETRY: 0
            SAM_CLI_CONTAINER_CONNECTION_TIMEOUT: 30
            sam_local_environment: 'true'

    frontend:
        container_name: frontend
        build:
            context: .
            dockerfile: ./docker/Dockerfile.frontend-dev
        env_file:
            - ./apps/frontend/envs/.env.development
        ports:
            - 8080:8080
        restart: always
        networks:
            - dashboard_network
        volumes:
            - type: bind
              source: ./apps/frontend/src
              target: /dashboard/apps/frontend/src
            - /dashboard/apps/frontend/node_modules

networks:
    dashboard_network:
        driver: bridge

The "frontend" service works as intended and succeeds.

Root/apps/pixel-api/template.yaml:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM template for running locally API Gateway & Lambda function for Pixel API function

Globals:
    Api:
        Cors:
            AllowMethods: "'POST,OPTIONS'"
            AllowHeaders: "'content-type','x-api-key','x-amz-security-token','authorization','x-amz-user-agent','x-amz-date'"
            AllowOrigin: "'*'"
            AllowCredentials: "'*'"

Resources:
    AwsApiGateway:
        Type: AWS::Serverless::HttpApi
        Properties:
            Name: AWS API Gateway - Pixel API
            StageName: development

    MyLambdaFunction:
        Type: 'AWS::Serverless::Function'
        Properties:
            Runtime: nodejs20.x
            Handler: index.handler
            CodeUri: build
            Timeout: 30
            Description: 'Pixel API Lambda function'
            Environment:
                Variables:
                    sam_local_environment: 'true'
            Events:
                Api:
                    Type: HttpApi
                    Properties:
                        ApiId: !Ref AwsApiGateway
                        Path: /
                        Method: POST

I also have "Root/apps/pixel-api/build/index.js` file as for the Lambda function NodeJS function.

I also have the Dockerfile for the pixel-api service:

Root/docker/Dockerfile.pixel-api-dev:

FROM public.ecr.aws/docker/library/docker

ENV PIP_BREAK_SYSTEM_PACKAGES 1

RUN apk update && \
    apk add gcc py-pip python3-dev libffi-dev musl-dev && \
    pip install --upgrade pip setuptools wheel && \
    pip install --upgrade aws-sam-cli

WORKDIR /var/opt

ENTRYPOINT []
CMD ["/bin/bash"]

When I run the services using docker-compose up -d, both services succeeds. But when I run curl -X POST http://localhost:3000 - invoking the Lambda function fails:

Mounting MyLambdaFunction at http://0.0.0.0:3000/ [POST]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. If you used sam build before running local commands, you will need to re-run sam build for the changes to be picked up. You only need to restart SAM CLI if you update your AWS SAM template
2024-03-20 07:24:30 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:3000
 * Running on http://172.20.0.2:3000
2024-03-20 07:24:30 Press CTRL+C to quit
Invoking index.handler (nodejs20.x)
Local image was not found.
Removing rapid images for repo public.ecr.aws/sam/emulation-nodejs20.x
Building image..........................................................................................................................................................................................................................................................................................................................................................
Using local image: public.ecr.aws/lambda/nodejs:20-rapid-x86_64.

Mounting /MY_MACHINE_PATH/dashboard/.aws-sam/build/build as /var/task:ro,delegated, inside runtime container
START RequestId: 3e76c0ee-26bb-4250-983c-f60252289641 Version: $LATEST
2024-03-20T07:25:35.076Z        undefined       ERROR   Uncaught Exception  {"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'index'\nRequire stack:\n- /var/runtime/index.mjs","stack":["Runtime.ImportModuleError: Error: Cannot find module 'index'","Require stack:","- /var/runtime/index.mjs","    at _loadUserApp (file:///var/runtime/index.mjs:1087:17)","    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)","    at async start (file:///var/runtime/index.mjs:1282:23)","    at async file:///var/runtime/index.mjs:1288:1"]}
20 Mar 2024 07:25:35,212 [ERROR] (rapid) Init failed error=Runtime exited with error: exit status 129 InvokeID=
20 Mar 2024 07:25:35,216 [ERROR] (rapid) Invoke failed error=Runtime exited with error: exit status 129 InvokeID=33a8293f-9315-4d2a-808f-265f5620f20f
20 Mar 2024 07:25:35,218 [ERROR] (rapid) Invoke DONE failed: Sandbox.Failure

2024-03-20 07:25:36 192.168.65.1 - - [20/Mar/2024 07:25:36] "POST / HTTP/1.1" 500 -

My questions:

  1. Note that I didn't run sam build at all, I don't even have sam CLI on my host machine. If I need to do so, how can I do it inside the Docker?
  2. After trying to use nodejs20.x version for my function, as you can see from the logs I attached:
Local image was not found.
Removing rapid images for repo public.ecr.aws/sam/emulation-nodejs20.x
Building image..........................................................................................................................................................................................................................................................................................................................................................
Using local image: public.ecr.aws/lambda/nodejs:20-rapid-x86_64.

So, OK, no image, but it runs after first request to lambda fired. Is there a way to do it ahead of time, on Dockerfile.pixel-api-dev? So first request won't need to wait the image to download...

  1. And of-course the main question - why did it fail to find my Lambda function?

  2. Also, how can I enable hot reload for this lambda function so every time I modify the Root/apps/pixel-api/build/index.js file, the lambda functions refreshes on localhost:3000? Note that comment from the logs I attached:

You do not need to restart/reload SAM CLI while working on your functions,
changes will be reflected instantly/automatically.
If you used sam build before running local commands, you will need to re-run sam build for the changes to be picked up

Again, I don't want to use sam CLI on my host machine, if possible.

Upvotes: 5

Views: 618

Answers (1)

Tal Rofe
Tal Rofe

Reputation: 1864

This is not a direct solution but rather migration solution to other framework. As I don't mind the underlying framework used to test my Lambda function code, this solution is good for me.

I migrated the development framework to use the serverless framework instead. Then I just created this serverless.yaml file:

org: kynesis
app: pixel-api
console: true
service: pixel-http-api
frameworkVersion: '3'
configValidationMode: error

provider:
    name: aws
    httpApi:
        cors:
            allowedOrigins:
                - '*'
            allowedHeaders:
                - Content-Type
            allowedMethods:
                - POST
            allowCredentials: false

custom:
    serverless-offline:
        # * https://forum.serverless.com/t/possible-to-run-serverless-in-a-docker-container/5764/4
        host: 0.0.0.0

plugins:
    - serverless-offline

functions:
    pixel-api:
        name: pixel-api
        runtime: nodejs20.x
        handler: ./build/index.handler
        events:
            - httpApi:
                  path: /
                  method: POST

modified my docker compose file to:

version: '3.8'

services:
    pixel-api:
        container_name: pixel-api
        build:
            context: .
            dockerfile: ./docker/Dockerfile.pixel-api-dev
        ports:
            - 3000:3000
        restart: always
        networks:
            - dashboard_network
        volumes:
            - type: bind
              source: ./apps/pixel-api/src
              target: /dashboard/apps/pixel-api/src
            - /dashboard/apps/pixel-api/node_modules
            - /dashboard/node_modules

    frontend:
        container_name: frontend
        build:
            context: .
            dockerfile: ./docker/Dockerfile.frontend-dev
        env_file:
            - ./apps/frontend/envs/.env.development
        ports:
            - 8080:8080
        restart: always
        networks:
            - dashboard_network
        volumes:
            - type: bind
              source: ./apps/frontend/src
              target: /dashboard/apps/frontend/src
            - /dashboard/apps/frontend/node_modules
            - /dashboard/node_modules

networks:
    dashboard_network:
        driver: bridge

modified my Dockerfile to

FROM node:20.11.1

RUN npm i -g [email protected]

WORKDIR /dashboard

COPY ./package.json ./pnpm-workspace.yaml ./.npmrc ./
COPY ./apps/pixel-api/package.json ./apps/pixel-api/

RUN pnpm i -w
RUN pnpm --filter pixel-api i

COPY ./tsconfig.base.json ./nx.json ./
COPY ./apps/pixel-api/ ./apps/pixel-api/

CMD ["pnpm", "exec", "nx", "start:dev", "blabla"]

Then I also configured nodemon.json file to rebuild my Lambda code and restart the serverless framework server:

{
    "$schema": "https://json.schemastore.org/nodemon.json",
    "watch": ["./src", "./serverless.yaml"],
    "ext": "ts,json",
    "exec": "node ./esbuild.js && sls offline --httpPort 3000"
}

Upvotes: 0

Related Questions