Cloud Run でのエンドユーザー認証のチュートリアル

このチュートリアルでは、以下で構成される投票サービスの作成方法について説明します。

  • ブラウザベースのクライアント。次の処理を行います。

    1. Identity Platform を使用して ID トークンを取得する。
    2. ユーザーがお気に入りの動物に投票する。
    3. 取得した ID トークンをリクエストに付加して、投票を処理する Cloud Run サーバーに送信する。
  • Cloud Run サーバー。次の処理を行います。

    1. 有効な ID トークンが提供され、エンドユーザーが正しく認証されていることを確認する。
    2. エンドユーザーの投票を処理する。
    3. 独自の認証情報を使用して、投票結果を Cloud SQL に送信して保管する。
  • 投票結果を保存する PostgreSQL データベース。

説明をわかりやすくするため、このチュートリアルでは Google をプロバイダとして使用します。ID トークンを取得するには、ユーザーは Google アカウントを使用して認証を行う必要があります。ただし、ユーザーのログインに他のプロバイダや認証方法を使用することもできます。

このサービスは、Secret Manager を使用して Cloud SQL インスタンスへの接続で使用されるセンシティブ データを保護し、セキュリティ リスクを最小限に抑えます。また、最小権限のサービス ID を使用して、データベースへのアクセスを保護します。

gcloud のデフォルトを設定する

Cloud Run サービスを gcloud のデフォルトに構成するには:

  1. デフォルト プロジェクトを設定します。

    gcloud config set project PROJECT_ID

    PROJECT_ID は、このチュートリアルで作成したプロジェクトの名前に置き換えます。

  2. 選択したリージョン向けに gcloud を構成します。

    gcloud config set run/region REGION

    REGION は、任意のサポートされている Cloud Run のリージョンに置き換えます。

Cloud Run のロケーション

Cloud Run はリージョナルです。つまり、Cloud Run サービスを実行するインフラストラクチャは特定のリージョンに配置され、そのリージョン内のすべてのゾーンで冗長的に利用できるように Google によって管理されます。

レイテンシ、可用性、耐久性の要件を満たしていることが、Cloud Run サービスを実行するリージョンを選択する際の主な判断材料になります。一般的には、ユーザーに最も近いリージョンを選択できますが、Cloud Run サービスで使用されている他の Google Cloudプロダクトのロケーションも考慮する必要があります。 Google Cloud プロダクトを複数のロケーションで使用すると、サービスのレイテンシだけでなく、コストにも影響を及ぼす可能性があります。

Cloud Run は、次のリージョンで利用できます。

ティア 1 料金を適用

  • asia-east1(台湾)
  • asia-northeast1(東京)
  • asia-northeast2(大阪)
  • asia-south1(ムンバイ、インド)
  • europe-north1(フィンランド) リーフアイコン 低 CO2
  • europe-north2(ストックホルム) リーフアイコン 低 CO2
  • europe-southwest1(マドリッド) リーフアイコン 低 CO2
  • europe-west1(ベルギー) リーフアイコン 低 CO2
  • europe-west4(オランダ) リーフアイコン 低 CO2
  • europe-west8(ミラノ)
  • europe-west9(パリ) リーフアイコン 低 CO2
  • me-west1(テルアビブ)
  • northamerica-south1(メキシコ)
  • us-central1(アイオワ) リーフアイコン 低 CO2
  • us-east1(サウスカロライナ)
  • us-east4(北バージニア)
  • us-east5(コロンバス)
  • us-south1(ダラス) リーフアイコン 低 CO2
  • us-west1(オレゴン) リーフアイコン 低 CO2

