AWS API-Gateway与SNS的通信

26

我正在构建一个API,这个API将由Lambda函数提供服务,但我需要这些函数是异步的,所以我不直接将API网关连接到Lambda函数,而是使用“AWS服务代理”来发布SNS消息,然后让Lambda函数订阅相关的SNS主题,以便它接收请求的传递。下面是一张说明流程的图片:

enter image description here

我已经测试了Lambda函数和SNS之间的发布/订阅消息,但我在API-Gateway到SNS的交接处遇到了困难。文档比较简略,但我目前认为必须在POST请求中发送以下属性:

  1. Action:API网关在UI中提供此选项,我已经放置了“Publish”操作,这是适当的SNS操作

  2. Message:POST消息的正文应该是JSON文档。它将由Web客户端传递,并通过网关代理到SNS。

  3. TopicArn:表示我们要发布到的SNS主题。在我的设计中,这将是一个静态值/终结点,因此我希望Web客户端不必也能传递这个值,但如果更容易这样做,那也可以。

我尝试了很多方法,但卡住了。希望能在某个地方找到一个好的代码示例,但任何帮助都将不胜感激。


想补充一下我目前的尝试情况:

我已经尝试发布我的API,并使用Postman尝试获取有效的响应。这是Postman的截图(一个用于标题变量,一个用于JSON正文):

header variables json body

这导致以下错误消息:

{
   "Error": {
     "Code": "InvalidParameter",
     "Message": "Invalid parameter: TopicArn or TargetArn Reason: no value for required parameter",
     "Type": "Sender"
  },
  "RequestId": "b33b7700-e8a3-58f7-8ebe-39e4e62b02d0"
}

错误似乎表明TopicArn参数未被发送到SNS,但我已在API-Gateway中包含了以下内容:

enter image description here


你最终解决了这个问题吗?我遇到了完全相同架构的类似问题。在我发布一个详细描述我的问题的单独问题之前,我想先看看你是否能够使其正常工作。 - AlexGad
2
哎呀,我已经发布了一个答案,但它没有被提交。我明天会尝试解决这个问题。 - ken
你是否已经成功使用POST请求来运行它?我唯一成功的方法是将TopicArn和Message设置为查询字符串参数(就像下面的答案所建议的那样),因为即使我设置了POST方法,请求体也总是被忽略。然而,我需要请求体中的内容,但似乎没有办法将其复制为查询字符串参数。 - nanestev
是的,目前似乎无法使用请求正文。这个问题会在某个时间点得到解决,但目前没有具体日期。 - ken
9个回答

13
如果有人仍在寻找解决原始问题的方法,即仅通过API网关将JSON请求正文代理到SNS主题,则可能是可行的。按照Ken上面的描述创建网关。然后简单地将正文代理到Integration Request的查询参数。您还可以在此处硬编码Subject、TopicArn等,或者使用JsonPath从请求正文中映射这些内容。例如:
{
   //body
   "topic": "arn:aws:sns:1234567:topic"
}

可以映射为一个标题,如下:

method.request.body.topic

来说同样的话。这真的应该成为被接受的答案。 - Nick Cox
1
我认为截图是错误的。它应该只是 stageVariables.topicArn,而不是 '$stageVariables.TopicArn' - Nick Cox
这就是有效的方法。非常感谢。花了三天时间才搞清楚到底出了什么问题!! - usert4jju7
你如何硬编码主题和话题?我不想让调用者指定它们。谢谢。 - Matteo
好的,问题解决了。你只需要在集成请求映射中使用引号包裹 'your-topic-arn' 就可以了。 - Matteo

13

我来自 Api 网关团队。

我相信有一些格式可用于 HTTP 请求 Publish API,但这是我最初使用的一个:

AWS 区域 us-west-2

AWS 服务 sns

AWS 子域名

HTTP 方法 POST

Action 发布

== 查询字符串 ==

主题 'foo'
消息 'bar'
TopicArn 'arn:aws:sns:us-west-2:xxxxxxxxxxxx:test-api'

这对我来说可以发布消息。

如果您还有疑问,请告诉我。

杰克


3
我该如何将请求体和请求中发送的参数映射到 SNS 的主题(TopicArn)、消息(Message)和主题(Subject)? - Rudresh Ajgaonkar
他在一个项目符号中提到消息应该使用POST方式。我不明白为什么AWS推荐的文档是将消息编码为查询字符串。 - eggmatters

