Cloud Run への安全なデプロイ

1. 概要

セキュリティを強化するために Cloud Run にサービスをデプロイするデフォルトの手順を変更してから、デプロイされたアプリに安全な方法でアクセスする方法を確認します。アプリが「パートナー登録サービス」であるこれは、Cymbal Eats と連携して料理の注文を処理する企業が使用する Cymbal Eats アプリケーションのものです。

学習内容

アプリを Cloud Run にデプロイする際のデフォルトの最小手順に少しの変更を加えることで、セキュリティを大幅に高めることができます。既存のアプリとデプロイの手順に沿ってデプロイ手順を変更し、デプロイ済みアプリのセキュリティを向上させます。

その後、アプリへのアクセスを認可し、認可されたリクエストを行う方法を確認します。

これは、アプリケーション デプロイのセキュリティを網羅した説明ではありません。わずかな労力でセキュリティを向上させる、将来のすべてのアプリケーション デプロイに加えることができる変更について説明します。

2. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。この値はいつでも更新できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常、それが何であるかは関係ありません。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常は PROJECT_ID として識別されます)。生成された ID が気に入らない場合は、別のランダムな ID を生成できます。または、ご自身でお試しになることもできます。このステップを終えた後は変更できず、プロジェクト期間中は維持されます。
  • なお、3 つ目の値は、一部の API で使用される [プロジェクト番号] です。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルの終了後に課金されないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクト全体を削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell をアクティブにする

  1. Cloud Console で、[Cloud Shell をアクティブにする] 853e55310c205094.png をクリックします。

55efc1aaa7a4d3ad.png

Cloud Shell を初めて起動する場合は、その内容を説明する中間画面(スクロールしなければ見えない範囲)が表示されます。その場合は、[続行] をクリックします(今後表示されなくなります)。この中間画面は次のようになります。

9c92662c6a846a5c.png

Cloud Shell のプロビジョニングと接続に少し時間がかかる程度です。

9f0e51b578fecce5.png

この仮想マシンには、必要な開発ツールがすべて含まれています。仮想マシンは Google Cloud で稼働し、永続的なホーム ディレクトリが 5 GB 用意されているため、ネットワークのパフォーマンスと認証が大幅に向上しています。このコードラボでの作業のほとんどは、ブラウザまたは Chromebook から実行できます。

Cloud Shell に接続すると、すでに認証は完了しており、プロジェクトに各自のプロジェクト ID が設定されていることがわかります。

  1. Cloud Shell で次のコマンドを実行して、認証されたことを確認します。
gcloud auth list

コマンド出力

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Cloud Shell で次のコマンドを実行して、gcloud コマンドがプロジェクトを認識していることを確認します。
gcloud config list project

コマンド出力

[core]
project = <PROJECT_ID>

上記のようになっていない場合は、次のコマンドで設定できます。

gcloud config set project <PROJECT_ID>

コマンド出力

Updated property [core/project].

環境設定

このラボでは、Cloud Shell コマンドラインでコマンドを実行します。通常はコマンドをコピーしてそのまま貼り付けることができますが、プレースホルダの値を正しい値に変更しなければならない場合があります。

  1. 後のコマンドで使用するために、環境変数にプロジェクト ID を設定します。
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
  1. アプリを実行する Cloud Run サービス API、NoSQL データ ストレージを提供する Firestore API、デプロイ コマンドで使用する Cloud Build API、ビルド時にアプリケーション コンテナを保持するために使用する Artifact Registry を有効にします。
gcloud services enable \
  run.googleapis.com \
  firestore.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com
  1. Firestore データベースをネイティブ モードで初期化します。このコマンドは App Engine API を使用するため、先に有効にする必要があります。

このコマンドでは、App Engine のリージョン(使用しませんが、履歴上の理由から作成する必要があります)と、データベース用のリージョンを指定する必要があります。App Engine には us-central、データベースには nam5 を使用します。nam5 は米国のマルチリージョン ロケーションです。マルチリージョンのロケーションでは、データベースの可用性と耐久性が最大化されます。

gcloud services enable appengine.googleapis.com

gcloud app create --region=us-central
gcloud firestore databases create --region=nam5
  1. サンプルアプリのリポジトリのクローンを作成し、以下のディレクトリに移動します。
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git

cd cymbal-eats/partner-registration-service

3. README を確認する

エディタを開き、アプリを構成するファイルを確認します。このアプリのデプロイに必要な手順を説明する README.md を確認します。手順によっては、暗黙的または明示的なセキュリティ上の判断が必要となる場合があります。ここで説明するように、これらの選択肢のいくつかは、デプロイ済みアプリのセキュリティを向上させるために変更します。

ステップ 3 - npm install を実行する