ティア 2 料金を適用

  • africa-south1(ヨハネスブルグ)
  • asia-east2(香港)
  • asia-northeast3(ソウル、韓国)
  • asia-southeast1(シンガポール)
  • asia-southeast2 (ジャカルタ)
  • asia-south2(デリー、インド)
  • australia-southeast1(シドニー)
  • australia-southeast2(メルボルン)
  • europe-central2(ワルシャワ、ポーランド)
  • europe-west10(ベルリン)
  • europe-west12(トリノ)
  • europe-west2(ロンドン、イギリス) リーフアイコン 低 CO2
  • europe-west3(フランクフルト、ドイツ)
  • europe-west6(チューリッヒ、スイス) リーフアイコン 低 CO2
  • me-central1(ドーハ)
  • me-central2(ダンマーム)
  • northamerica-northeast1(モントリオール) リーフアイコン 低 CO2
  • northamerica-northeast2(トロント) リーフアイコン 低 CO2
  • southamerica-east1(サンパウロ、ブラジル) リーフアイコン 低 CO2
  • southamerica-west1(サンティアゴ、チリ) リーフアイコン 低 CO2
  • us-west2(ロサンゼルス)
  • us-west3(ソルトレイクシティ)
  • us-west4(ラスベガス)

Cloud Run サービスをすでに作成している場合は、Google Cloud コンソールの Cloud Run ダッシュボードにリージョンが表示されます。

サンプルコードを取得する

使用するコードサンプルを取得するには:

  1. ローカルマシンにサンプルアプリのリポジトリのクローンを作成します。

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  2. Cloud Run のサンプルコードが含まれているディレクトリに移動します。

    Node.js

    cd nodejs-docs-samples/run/idp-sql/

    Python

    cd python-docs-samples/run/idp-sql/

    Java

    cd java-docs-samples/run/idp-sql/

アーキテクチャを可視化する

アーキテクチャ図
上の図は、エンドユーザーが Identity Platform によって表示された Google ログイン ダイアログを使用してログインした後、ユーザーの ID とともに再度 Cloud Run へリダイレクトされる様子を示しています。
  1. エンドユーザーが、Cloud Run サーバーに最初のリクエストを行います。

  2. クライアントがブラウザに読み込まれます。

  3. ユーザーが Identity Platform の Google ログイン ダイアログでログイン認証情報を入力します。ログインしたユーザーにアラートが表示されます。

  4. 制御がサーバーにリダイレクトされます。エンドユーザーがクライアントを使用して投票します。クライアントは、Identity Platform から ID トークンを取得して、投票リクエスト ヘッダーに追加します。

  5. サーバーは、リクエストを受信すると、Identity Platform ID トークンを検証してエンドユーザーが適切に認証されていることを確認します。次に、サーバーは、独自の認証情報を使用して Cloud SQL に投票を送信します。

コアコードについて

このサンプルは、以下のようにクライアントとサーバーとして実装されます。

Identity Platform との統合: クライアント側のコード

このサンプルでは、Firebase SDK を使用して Identity Platform と統合し、ユーザーのログインと管理を行います。Identity Platform に接続するため、クライアント側の JavaScript はプロジェクトの認証情報への参照を構成オブジェクトとして保持し、必要な Firebase JavaScript SDK をインポートします。

const config = {   apiKey: 'API_KEY',   authDomain: 'PROJECT_ID.firebaseapp.com', };
<!-- Firebase App (the core Firebase SDK) is always required and must be listed first--> <script src="https://www.gstatic.com/firebasejs/7.18/firebase-app.js"></script> <!-- Add Firebase Auth service--> <script src="https://www.gstatic.com/firebasejs/7.18/firebase-auth.js"></script>

Firebase JavaScript SDK は、ポップアップ ウィンドウを表示してエンドユーザーに Google アカウントへのログインを求めることにより、ログインフローを処理します。次に、サービスにリダイレクトします。

