0

I have a private subnet with an ECS cluster where services, without public IP addresses, run behind an Application Load Balancer.

The Application Load Balancer is also private.

I have an API Gateway integrated through HTTP with that ALB (see this) using a VPC Link.

All my microservices work perfectly fine like that.

But now I need to add support for websockets and I need to create an API Gateway with protocol type WEBSOCKET, that has routes with HTTP integation, so that the websocket routes $connect, $disconnect, sendmessage are transformed into HTTP requests.

The problem I have is that in order to use private integration I must specify the ALB's Arn, and I cannot specify a URL (see this). Regarding IntegrationUri:

For an HTTP API private integration, specify the ARN of an Application Load Balancer listener, Network Load Balancer listener, or AWS Cloud Map service. If you specify the ARN of an AWS Cloud Map service, API Gateway uses DiscoverInstances to identify resources. You can use query parameters to target specific resources. To learn more, see DiscoverInstances. For private integrations, all resources must be owned by the same AWS account.

So in IntegrationUri, instead of specifying something such as https://mypublicdomain.com/My.Service/connect where I could easily add the service path as part of the URL, I am forced to add the Application Load Balancer ARN, arn:aws:myalb...

and therefore I cannot play with URL paths to have the websocket routes integrate with a specific service behind the ALB.

In other words, How can I associate websocket routes at API Gateway with a specific service behind a private ALB?

PS: I have thought of using some listener rules with path pattern conditions that grab all traffic going to the ALB root / to the desired service, but that's far from ideal because I would like to route based on something more obvious.


CloudFormation Sample:

Resources:
  websocketApiGateway:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: websocket-gateway
      Description: Api Gateway for websocket
      ProtocolType: WEBSOCKET
      DisableExecuteApiEndpoint: false
      RouteSelectionExpression: $request.body.action

  connectRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref websocketApiGateway
      RouteKey: $connect
      AuthorizationType: NONE
      OperationName: ConnectRoute
      RouteResponseSelectionExpression: $default
      Target: !Join
        - /
        - - integrations
          - !Ref connectIntegration

  connectIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref websocketApiGateway
      Description: Websocket $connect integration
      IntegrationType: HTTP_PROXY
      IntegrationMethod: POST
      IntegrationUri: # I cannot use something like https://mypublicdomain.com/My.Service/connect
        Fn::ImportValue: !Sub my-alb-http-listener-id
      RequestParameters:
        "integration.request.header.domainName": "context.domainName"
        "integration.request.header.stage": "context.stage"
        "integration.request.header.connectionId": "context.connectionId"
      PayloadFormatVersion: 1.0

  connectRouteResponse:
    Type: AWS::ApiGatewayV2::RouteResponse
    Properties:
      ApiId: !Ref websocketApiGateway
      RouteId: !Ref connectRoute
      RouteResponseKey: $default

UPDATE 1: Just after writing the question I thought.. If I can't route based on URI, maybe I can route at the ALB http listener based on some http header that I can set in the websocket's api gateway integration. So I'll keep that as a workaround in case I don't find a way to rewrite url path

UPDATE 2: This question is similar, but the answer is not detailed enough Rewrite destination path in api gateway integration request

diegosasw
  • 13,734
  • 16
  • 95
  • 159
  • Hello, did you find a solution for your problem? I have the same case with small difference. I want to redirect traffic from /foo/bar/baz to /bar/baz (ALB1) and /foo1/bar1/baz1 to /bar1/baz1 (ALB2). I am using http proxy /foo/{proxy+} in routes to send {proxy} part to ECS backend – Soufiene Aug 24 '21 at 15:56
  • @Soufiene I don't have a solution but I implemented a workaround. See my answer in case you don't find a solution to your problem. – diegosasw Aug 25 '21 at 13:11
  • Did you try to override path in request parameters in integration? request_parameters = { "overwrite:path" = "/servicepath/ws/commands" "overwrite:host" = "domain.com" } – aelimill Aug 30 '21 at 12:14
  • @aelimill no.. I suppose going that way I'd have to use regular expressions, unless there is a way to echo anything it receives into the integration. Haven't tried though. – diegosasw Aug 30 '21 at 13:51

1 Answers1

0

I didn't really solved it, but I have a workaround.

At API Gateway level websockets requests are transformed to HTTP requests back to the API Gateway. Not ideal as it'd be better to send the http request internally to the integration, but it works and hopefully it doesn't add much latency since the destination is the very same api gateway. I also add some request headers that allow me, at the web application's level, to extract the needed info that was originally at the websocket message.

listenerRule:
  Type: AWS::ElasticLoadBalancingV2::ListenerRule
  Properties:
    Actions:
      - Type: forward
        TargetGroupArn: !Ref targetGroup
    Conditions:
      - Field: path-pattern
        PathPatternConfig: 
            Values:
              - !Sub
                - ${servicePath}*
                - servicePath: !FindInMap [microservice, settings, path]
    ListenerArn: 
      Fn::ImportValue: !Sub ${prefix}-alb-http-listener-id
    Priority: 2

Now at the api gateway I have my websocket routes. Something like this

wsCommandRoute:
  Type: AWS::ApiGatewayV2::Route
  Properties:
    ApiId: !Ref wsApiGateway
    RouteKey: command
    AuthorizationType: NONE
    OperationName: CommandRoute
    RouteResponseSelectionExpression: $default
    Target: !Join
      - /
      - - integrations
        - !Ref wsCommandIntegration

wsCommandIntegration:
  Type: AWS::ApiGatewayV2::Integration
  Properties:
    ApiId: !Ref wsApiGateway
    Description: Websocket command integration
    IntegrationType: HTTP_PROXY
    IntegrationMethod: POST
    IntegrationUri:
      !Sub 
        - https://${domainName}${websocketServicePath}/ws/commands
        - domainName: !FindInMap [gateways, api, domainName]
          websocketServicePath: 
            Fn::ImportValue: !Sub ${prefix}-websocket-service-path
    RequestParameters:
      "integration.request.header.domainName": "context.domainName"
      "integration.request.header.stage": "context.stage"
      "integration.request.header.connectionId": "context.connectionId"
    PayloadFormatVersion: 1.0

wsCommandRouteResponse:
  Type: AWS::ApiGatewayV2::RouteResponse
  Properties:
    ApiId: !Ref wsApiGateway
    RouteId: !Ref wsCommandRoute
    RouteResponseKey: $default

If anybody has a better way, I'll be happy to mark it as the valid answer.

diegosasw
  • 13,734
  • 16
  • 95
  • 159