terraform-aws-modules / terraform-aws-apigateway-v2

Terraform module to create AWS API Gateway v2 (HTTP/WebSocket) 🇺🇦
https://registry.terraform.io/modules/terraform-aws-modules/apigateway-v2/aws
Apache License 2.0
144 stars 188 forks source link

Websocket Example #8

Closed xavicolomer closed 2 years ago

xavicolomer commented 3 years ago

Hi there!

Is there any websockets template I can base on?

I tried with one of the examples but the domain and domain_certificate_arn are required. All I would need is a simple websockets endpoint integrated with a lambda.

Thanks!

antonbabenko commented 3 years ago

¡Hola @xavicolomer !

I have not implemented full support for WebSocket in this module, because I have not used them in my projects (yet) but maybe you can see what is missing and add the required code for that?

xavicolomer commented 3 years ago

Ok, I will give it a look!

I didn't know if it was fully developed. Thanks!

sjf3 commented 3 years ago

don't use the $default stage name hardcoded in the module, but I managed to get this working with websockets in my own fork with this tweak

lllama commented 3 years ago

@sjf3 are you able to share an example? I have used create_default_stage = false to prevent $default being created (but I then end up with no stages). My lambda is being created and the routes are all set to AWS_PROXY but they're not getting wired up.

I'm still tweaking my files but any pointers would be really helpful.

