見出し画像

Cloud Run + API Gateway で自作 API を作って検証してみた

0. TL;DR

Google Cloud Platform(GCP) には Apigee や Cloud Endpoints といった API 管理プラットフォームが既存してますが Apigee のような高機能は必要なく、もっと手軽に扱いたい。もう一つの Cloud Endpoints は機能的には満たしているのですが、バックエンドの前に ESP(Extensible Service Proxy) と呼ばれる Proxy を配置しなければならず、構成が面倒くさい… 😢

引用:クイックスタート: ESPv2 を使用して Cloud Run 用の Cloud Endpoints OpenAPI を設定する  |  OpenAPI を使用した Cloud Endpoints  |  Google Cloud

もうぜんぶフルマネージドでまるっと提供してくれるプラットフォームがほしいと思っていたところにいい感じでお任せできる API Gateway が登場してくれました!

なお、本記事ではタイトルにあるとおり API Gateway にフォーカスするため Cloud Run について(実装解説やデプロイのやり方など)は触れません。

1.参考にした記事 〜謝辞〜

2. API Gateway とは?

GCP のサーバーレス バックエンド(Cloud Functions、Cloud Run、App Engine など)で自作した API を管理するプロダクトです。構成は以下のとおりになります。

画像1
引用:API Gateway の概要  |  API Gateway のドキュメント  |  Google Cloud

API の認証やモニタリング、ロギングなどをフルマネージドで提供してくれま
す。ちなみに CORS にも対応しています(検証済み)。
また、公式ドキュメントによると API Gateway でサポートされる認証方法は以下のとおりのようです。

  • API キー

  • サービス アカウント

  • ユーザー認証

    • Firebase Authentication

    • Auth0

    • Okta

    • Google ID トークン 

3. 成果物

構成は前述の「2. API Gateway とは?」で引用した Google 公式ドキュメントの構成図のとおりで API Gateway の後ろが Cloud Run になるだけです。
認証は Firebase Authentication とサービス アカウントを試してみました。

クライアント(vscode)から API を実行したイメージは次のようになります。正しい認証情報が渡されないと 401 エラーが返り API が実行されないことがわかります。

上記のイメージを上から順に解説すると、下記のようになります。

  1. JSON Web Token(JWT) なしで API にリクエストを出した時

  2. 有効期限切れの JWT で API にリクエストを出した時

  3. 正しい JWT を付けて API にリクエストを出した時

4. Cloud Run 側(バックエンド)を作る

Cloud Run 側に API エンドポイントを作ります。今回はサンプル API として GET および POST を用意し、さらに認証あり/なしの計4つの API を作成しました。

Go 言語で書きます。私はふだん gin フレームワークを使ってます。

4.1 認証なし GET リクエスト API

GET https://**********-an.a.run.app/api/v1/sample

コードは以下のとおり。固定の JSON レスポンスを返すだけです。

func (h *Handler) GetSampleHandler(c *gin.Context) {
    c.Writer.Header().Set("Content-Type""application/json; charset=UTF-8")

    result := gin.H{
	"text""げっと",
	"id":   {GCP project id},
    }
    c.JSON(http.StatusOK, result)
}

4.2 認証なし POST リクエスト API

POST https://**********-an.a.run.app/api/v1/sample

コードは以下のとおり。リクエスト ボディで受け取った値(text)をそのままオウム返しします。

