複数のバックエンドのリソースをAmazon API Gatewayで統合する


前提

システム開発をしていると「A機能はLambdaで作るべきで、B機能はコンテナで作るべきで、でもそれぞれ実はURIは同じにしておく方がREST的にキレイになるんだよね」なことがある。…いや、あまりないか?ともあれ、そんな時にはnginxとかでプロキシを作って振り分けても良いのだけど、せっかくパブリッククラウドを使っているのだからマネージドサービス使ってみたいよね、と思ってやってみた。

Spring BootなWebアプリの動いているコンテナと、Lambda関数がそれぞれあるような構成。

Lambda関数

この記事のために作り直すのも面倒なので、以下の記事で作ったものを転用する。

Amazon API Gateway/ALBのバックエンドで動くLambda関数をJava(Eclipse+maven)で実装する

Spring BootなWebアプリの動いているコンテナ

あまりこだわりもないので、適当なパラメータを受けてJSONを返すコンテナを作ってみる。
リソースは/person
パラメータは、クエリかリクエストボディ(JSON)でidを読み込んで、それに対応した名前と年齢をJSONで返すような仕様で。

WebController.java
package com.example;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

@RestController
public class WebController {

    @RequestMapping("/person")
    @ResponseBody
    public Person response(
            @RequestBody(required = false) Person person,
            @RequestParam(name="id", required = false) String reqParamId) {
        Person response = null;
        String id = null;

        if( reqParamId != null ){
            id = reqParamId;
        }else{
            id = Integer.toString(person.getId());
        }

        if(id.equals("11111")) {
            response = new Person(Integer.parseInt(id), "Taro", 35);
        }else if(id.equals("22222")) {
            response = new Person(Integer.parseInt(id), "Hanako", 32);
        }

        return response;
    }
}
Person.java
package com.example;

public class Person {
    private int Id;
    private String Name;
    private int Age;

    // 適当な getter, setter
    // 中略

    public Person(int id, String name, int age) {
        Id = id;
        Name = name;
        Age = age;
    }
}

pom.xml は spring-boot-starter, spring-boot-starter-web があればOK。
ビルドは適当にmvn packageしてから適当なDockerfileでdocker buildして、ECRにpushする。
この辺も以下の過去記事を参考に。

AWSでSpringBootベースのWebアプリを起動してみる(Docker on EC2編/ECS+Fargate編)

API Gatewayの設定

↓こんな感じで定義する。

リソース メソッド 転送先
LambdaTest ANY Lambda関数
person GET Spring BootなWebアプリ
person POST Lambda関数
person PUT Spring BootなWebアプリ

手でポチポチ設定するのも面倒なので、以下のSwagger+API Gateway拡張をインポートする。
分かりにくいかもしれないけど、インポートはここから可能。

---
swagger: "2.0"
info:
  description: "Created by AWS Lambda"
  version: "2020-04-25T10:56:39Z"
  title: "API GatewayのAPI名"
host: "[API Gatewayのドメイン]"
basePath: "/default"
schemes:
- "https"
paths:
  /LambdaTest:
    x-amazon-apigateway-any-method:
      responses:
        200:
          description: "200 response"
      x-amazon-apigateway-integration:
        uri: "[Lambda関数のARN]"
        responses:
          .*:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        type: "aws_proxy"
  /person:
    get:
      produces:
      - "application/json"
      responses:
        200:
          description: "200 response"
          schema:
            $ref: "#/definitions/Empty"
      x-amazon-apigateway-integration:
        uri: "http://[NLBのドメイン]/person"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        connectionType: "VPC_LINK"
        connectionId: "[VPCLinkのID]"
        httpMethod: "GET"
        type: "http_proxy"
    post:
      produces:
      - "application/json"
      responses:
        200:
          description: "200 response"
          schema:
            $ref: "#/definitions/Empty"
      x-amazon-apigateway-integration:
        uri: "[Lambda関数のARN]"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
        type: "aws_proxy"
    put:
      produces:
      - "application/json"
      responses:
        200:
          description: "200 response"
          schema:
            $ref: "#/definitions/Empty"
      x-amazon-apigateway-integration:
        uri: "http://[NLBのドメイン]/person"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        connectionType: "VPC_LINK"
        connectionId: "[VPCLinkのID]"
        httpMethod: "GET"
        type: "http_proxy"
definitions:
  Empty:
    type: "object"
    title: "Empty Schema"

コンテナについては、内部向けのHTTPサーバを統合する場合は、VPCLinkで繋いだNLBにぶら下げる必要がある。この辺の記事を参考にしよう。ほぼこの記事とやってることは同じだけど気にしない………。

さて、ここまでやったら、curlで動作確認をしてみると、期待通りの動作になっているのが分かる。

Lambda向けのリクエスト①
$ curl --include -X GET https://[API Gatewayのドメイン]/default/LambdaTest
HTTP/2 200
date: Sat, 25 Apr 2020 12:52:11 GMT
content-type: application/json
content-length: 42
x-amzn-requestid: ********
x-amz-apigw-id: ********
x-amzn-trace-id: ********

{"greetings":"Hello from Lambda Ver 1.11"}
Lambda向けのリクエスト②
$ curl --include -X POST https://[API Gatewayのドメイン]/default/person
HTTP/2 200
date: Sat, 25 Apr 2020 12:54:16 GMT
content-type: application/json
content-length: 42
x-amzn-requestid: ********
x-amz-apigw-id: ********
x-amzn-trace-id: ********

{"greetings":"Hello from Lambda Ver 1.11"}
Spring BootなWebアプリ向けのリクエスト①
$ curl --include -X GET https://[API Gatewayのドメイン]/default/person?id=11111
HTTP/2 200
date: Sat, 25 Apr 2020 12:55:24 GMT
content-type: application/json
content-length: 35
x-amzn-requestid: ********
x-amzn-remapped-connection: keep-alive
x-amz-apigw-id: ********
x-amzn-remapped-date: Sat, 25 Apr 2020 12:55:24 GMT

{"name":"Taro","id":11111,"age":35}[
Spring BootなWebアプリ向けのリクエスト②
$ curl --include -X PUT -H 'Content-Type:application/json' -d '{"id":"11111"}' https://[API Gatewayのドメイン]/default/person?id=11111
HTTP/2 200
date: Sat, 25 Apr 2020 12:56:33 GMT
content-type: application/json
content-length: 35
x-amzn-requestid: ********
x-amzn-remapped-connection: keep-alive
x-amz-apigw-id: ********
x-amzn-remapped-date: Sat, 25 Apr 2020 12:56:32 GMT

{"name":"Taro","id":11111,"age":35}