AWS Lambda с отображением маршрутов веб-сокетов на конечные точки http в контроллерах и тестированием

#c# #asp.net-core #websocket #aws-lambda #aws-api-gateway

#c# #asp.net-ядро #websocket #aws-lambda #aws-api-gateway

Вопрос:

Я новичок в AWS Lambda, увлекательная штука. Я играю с некоторыми сценариями, интегрирующими AWS Lambda netcoreapp3.1 , разработанную с помощью AWS API Gateway. Я видел, что AWS Api Gateway поддерживает управление веб-сокетами, и это здорово, потому что он поддерживает открытые соединения и отправляет любой фрейм на серверную лямбду, а также предоставляет некоторую защищенную конечную точку POST для отправки сообщений из серверной лямбды клиенту, подключенному к веб-сокету шлюза. Вся тяжелая работа там.

Я также видел, что существует некоторый пакет для разработчиков C #, который позволяет использовать лямбда-приложения так, как если бы они были стандартным веб-api. См. https://aws.amazon.com/blogs/compute/announcing-aws-lambda-supports-for-net-core-3-1 Это фантастика, потому что я, честно говоря, не понимаю, почему некоторые разработчики всегда пытаются иметь одну лямбду на конечную точку, когда лямбда может использоваться для внутренней маршрутизации к любой конечной точке и использовать все преимущества универсального хостинга для aspnet core 3.1 плюс ведение журнала, контейнер для внедрения зависимостей из коробкии т.д. и очень хороший способ протестировать все, как если бы это было приложение web api, не беспокоясь о точке входа в лямбда. Смотрите пакет https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon .Лямбда.AspNetCoreServer

Точка входа lambda выглядит следующим образом. Как вы можете видеть, я просто наследую от APIGatewayProxyFunction класса пакета, и все использует то же самое, как если бы я запускал его локально как веб-приложение kestrel.

 public class LambdaEntryPoint
    : APIGatewayProxyFunction
{
    protected override void Init(IWebHostBuilder builder)
    {
        builder
            .UseStartup<Startup>();
    }
}
 

Program.cs Локальная точка входа или при запуске как веб-приложение kestrel

 public static class LocalEntryPoint
{
    private static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    private static IHostBuilder CreateHostBuilder(string[] args)
    {
        return Host
            .CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(
                webBuilder => { webBuilder.UseStartup<Startup>(); });
    }
}
 

Startup.cs Выглядит как любой стандартный запуск, с регистрациями и http-конвейером, и ничего не знает о том, выполняется ли он как lambda или как стандартное веб-приложение (здорово, не так ли? Если бы я только мог использовать это приятным способом для веб-сокетов и протестировать его …)

 public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }
}
 

Having said that, I haven’t been lucky to find any example on how to use an AWS Lambda with AspNetCore for web socket connectivity.

Ideally I’d like my lambda application to be a web api with local and lambda entrypoint as described in that package (it has some nice samples) or by using the dotnet template called serverless.AspNetCoreWebAPI , but where I could also use it to map web socket routes, so that the same lambda application works with API Gateway REST and API Gateway Websocket

I have seen some workarounds like this https://github.com/aws/aws-lambda-dotnet/issues/644#issuecomment-621528565 where the methods MarshallRequest and PostMarshallRequestFeature are overriden, but I don’t fully understand some things such as some class types and that brings me to the following questions:

  • Can I have a lambda to serve both integration with REST and WebSocket Api Gateway? Is there any potential issue/recommendation to avoid it?
  • How does API Gateway invoke the lambda function and how could I test this functionality? I’ve had a look at the https://github.com/aws/aws-lambda-dotnet/tree/master/Tools/LambdaTestTool and configured it to debug it with rider, but I am absolutely lost when having to test some websocket input. How to do that?

Ta.


UPDATE 1 2021-02-23:

Thanks. As per the event payload samples here they are:

  • When $connect with wscat -c wss://{api_id}.execute-api.eu-west-3.amazonaws.com/Prod the APIGatewayProxyRequest is:
     {
        "Resource": null,
        "Path": null,
        "HttpMethod": null,
        "Headers": {
            "Host": "xxx.execute-api.eu-west-3.amazonaws.com",
            "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits",
            "Sec-WebSocket-Key": "xxx==",
            "Sec-WebSocket-Version": "13",
            "X-Amzn-Trace-Id": "Root=1-6034bfdc-53e605153673d3cb0d0c4177",
            "X-Forwarded-For": "185.153.165.122",
            "X-Forwarded-Port": "443",
            "X-Forwarded-Proto": "https"
        },
        "MultiValueHeaders": {
            "Host": [
                "xxx.execute-api.eu-west-3.amazonaws.com"
            ],
            "Sec-WebSocket-Extensions": [
                "permessage-deflate; client_max_window_bits"
            ],
            "Sec-WebSocket-Key": [
                "xxx=="
            ],
            "Sec-WebSocket-Version": [
                "13"
            ],
            "X-Amzn-Trace-Id": [
                "Root=1-6034bfdc-53e605153673ddcb0d0cxxx"
            ],
            "X-Forwarded-For": [
                "185.153.165.122"
            ],
            "X-Forwarded-Port": [
                "443"
            ],
            "X-Forwarded-Proto": [
                "https"
            ]
        },
        "QueryStringParameters": null,
        "MultiValueQueryStringParameters": null,
        "PathParameters": null,
        "StageVariables": null,
        "RequestContext": {
            "Path": null,
            "AccountId": null,
            "ResourceId": null,
            "Stage": "Prod",
            "RequestId": "xxx=",
            "Identity": {
                "CognitoIdentityPoolId": null,
                "AccountId": null,
                "CognitoIdentityId": null,
                "Caller": null,
                "ApiKey": null,
                "ApiKeyId": null,
                "AccessKey": null,
                "SourceIp": "185.153.165.122",
                "CognitoAuthenticationType": null,
                "CognitoAuthenticationProvider": null,
                "UserArn": null,
                "UserAgent": null,
                "User": null,
                "ClientCert": null
            },
            "ResourcePath": null,
            "HttpMethod": null,
            "ApiId": "xxx",
            "ExtendedRequestId": "xxx=",
            "ConnectionId": "xxx=",
            "ConnectionAt": 0,
            "DomainName": "xxx.execute-api.eu-west-3.amazonaws.com",
            "DomainPrefix": null,
            "EventType": "CONNECT",
            "MessageId": null,
            "RouteKey": "$connect",
            "Authorizer": null,
            "OperationName": null,
            "Error": null,
            "IntegrationLatency": null,
            "MessageDirection": "IN",
            "RequestTime": "23/Feb/2021:08:42:04  0000",
            "RequestTimeEpoch": 1614069724309,
            "Status": null
        },
        "Body": null,
        "IsBase64Encoded": false
    }
     

    and the APIGatewayProxyResponse is:

     {
        "statusCode": 200,
        "headers": null,
        "multiValueHeaders": null,
        "body": "Connected.",
        "isBase64Encoded": false
    }
     
  • When sendmessage {"message":"sendmessage", "data":"hello world"} the APIGatweayProxyRequest is:
     {
        "Resource": null,
        "Path": null,
        "HttpMethod": null,
        "Headers": null,
        "MultiValueHeaders": null,
        "QueryStringParameters": null,
        "MultiValueQueryStringParameters": null,
        "PathParameters": null,
        "StageVariables": null,
        "RequestContext": {
            "Path": null,
            "AccountId": null,
            "ResourceId": null,
            "Stage": "Prod",
            "RequestId": "xxx=",
            "Identity": {
                "CognitoIdentityPoolId": null,
                "AccountId": null,
                "CognitoIdentityId": null,
                "Caller": null,
                "ApiKey": null,
                "ApiKeyId": null,
                "AccessKey": null,
                "SourceIp": "185.153.165.122",
                "CognitoAuthenticationType": null,
                "CognitoAuthenticationProvider": null,
                "UserArn": null,
                "UserAgent": null,
                "User": null,
                "ClientCert": null
            },
            "ResourcePath": null,
            "HttpMethod": null,
            "ApiId": "xxx",
            "ExtendedRequestId": "xxx=",
            "ConnectionId": "xxx=",
            "ConnectionAt": 0,
            "DomainName": "4air0zk9w1.execute-api.eu-west-3.amazonaws.com",
            "DomainPrefix": null,
            "EventType": "MESSAGE",
            "MessageId": "xxx=",
            "RouteKey": "sendmessage",
            "Authorizer": null,
            "OperationName": null,
            "Error": null,
            "IntegrationLatency": null,
            "MessageDirection": "IN",
            "RequestTime": "23/Feb/2021:08:42:33  0000",
            "RequestTimeEpoch": 1614069753002,
            "Status": null
        },
        "Body": "{"message":"sendmessage", "data":"hello world"}",
        "IsBase64Encoded": false
    }
     

    и APIGatewayProxyResponse является:

     {
        "statusCode": 200,
        "headers": null,
        "multiValueHeaders": null,
        "body": "Data sent to 1 connection",
        "isBase64Encoded": false
    }
     
  • Когда $disconnect запрос APIGateway выполняется:
     {
        "Resource": null,
        "Path": null,
        "HttpMethod": null,
        "Headers": {
            "Host": "xxx.execute-api.eu-west-3.amazonaws.com",
            "x-api-key": "",
            "X-Forwarded-For": "",
            "x-restapi": ""
        },
        "MultiValueHeaders": {
            "Host": [
                "xxx.execute-api.eu-west-3.amazonaws.com"
            ],
            "x-api-key": [
                ""
            ],
            "X-Forwarded-For": [
                ""
            ],
            "x-restapi": [
                ""
            ]
        },
        "QueryStringParameters": null,
        "MultiValueQueryStringParameters": null,
        "PathParameters": null,
        "StageVariables": null,
        "RequestContext": {
            "Path": null,
            "AccountId": null,
            "ResourceId": null,
            "Stage": "Prod",
            "RequestId": "xxx=",
            "Identity": {
                "CognitoIdentityPoolId": null,
                "AccountId": null,
                "CognitoIdentityId": null,
                "Caller": null,
                "ApiKey": null,
                "ApiKeyId": null,
                "AccessKey": null,
                "SourceIp": "185.153.165.122",
                "CognitoAuthenticationType": null,
                "CognitoAuthenticationProvider": null,
                "UserArn": null,
                "UserAgent": null,
                "User": null,
                "ClientCert": null
            },
            "ResourcePath": null,
            "HttpMethod": null,
            "ApiId": "xxx",
            "ExtendedRequestId": "xxxA=",
            "ConnectionId": "xxx=",
            "ConnectionAt": 0,
            "DomainName": "xxx.execute-api.eu-west-3.amazonaws.com",
            "DomainPrefix": null,
            "EventType": "DISCONNECT",
            "MessageId": null,
            "RouteKey": "$disconnect",
            "Authorizer": null,
            "OperationName": null,
            "Error": null,
            "IntegrationLatency": null,
            "MessageDirection": "IN",
            "RequestTime": "23/Feb/2021:08:42:41  0000",
            "RequestTimeEpoch": 1614069761070,
            "Status": null
        },
        "Body": null,
        "IsBase64Encoded": false
    }
     

    и APIGatewayProxyResponse является:

     {
        "statusCode": 200,
        "headers": null,
        "multiValueHeaders": null,
        "body": "Disconnected.",
        "isBase64Encoded": false
    }
     

Комментарии:

1. Привет. Это хороший и интересный вопрос. К сожалению, я не знаю ответа, и, как вы упомянули, моя компания сопоставляет отдельные лямбды непосредственно с конечными точками. Хотя мне любопытно, что произойдет, если вы добавите пакет SignalR и сопоставите свои концентраторы с конечными точками, как мы делаем это в стандарте asp.net основное приложение? Вот так: endpoints.MapControllers(); endpoints.MapHub<YourHub>("/signalr/yourHub"); ?

2. Из того, что я прочитал здесь github.com/dotnet/aspnetcore/issues/9522 это не что-то легкое или даже возможное. В конце шлюз api обрабатывает соединения, и лямбда «просто» должна реагировать на подключение, отключение и другие события