アプリで使用されているサードパーティ ソフトウェアの来歴と整合性を把握することは重要です。ソフトウェア サプライ チェーンのセキュリティの管理は、Cloud Run にデプロイされたアプリだけでなく、あらゆるソフトウェアの構築に関連しています。このラボではデプロイに重点を置いているため、この領域については説明しませんが、このトピックは別途調査することをおすすめします。

ステップ 4、5 - deploy.sh を編集して実行する

この手順では、アプリを Cloud Run にデプロイし、ほとんどのオプションをデフォルトのままにします。このステップは、次の 2 つの方法でデプロイの安全性を高めるために、変更します。

  1. 未認証のアクセスは許可しないでください。データ探索中に試してみるのに便利な方法ですが、これは商用パートナーが使用するウェブサービスであり、常にユーザーを認証する必要があります。
  2. API やリソースに必要以上に多くのアクセス権を持つデフォルトのサービス アカウントではなく、必要な権限のみを持つ専用のサービス アカウントをアプリケーションで使用するように指定します。これは最小権限の原則と呼ばれ、アプリケーション セキュリティの基本的な概念です。

手順 6 ~ 11 - サンプル ウェブ リクエストを作成して正しい動作を確認する

アプリケーションのデプロイで認証が必要になるため、リクエストにはリクエスト元の ID の証明を含める必要があります。これらのファイルを変更する代わりに、コマンドラインから直接リクエストを行います。

4. サービスを安全にデプロイする

deploy.sh スクリプトで、未認証のアクセスを許可しないことと、最小限の権限を持つ専用のサービス アカウントを使用するという 2 つの変更が、必要に応じて特定されました。

まず新しいサービス アカウントを作成してから、deploy.sh スクリプトを編集してそのサービス アカウントを参照して未認証のアクセスを禁止します。その後、変更したスクリプトを実行してサービスをデプロイしてから、変更した deploy.sh スクリプトを実行します。

サービス アカウントを作成し、Firestore/Datastore に必要なアクセス権を付与する

gcloud iam service-accounts create partner-sa

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:partner-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role=roles/datastore.user

deploy.sh の編集

deploy.sh ファイルを変更して未認証のアクセスを禁止し(–no-allow-unauthenticated)、デプロイ済みのアプリの新しいサービス アカウント(–service-account)を指定します。実際のプロジェクトの ID になるよう GOOGLE_PROJECT_ID を修正します。

次のように、最初の 2 行を削除し、残りの 3 行を変更します。

gcloud run deploy $SERVICE_NAME \
  --source . \
  --platform managed \
  --region ${REGION} \
  --no-allow-unauthenticated \
  --project=$PROJECT_ID \
  --service-account=partner-sa@${PROJECT_ID}.iam.gserviceaccount.com

サービスをデプロイする

コマンドラインから、deploy.sh スクリプトを実行します。

./deploy.sh

デプロイが完了すると、コマンド出力の最後の行に新しいアプリの Service URL が表示されます。URL を環境変数に保存します。

export SERVICE_URL=<URL from last line of command output>

次に、curl ツールを使用してアプリから注文を取得してみましょう。

curl -i -X GET $SERVICE_URL/partners

curl コマンドの -i フラグは、出力にレスポンス ヘッダーを含めるように指示しています。出力の最初の行は次のようになります。

HTTP/2 403

アプリは、未認証のリクエストを拒否するオプション付きでデプロイされました。この curl コマンドには認証情報が含まれていないため、Cloud Run で拒否されます。実際にデプロイされたアプリケーションは、このリクエストの実行やデータの受信さえしません。

5. 認証済みリクエストを行う

デプロイされたアプリは、ウェブ リクエストを行うことで呼び出されます。ウェブ リクエストを許可するには、Cloud Run で認証を行う必要があります。ウェブ リクエストは、次の形式の Authorization ヘッダーを含めることで認証されます。

Authorization: Bearer identity-token

ID トークンは、信頼できる認証プロバイダによって発行される、暗号で署名され、エンコードされた短期間の文字列です。この場合、Google が発行した期限切れでない有効な ID トークンが必要です。

ユーザー アカウントとしてリクエストを行う

Google Cloud CLI ツールは、デフォルトの認証済みユーザーにトークンを提供できます。次のコマンドを実行して、自分のアカウントの ID トークンを取得し、ID_TOKEN 環境変数に保存します。

export ID_TOKEN=$(gcloud auth print-identity-token)

デフォルトでは、Google が発行した ID トークンは 1 時間有効です。次の curl コマンドを実行して、許可されていないため以前に拒否されたリクエストを作成します。このコマンドには、必要なヘッダーが含まれます。

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

