はじめに
チームメンバーがAWSのAPI Gatewayをプロダクション環境に導入するということで、GCPではどんな感じだろうかと思い勉強してみました。
ついでによくあるサーバーレスアーキテクチャであるAPI Gateway + Cloud Functions + Cloud Datastoreの構成でAPIを構築してみました。
API Gatewayは最近β版リリースしたということで文献が少ないと思うので、GCPでサーバーレスしたい!という方の参考になればと思います。
API Gateway とは ?
サーバーレス ワークロード向けのフルマネージド ゲートウェイ
API ゲートウェイを利用して、Google Cloud サーバーレス バックエンド(Cloud Functions、Cloud Run、App Engine など)向け API を作成、保護、モニタリングできます。
公式ドキュメントより抜粋
APIのエンドポイントが複数あるとアクセス先のURLが異なってしまうところ、API Gatewayをかますことでアクセス先URLの一元化が可能になります。
また、今回のようなCLoud Functionsを複数作成すると監視する場合、各関数を見にいかなければならないところをAPI Gatewayを監視することで完結するといったメリットがあります。
構築するサーバーレスシステム
まずはAPIGatwayを構築します。
API Gatewayの作成
コンソールから作成する際に表示名とAPIIDを入力します。
この時表示された
作成すると、次にAPI仕様を決める画面になりますがここはスキップします。
ひとまずコンソール上に入力した名前でAPIが作成されているかと思います。
API仕様の決定およびアップロード
ここがハマりどころかもしれません。(自分はうまくいかなくて試行錯誤しました。。)
先ほど作成したAPIを選択して「構成」タブにからアップロードを押すと「API構成のアップロード」画面にいきます。
ここでOpenAPI Spec2.0でAPI仕様を決めます。
OpenAPIとは、REST API のインターフェースを記述するための仕様でインターフェースの定義はJSONまたはYAMLで記述します。
定義ファイルから自動的にAPIのドキュメントを生成可能だったりするようです。
一から書くのは大変なので、今回は下記のサイトで編集しました。文法が合っているか確認できるのでとても便利です。
Swagger Editor
OpenAPIは3.0.0で対応していて、Swaggerなら2.0で対応しているので、今回はswagger:"2.0"
で記述します。
題材としてPetostoreの簡易版を記述してみます。
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
各pathsのaddressがAPI GatewayのバックエンドのAPIのエンドポイントになります。この後、この仕様通りにレスポンスを返すCloud Functionsを作って行きます。
このファイルをアップロードするとAPIの構成に作成した仕様が追加されます。
ゲートウェイへのデプロイについては、ゲートウェイを別途作成するので今期はデプロイしていません。
ゲートウェイの作成
ここではゲートウェイが作成されるロケーションとどの構成をデプロイするかを決定します。
ロケーションはus-central1, europe-west1, asia-east1のみが選択可能です。
これでAPI Gatewayのエンドポイントが作成されます。
Cloud Functinosで関数の作成
Cloud Fuctionsについては既存の記事があるので簡単にまとめていきます。こちら参考にさせていただきました。
Google Cloud FunctionsでPythonを利用してみた(Beta利用)
Authenticationについては「未認証を許可」としています。後ほど、構築したアーキテクチャをセキュリティ高くできるか検証するためあえてガバガバにしています。
また接続についても「すべてのトラフィックを許可する」に設定しています。
Cloud Datastoreとの連携
Funtionsの中でDatastoreにアクセスさせてたかったので、こちらの記事を参考にコードを作成しました。
Python3でDatastore を動かしてみた
構成は、下記のようにしています。
- Namespace: Petes
- Kind: pet
- Entity: id, name
特定のnamespaceにアクセスしたかったのでclientを定義する際に引数でnamespaceを渡すようにしています。
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
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'
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
なので、それぞれ叩いて見ます。
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触ってみたいと思ってる人の手助けになれば幸いです。