【GCP】API Gateway × Cloud Functions × BigQueryでAPIを作ってみる

みなさん、こんにちは！

アプリケーションの機能を外部から利用できる仕組みであるAPI（Application Programming Interface）。
最近ではChatGPTをはじめとした生成系AIサービスの利用拡大などに伴い、自作プログラムにAPIを組み込んで利用したり、システムやサービス間をAPIで疎結合化したり、APIでのデータ連携が主流になってきたりと、APIが身近な存在になっているのではないかと思います。

ところで、APIを利用することはよくありますが、作るとなるとどうでしょうか？
裏側の仕組みがよくわからないだけに、なんだか難しそうなイメージがありますよね。
実は、クラウドサービス（今回はGCP）を利用することで、そんなAPIを手軽に実現することができます。

具体的なAPIの作り方について、以降で詳しく見ていきましょう！

## 今回作るAPI
今回作成するのは、以下のようなJSON形式でユーザのID、名前、メールアドレスを受け取り、BigQuery上のテーブルに対して操作を行うシンプルなAPIです。
```ruby:qiita.rb
{
    "id": 1,
    "name": "レック太郎",
    "email": "req-taro@example.com"
}
```
機能としては、

 * ユーザ登録
 * ユーザ削除

の2つを考えます。構成図は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/b759b8bb-e44c-4997-acd7-75e829a9f943.png)

リクエストをAPIゲートウェイが受け付け、登録・削除に応じて2つのファンクションのどちらかが起動され、BigQueryに対して操作を行うという流れです。

## テーブル準備
まず、ユーザデータを管理するためのテーブルをBigQuery上に作成します。
今回は簡単に以下のようなusersテーブルを用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/bda11110-7b98-4e4c-a595-9858f8dd8793.png)

## バックエンド設定
### Cloud Functions
今回作るAPIのバックエンドとなるCloud Functionsのファンクションを作成していきます。

#### ①サービスアカウント設定
最初に、Cloud Functions用のサービスアカウントを作成します。
今回はCloud FunctionsからBigQueryにアクセスしたいので、BigQuery管理者の権限を付けておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/bd002f0c-a421-47f4-a1eb-d631ec18e64a.png)

#### ②ファンクション作成（ユーザ登録）
ファンクションの作成画面に移り、ユーザ登録用ファンクションtest-api-addの設定を行います。

今回はHTTPリクエストを投げる形なので、トリガーはHTTPSです。
ランタイムサービスアカウントには先ほど作成したサービスアカウントを指定します。
ランタイム環境変数はファンクション起動後にプログラム内で参照できる環境変数で、今回はusersテーブルのテーブルIDを指定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/a373988e-cc68-4faf-b227-0550440b8998.png)

次にプログラムの設定です。

プログラムは「ランタイム」タブからPython, Go, Java, Node.js等を選ぶことができます。
今回は以下のようなPythonプログラムを用意しました。
main.pyがエントリポイントを含むメインプログラムです。

main.py
```ruby:qiita.rb
import os
import json

import functions_framework
from pydantic import BaseModel
from google.cloud import bigquery

class User(BaseModel):
    id: int
    name: str
    email: str

def entrypoint(request):
    # リクエストデータ取得
    request_json = request.get_json()

    # ユーザデータ取得
    user = User(
        id=request_json.get("id"),
        name=request_json.get("name"),
        email=request_json.get("email")
    )

    # ユーザ登録
    add_user(user)

    # レスポンス（正常終了）
    response_json = {
        "code": 200,
        "message": "ユーザ登録に成功しました。"
    }
    response_body = json.dumps(response_json, ensure_ascii=False).encode("utf-8")
    return response_body, 200, {"Content-type": "application/json"}

def add_user(user: User):
    # BigQueryクライアント設定
    bq_client = bigquery.Client()

    TABLE_ID = os.environ.get("TABLE_ID")

    # クエリ作成
    query = f"""
    INSERT INTO `{TABLE_ID}` (id, name, email)
    VALUES (@id, @name, @email);
    """
    job_config = bigquery.QueryJobConfig(
        query_parameters=[
            bigquery.ScalarQueryParameter("id", "INTEGER", user.id),
            bigquery.ScalarQueryParameter("name", "STRING", user.name),
            bigquery.ScalarQueryParameter("email", "STRING", user.email),
        ]
    )

    try:
        result = bq_client.query(query, job_config=job_config, timeout=60).result()
    except Exception as e:
        raise Exception("クエリ実行に失敗しました。") from e
```
requirements.txt
```ruby:qiita.rb
functions-framework
requests
pydantic
google-cloud-bigquery
```