function signIn() {   const provider = new firebase.auth.GoogleAuthProvider();   provider.addScope('https://www.googleapis.com/auth/userinfo.email');   firebase     .auth()     .signInWithPopup(provider)     .then(result => {       // Returns the signed in user along with the provider's credential       console.log(`${result.user.displayName} logged in.`);       window.alert(`Welcome ${result.user.displayName}!`);     })     .catch(err => {       console.log(`Error during sign in: ${err.message}`);       window.alert('Sign in failed. Retry or check your browser logs.');     }); }

ユーザーがログインに成功すると、クライアントは Firebase メソッドを使用して ID トークンを作成します。クライアントが、この ID トークンをサーバーに対するリクエストの Authorization ヘッダーに追加します。

async function vote(team) {   if (firebase.auth().currentUser) {     // Retrieve JWT to identify the user to the Identity Platform service.     // Returns the current token if it has not expired. Otherwise, this will     // refresh the token and return a new one.     try {       const token = await firebase.auth().currentUser.getIdToken();       const response = await fetch('/', {         method: 'POST',         headers: {           'Content-Type': 'application/x-www-form-urlencoded',           Authorization: `Bearer ${token}`,         },         body: 'team=' + team, // send application data (vote)       });       if (response.ok) {         const text = await response.text();         window.alert(text);         window.location.reload();       }     } catch (err) {       console.log(`Error when submitting vote: ${err}`);       window.alert('Something went wrong... Please try again!');     }   } else {     window.alert('User not signed in.');   } }

Identity Platform との統合: サーバーサイドのコード

サーバーでは、Firebase Admin SDK を使用して、クライアントから送信されたユーザー ID トークンを確認します。提供された ID トークンが正しい形式で、期限切れではなく、適切に署名されていれば、メソッドはデコードされた ID トークンを返します。そのユーザーの Identity Platform uid が、サーバーにより抽出されます。

Node.js

const firebase = require('firebase-admin'); // Initialize Firebase Admin SDK firebase.initializeApp();  // Extract and verify Id Token from header const authenticateJWT = (req, res, next) => {   const authHeader = req.headers.authorization;   if (authHeader) {     const token = authHeader.split(' ')[1];     // If the provided ID token has the correct format, is not expired, and is     // properly signed, the method returns the decoded ID token     firebase       .auth()       .verifyIdToken(token)       .then(decodedToken => {         const uid = decodedToken.uid;         req.uid = uid;         next();       })       .catch(err => {         req.logger.error(`Error with authentication: ${err}`);         return res.sendStatus(403);       });   } else {     return res.sendStatus(401);   } };

Python

def jwt_authenticated(func: Callable[..., int]) -> Callable[..., int]:     """Use the Firebase Admin SDK to parse Authorization header to verify the     user ID token.      The server extracts the Identity Platform uid for that user.     """      @wraps(func)     def decorated_function(*args: a, **kwargs: a) -> a:         header = request.headers.get("Authorization", None)         if header:             token = header.split(" ")[1]             try:                 decoded_token = firebase_admin.auth.verify_id_token(token)             except Exception as e:                 logger.exception(e)                 return Response(status=403, response=f"Error with authentication: {e}")         else:             return Response(status=401)          request.uid = decoded_token["uid"]         return func(*args, **kwargs)      return decorated_function  

Java

/** Extract and verify Id Token from header */ private String authenticateJwt(Map<String, String> headers) {   String authHeader =       (headers.get("authorization") != null)           ? headers.get("authorization")           : headers.get("Authorization");   if (authHeader != null) {     String idToken = authHeader.split(" ")[1];     // If the provided ID token has the correct format, is not expired, and is     // properly signed, the method returns the decoded ID token     try {       FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken);       String uid = decodedToken.getUid();       return uid;     } catch (FirebaseAuthException e) {       logger.error("Error with authentication: " + e.toString());       throw new ResponseStatusException(HttpStatus.FORBIDDEN, "", e);     }   } else {     logger.error("Error no authorization header");     throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);   } }

サーバーを Cloud SQL に接続する

サービスは、/cloudsql/CLOUD_SQL_CONNECTION_NAME の形式を使用して、Cloud SQL インスタンスの Unix ドメイン ソケットに接続します。

Node.js

/**  * Connect to the Cloud SQL instance through UNIX Sockets  *  * @param {object} credConfig The Cloud SQL connection configuration from Secret Manager  * @returns {object} Knex's PostgreSQL client  */ const connectWithUnixSockets = async credConfig => {   const dbSocketPath = process.env.DB_SOCKET_PATH || '/cloudsql';   // Establish a connection to the database   return Knex({     client: 'pg',     connection: {       user: credConfig.DB_USER, // e.g. 'my-user'       password: credConfig.DB_PASSWORD, // e.g. 'my-user-password'       database: credConfig.DB_NAME, // e.g. 'my-database'       host: `${dbSocketPath}/${credConfig.CLOUD_SQL_CONNECTION_NAME}`,     },     ...config,   }); };

Python

def init_unix_connection_engine(     db_config: dict[str, int] ) -> sqlalchemy.engine.base.Engine:     """Initializes a Unix socket connection pool for a Cloud SQL instance of PostgreSQL.      Args:         db_config: a dictionary with connection pool config      Returns:         A SQLAlchemy Engine instance.     """     creds = credentials.get_cred_config()     db_user = creds["DB_USER"]     db_pass = creds["DB_PASSWORD"]     db_name = creds["DB_NAME"]     db_socket_dir = creds.get("DB_SOCKET_DIR", "/cloudsql")     cloud_sql_connection_name = creds["CLOUD_SQL_CONNECTION_NAME"]      pool = sqlalchemy.create_engine(         # Equivalent URL:         # postgres+pg8000://<db_user>:<db_pass>@/<db_name>         #                         ?unix_sock=<socket_path>/<cloud_sql_instance_name>/.s.PGSQL.5432         sqlalchemy.engine.url.URL.create(             drivername="postgresql+pg8000",             username=db_user,  # e.g. "my-database-user"             password=db_pass,  # e.g. "my-database-password"             database=db_name,  # e.g. "my-database-name"             query={                 "unix_sock": f"{db_socket_dir}/{cloud_sql_connection_name}/.s.PGSQL.5432"                 # e.g. "/cloudsql", "<PROJECT-NAME>:<INSTANCE-REGION>:<INSTANCE-NAME>"             },         ),         **db_config,     )     pool.dialect.description_encoding = None     logger.info("Database engine initialized from unix connection")      return pool  

Java

Spring Cloud Google Cloud PostgreSQL スターター インテグレーションを使用して、Spring JDBC ライブラリを使用する Cloud SQL の PostgreSQL データベースを操作します。DataSource Bean を自動的に構成するように Cloud SQL for MySQL 構成を設定します。これは、Spring JDBC と一体となり、データベースのクエリや変更などのオペレーションを可能にする JdbcTemplate オブジェクト Bean を提供します。

# Uncomment and add env vars for local development # spring.datasource.username=${DB_USER} # spring.datasource.password=${DB_PASSWORD} # spring.cloud.gcp.sql.database-name=${DB_NAME} # spring.cloud.gcp.sql.instance-connection-name=${CLOUD_SQL_CONNECTION_NAME}  
private final JdbcTemplate jdbcTemplate;  public VoteController(JdbcTemplate jdbcTemplate) {   this.jdbcTemplate = jdbcTemplate; }

Secret Manager で機密性の高い構成を処理する

Cloud SQL 構成などのセンシティブ データは、Secret Manager によって一元管理され、安全に保管されます。このサービスは、Secret Manager から環境変数を介してランタイムに Cloud SQL 認証情報を挿入します。詳しくは、Cloud Run でのシークレットの使用をご覧ください。

Node.js

// CLOUD_SQL_CREDENTIALS_SECRET is the resource ID of the secret, passed in by environment variable. // Format: projects/PROJECT_ID/secrets/SECRET_ID/versions/VERSION const {CLOUD_SQL_CREDENTIALS_SECRET} = process.env; if (CLOUD_SQL_CREDENTIALS_SECRET) {   try {     // Parse the secret that has been added as a JSON string     // to retrieve database credentials     return JSON.parse(CLOUD_SQL_CREDENTIALS_SECRET.toString('utf8'));   } catch (err) {     throw Error(       `Unable to parse secret from Secret Manager. Make sure that the secret is JSON formatted: ${err}`     );   } }

Python

def get_cred_config() -> dict[str, str]:     """Retrieve Cloud SQL credentials stored in Secret Manager     or default to environment variables.      Returns:         A dictionary with Cloud SQL credential values     """     secret = os.environ.get("CLOUD_SQL_CREDENTIALS_SECRET")     if secret:         return json.loads(secret)

Java

/** Retrieve config from Secret Manager */ public static HashMap<String, Object> getConfig() {   String secret = System.getenv("CLOUD_SQL_CREDENTIALS_SECRET");   if (secret == null) {     throw new IllegalStateException("\"CLOUD_SQL_CREDENTIALS_SECRET\" is required.");   }   try {     HashMap<String, Object> config = new Gson().fromJson(secret, HashMap.class);     return config;   } catch (JsonSyntaxException e) {     logger.error(         "Unable to parse secret from Secret Manager. Make sure that it is JSON formatted: "             + e);     throw new RuntimeException(         "Unable to parse secret from Secret Manager. Make sure that it is JSON formatted.");   } }

Identity Platform を設定する

Identity Platform は、 Google Cloud コンソールで手動で設定する必要があります。

  1. Google Cloud コンソールで、Identity Platform API を有効にします。

    API の有効化

  2. プロジェクトを構成します。

    1. 新しいウィンドウで、[Google Auth Platform] > [概要] ページに移動します。

      [概要] に移動

    2. [使ってみる] をクリックし、プロジェクトの構成設定手順に沿って操作します。

    3. [アプリ情報] ダイアログで、次の操作を行います。

      1. アプリケーション名を入力します。
      2. 表示されたユーザー サポートメールのいずれかを選択します。
    4. [オーディエンス] ダイアログで [外部] を選択します。

    5. [連絡先情報] ダイアログで、連絡先のメールアドレスを入力します。

    6. ユーザーデータに関するポリシーに同意し、[作成] をクリックします。

  3. OAuth クライアント ID とシークレットを作成して取得します。

    1. Google Cloud コンソールで、[API とサービス] > [認証情報] ページに移動します。

      [認証情報] に移動

    2. ページの上部にある [認証情報を作成] をクリックし、[OAuth client ID] を選択します。

    3. [アプリケーションの種類] で [ウェブ アプリケーション] を選択し、名前を入力します。

    4. [作成] をクリックします。

    5. client_idclient_secret の値は、次のステップで使用します。

  4. Google をプロバイダとして構成します。

    1. Google Cloud コンソールで、[ID プロバイダ] ページに移動します。

      [ID プロバイダ] に移動

    2. [プロバイダを追加] をクリックします。

    3. リストから [Google] を選択します。

    4. [ウェブ SDK 構成] 設定で、前のステップで取得した client_idclient_secret の値を入力します。

    5. [アプリケーションの構成] で [詳細を設定] をクリックします。

  5. 構成をアプリケーションにコピーします。

    • apiKeyauthDomain の値をサンプルの static/config.js にコピーして、Identity Platform Client SDK を初期化します。

サービスのデプロイ

手順に沿って、インフラストラクチャのプロビジョニングとデプロイを完了します。

  1. コンソール または CLI を使用して、PostgSQL データベースの Cloud SQL インスタンスを作成します。

    gcloud sql instances create CLOUD_SQL_INSTANCE_NAME \     --database-version=POSTGRES_16 \     --region=CLOUD_SQL_REGION \     --cpu=2 \     --memory=7680MB \     --root-password=DB_PASSWORD
  2. Cloud SQL の認証情報の値を postgres-secrets.json に追加します。

    Node.js

    {   "CLOUD_SQL_CONNECTION_NAME": "PROJECT_ID:REGION:INSTANCE",   "DB_NAME": "postgres",   "DB_USER": "postgres",   "DB_PASSWORD": "PASSWORD_SECRET" } 

    Python

    {   "CLOUD_SQL_CONNECTION_NAME": "PROJECT_ID:REGION:INSTANCE",   "DB_NAME": "postgres",   "DB_USER": "postgres",   "DB_PASSWORD": "PASSWORD_SECRET" } 

    Java

    {   "spring.cloud.gcp.sql.instance-connection-name": "PROJECT_ID:REGION:INSTANCE",   "spring.cloud.gcp.sql.database-name": "postgres",   "spring.datasource.username": "postgres",   "spring.datasource.password": "PASSWORD_SECRET" }

  3. コンソールまたは CLI を使用して、バージョニングされたシークレットを作成します。

    gcloud secrets create idp-sql-secrets \     --replication-policy="automatic" \     --data-file=postgres-secrets.json
  4. コンソール または CLI を使用して、サーバーのサービス アカウントを作成します。

    gcloud iam service-accounts create idp-sql-identity
  5. コンソール または CLI を使用して、Secret Manager と Cloud SQL にアクセスするためのロールを付与します。

    1. サーバーに関連付けられたサービス アカウントに、作成したシークレットへのアクセスを許可します。

      gcloud secrets add-iam-policy-binding idp-sql-secrets \   --member serviceAccount:idp-sql-identity@PROJECT_ID.iam.gserviceaccount.com \   --role roles/secretmanager.secretAccessor
    2. サーバーに関連付けられたサービス アカウントに Cloud SQL へのアクセスを許可します。

      gcloud projects add-iam-policy-binding PROJECT_ID \   --member serviceAccount:idp-sql-identity@PROJECT_ID.iam.gserviceaccount.com \   --role roles/cloudsql.client
  6. Artifact Registry を作成します。

    gcloud artifacts repositories create REPOSITORY \     --repository-format docker \     --location REGION
    • REPOSITORY はリポジトリの名前です。プロジェクト内のリポジトリのロケーションごとに、リポジトリ名は一意でなければなりません。
  7. Cloud Build を使用してコンテナ イメージをビルドします。

    Node.js

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/idp-sql

    Python

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/idp-sql

    Java

    このサンプルでは、Jib を使用して一般的な Java ツールにより Docker イメージをビルドします。Jib は、Dockerfile や Docker をインストールせずにコンテナのビルドを最適化します。Jib を使用して Java コンテナを構築する方法の詳細を確認します。

    1. Docker を承認して Artifact Registry に push するには、gcloud 認証ヘルパーを使用します。

      gcloud auth configure-docker

    2. Jib Maven プラグインを使用して、コンテナをビルドし Artifact Registry に push します。

      mvn compile jib:build -Dimage=REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/idp-sql

  8. コンソールまたは CLI を使用して、コンテナ イメージを Cloud Run にデプロイします。このサーバーは、未認証のアクセスを許可するようにデプロイされています。これは、ユーザーがクライアントを読み込んで処理を開始できるようにするためです。サーバーは、投票リクエストに追加された ID トークンを手動で検証し、エンドユーザーを認証します。

    gcloud run deploy idp-sql \     --image REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/idp-sql \     --allow-unauthenticated \     --service-account idp-sql-identity@PROJECT_ID.iam.gserviceaccount.com \     --add-cloudsql-instances PROJECT_ID:REGION:CLOUD_SQL_INSTANCE_NAME \     --update-secrets CLOUD_SQL_CREDENTIALS_SECRET=idp-sql-secrets:latest

    また、--service-account--add-cloudsql-instances--update-secrets の各フラグにも注意してください。これらはそれぞれ、サービス ID、Cloud SQL インスタンス接続、環境変数としてバージョン付きシークレット名を指定します。

最後の仕上げ

Identity Platform では、ユーザーがログインした後に、Cloud Run サービスの URL を許可されたリダイレクトとして承認する必要があります。

  1. [ID プロバイダ] ページでペンのアイコンをクリックして、Google プロバイダを編集します。

  2. 右側の [承認済みドメイン] で [ドメインを追加] をクリックし、Cloud Run サービスの URL を入力します。

    ビルドまたはデプロイの後、サービス URL はログで確認できます。また、次のコマンドを使用して確認することもできます。

    gcloud run services describe idp-sql --format 'value(status.url)'
  3. [API とサービス] > [認証情報] ページに移動

    1. OAuth クライアント ID の横にある鉛筆アイコンをクリックして編集し、Authorized redirect URIs click the [URI を追加] ボタンをクリックします。

    2. フィールドに、次の URL をコピーして貼り付け、ページ下部の [保存] ボタンをクリックします。

    https://PROJECT_ID.firebaseapp.com/__/auth/handler

試してみる

完成したサービスを試すには:

  1. ブラウザで、前述のデプロイの手順により提供された URL に移動します。

  2. [Google でログイン] ボタンをクリックして、認証フローを行います。

  3. 投票します。

    次のようになります。

    ユーザー インターフェースには、各チームへの投票数と投票の一覧が表示されます。

これらのサービスの開発を継続する場合は、 Google Cloud の他のサービスへの Identity and Access Management(IAM)アクセスが制限されます。他の多くのサービスにアクセスするには、追加の IAM ロールをこれらのサービスに付与する必要があることにご注意ください。