6

最终在AWS支持的帮助下,我成功解决了这个问题。以下是我的解决方案:

  • 首先,即使你发送了一个POST请求,你也不能像预期的那样在消息体中发送JSON消息。
  • 相反,你必须对JSON进行URL编码,并将其作为查询参数传递。
  • 同时记住,你发送的JSON应该以default为根对象,在SNS世界中,default表示“默认通道”
  • 最后,当Lambda捕获到SNS事件时,你必须过滤掉许多噪音才能获取到你的JSON消息。为此,我创建了以下函数,在我的Lambda函数中使用:

/**
 * When this is run in AWS it is run "through" a SNS
 * event wconfig.ich adds a lot of clutter to the event data,
 * this tests for SNS data and normalizes when necessary
 */
function abstractSNS(e) {
  if (e.Records) {
    return JSON.parse(decodeURIComponent(e.Records[0].Sns.Message)).default;
  } else {
    return e;
  }
}

/**
 * HANDLER
 * This is the entry point for the lambda function
 */
exports.handler = function handler(event, context) {
  parent.event = abstractSNS(event);


你好,请问您能否提供一些关于您的解决方案的详细信息,例如哪些部分需要更改/与正常设置有何不同?我已经遇到这个问题两天了,卡住了。问题是我对您之前给出的解决方案不清楚。先谢谢了。 - William Francis Gomes
@WilliamFrancisGomes 不确定还有什么需要补充的... "handler"函数会给你一个event对象,但如果你使用SNS来触发事件,你将得到完整的SNS事件。对我来说,除了e.Records[0].Sns.Message中的内容外,SNS的细节并不是很有用,因此上述函数展开了我关心的部分。 - ken
请注意,现在API Gateway提供了一种直接调用Lambda并异步调用它的方式,因此在某些情况下您不需要使用SNS。 - ken
我的问题在这里详细描述:https://dev59.com/yJjga4cB1Zd3GeqPN7BD - William Francis Gomes
@WilliamFrancisGomes,你想要做的事情肯定是可以实现的,也是使用SNS的一个很好的用例。在哪个环节出了问题?API-Gateway是否触发了SNS消息(如果你有任何疑问,你可以通过电子邮件订阅SNS主题)?如果是这样,你的Lambda函数是否执行?我需要更多的洞察力来确定链条中哪些环节没有正常工作。 - ken
@ken,仍有一些用例需要从API Gateway发布消息到SNS而不是调用Lambda。其中一个原因是Lambda冷启动问题,我的应用程序需要实时发布数据。 - Thanh Pham

4
您可以使用API Gateway将Lambda函数配置为AWS服务代理,以异步调用它。该配置基本上与您在此GitHub示例中看到的相同,唯一的异常是Lambda调用的uri更改为/invoke-async/而不仅仅是/invoke/。

我有点困惑你的例子。这个YAML文件放在哪里?API-Gateway能导入Swagger定义吗? - ken
请注意,invoke-async已经被弃用...他们建议使用invoke代替(我相信有一个参数可以将其设置为async)。 - ken
能否导出使用UI设置的API的Swagger YAML定义?看到它以YAML形式而不仅仅是UI会很好。 - ken

3
以下是一份逐步指南,供那些在查看以上答案后仍然无法理解的人使用。变量名区分大小写,请确保精确无误。
  1. 打开Post方法
    a. 选择方法请求
    b. 将请求验证器更改为验证正文、查询字符串参数和标头
    c. 展开URL查询字符串参数
    d. 添加以下两个查询字符串参数
    名称:TopicArn ----> 选择必填项
    名称:Message -----> 选择必填项

  2. 返回到Post方法并打开集成请求
    a. 展开URL查询字符串参数
    b. 添加以下两个查询字符串参数
    名称:TopicArn 映射自:method.request.querystring.TopicArn
    名称:Message 映射自:method.request.querystring.Message

  3. 在测试时,将下面的命令更改为与您的SNS ARN匹配,并将其放入查询字符串中。
    TopicArn=arn:aws:sns:us-west-2:1234567890:SNSName&Message="Hello from API Gateway"

来源/更多信息:
API网关代理集成服务指南
SNS发布方法文档


1
我只是在推测(没有尝试过),但我认为您没有正确发送消息...根据AWS的文档(http://docs.aws.amazon.com/sns/latest/api/API_Publish.html),您需要以似乎是application/x-www-form-urlencoded编码的方式POST消息,如下所示:
POST http://sns.us-west-2.amazonaws.com/ HTTP/1.1
...
Action=Publish
&Message=%7B%22default%22%3A%22This+is+the+default+Message%22%2C%22APNS_SANDBOX%22%3A%22%7B+%5C%22aps%5C%22+%3A+%7B+%5C%22alert%5C%22+%3A+%5C%22You+have+got+email.%5C%22%2C+%5C%22badge%5C%22+%3A+9%2C%5C%22sound%5C%22+%3A%5C%22default%5C%22%7D%7D%22%7D
&TargetArn=arn%3Aaws%3Asns%3Aus-west-2%3A803981987763%3Aendpoint%2FAPNS_SANDBOX%2Fpushapp%2F98e9ced9-f136-3893-9d60-776547eafebb
&SignatureMethod=HmacSHA256
&AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE
&SignatureVersion=2
&Version=2010-03-31
&Signature=vmqc4XRupKAxsDAdN4j4Ayw5LQljXMps3kss4bkDfCk%3D
&Timestamp=2013-07-18T22%3A44%3A09.452Z
&MessageStructure=json

即消息正文的格式类似于浏览器编码表单数据。您的消息可以是JSON格式,但仍需要像表单字段一样进行编码(这是一个笨拙的比喻 :))。
此外,根据常见参数文档(http://docs.aws.amazon.com/sns/latest/api/CommonParameters.html),您还需要填写一些其他必填字段(例如通常的访问密钥、签名等)。
您没有说明您正在使用哪种语言编写API Gateway - 可能有适用于它的AWS SDK,您可以使用它来代替手动组合REST请求。

关于编程语言,此流程的 Lambda 函数为 Node JS,调用的网络客户端为 JS SPA(EmberJS)。在消息转换发生的中间部分,全部都是 API Gateway 配置。 - ken
你所提供的示例信息看起来被URL编码了,正上方的示例似乎暗示着你可以将JSON传递到消息体中(至少我是这样理解的)。 - ken
此外,我假设签名并传递访问密钥都由API-Gateway处理,因为这将打破他们似乎在推广的“仅配置”方法(并且UI确实要求执行arn)。 - ken
抱歉 - 我的回答完全错了。我以为API网关是你编写的组件(没有意识到AWS有一个叫这个名字的产品 :)。 - xpa1492
无论如何,感谢您的帮助。我需要尽可能多的帮助。 :) - ken

1
我会这样做:
WebApp --> 网关 --> Lambda(使用Boto3发布到SNS)--> SNS --> Lambda
我认为,事情会更简单。

你能再解释一下吗? - abhiarora
好的,我们将网关连接到Lambda。根据文档:https://cloudonaut.io/create-a-serverless-restful-api-with-api-gateway-cloudformation-lambda-and-dynamodb/,然后使用http://boto3.readthedocs.io/en/latest/reference/services/sns.html#SNS.Client.publish.....将Lambda连接到SNS。 - Anand Bajpai
请告诉我这是否有帮助,否则我将发布带有图像的内容。唯一需要注意的是这将使用Python。 - Anand Bajpai

1
还要记住参数是区分大小写的;我也收到了OP的错误:"Message": "Invalid parameter: TopicArn or TargetArn Reason: no value for required parameter" 唯一的问题是参数的大小写敏感性(特别是应为:"TopicArn"和"Message")。这些设置在Method Execution | POST - Integration Request部分中,在“Name”字段中设置。
“Mapped from”的大小写很重要,它必须与从方法请求配置发送的参数匹配,但发送到SNS的是“Integration Request”的“Name”字段,这就是我错了的地方。

enter image description here


0
如果有人在2023年使用Terraform和OpenAPI配置寻找一个例子,这是我使用的内容。
resource "aws_api_gateway_rest_api" "MyApi" {
  name = "${var.name}-${var.environment}"
  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Principal" : {
          "AWS" : aws_iam_role.api-gateway-access-role.arn
        },
        "Action" : "execute-api:Invoke",
        "Resource" : "arn:aws:execute-api:${var.aws_region}:${var.aws_account_id}:*/*/*/*"
        "Condition" : {
          "StringEquals" : {
            "aws:SourceVpc" : data.terraform_remote_state.vpc.outputs.vpc_id
          }
        }
      }
    ]
  })
  body = jsonencode({
    "openapi" : "3.0.1",
    "info" : {
      "title" : "MyApi",
      "version" : "0.1.0"
    },
    "paths" : {
      "/my-in-principle" : {
        "post" : {
          "x-amazon-apigateway-auth" : {
            "type" : "AWS_IAM"
          },
          "x-amazon-apigateway-integration" : {
            "uri" : "arn:aws:apigateway:${var.aws_region}:sns:path/${aws_sns_topic.my-request.arn}?Action=Publish&TopicArn=${aws_sns_topic.my-request.arn}",
            "passthroughBehavior" : "when_no_templates",
            "credentials" : aws_iam_role.sns-access-role.arn,
            "httpMethod" : "POST",
            "type" : "aws",
            "requestTemplates" : {
              "application/json" : "{\"Message\": $input.json('$')}"
            },
            "requestParameters" : {
              "integration.request.querystring.Message" : "method.request.body"
            }
            "responses" : {
              "400" : {
                "statusCode" : "400",
                "contentHandling" : "CONVERT_TO_TEXT"
                "responseTemplates" : {
                  "application/json" : "$input.json('$')"
                },
              },
              "500" : {
                "statusCode" : "500",
                "contentHandling" : "CONVERT_TO_TEXT"
                "responseTemplates" : {
                  "application/json" : "$input.json('$')"
                },
              },
              "200" : {
                "statusCode" : "200",
                "contentHandling" : "CONVERT_TO_TEXT"
                "responseTemplates" : {
                  "application/json" : "$input.json('$')"
                },
              }
            }
          },
          "responses" : {
            "200" : {
              "description" : "Successful Response"
            },
            "400" : {
              "description" : "Bad Request"
            },
            "500" : {
              "description" : "Internal Server Error"
            }
          }
        }
      }
    }
  })
  endpoint_configuration {
    types            = ["PRIVATE"]
    vpc_endpoint_ids = [data.aws_vpc_endpoint.execute-api.id]
  }
}