エントリポイントは**リクエストデータが引数として渡される関数**であり、リクエストを処理する本体部分となります。
赤線部分の名前が一致していないとエラーになるのでご注意ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/e8d2a8ba-79b3-4ad2-b187-6523831407dc.png)

デプロイに成功すると、以下のような画面でファンクションの詳細を確認することができます。
一部設定を除いて後から編集も可能で、メモリやCPUを増強したい場合は簡単に変更可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/208c46a1-3b77-4642-9233-7209d4b53b50.png)

#### ③ファンクション作成（ユーザ削除）
ユーザ削除用ファンクションtest-api-delの設定を行います。
②と基本的に同じですが、main.pyのプログラムのみ異なります。こちらのmain.pyは以下の通りです。

main.py
```ruby:qiita.rb
import os
import json
from typing import Optional

import functions_framework
from pydantic import BaseModel
from google.cloud import bigquery

class User(BaseModel):
    id: int
    name: Optional[str]
    email: Optional[str]

def entrypoint(request):
    # リクエストデータ取得
    request_json = request.get_json()

    # ユーザデータ取得
    user = User(
        id=request_json.get("id"),
        name=request_json.get("name"),
        email=request_json.get("email")
    )

    # ユーザ削除
    del_user(user)

    # レスポンス（正常終了）
    response_json = {
        "code": 200,
        "message": "ユーザ削除に成功しました。"
    }
    response_body = json.dumps(response_json, ensure_ascii=False).encode("utf-8")
    return response_body, 200, {"Content-type": "application/json"}

def del_user(user: User):
    # BigQueryクライアント設定
    bq_client = bigquery.Client()

    TABLE_ID = os.environ.get("TABLE_ID")

    # クエリ作成
    query = f"""
    DELETE FROM {TABLE_ID}
    WHERE id = @id
    """
    job_config = bigquery.QueryJobConfig(
        query_parameters=[
            bigquery.ScalarQueryParameter("id", "INTEGER", user.id),
        ]
    )

    # クエリ実行
    try:
        result = bq_client.query(query, job_config=job_config, timeout=60).result()
    except Exception as e:
        raise Exception("クエリ実行に失敗しました。") from e
```
こちらも同じようにデプロイします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/626e0cc9-16d8-4da3-92df-13d5043d7cde.png)

以上でCloud Functionsの設定は完了です。

## フロントエンド設定
### API Gateway
#### ①サービスアカウント設定
Cloud Functionsと同様、サービスアカウントの作成と設定を行います。
Cloud Functionsの呼び出しで「未認証の呼び出しを許可」を選択していた場合、権限付与の設定は必要ありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/2faa4b22-8d39-4a78-8723-08eb52327ea7.png)

#### ②API仕様作成
APIゲートウェイを作成するにあたり、OpenAPI仕様を作成する必要があります。
OpenAPIというのは、APIの仕様を定義するためのフォーマットであり、いわば**APIの設計書**です。
APIのアクセス先のパスとそれに対応するバックエンド、リクエストとレスポンスの内容などをYAMLまたはJSON形式で記述します。

今回設定するOpenAPI仕様は以下の通りです。