sjf3 commented 3 years ago
# Default stage
resource "aws_apigatewayv2_stage" "default" {
  count = var.create && var.create_default_stage ? 1 : 0

  api_id      = aws_apigatewayv2_api.this[0].id
  name        = var.protocol_type == "HTTP" ? "$default" : "default" // changed to work with websockets
  auto_deploy = true
...

I changed that one line and it all just works. You do need a stage. The name just can't be $default

sjf3 commented 3 years ago

@lllama looks like I also made this edit as well

resource "aws_apigatewayv2_route_response" "this" {
  for_each           = var.create && var.create_routes_and_integrations && var.protocol_type == "WEBSOCKET" ? var.integrations : {}
  api_id             = aws_apigatewayv2_api.this[0].id
  route_id           = aws_apigatewayv2_route.this[each.key].id
  route_response_key = "$default" // must be exactly this hardcoded value
}
lllama commented 3 years ago

Thanks @sjf3. I've made your two changes but my routes still aren't getting wired up. Here's the terraform I'm using:

provider "aws" {
  region = "eu-west-2"

  # Make it faster by skipping something
  skip_get_ec2_platforms      = true
  skip_metadata_api_check     = true
  skip_region_validation      = true
  skip_credentials_validation = true

  # skip_requesting_account_id should be disabled to generate valid ARN in apigatewayv2_api_execution_arn
  skip_requesting_account_id = false
}

locals {
  domain_name = "terraform-aws-modules.modules.tf" # trimsuffix(data.aws_route53_zone.this.name, ".")
  subdomain   = "complete-http"
}

###################
# HTTP API Gateway
###################

module "api_gateway" {
  source = "../terraform-aws-apigateway-v2"

  name                       = "test-ws-test"
  description                = "My awesome test Websocket API Gateway"
  protocol_type              = "WEBSOCKET"
  route_selection_expression = "$request.body.action"

  create_api_domain_name = false

  default_stage_access_log_destination_arn = aws_cloudwatch_log_group.logs.arn
  default_stage_access_log_format          = "$context.identity.sourceIp - - [$context.requestTime] \"$context.httpMethod $context.routeKey $context.protocol\" $context.status $context.responseLength $context.requestId $context.integrationErrorMessage"

  integrations = {
    "$connect" = {
      lambda_arn = module.lambda_function.this_lambda_function_arn
    },
    "$disconnect" = {
      lambda_arn = module.lambda_function.this_lambda_function_arn
    },
    "$default" = {
      lambda_arn = module.lambda_function.this_lambda_function_arn
    }
  }

  tags = {
    Name = "dev-api-new"
  }
}

resource "random_pet" "this" {
  length = 2
}

resource "aws_cloudwatch_log_group" "logs" {
  name = "test-ws-test"
}

module "lambda_function" {
  source = "terraform-aws-modules/lambda/aws"

  function_name = "${random_pet.this.id}-lambda"
  description   = "My awesome lambda function"
  handler       = "handler.handler"
  runtime       = "python3.8"

  publish = true

  source_path = "../aws"

  allowed_triggers = {
    AllowExecutionFromAPIGateway = {
      service    = "apigateway"
      source_arn = "${module.api_gateway.this_apigatewayv2_api_execution_arn}/*/*/*"
    }
  }
}

I've used this terraform that I found https://github.com/ustaxcourt/ef-cms/blob/staging/web-api/terraform/api/websockets.tf and have been able to create an API that's wired up, so I'm going to try and work out what the differences are.

lllama commented 3 years ago

Hopefully everyone spotted my deliberate mistake - I needed to use the invoke arn for the lambda functions:

integrations = {
    "$connect" = {
      lambda_arn = module.lambda_function.this_lambda_function_invoke_arn
    },
    "$disconnect" = {
      lambda_arn = module.lambda_function.this_lambda_function_invoke_arn
    },
    "$default" = {
      lambda_arn = module.lambda_function.this_lambda_function_invoke_arn
    }
  }

I also had one too many * in my allowed_triggers in the lambda module. This sorted out the 500 errors I was getting.

I was getting 429 errors when connecting but these look like they are fixed once I enable throttling on the API stage. When I apply my terraform, the console shows that throttling is disabled. If I enabled it, save my config, then try to disable it again and save, then the console complains that this is an invalid throttling setting, which makes me think that throttling is required (for websockets only?) However, I've just done a destroy/apply cycle and the new API seems happy without a throttling setting, so now I'm just confused.

In one final twist, I have also just backed out the $default -> default stage name change, and the new API runs as expected. @sjf3 can I check how you were testing your websocket API? I was using https://github.com/vi/websocat which works well enough but when I pasted the execution URI containing $default into my bash prompt, then bash was expanding it to an empty string, which caused 403 errors. Wrapping the URI in single quotes fixed that.

sjf3 commented 3 years ago

I followed this guide when initially getting setup. https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-wscat.html

lllama commented 3 years ago

So looking at that example, they have the following command for invoking the API:

wscat -c wss://aabbccddee.execute-api.us-east-1.amazonaws.com/test/

and test is the stage name. If you replace that with $default, then there's a strong chance your shell will replace that with nothing. So

wscat -c wss://aabbccddee.execute-api.us-east-1.amazonaws.com/$default/

becomes

wscat -c wss://aabbccddee.execute-api.us-east-1.amazonaws.com//

Which will give 403 or some other error. If you were to wrap the URL in single quotes:

wscat -c 'wss://aabbccddee.execute-api.us-east-1.amazonaws.com/$default/'

then the shell shouldn't make any changes and wscat should be able to connect.

sjf3 commented 3 years ago

$default is the default route key if another route key doesn't match. This isn't REST you don't have path based routing. In this example wscat -c wss://aabbccddee.execute-api.us-east-1.amazonaws.com/test/ test is the stage $default will handle anything after $connect if another route doesn't match. You can use jsonpath expressions to match routes based on the messages sent. Like if you were doing jsonRPC 2.0 over websockets you could match on the method string and route to different lambda handlers or for only handling a few routes you can just put a switch statement in the $default route handler.

lllama commented 3 years ago

Agreed but the changing of the stage name:

# Default stage
resource "aws_apigatewayv2_stage" "default" {
  count = var.create && var.create_default_stage ? 1 : 0

  api_id      = aws_apigatewayv2_api.this[0].id
  name        = var.protocol_type == "HTTP" ? "$default" : "default" // changed to work with websockets
  auto_deploy = true
...

will affect the test portion of the URI, rather than any routing. API gateway will do "path-based" routing for any stages that are defined. Only once wscat and friends have connected to this URI will the route key come into play. Having $default as the stage appears to just be a coincidence with it having the same name as the $default route in websockets.

lllama commented 3 years ago

So I can confirm that this works out of the box with websockets. @antonbabenko, would you be interested in an example that implements the AWS Chat code sample? (https://docs.aws.amazon.com/code-samples/latest/catalog/code-catalog-python-example_code-apigateway-websocket.html) If so, I'll put a pull request together.

antonbabenko commented 3 years ago

@lllama Yes, it would be great if you can add a new folder examples/complete-websocket similar to examples/complete-http with real WebSocket configuration and Python code.

I don't have immediate plans to use WebSockets features in the near future myself, so any help is highly appreciated.

svenlito commented 2 years ago

@lllama Hi there, still interested in contributing this example?

lllama commented 2 years ago

Just ACKing this request. I'll try and put together a PR when I get a minute. (semi-related XKCD: https://xkcd.com/979/ )

lllama commented 2 years ago

@svenlito See #46

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has been open 30 days with no activity. Remove stale label or comment or this issue will be closed in 10 days

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has been open 30 days with no activity. Remove stale label or comment or this issue will be closed in 10 days

github-actions[bot] commented 2 years ago

This issue was automatically closed because of stale in 10 days

github-actions[bot] commented 1 year ago

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

antonbabenko commented 4 weeks ago

This issue has been resolved in version 5.0.0 :tada: