Search code examples
amazon-web-servicesaws-lambdaaws-sam-cli

Why Can't my AWS Lambda Find my Handler's Type When I run `sam local invoke -e event.json`


I am trying to write a .net6.0 AWS Lambda using a docker container. The .net solution has 3 projects that I need to deploy in my Lambda. Let's call them "EventListener," "EventListenerInterfaces," and "EventlistenerModels."

Each of these projects is in their own directory beneath the dockerfile

C:\src\event-listener/
 |-events/
 |  |-myEvent.json
 |-EventListener/
 |  |-EventListener.csproj
 |  |-EventListener.cs
 |  |-<various other cs files>
 |-EventListenerInterfaces/
 |  |-EventListenerInterfaces.csproj
 |  |-<various cs files>
 |-EventListenerModels/
 |  |-EventListenerModels.csproj
 |  |-<various cs files>
 |-Dockerfile
 |-aws-lambda-tools-defaults.json
 |-template.yaml

The cs file with the entry point I want for my lambda is:

using Amazon.Lambda.Core;
using Amazon.Lambda.SNSEvents;
using EventListener.Classes;
using EventListener.Classes.Events;
using EventListenerInterfaces.Interfaces;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace EventListener;

public class EventListener
{
    public static async Task<string> QueryMemberInfo(ILambdaContext context)
    {
        //set up the HttpClient for sending requests to Manage (for now, will later be used to hit IAM endpoints)
        LambdaHttpClient httpClient = new LambdaHttpClient();

        //async call to get Member information about Member_RecId 160
        HttpResponseMessage response = await httpClient.GetAsync("v4_6_release/apis/3.0/system/members/160");
        //print out the request's response
        return response.Content.ReadAsStringAsync().Result;
    }


    public void EventHandler(SNSEvent.SNSMessage incomingMessage, ILambdaContext context)
    {
        if (incomingMessage != null)
        {
            EventHandlerFactory EventFactory = new EventHandlerFactory();
            IEventHandler Event = EventFactory.GetEvent(incomingMessage);
            Event.PerformEventAction();
        }
        else
        {
            LambdaLogger.Log("Incoming message was null.");
        }
    }
}

I want the entry point to be the EventHandler method.

From what I understand about assemblies, namespaces, types, and methods, the following should be true of this EventListener class:

  • Assembly: EventListener
  • Namespace: EventListener
  • Class: EventListener
  • Method: EventHandler

Thus, the full signature that the Lambda should be looking for is:

EventListener::EventListener.EventListener::EventHandler

My Dockerfile, which is a modified version of the one generated by the sam cli is as follows:

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-image

ARG FUNCTION_DIR="/build"
ARG EVENT_LISTENER_DIR="${FUNCTION_DIR}/EventListener"
ARG INTERFACES_DIR="${FUNCTION_DIR}/EventListenerInterfaces"
ARG MODELS_DIR="${FUNCTION_DIR}/EventListenerModels"
ARG SAM_BUILD_MODE="run"
ENV PATH="/root/.dotnet/tools:${PATH}"
ENV LAMBDA_NET_SERIALIZER_DEBUG="true"

RUN apt-get update && apt-get -y install zip

RUN mkdir $FUNCTION_DIR
WORKDIR $FUNCTION_DIR
COPY aws-lambda-tools-defaults.json $FUNCTION_DIR/

COPY EventListener/EventListener.csproj $EVENT_LISTENER_DIR/
COPY EventListenerInterfaces/EventListenerInterfaces.csproj $INTERFACES_DIR/
COPY EventListenerModels/EventListenerModels.csproj $MODELS_DIR/

RUN dotnet tool install -g Amazon.Lambda.Tools

# Build and Copy artifacts depending on build mode.
RUN mkdir -p build_artifacts
RUN if [ "$SAM_BUILD_MODE" = "debug" ]; \
        then dotnet lambda package --project-location /build/EventListener/ --configuration Debug; \
        else dotnet lambda package --project-location /build/EventListener/ --configuration Release; \
    fi
