Cloud Runで認証付きのAPIを作成する
Posted at: 2019-05-08
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」に従って進めます。
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のものを選択します。
あとはデプロイをクリックすれば新しいコンテナイメージでアップデートされます。
ブラウザからの通信も通るようになりました。
Cloud Runに認証を設定する
現時点では全てのユーザが作成したCloud Runサービスを利用できる状態ですが、実際には認証を設定して特定のユーザのみアクセスできるようにしたいケースも多いかと思いますので、Googleアカウントによる認証を設定してみます。 サンプルとしてGoogleアカウントのTokenを含んだアクセスのみ通信ができるAPIを作成してみます。
ただし、Cloud Runで認証付きのAPIを作成するのは少々面倒です。先ほど試した通り、Cloud RunはデフォルトではCORSに対応していないのですが、認証のためにAuthorizationヘッダを付与してブラウザから通信を行うとpreflightが発生し、そのpreflightはコンテナよりも手前で処理されてしまうためクロスドメイン制約に引っかかってしまいます。
それを避けるために、Cloud Endpointsを使用して認証を行う方法を試してみます。
まずは作成したCloud Runサービスの「役割/メンバー」から「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」に入力しておきます。
最後にエンドポイントの設定を行なっていきます。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として使用することができました。
まとめ
Cloud Endpointsと組み合わせる必要があったり、公式ドキュメントの記述が怪しかったりとでやや詰まりましたが結果としてはかなり少ない手数でAPIを構築できることがわかりました。
Dockerを使用することでバックエンドの構成は自由になるので、静的ホスティングサービスと組み合わせればお手軽に自由度の高いシステムが組めそうですね。