test-api-config.yaml
```ruby:qiita.rb
swagger: '2.0'
info:
  title: test-api
  description: テストAPI
  version: 1.0.0
schemes:
  - https
tags:
  - name: user
    description: ユーザ操作
produces:
  - application/json
paths:
  /api/v1/add:
    post:
      tags:
        - user
      summary: ユーザ情報を登録する
      description: ユーザ情報を登録する
      operationId: addUser
      x-google-backend:
        address: https://asia-northeast1-xxxxxxxxxxxxx.cloudfunctions.net/test-api-add
        path_translation: APPEND_PATH_TO_ADDRESS
        deadline: 60
      parameters:
        - in: body
          name: body
          description: ユーザ情報登録用データ
          required: true
          schema:
            $ref: '#/definitions/User'
      responses:
        '200':
          description: '成功'
          schema:
            $ref: '#/definitions/Response'
  /api/v1/del:
    post:
      tags:
        - user
      summary: ユーザ情報を削除する
      description: ユーザ情報を削除する
      operationId: delUser
      x-google-backend:
        address: https://asia-northeast1-xxxxxxxxxxxxx.cloudfunctions.net/test-api-del
        path_translation: APPEND_PATH_TO_ADDRESS
        deadline: 60
      parameters:
        - in: body
          name: body
          description: ユーザ情報削除用データ
          required: true
          schema:
            $ref: '#/definitions/User'
      responses:
        '200':
          description: '成功'
          schema:
            $ref: '#/definitions/Response'
definitions:
  User:
    type: object
    properties:
      id:
        type: integer
        example: 1
      name:
        type: string
        example: 'レック太郎'
      email:
        type: string
        example: 'req-taro@example.com'
  Response:
    type: object
    properties:
      code:
        type: integer
        example: 200
      message:
        type: string
        example: '成功'
```

`x-google-backend`で、先ほど作成したファンクションのURLを指定しています。
`paths`に指定した各パスが対象の機能をもつバックエンドに対応付けられるため、APIのURLは以下のようになります。

 * ユーザ登録：＜APIゲートウェイのURL＞/api/v1/add
 * ユーザ削除：＜APIゲートウェイのURL＞/api/v1/del

なお、OpenAPIは[Swagger Editor](https://editor.swagger.io/)等のツールを使って記述します。
今回のOpenAPI仕様をSwagger Editorで確認した場合、右側のようにドキュメント化された状態のものを見ることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/b83bd0d8-1970-4484-8b32-d5bc16dbc86e.png)

#### ③ゲートウェイ作成
API Gatewayのコンソール画面からゲートウェイを作成します。
API名や構成を設定し、「ゲートウェイを作成」を押すとゲートウェイの作成が始まります。
作成には少し時間がかかります。（5～10分程度）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/d8f773ba-3dc4-4f58-9b36-a095ae21188d.png)
※本記事執筆時点で、OpenAPIはバージョン2.0のみサポートされているようです。バージョン3.0以上のOpenAPI仕様はエラーになるためご注意ください。

問題なく作成完了すれば、ゲートウェイのURLが払い出されます。
画像の赤枠部分が今回作成したAPI（ゲートウェイ）本体のURLです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/6019c60e-8453-462b-be7b-73f862320577.png)

ちなみに、ゲートウェイのURLはデフォルトの場合`https://xxxxxx.an.gateway.dev`のようになりますが、ロードバランサを利用してカスタムドメインを設定することも可能です。
詳しくは[API ゲートウェイの HTTP(S) 負荷分散スタートガイド](https://cloud.google.com/api-gateway/docs/gateway-serverless-neg?hl=ja)をご参照ください。

以上で今回のAPIは完成です！

## 動作確認
完成したAPIがちゃんと動くかどうか見てみましょう。
今回作成したAPIは未認証の呼び出しを許可しているので、対象プロジェクト以外の環境からでもアクセス可能です。
試しにローカル端末から、Talend API Testerというツールを使ってリクエストを送ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/17aaa7db-50d4-466f-a1fa-24b9234c1a70.png)

ユーザ登録リクエストを送ると、期待通り「ユーザ登録に成功しました。」というレスポンスが返ってきました。
実際にusersテーブルを確認してみると...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956386/4644d286-1900-4d39-be41-6f81c6d20005.png)

無事に「レック 太郎」さんのデータが入っていました。
同様にユーザ削除を試してみると、「レック 太郎」さんのデータが削除されることが確認できます。

## あとがき
今回作成したAPIは簡易的なものなので、認証まわりやリクエストの処理、エラーハンドリングなど突っ込みどころの多いものになっていますが、APIの作り方についてざっくりとイメージを掴んでいただけたのではないでしょうか。

今回ご紹介したように、API GatewayやCloud Functionsといったサービスを使うことで、APIを手軽に作ることができます。
サーバレスであるためリソースの管理がいらないことに加えて、利用状況に応じて後から柔軟にリソースの増強ができたり、自前であれこれ用意するのに比べ低コストで運用できる点が魅力的です。