コマンド出力は HTTP/2 200 で始まります。これは、リクエストが受け入れられ、尊重されていることを示しています。(1 時間待ってからこのリクエストをもう一度試すと、トークンの有効期限が切れているため、リクエストは失敗します)。レスポンスの本文は、出力の最後、空白行の後にあります。

{"status":"success","data":[]}

パートナーはまだいません。

次の 2 つの curl コマンドで、ディレクトリにあるサンプル JSON データを使用してパートナーを登録します。

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner.json" \
  $SERVICE_URL/partner

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner2.json" \
  $SERVICE_URL/partner

登録済みのすべてのパートナーを表示するには、前の GET リクエストを繰り返します。

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

はるかに多くのコンテンツを含む JSON データが表示され、2 社の登録済みパートナーに関する情報が表示されます。

未承認のアカウントとしてリクエストを送信する

直前のステップで行われた認証済みリクエストは、認証されただけでなく、認証済みのユーザー(お客様のアカウント)が承認されたことによっても成功しました。つまり、そのアカウントにはアプリを呼び出す権限があったということです。すべての認証済みアカウントに対して、このような操作を行う権限があるわけではありません。

前のリクエストで使用されているデフォルトのアカウントは承認されています。このアカウントは、アプリを含むプロジェクトを作成し、そのアカウント内で Cloud Run アプリケーションを呼び出す権限をデフォルトで付与したアカウントであるためです。この権限は必要に応じて取り消すことができますが、本番環境アプリケーションに適しています。今すぐ行う代わりに、権限やロールが割り当てられていない新しいサービス アカウントを作成し、そのアカウントを使用して、デプロイされたアプリへのアクセスを試みます。

  1. tester という名前のサービス アカウントを作成します。
gcloud iam service-accounts create tester
  1. この新しいアカウントの ID トークンは、先ほどデフォルトのアカウントの ID トークンを取得するのとほぼ同じ方法で取得できます。ただし、この場合、デフォルト アカウントにサービス アカウントの権限を借用する権限が必要です。アカウントにこの権限を付与します。
export USER_EMAIL=$(gcloud config list account --format "value(core.account)")

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$USER_EMAIL" \
  --role=roles/iam.serviceAccountTokenCreator
  1. 次のコマンドを実行して、この新しいアカウントの ID トークンを TEST_IDENTITY 環境変数に保存します。コマンドでエラー メッセージが表示された場合は、1 ~ 2 分待ってから再試行してください。
