Skip to content

Instantly share code, notes, and snippets.

@farhad-taran
Last active November 16, 2023 22:25
Show Gist options
  • Save farhad-taran/07f4bac041db060326a101849b1919d2 to your computer and use it in GitHub Desktop.
Save farhad-taran/07f4bac041db060326a101849b1919d2 to your computer and use it in GitHub Desktop.
AWS api gateway - enqueue api gateway incoming requests onto SQS for decouple processing

The following terraform script allows us to have incoming api gateway requests be enqueued onto sqs for late processing.

locals {
  api_gateway_name = "order-status-webhooks-gateway"
}

data "aws_region" "current" {}
resource "aws_iam_role" "api" {
  name = local.api_gateway_name

  tags = {
    "name" = local.api_gateway_name
  }

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_policy" "api" {
  name = local.api_gateway_name
  tags = {
    "name" = local.api_gateway_name
  }
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:DescribeLogGroups",
          "logs:DescribeLogStreams",
          "logs:PutLogEvents",
          "logs:GetLogEvents",
          "logs:FilterLogEvents"
        ],
        "Resource": "*"
      },
      {
        "Effect": "Allow",
        "Action": [
          "sqs:GetQueueUrl",
          "sqs:ChangeMessageVisibility",
          "sqs:ListDeadLetterSourceQueues",
          "sqs:SendMessageBatch",
          "sqs:PurgeQueue",
          "sqs:ReceiveMessage",
          "sqs:SendMessage",
          "sqs:GetQueueAttributes",
          "sqs:CreateQueue",
          "sqs:ListQueueTags",
          "sqs:ChangeMessageVisibilityBatch",
          "sqs:SetQueueAttributes"
        ],
        "Resource": "${aws_sqs_queue.queue.arn}"
      },
      {
        "Effect": "Allow",
        "Action": "sqs:ListQueues",
        "Resource": "*"
      }      
    ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "api" {
  role       = "${aws_iam_role.api.name}"
  policy_arn = "${aws_iam_policy.api.arn}"
}

resource "aws_api_gateway_rest_api" "api" {
  name        = local.api_gateway_name
  description = "dragontail uses this api as a webhook endpoint to send us updates on orders"
  tags = {
    "name" = local.api_gateway_name
  }
}

resource "aws_api_gateway_resource" "dragontail" {
  path_part   = "dragontail"
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
  rest_api_id = aws_api_gateway_rest_api.api.id
}

resource "aws_api_gateway_resource" "order_status" {
  path_part   = "order-status"
  parent_id   = aws_api_gateway_resource.dragontail.id
  rest_api_id = aws_api_gateway_rest_api.api.id
}

resource "aws_api_gateway_method" "api" {
  rest_api_id   = aws_api_gateway_rest_api.api.id
  resource_id   = aws_api_gateway_resource.order_status.id
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_usage_plan" "usage_plan" {
  name = local.api_gateway_name

  api_stages {
    api_id = aws_api_gateway_rest_api.api.id
    stage = aws_api_gateway_stage.prod.stage_name
  }
  
  tags = {
    "name" = local.api_gateway_name
  }
}

resource "aws_api_gateway_integration" "api" {
  rest_api_id             = "${aws_api_gateway_rest_api.api.id}"
  resource_id             = "${aws_api_gateway_resource.order_status.id}"
  http_method             = "POST"
  type                    = "AWS"
  integration_http_method = "POST"
  passthrough_behavior    = "NEVER"
  credentials             = "${aws_iam_role.api.arn}"
  uri                     = "arn:aws:apigateway:${data.aws_region.current.name}:sqs:path/${aws_sqs_queue.queue.name}"

  request_parameters = {
    "integration.request.header.Content-Type" = "'application/x-www-form-urlencoded'"
  }

  request_templates = {
    "application/json" = "${file("sqs-integration-request-mapping.template")}"
  }
  
  /*
  
  # Alternative way of adding body and message attributes to SQS event 
  
    request_templates = {
    # tells to pass http body as sqs message body and http request path 
    # as an attribute called 'path' and 'sourceIp' as these will be used in the lambda
    "application/json" = join("&",
    [
        "Action=SendMessage",
        "MessageBody=$input.body",
        "MessageAttribute.1.Name=path",
        "MessageAttribute.1.Value.StringValue=$util.urlEncode($context.path)",
        "MessageAttribute.1.Value.DataType=String",
        "MessageAttribute.2.Name=sourceIp",
        "MessageAttribute.2.Value.StringValue=$util.urlEncode($context.identity.sourceIp)",
        "MessageAttribute.2.Value.DataType=String"
    ])
  }
  
  */

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_integration_response" "response_200" {
  rest_api_id       = "${aws_api_gateway_rest_api.api.id}"
  resource_id       = "${aws_api_gateway_resource.order_status.id}"
  http_method       = "${aws_api_gateway_method.api.http_method}"
  status_code       = "${aws_api_gateway_method_response.response_200.status_code}"
  selection_pattern = "^2[0-9][0-9]"                                       // regex pattern for any 200 message that comes back from SQS

  response_templates = {
    "application/json" = "{\"message\": \"enqueued for processing.\"}"
  }

  depends_on = [
    aws_api_gateway_integration.api
  ]
}

resource "aws_api_gateway_method_response" "response_200" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  resource_id = "${aws_api_gateway_resource.order_status.id}"
  http_method = "${aws_api_gateway_method.api.http_method}"
  status_code = 200

  response_models = {
    "application/json" = "Empty"
  }
}

resource "aws_api_gateway_stage" "prod" {
  stage_name = "prod"
  rest_api_id = aws_api_gateway_rest_api.api.id
  deployment_id = aws_api_gateway_deployment.deployment.id
  
  lifecycle {
    create_before_destroy = true
  }
  
  tags = {
    "name" = local.api_gateway_name
  }
}

resource "aws_api_gateway_deployment" "deployment" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"

  depends_on = [
    aws_api_gateway_integration.api, 
    aws_api_gateway_method.api,
  ]

  /*
    terraform is not able to detect certain changes and will cause api gateway to use the old componenets
    in order to force it to detect new tf changes the following hack is used, more info at:
    https://github.com/hashicorp/terraform/issues/6613#issuecomment-322264393   
  */
  triggers = {
    redeployment = join(",",[
      filemd5("sqs-integration-request-mapping.template"),
      filemd5("api-gateway.tf")
    ])
  }

  lifecycle {
    create_before_destroy = true
  }
}

And the request template:

Action=SendMessage&MessageBody={
  "requestBody" : $input.json('$'),
  "headers": {
    #foreach($param in $input.params().header.keySet())
    "$param": "$util.escapeJavaScript($input.params().header.get($param))" #if($foreach.hasNext),#end
    #end  
  },
  "context": {
    "path": $util.urlEncode($context.path),
    "sourceIp": $util.urlEncode($context.identity.sourceIp)
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment