zenpachi blog

Cloud Runで認証付きのAPIを作成する

このエントリーをはてなブックマークに追加

Google Cloud Next '19にて発表されたCloud Runを触ってみます。
公式ガイドに従ってコンテナの作成からデプロイまで試し、認証付きのAPIを作成します。

Cloud Runとは?

公式ドキュメントによると

Run stateless HTTP containers on a fully managed environment or in your own GKE cluster.

とのことで、マネージド環境もしくはGKEクラスタでコンテナを実行できるようですね。
つまり、コンテナを利用することでAWS Lambdaなどのようなサーバレスなサービスを任意の環境で実行することができるようです。
詳細な説明はあんまり自信がないので公式サイトを見てください。また、2019/05/08現在Cloud Runはβ版なので今後仕様が大きく変更になる可能性があります。

まずはクイックスタートガイドに従ってサンプルを起動させてみる

Quickstarts  |  Cloud Run  |  Google Cloud

サンプルとして構築済みコンテナを利用することができるようなので「Deploy a Prebuilt Sample Container」に従って進めます。

Quickstarts

GCPプロジェクトを作成。

GCPプロジェクトを作成

Cloud Runの設定をしていきます。
まずは「サービスを作成」をクリックして、

サービスを作成

設定内容を入力していきます。
「コンテナイメージのURL」にはあらかじめ用意されているサンプルコンテナのURLである「gcr.io/cloudrun/hello 」を入力します。
とりあえず誰でもアクセスできるように「未認証の呼び出しを許可」にチェックを入れておきます。

未認証の呼び出しを許可

「作成」をクリックすると10秒もせずに以下のような画面が表示され、デプロイが完了したようです。速い。

デプロイ完了

あとは表示されているURLへアクセスすれば画面が表示されます。
驚くほど簡単ですね。

独自のコンテナをビルドしてデプロイする

Quickstart: Build and Deploy  |  Cloud Run  |  Google Cloud

独自のDockerfileを作成し、ビルド〜デプロイまでをやってみます。 cloud SDKのインストールについては割愛します。

gcloud beta componentのインストール

$ gcloud components install beta

componentをアップデート

$ gcloud components update

今回はサンプルにあるPythonコードを使用します。

import os

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    target = os.environ.get('TARGET', 'World')
    return 'Hello {}!\n'.format(target)

if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))
# Use the official Python image.
# https://hub.docker.com/_/python
FROM python:3.7

# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . .

# Install production dependencies.
RUN pip install Flask gunicorn

# Run the web service on container startup. Here we use the gunicorn
# webserver, with one worker process and 8 threads.
# For environments with multiple CPU cores, increase the number of workers
# to be equal to the cores available.
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:app

Cloud Buildを使用してコンテナイメージを生成します。

$ gcloud builds submit --tag gcr.io/[PROJECT-ID]/helloworld

生成したコンテナイメージを使用してCloud Runへデプロイします。

$ gcloud beta run deploy --image gcr.io/[PROJECT-ID]/helloworld

リージョン、サービス名を聞かれるので適当に決めます。
アクセス権限はひとまず全てのユーザがアクセスできるようにします。

Please specify a region:
 [1] us-central1
 [2] cancel
Please enter your numeric choice:  1

To make this the default region, run `gcloud config set run/region us-central1`.

Service name: (helloworld):  cloudrunsample2
Deploying container to Cloud Run service [cloudrunsample2] in project [PROJECT-ID] region [us-central1]
Allow unauthenticated invocations to new service [cloudrunsample2]?
(y/N)?  y

これで少し待つとデプロイが完了します。
表示されたURLへアクセスすると「Hello World!」と表示されました。

Cloud RunをAPIとして使用する

WebページからCloud Runを使用してみます。

<html lang="en">
  <body>
    <h1>Cloud Run Sample</h1>
    <script>
      const onLoad = () => {
        fetch('https://<Cloud Run Host>/', {
          mode: 'cors',
        })
          .then(response => response.text())
          .then(text => console.log(text));
      };
      window.onload = onLoad;
    </script>
  </body>
</html>

このようなhtmlページを用意し、適当なビルトインサーバなどで起動します。
<Cloud Run Host>部分は実際のCloud RunのURLに差し替えます。

このページにアクセスをすると、下記のようにクロスドメイン制約によりエラーとなります。

クロスドメインエラー

Cloud Runで作成したAPIはデフォルトではCORS対応はしていないので、自前で設定してやる必要があります。
app.pyとDockerfileを以下のように変更します。

import os

from flask import Flask
from flask_cors import CORS # 追加

app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}}) # 追加

@app.route('/')
def hello_world():
    target = os.environ.get('TARGET', 'World')
    return 'Hello {}!\n'.format(target)

if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))
# Use the official Python image.
# https://hub.docker.com/_/python
FROM python:3.7

# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . .

# Install production dependencies.
# ↓flask-corsを追加
RUN pip install Flask gunicorn flask-cors

# Run the web service on container startup. Here we use the gunicorn
# webserver, with one worker process and 8 threads.
# For environments with multiple CPU cores, increase the number of workers
# to be equal to the cores available.
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:app

変更を反映するために再びCloud Buildを使用してコンテナイメージを更新します。

$ gcloud builds submit --tag gcr.io/[PROJECT-ID]/helloworld

Cloud Runサービスを再デプロイします。
先ほどはコマンドでデプロイしましたが、今度は管理画面から行ってみます。
Cloud Runサービスの詳細ページ上部「新しいバージョンをデプロイ」をクリックし、

新しいバージョンをデプロイ

「コンテナイメージのURL」からlatestのものを選択します。

latestなコンテナイメージを選択

あとはデプロイをクリックすれば新しいコンテナイメージでアップデートされます。
ブラウザからの通信も通るようになりました。

ブラウザからの通信に成功

Cloud Runに認証を設定する

現時点では全てのユーザが作成したCloud Runサービスを利用できる状態ですが、実際には認証を設定して特定のユーザのみアクセスできるようにしたいケースも多いかと思いますので、Googleアカウントによる認証を設定してみます。 サンプルとしてGoogleアカウントのTokenを含んだアクセスのみ通信ができるAPIを作成してみます。  

ただし、Cloud Runで認証付きのAPIを作成するのは少々面倒です。先ほど試した通り、Cloud RunはデフォルトではCORSに対応していないのですが、認証のためにAuthorizationヘッダを付与してブラウザから通信を行うとpreflightが発生し、そのpreflightはコンテナよりも手前で処理されてしまうためクロスドメイン制約に引っかかってしまいます。

それを避けるために、Cloud Endpointsを使用して認証を行う方法を試してみます。

まずは作成したCloud Runサービスの「役割/メンバー」から「allUsers」を削除します。これで通常のアクセスはできなくなります。

allUsersの削除

次にCloud Endpointを設定します。
公式ドキュメントはこちら

ESP(Extensible Service Proxy)コンテナをCloud Runにデプロイします。
CLOUD_RUN_SERVICE_NAMEはエンドポイントとして使用するCloud Runサービスの名前(任意)、ESP_PROJECT_IDはGCPプロジェクトのIDを指定します。

$ gcloud beta run deploy CLOUD_RUN_SERVICE_NAME \
    --image="gcr.io/endpoints-release/endpoints-runtime-serverless:1.30.0" \
    --allow-unauthenticated \
    --project=ESP_PROJECT_ID

次にOAuthの設定を行なっていきます。
GCPコンソールの APIとサービス > 認証情報 > OAuth同意画面 から同意画面の設定を行います。
アプリケーション名に任意の名前を入力して保存します。

同意画面の設定

GCPコンソールの APIとサービス > 認証情報 > 認証情報を作成 > OAuthクライアントID > ウェブアプリケーション からクライアントIDの設定を行います。
今回はサンプルとしてlocalhost:8000からアクセスするためlocalhost:8000を「承認済みのJavaScript生成元」と「承認済みのリダイレクトURI」に入力しておきます。

OAuthクライアントIDの作成

最後にエンドポイントの設定を行なっていきます。OpenAPIドキュメントの形式で記述します。
HOSTには先ほど作成したESPコンテナを使用したCloud Runサービスのhostを指定します。
https://backend-hash-uc.a.run.appにはバックエンドとして使用するCloud RunサービスのURLを指定します。
CLIENT-IDには先ほど作成した認証情報のクライアントIDを指定します。

swagger: '2.0'
info:
  title: Cloud Endpoints + Cloud Run
  description: Sample API on Cloud Endpoints with a Cloud Run backend
  version: 1.0.0
host: HOST
schemes:
  - https
produces:
  - application/json
x-google-backend:
  address: https://backend-hash-uc.a.run.app