func (h *Handler) PostSampleHandler(c *gin.Context) {
	c.Writer.Header().Set("Content-Type""application/json; charset=UTF-8")

	body, _ := ioutil.ReadAll(c.Request.Body)
	data, err := simplejson.NewJson(body)
	if err != nil {
		httputil.NewError(c, http.StatusInternalServerError, fmt.Errorf("json parse error: %v", err))
		returnelse if _, ok := data.CheckGet("text"); !ok {
		httputil.NewError(c, http.StatusBadRequest, fmt.Errorf("bad request."))
		return
	}

	result := gin.H{
		"text":  "ぽすと",
		"input": data.Get("text").MustString(),
		"id":    {GCP project id},
	}
	c.JSON(http.StatusOK, result)
}

4.3 認証(Firebase Authentication)あり GET リクエスト API

GET https://**********-an.a.run.app/api/v1/secure

コードは以下のとおり。このサンプル API は Firebase Authentication によるユーザー認証での利用を想定してます。後述しますが API Gateway は認証結果を X-Apigateway-Api-Userinfo ヘッダーに入れて、バックエンド API(今回は Cloud Run)に送信してくれます。
なので、この API ではその認証結果を取得し、認証済みユーザーのメールアドレスをレスポンスとして返すようにしてます。

func (h *Handler) GetSecureHandler(c *gin.Context) {
	c.Writer.Header().Set("Content-Type""application/json; charset=UTF-8")

	if c.GetHeader("X-Apigateway-Api-Userinfo") == "" {
		httputil.NewError(c, http.StatusUnauthorized, fmt.Errorf("called unauthenticated."))
		return
	}

	decodedBytes, err := base64.RawURLEncoding.DecodeString(c.GetHeader("X-Apigateway-Api-Userinfo"))
	if err != nil {
		httputil.NewError(c, http.StatusInternalServerError, fmt.Errorf("decoding failed: %v", err))
		return
	}

	user, err := simplejson.NewJson(decodedBytes)
	if err != nil {
		httputil.NewError(c, http.StatusInternalServerError, fmt.Errorf("json parse error: %v", err))
		return
	}

	result := gin.H{
		"text":  "せきゅあ",
		"id":    {GCP project id},
		"email": user.Get("email").MustString(),
	}
	c.JSON(http.StatusOK, result)
}

4.4 認証(サービス アカウント)あり POST リクエスト API

POST https://**********-an.a.run.app/api/v1/secure

コードは以下のとおり。このサンプル API は サービス アカウント認証での利用を想定してます。
API Gateway の認証結果を取得し、サービス アカウント キーをレスポンスとして返すようにしてます。

func (h *Handler) PostSecureHandler(c *gin.Context) {
	c.Writer.Header().Set("Content-Type""application/json; charset=UTF-8")

	if c.GetHeader("X-Apigateway-Api-Userinfo") == "" {
		httputil.NewError(c, http.StatusUnauthorized, fmt.Errorf("called unauthenticated."))
		return
	}

	decodedBytes, err := base64.RawURLEncoding.DecodeString(c.GetHeader("X-Apigateway-Api-Userinfo"))
	if err != nil {
		httputil.NewError(c, http.StatusInternalServerError, fmt.Errorf("decoding failed: %v", err))
		return
	}

	user, err := simplejson.NewJson(decodedBytes)
	if err != nil {
		httputil.NewError(c, http.StatusInternalServerError, fmt.Errorf("json parse error: %v", err))
		return
	}

	body, _ := ioutil.ReadAll(c.Request.Body)
	data, err := simplejson.NewJson(body)
	if err != nil {
		httputil.NewError(c, http.StatusInternalServerError, fmt.Errorf("json parse error: %v", err))
		returnelse if _, ok := data.CheckGet("text"); !ok {
		httputil.NewError(c, http.StatusBadRequest, fmt.Errorf("bad request."))
		return
	}

	result := gin.H{
		"text":  "せきゅあ",
		"input": data.Get("text").MustString(),
		"id":    {GCP project id},
		"email": user.Get("email").MustString(),
	}
	c.JSON(http.StatusOK, result)
}

以上でバックエンド(Cloud Run)側の実装は完了です。
ローカルでのテストが済んだら Cloud Run にデプロイします。なお、認証は API Gateway に担当してもらうので Cloud Run では未認証(公開)にしておきます。

5. API Gateway を作る

gcloud コマンドライン ツールでも作成可能ですが、今回は GUI から作成する方法を紹介します。
GCP コンソールから API Gateway に移動し、ゲートウェイを作成していきます。まずは API を定義します。

次に API 構成を定義します。次節で解説する API 定義をアップロードします。また、サービス アカウントの選択はバックエンドにアクセスするために必要なロールを割り当てたサービス アカウント指定します。今回はバックエンドに Cloud Run を配置しているので 、それ専用のサービス アカウントを作成しています。

さらに、ゲートウェイ サービス アカウントには、バックエンド サービスにアクセスするために必要な権限が必要です。たとえば、バックエンドを Cloud Functions として実装する場合、少なくとも Cloud Functions 起動元のロールをサービス アカウントに割り当てる必要があります。Cloud Run バックエンドの場合、ロールは Cloud Run 起動元です。API 構成に関連付けられた権限を制限することで、バックエンド システムのセキュリティを強化できます。

開発環境の構成  |  API Gateway のドキュメント  |  Google Cloud

最後にゲートウェイを定義します。リージョンは利用する地域に合わせて選択してください。

API Gateway の定義が完成すると、ゲートウェイの URL が割り当てられます。これが API のエンドポイントになります。

6. API 定義を作る

OpenAPI 仕様 2.0 に準拠した API 定義を作成します。これは API の振る舞いを YAML で記述したものになります。
これをゼロから書くのが面倒な私は gin-swagger を利用して自動生成し API Gateway 用のベース(雛形)にしています(ちなみに自動生成された swagger.yaml のままでは API Gateway で扱うことはできません)。

完成した API 定義は少し長くなりますが以下になります。なお、固有の識別子や機密情報などは伏せ字にしてあります。

〜 ここから先は有料記事となります 〜
投げ銭として寄付していただけると嬉しいです♪

ここから先は

14,757字 / 7画像

¥ 5,000

この記事が気に入ったらサポートをしてみませんか?