【API Gateway】β版のAPI Gateway + Cloud Functions + Cloud Datastoreでお手軽サーバレス【Functions】【Datastore】

# はじめに
チームメンバーがAWSのAPI Gatewayをプロダクション環境に導入するということで、GCPではどんな感じだろうかと思い勉強してみました。

ついでによくあるサーバーレスアーキテクチャであるAPI Gateway + Cloud Functions + Cloud Datastoreの構成でAPIを構築してみました。

API Gatewayは最近β版リリースしたということで文献が少ないと思うので、GCPでサーバーレスしたい！という方の参考になればと思います。

![スクリーンショット 2020-11-05 0.48.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/d63383a0-c671-b25e-b647-d20c4ca115e3.png)

# API Gateway とは ？

>サーバーレス ワークロード向けのフルマネージド ゲートウェイ
API ゲートウェイを利用して、Google Cloud サーバーレス バックエンド（Cloud Functions、Cloud Run、App Engine など）向け API を作成、保護、モニタリングできます。
[公式ドキュメント](https://cloud.google.com/api-gateway?hl=ja)より抜粋

APIのエンドポイントが複数あるとアクセス先のURLが異なってしまうところ、API Gatewayをかますことでアクセス先URLの一元化が可能になります。

また、今回のようなCLoud Functionsを複数作成すると監視する場合、各関数を見にいかなければならないところをAPI Gatewayを監視することで完結するといったメリットがあります。

# 構築するサーバーレスシステム
まずはAPIGatwayを構築します。

## API Gatewayの作成
コンソールから作成する際に表示名とAPIIDを入力します。
この時表示された

![スクリーンショット 2020-11-08 16.07.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/4fd66d13-c912-00fb-ccf9-602b87215cf5.png)

作成すると、次にAPI仕様を決める画面になりますがここはスキップします。

![スクリーンショット 2020-11-08 16.13.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/2def92df-71c6-eec1-3750-e3e5a5836849.png)

ひとまずコンソール上に入力した名前でAPIが作成されているかと思います。

## API仕様の決定およびアップロード
ここがハマりどころかもしれません。(自分はうまくいかなくて試行錯誤しました。。)
先ほど作成したAPIを選択して「構成」タブにからアップロードを押すと「API構成のアップロード」画面にいきます。

ここでOpenAPI Spec2.0でAPI仕様を決めます。
![スクリーンショット 2020-11-08 16.27.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/682baff9-985f-6f9a-8db5-68a06d1b4e83.png)


OpenAPIとは、REST API のインターフェースを記述するための仕様でインターフェースの定義はJSONまたはYAMLで記述します。
定義ファイルから自動的にAPIのドキュメントを生成可能だったりするようです。

一から書くのは大変なので、今回は下記のサイトで編集しました。文法が合っているか確認できるのでとても便利です。
[Swagger Editor](https://editor.swagger.io/)
OpenAPIは3.0.0で対応していて、Swaggerなら2.0で対応しているので、今回は```swagger:"2.0"```で記述します。

題材としてPetostoreの簡易版を記述してみます。

```swagger2-function.yaml
swagger: "2.0"
info:
  title: qiita-pet 
  description: API specification for qiita
  version: "1.0.0"
schemes:
  - "https"
produces:
  - application/json
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      x-google-backend:
        address: https://asia-northeast1-[プロジェクト名].cloudfunctions.net/qiita-pets-allget 
      responses:
        '200':
          description: A successful response
          schema:
            type: array
            items:
              $ref: '#/definitions/Pet'
        default:
          description: error payload
          schema:
            $ref: '#/definitions/Error'
    post:
      summary: Create a pet
      operationId: createPets
      tags:
        - pets
      x-google-backend:
        address: https://asia-northeast1-[プロジェクト名].cloudfunctions.net/qiita-pets-post
      responses:
        '201':
          description: Null response
          schema:
            type: string
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/Error'
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      tags:
        - pets
      x-google-backend:
        address: https://asia-northeast1-[プロジェクト名].cloudfunctions.net/qiita-pets-idget?petId={petId}
      responses:
        '200':
          description: A successful response
          schema:
            type: array
            items:
              $ref: '#/definitions/Pet'
        default:
          description: error payload
          schema:
            $ref: '#/definitions/Error'
    parameters:
      - name: petId
        in: path
        required: true
        description: The id of the pet to retrieve
        type: string
definitions:
  Pet:
    required:
    - id
    - name
    type: object
    properties:
      id:
        type: integer 
      name:
        type: string
  Error:
    type: object
    properties:
      message:
        type: string
      code:
        type: integer
```

Swagger2.0の詳細については下記を参考にしました。
[OpenAPI Specification](https://swagger.io/specification/v2/)

各pathsのaddressがAPI GatewayのバックエンドのAPIのエンドポイントになります。この後、この仕様通りにレスポンスを返すCloud Functionsを作って行きます。

このファイルをアップロードするとAPIの構成に作成した仕様が追加されます。
ゲートウェイへのデプロイについては、ゲートウェイを別途作成するので今期はデプロイしていません。

![スクリーンショット 2020-11-08 18.06.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/e05da14f-bbaf-3414-2607-8ce1ca1fa3b8.png)

## ゲートウェイの作成
ここではゲートウェイが作成されるロケーションとどの構成をデプロイするかを決定します。
ロケーションはus-central1, europe-west1, asia-east1のみが選択可能です。

![スクリーンショット 2020-11-08 18.17.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/58cea1d3-79e3-1674-1972-5431adb23745.png)

これでAPI Gatewayのエンドポイントが作成されます。

## Cloud Functinosで関数の作成
Cloud Fuctionsについては既存の記事があるので簡単にまとめていきます。こちら参考にさせていただきました。
[Google Cloud FunctionsでPythonを利用してみた(Beta利用)](https://qiita.com/kai_kou/items/dca21cdfd8375a247c2f)

![スクリーンショット 2020-11-08 18.28.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/8d2a42cd-8ec3-26d0-2752-424bae20a55b.png)

Authenticationについては「未認証を許可」としています。後ほど、構築したアーキテクチャをセキュリティ高くできるか検証するためあえてガバガバにしています。

また接続についても「すべてのトラフィックを許可する」に設定しています。

### Cloud Datastoreとの連携
Funtionsの中でDatastoreにアクセスさせてたかったので、こちらの記事を参考にコードを作成しました。
[Python3でDatastore を動かしてみた](https://qiita.com/tfuruya/items/fa529c6cf54adaa81aa7)

構成は、下記のようにしています。
* Namespace： Petes
* Kind： pet
* Entity: id, name

特定のnamespaceにアクセスしたかったのでclientを定義する際に引数でnamespaceを渡すようにしています。

```python:get_all_pets関数
def get_all_pets(request):
    from google.cloud import datastore
    import json
    
    project_id = "プロジェクト名"
    client = datastore.Client(project=project_id, namespace="Pets")

    # kindにエンティティ種類名を指定
    query = client.query(kind="pet")
    query.order=["id"]
    ret = {
        "response": list(query.fetch())
    } 
    return ret
```

```python:post_pet
def post_pet(request):
    from google.cloud import datastore
    import json
    
    request_json = request.get_json()
    project_id = "プロジェクト名"
    client = datastore.Client(project=project_id, namespace="Pets")

    content_type = request.headers['content-type']
    if content_type == 'application/json':
        request_json = request.get_json(silent=True)
        if request_json and 'id' in request_json and 'name' in request_json:
            id = request_json['id']
            name = request_json['name']
        else:
            return {
                "message": "error request",
                "code": 400
            }

    # kindにエンティティ種類名を指定
    key = client.key("pet")
    entity = datastore.Entity(key)
    entity['id'] = id
    entity['name'] = name
    client.put(entity)
    return 'success create'
```

```python:get_pet_id
def get_pet_id(request):
    from google.cloud import datastore
    import json
    
    project_id = "プロジェクト名"
    client = datastore.Client(project=project_id, namespace="Pets")

    if request.args and 'petId' in request.args:
        pet_id = request.args.get('petId')
        key = client.key("pet", int(pet_id))
        entity = client.get(key)
        return {
            "id": int(entity.get("id")),
            "name": entity.get("name")
        }
    else:
        return {
            "message": "error request",
            "code": 400
        }
```

## 動作検証
簡単にブラウザからアクセスして動作検証してみます。

今回作成したAPI Gatewayのエンドポイントは```https://qiita-pets-[uid].de.gateway.dev```なので、それぞれ叩いて見ます。
![スクリーンショット 2020-11-08 22.26.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/bfd6b2bd-7f9c-03f4-5747-f03a1a88e058.png)
![スクリーンショット 2020-11-08 22.30.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/c95070a0-3f30-edfb-4b9f-f6e116893280.png)

![スクリーンショット 2020-11-08 22.26.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/e0409538-e664-6657-6222-9f6b30fd32e3.png)
![スクリーンショット 2020-11-08 21.48.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/477dd452-52a1-0d69-07a9-6b6d74354d11.png)

![スクリーンショット 2020-11-08 22.23.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/11dcbcac-5e29-c9a8-ad4a-dde60494ac36.png)
![スクリーンショット 2020-11-08 22.21.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148317/1cb32f71-11c4-1ad1-b6fd-33a75af2ff17.png)

API GatewayのエンドポイントでバックエンドのCloud Functionに無事アクセスすることができました。

# セキュアに使えるか検証！！
プロダクション環境に入れることを考えたらセキュアにアクセスできるようにしたいですよね。
今の設定はパブリック・未認証許可ということでどこからでも誰でもアクセスできるようになっているので少しずつ制限をかけていきたいと思います。

## Cloud Functionsの設定
まずはFuctionのトラフィックを内部トラフィックに制限してみます。内部トラフィックは同一プロジェクトもしくは同一VPCSCからのトラフィックからのみ許可されるようです。

なのでイメージとしては同一プロジェクトのVMからのアクセスをAPI Gateway経由で許可されるといいなって感じです。

### 内部トラフィックのみを許可
同一プロジェクト内のVMからの直接Cloud Fucntionsへのアクセスは許可されましたが、API Gatewayを経由したアクセスは拒否されました。
API Gatewayからのアクセスは内部トラフィックではないんですね。。

## VPC Service Controlsの適用
VPC Service Controlsの公式ドキュメントを見る限り、まだサポートされているサービスに含まれていないようです。
基本的にはパブリックアクセスが前提になる感じでしょうか。(当たり前か、、)
https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products

## APIキーを用いた認証
プロジェクトで生成したAPIキーをAPI呼び出し時にクエリパラメータもしくはリクエストヘッダーに埋め込むことで認証が可能のようです。
これを設定するとAPIキーをもとに関連づけられているプロジェクtpを探してくれて有効なリクエスト以外弾いてくれるみたいです。

ただ注意事項として、リクエストの一部として扱うため中間者攻撃には脆弱な面があるとのことです。
APIキー単独の認証に頼るのは避けたほうがよくて、他の認証方法と合わせてつかってくれとの記載があったのご注意ください。

## その他の認証
APIキー以外にはJWTを利用したものやAuth0を使用したものなど認証方法は色々あるようです。
パブリックアクセスさせるけど、認証はちゃんとやってリスクヘッジしたい場合には実現できるソリューションが用意されているみたいです。

## セキュアに使えるのかのまとめ
個人的には認証方法が豊富に用意されている点はよいなと思いました。
ただ、　API Gateway → Functionsが内部トラフィックでないとなるとFunctions側はすべてのトラフィックを許可しないとAPI Gatewayからのトラフィック受け取れないのでしょうか。。
そうなると、Fuctions側のエンドポイントもパブリックにさらされて、自分がイメージしてた使い方はできなさそうでした。

# おわりに
2020年9月にベータ版リリースとなったAPI Gatewayをさわってみました。
アクセス先がCloud FunctionsやCloud Runといった様々なリソースがある場合にはエンドポイントを一元化できる点や一元化したことでバックエンドへのアクセスの監視がしやすくなる点は魅力的だと感じました。

プロダクション環境で利用することを考えるとパブリックからアクセスされる先は極力絞りたい気持ちがあるので、

* API Gatewayに認証を適用して権限のあるユーザーだけをアクセス
* バックエンドサービスまでのアクセスはAPI Gatewayからのアクセスしか受け入れない

などを実現できると安心かなと個人的に思いました。

ひとまず、API Gateway触ってみたいと思ってる人の手助けになれば幸いです。