paths:
  /hello:
    get:
      summary: Greet a user
      operationId: hello
      responses:
        '200':
          description: A successful response
          schema:
            type: string
      security:
        - google_id_token: []
    options:
      summary: options request
      operationId: options
      responses:
        '200':
          description: A successful response
          schema:
            type: string
securityDefinitions:
  google_id_token:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: "https://accounts.google.com"
    x-google-jwks_uri: "https://www.googleapis.com/oauth2/v3/certs"
    x-google-audiences: "CLIENT-ID.apps.googleusercontent.com"

「securityDefinitions」以下がGoogleアカウントによる認証を指定しています。
また、サンプルの都合上app.pyのルーティングも変更しておきます。コンテナイメージのビルドと、Cloud Runサービスの再デプロイまで行なっておきましょう。

@app.route('/')@app.route('/hello')
$ gcloud builds submit --tag gcr.io/[PROJECT-ID]/helloworld

次にエンドポイント設定をデプロイします。

$ gcloud endpoints services deploy openapi-run.yaml \
  --project ESP_PROJECT_ID

ESPの設定を行います。公式ドキュメントでは以下のように書かれていますが、実行するとエラーが表示されます。

$ gcloud beta run configurations update \
   --service CLOUD_RUN_SERVICE_NAME  \
   --set-env-vars ENDPOINTS_SERVICE_NAME=YOUR_SERVICE_NAME \
   --project ESP_PROJECT_ID

2019/05/08現在では以下が正しいようです。
CLOUD_RUN_SERVICE_NAMEはESPコンテナを展開したCloud Runサービスの名前を、YOUR_SERVICE_NAMEはエンドポイントサービスの名前に置き換えます。これはopenapi-run.yamlのHOSTで指定したものと同じです。

$ gcloud beta run services update CLOUD_RUN_SERVICE_NAME \
   --set-env-vars ENDPOINTS_SERVICE_NAME=YOUR_SERVICE_NAME

ESPにCloud Runサービスの実行権限を与えます。
こちらも公式ドキュメントでは以下のように書かれていますが、実行するとエラーが表示されます。

$ gcloud beta run add-iam-policy-binding BACKEND_SERVICE_NAME \
    --member "serviceAccount:ESP_PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
    --role "roles/run.invoker" \
    --project BACKEND_PROJECT_ID

2019/05/08現在では以下が正しいようです。
BACKEND_SERVICE_NAMEはバックエンドとして使用するCloud Runサービスの名前に、ESP_PROJECT_NUMBERはGCPプロジェクトのプロジェクト番号に、BACKEND_PROJECT_IDはGCPプロジェクトのプロジェクトIDに置き換えます。

$ gcloud beta run services add-iam-policy-binding BACKEND_SERVICE_NAME \
    --member "serviceAccount:ESP_PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
    --role "roles/run.invoker" \
    --project BACKEND_PROJECT_ID

これにより、バックエンドとして実行するCloud Runサービスの「役割/メンバー」が追加されます。

役割/メンバー

これで全ての準備ができました。サンプルとして以下のようなhtmlを作り、適当なビルトインサーバなどで8000番ポートで立ち上げます。
参考:Google Sign-In for Websites  |  Google Developers .
YOUR_CLIENT_IDは作成した認証情報のクライアントIDを使用します。 YOUR_ESP_HOSTはエンドポイントのhostに差し替えます。

<html lang="en">
  <head>
    <meta name="google-signin-scope" content="profile email">
    <meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com">
    <script src="https://apis.google.com/js/platform.js" async defer></script>
  </head>
  <body>
    <div class="g-signin2" data-onsuccess="onSignIn" data-theme="dark"></div>
    <script>
      function onSignIn(googleUser) {
        var id_token = googleUser.getAuthResponse().id_token;
        fetch('https://YOUR_ESP_HOST/hello', {
            headers: { 'Authorization': `Bearer ${id_token}` }
        })
          .then(response => response.text())
          .then(text => console.log(text));
      }
    </script>
  </body>
</html>

これでブラウザからアクセスし、Googleアカウントでログインを行うと、無事にCloud Runサービスを認証付きAPIとして使用することができました。

認証付きAPI完成

まとめ

Cloud Endpointsと組み合わせる必要があったり、公式ドキュメントの記述が怪しかったりとでやや詰まりましたが結果としてはかなり少ない手数でAPIを構築できることがわかりました。
Dockerを使用することでバックエンドの構成は自由になるので、静的ホスティングサービスと組み合わせればお手軽に自由度の高いシステムが組めそうですね。

参考

このエントリーをはてなブックマークに追加