export TEST_TOKEN=$( \
  gcloud auth print-identity-token \
    --impersonate-service-account \
    "tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
  1. 前と同じように認証済みウェブ リクエストを行いますが、今回は次の ID トークンを使用します。
curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

リクエストは認証済みですが、認可されていないため、コマンド出力は再び HTTP/2 403 で始まります。新しいサービス アカウントには、このアプリを呼び出す権限がありません。

アカウントの承認

Cloud Run サービスに対してリクエストを行うには、ユーザーまたはサービス アカウントに Cloud Run サービスに対する Cloud Run 起動元ロールが必要です。次のコマンドを使用して、テスター サービス アカウントにロールを付与します。

export REGION=us-central1
gcloud run services add-iam-policy-binding ${SERVICE_NAME} \
  --member="serviceAccount:tester@$PROJECT_ID.iam.gserviceaccount.com" \
  --role=roles/run.invoker \
  --region=${REGION}

新しいロールが更新されるまで 1 ~ 2 分待ってから、認証済みリクエストを繰り返します。最初に保存してから 1 時間以上経過している場合は、新しい TEST_TOKEN を保存します。

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

これで、コマンド出力は HTTP/1.1 200 OK で始まり、最後の行に JSON レスポンスが含まれます。このリクエストは Cloud Run で承認され、アプリで処理されました。

6. プログラムの認証とユーザーの認証

ここまでの認証済みリクエストでは、curl コマンドライン ツールを使用しています。代わりに使用できるツールやプログラミング言語は他にもあります。ただし、認証された Cloud Run リクエストを、通常のウェブページを備えたウェブブラウザから行うことはできません。ユーザーがウェブページでフォームを送信するためのリンクまたはボタンをクリックしても、ブラウザは認証済みリクエストに必要な Cloud Run の Authorization ヘッダーを追加しません。

Cloud Run の組み込みの認証メカニズムは、エンドユーザーではなくプログラムによる使用を想定しています。

注:

Cloud Run ではユーザー向けのウェブ アプリケーションをホストできますが、この種のアプリケーションでは、ユーザーからの未認証リクエストを許可するように Cloud Run を設定する必要があります。できます。アプリケーションでユーザー認証が必要な場合は、Cloud Run にユーザー認証を要求する代わりに、アプリケーションでユーザー認証を処理する必要があります。これは、Cloud Run 外部のウェブ アプリケーションと同じ方法で実行できます。その方法については、この Codelab の範囲外です。

ここまで、リクエスト例に対するレスポンスはウェブページではなく JSON オブジェクトでした。というのも、このパートナー登録サービスはプログラムで使用することを意図しており、JSON はプログラムにとって使いやすい形式だからです。次に、このデータを使用および使用するためのプログラムを記述して実行します。

Python プログラムからの認証済みリクエスト

プログラムは、標準の HTTP ウェブ リクエスト(ただし Authorization ヘッダーを含む)を介して、保護された Cloud Run アプリケーションの認証済みリクエストを行うことができます。これらのプログラムにおける唯一の新しい課題は、有効期限が切れていない有効な ID トークンを取得してヘッダーに配置することです。このトークンは、Google Cloud Identity and Access Management(IAM)を使用して Cloud Run によって検証されるため、IAM によって認識される認証機関によって発行され、署名されている必要があります。多くの言語で利用可能なクライアント ライブラリがあり、プログラムはこのトークンの発行をリクエストできます。この例で使用するクライアント ライブラリは、Python の google.auth です。一般的に、ウェブ リクエストを行うための Python ライブラリがいくつかあります。この例では、一般的な requests モジュールを使用しています。

まず、2 つのクライアント ライブラリをインストールします。

pip install google-auth
pip install requests

デフォルト ユーザーの ID トークンをリクエストする Python コードは次のようになります。

credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
identity_token = credentials.id_token

お使いのパソコンで Cloud Shell や標準のターミナル シェルなどのコマンドシェルを使用している場合は、そのシェル内で認証されたユーザーがデフォルトのユーザーになります。Cloud Shell では通常、ユーザーは Google にログインしています。それ以外の場合は、gcloud auth login または他の gcloud コマンドで認証されたユーザーになります。ユーザーがログインしたことがない場合は、デフォルト ユーザーは存在せず、このコードは失敗します。

プログラムが別のプログラムをリクエストする場合、一般的には個人の ID ではなく、リクエスト元のプログラムの ID を使用します。そのような場合のために、サービス アカウントがあります。Cloud Firestore などの API リクエスト時に使用する ID を提供する専用のサービス アカウントで Cloud Run サービスをデプロイしました。プログラムが Google Cloud Platform で実行されると、クライアント ライブラリは自動的にそのプログラムに割り当てられたサービス アカウントをデフォルトの ID として使用するため、どちらの状況でも同じプログラム コードを使用できます。

Authorization ヘッダーを追加してリクエストを行う Python コードは次のようになります。

auth_header = {"Authorization": "Bearer " + identity_token}
response = requests.get(url, headers=auth_header)

次の完全な Python プログラムは、Cloud Run サービスに認証済みのリクエストを送信し、登録されているすべてのパートナーを取得してから、名前と割り当てられた ID を出力します。以下のコマンドをコピーして実行し、このコードを print_partners.py ファイルに保存します。

cat > ./print_partners.py << EOF
def print_partners():
    import google.auth
    import google.auth.transport.requests
    import requests

    credentials, _ = google.auth.default()
    credentials.refresh(google.auth.transport.requests.Request())
    identity_token = credentials.id_token

    auth_header = {"Authorization": "Bearer " + identity_token}
    response = requests.get("${SERVICE_URL}/partners", headers=auth_header)

    parsed_response = response.json()
    partners = parsed_response["data"]

    for partner in partners:
        print(f"{partner['partnerId']}: {partner['name']}")


print_partners()
EOF

このプログラムはシェルコマンドを使用して実行します。まず、デフォルト ユーザーとして認証し、プログラムがこれらの認証情報を使用できるようにする必要があります。以下の gcloud auth コマンドを実行します。

gcloud auth application-default login

手順に沿ってログインを完了します。次に、コマンドラインからプログラムを実行します。

python print_partners.py

出力は次のようになります。

10102: Zippy food delivery
67292: Foodful

あなたの ID で認証されたため、プログラムのリクエストは Cloud Run サービスに到達しました。あなたはこのプロジェクトのオーナーであり、デフォルトでこのサービスを実行する権限を持っています。このプログラムは、より一般的にはサービス アカウントの ID で実行されます。Cloud Run や App Engine など、ほとんどの Google Cloud プロダクトで実行する場合、デフォルトの ID はサービス アカウントになり、個人アカウントの代わりに使用されます。

7. 完了

お疲れさまでした。これでこの Codelab は終了です。

次のステップ:

Cymbal Eats の他の Codelab を確認する:

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

プロジェクトの削除

課金を停止する最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。