resource "aws_sns_topic_policy" "my-request-topic-policy" {
  arn = aws_sns_topic.my-request.arn
  policy = jsonencode({
    Version = "2012-10-17",
    Id      = "default",
    Statement = [
      {
        Sid    = "AllowAPIGatewayToPublish",
        Effect = "Allow",
        Principal = {
          Service = "apigateway.amazonaws.com"
        },
        Action   = "sns:Publish",
        Resource = aws_sns_topic.my-request.arn,
        Condition = {
          ArnEquals = {
            "aws:SourceArn" = "arn:aws:execute-api:${var.aws_region}:${var.aws_account_id}:${aws_api_gateway_rest_api.MyApi.id}/*/*/*"
          }
        }
      }
    ]
  })
}

resource "aws_iam_role" "sns-access-role" {
  name = "sns-access-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "apigateway.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
  inline_policy {
    name = "sns-publish-policy"
    policy = jsonencode({
      Version = "2012-10-17",
      Statement = [
        {
          Effect = "Allow",
          Action = [
            "sns:Publish"
          ],
          Resource = [
            aws_sns_topic.my-request.arn
          ]
        }
      ]
    })
  }
}

resource "aws_api_gateway_method_settings" "MyMethodSettings" {
  rest_api_id = aws_api_gateway_rest_api.MyApi.id
  stage_name  = aws_api_gateway_stage.MyApiStage.stage_name
  method_path = "*/*"

  settings {
    throttling_rate_limit  = "5"
    throttling_burst_limit = "20"
  }
}

resource "aws_api_gateway_deployment" "MyDeployment" {
  rest_api_id = aws_api_gateway_rest_api.MyApi.id

  triggers = {
    redeployment = sha1(jsonencode([
      aws_api_gateway_rest_api.MyApi.body,
      aws_api_gateway_rest_api.MyApi.policy
    ]))
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_stage" "MyApiStage" {
  rest_api_id          = aws_api_gateway_rest_api.MyApi.id
  stage_name           = "v1"
  deployment_id        = aws_api_gateway_deployment.MyDeployment.id
  xray_tracing_enabled = true
}

resource "aws_lambda_permission" "MyLambdaInvokePermission" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.Async-Lambda.function_name
  principal     = "apigateway.amazonaws.com"

  # More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
  source_arn = "arn:aws:execute-api:${var.aws_region}:${var.aws_account_id}:${aws_api_gateway_rest_api.MyApi.id}/*/*/*"
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接