Cloud Run 教學課程:使用者驗證


本教學課程將說明如何建立投票服務,其中包含:

  • 以瀏覽器為基礎的用戶端,具備下列功能:

    1. 使用 Identity Platform 擷取 ID 權杖。
    2. 允許使用者為自己最喜歡的寵物投票。
    3. 將該 ID 符記新增至處理投票要求的 Cloud Run 伺服器。
  • 符合下列條件的 Cloud Run 伺服器:

    1. 檢查是否提供有效的 ID 權杖,確保使用者已完成驗證。
    2. 處理使用者的投票。
    3. 使用自己的憑證,將投票結果傳送至 Cloud SQL 進行儲存。
  • 用來儲存投票結果的 PostgreSQL 資料庫。

為求簡單起見,本教學課程使用 Google 做為提供者:使用者必須使用 Google 帳戶進行驗證,才能取得 ID 權杖。不過,您可以使用其他提供者或驗證方法讓使用者登入

這項服務會使用 Secret Manager 保護用於連線至 Cloud SQL 執行個體的機密資料,進而降低安全性風險。並使用最低權限服務身分來確保資料庫的存取安全。

目標

撰寫、建構及部署服務至 Cloud Run,瞭解如何:

  • 使用 Identity Platform 驗證 Cloud Run 服務後端的使用者。

  • 為服務建立最低權限身分,以便授予最少的 Google Cloud 資源存取權。

  • 將 Cloud Run 服務連線至 PostgreSQL 資料庫時,請使用 Secret Manager 處理機密資料。

費用

在本文件中,您會使用 Google Cloud的下列計費元件:

您可以使用 Pricing Calculator 根據預測用量產生預估費用。 新 Google Cloud 使用者可能符合申請免費試用的資格。

事前準備

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  5. Make sure that billing is enabled for your Google Cloud project.

  6. Enable the Cloud Run, Secret Manager, Cloud SQL, Artifact Registry, and Cloud Build APIs.

    Enable the APIs

  7. 必要的角色

    如要取得完成本教學課程所需的權限,請要求管理員為您授予專案的下列 IAM 角色:

    如要進一步瞭解如何授予角色,請參閱「管理專案、資料夾和機構的存取權」。

    您或許還可透過自訂角色或其他預先定義的角色取得必要權限。

設定 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 定價

採用級別 2 定價

如果您已建立 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 登入對話方塊登入,然後以使用者身分重新導向至 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 資料庫互動。設定 MySQL 適用的 Cloud SQL 設定,以便自動設定 DataSource bean。搭配使用 Spring JDBC 時,這個 bean 會提供 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 處理機密設定

Secret Manager 可集中安全地儲存機密資料,例如 Cloud SQL 設定。伺服器會在執行階段透過環境變數,從 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

您必須在 Google Cloud 控制台手動設定 Identity Platform。

  1. 在 Google Cloud 控制台啟用 Identity Platform API:

    啟用 API

  2. 設定專案:

    1. 在新的視窗中,依序前往「Google Auth Platform」>「Overview」(總覽) 頁面。

      前往總覽頁面

    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 控制台的「Identity Providers」(識別資訊提供者) 頁面。

      前往「Identity Providers」

    2. 按一下「Add A Provider」

    3. 從清單中選取「Google」Google

    4. 在 Web SDK 設定中,輸入上一個步驟的 client_idclient_secret 值。

    5. 按一下「設定應用程式」下方的「設定詳細資料」

  5. 將設定複製到應用程式:

    • apiKeyauthDomain 值複製到範例的 static/config.js,即可初始化 Identity Platform 用戶端 SDK。

部署服務

按照步驟完成基礎架構佈建及部署作業:

  1. 使用控制台或 CLI 建立含有 PostgreSQL 資料庫的 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 建構 Docker 映像檔,並使用常見的 Java 工具。Jib 可在不需要 Dockerfile 或安裝 Docker 的情況下,最佳化容器建構作業。進一步瞭解如何使用 Jib 建構 Java 容器

    1. 使用 gcloud 憑證輔助程式授權 Docker 推送至 Artifact Registry。

      gcloud auth configure-docker

    2. 使用 Jib Maven 外掛程式建構容器,並將容器推送至 Artifact Registry。

      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 分別指定服務身分、Cloud SQL 執行個體連線,以及機密金鑰名稱和版本 (做為環境變數)。

最後修飾

Identity Platform 要求您在使用者登入後,將 Cloud Run 服務網址授權為允許的重新導向網址:

  1. 按一下「識別資訊提供者」頁面中的筆圖示,即可編輯 Google 供應商。

  2. 按一下右側面板「已授權的網域」下方的「新增網域」,然後輸入 Cloud Run 服務網址。

    您可以在建構或部署作業後,在記錄中找到服務網址,也可以隨時使用以下方式找出服務網址:

    gcloud run services describe idp-sql --format 'value(status.url)'
  3. 依序前往「API 和服務」>「憑證」頁面

    1. 按一下 OAuth 用戶端 ID 旁的鉛筆圖示,即可編輯該 ID 和 Authorized redirect URIs click the 下方的「新增 URI」按鈕。

    2. 在欄位中複製並貼上以下網址,然後點選頁面底部的「儲存」按鈕。

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

立即體驗

如要試用完整的服務,請按照下列步驟操作:

  1. 在瀏覽器中前往上方部署步驟提供的網址。

  2. 按一下「使用 Google 帳戶登入」按鈕,然後完成驗證流程。

  3. 快來投票吧!

    內容應如下所示:

    使用者介面會顯示每個團隊的票數,以及票數清單。

如果您選擇繼續開發這些服務,請注意,這些服務的 Identity and Access Management (IAM) 存取權僅限於 Google Cloud ,因此需要額外的 IAM 角色才能存取許多其他服務。

清除所用資源

如果您是為了這個教學課程建立新專案,請刪除專案。如果您使用現有專案,且希望保留該專案而不採用本教學課程中的變更,請刪除為教學課程建立的資源

刪除專案

如要避免付費,最簡單的方法就是刪除您為了本教學課程所建立的專案。

如要刪除專案:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

刪除教學課程資源

  1. 刪除您在本教學課程中部署的 Cloud Run 服務:

    gcloud run services delete SERVICE-NAME

    其中 SERVICE-NAME 是您選擇的服務名稱。

    您也可以從 Google Cloud 控制台刪除 Cloud Run 服務。

  2. 移除您在教學課程設定期間新增的 gcloud 預設區域設定:

     gcloud config unset run/region
    
  3. 移除專案設定:

     gcloud config unset project
    
  4. 刪除本教學課程中建立的其他 Google Cloud 資源:

後續步驟