RUN if [ "$SAM_BUILD_MODE" = "debug" ]; \
        then cp -r ${EVENT_LISTENER_DIR}/bin/Debug/net6.0/publish/* /build/build_artifacts; \
        else cp -r ${EVENT_LISTENER_DIR}/bin/Release/net6.0/publish/* /build/build_artifacts; \
    fi

FROM public.ecr.aws/lambda/dotnet:6

COPY --from=build-image /build/build_artifacts/ /var/task/
# Command can be overwritten by providing a different command in the template directly.
CMD ["EventListener::EventListener.EventListener::EventHandler"]

As you can see, I have the Assembly::Namespace.Type::Method signature in the last line of the Dockerfile.

Executing sam build works just fine, and succeeds. The command docker build . run from the C:\src\event-listener directory also succeeds.

However, when I run sam local invoke -e .\events\myevent.json I get the following error:

fail    Amazon.Lambda.RuntimeSupport.ExceptionHandling.LambdaValidationException: Unable to load type 'EventListener.EventListener' from assembly 'EventListener'.
   at Amazon.Lambda.RuntimeSupport.Bootstrap.UserCodeLoader.Init(Action`1 customerLoggingAction) in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs:line 112
   at Amazon.Lambda.RuntimeSupport.Bootstrap.UserCodeInitializer.InitializeAsync() in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeInitializer.cs:line 46
   at Amazon.Lambda.RuntimeSupport.LambdaBootstrap.InitializeAsync() in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs:line 155
...

I also tried adding /var/task/ to the assembly name in the final line of the Dockerfile (thinking that maybe it couldn't find the assembly), but that ended up giving me the following error:

fail    System.NullReferenceException: Object reference not set to an instance of an object.
   at Amazon.Lambda.RuntimeSupport.Bootstrap.UserCodeLoader.Init(Action`1 customerLoggingAction) in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs:line 105
   at Amazon.Lambda.RuntimeSupport.Bootstrap.UserCodeInitializer.InitializeAsync() in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeInitializer.cs:line 46
   at Amazon.Lambda.RuntimeSupport.LambdaBootstrap.InitializeAsync() in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs:line 155

At this point, I'm certain that something is misconfigured, but I can't for the life of me figure out where that configuration could be wrong.

I've found multiple threads here on StackOverflow and elsewhere that suggested issues with my template.yaml or my aws-lambda-tools-defaults.json but none of the suggestions in those threads produced any different results, or even different errors.

For reference, my template.yaml is as follows:

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: C# Event Listener Lambda
Resources:
    EventListener:
      Type: AWS::Serverless::Function
      Properties:
        PackageType: Image
        #CodeUri: ./EventListenerDotNet6
        #Handler: EventListenerDotNet6::EventListenerDotNet6.Function::FunctionHandler
        #Runtime: dotnet6
        #Architectures:
        #  - x86_64
        Environment:
          Variables:
            BASE_URL: "url_goes_here"
            AUTHORIZATION_HEADER: "authorization_goes_here"
            CLIENT_ID: "clientId_goes_here"
      Metadata:
        DockerTag: EventListener
        DockerContext: ./
        Dockerfile: Dockerfile

My aws-lambda-tools-defaults.json is as follows:

{
    "Information": [
      "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
      "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
      "dotnet lambda help",
      "All the command line options for the Lambda command can be specified in this file."
    ],
    "profile": "<REDACTED>",
    "region": "us-east-1",
    "configuration": "Release",
    "function-architecture": "x86_64",
    "function-runtime": "dotnet6",
    "function-memory-size": 256,
    "function-timeout": 30,
    "function-handler": "EventListener::EventListener.EventListener::EventHandler"
  }

Please help me!


Solution

  • Did you confirm that your DLLs are created in the image? Based on your Dockerfile, it looks like you're only copying the .csproj files into your image, not the rest of the files for it to build properly

    COPY EventListener/EventListener.csproj $EVENT_LISTENER_DIR/
    COPY EventListenerInterfaces/EventListenerInterfaces.csproj $INTERFACES_DIR/
    COPY EventListenerModels/EventListenerModels.csproj $MODELS_DIR/
    

    Try copying the entire contents of the directories

    COPY EventListener/ $EVENT_LISTENER_DIR/
    COPY EventListenerInterfaces/ $INTERFACES_DIR/
    COPY EventListenerModels/ $MODELS_DIR/
    

    If the DLLs aren't present in the container, I'd expect to see the error you're getting