使用 Workload Identity Federation 跨雲端存取 GCP 的資源
我最近接到一個需求,需要在 AWS Lambda 中跨雲端存取 GCP 上面的資源,傳統上跨雲存取,我們通常會使用秘密金鑰,例如在 GCP 上產生一組服務帳戶金鑰,然後放在 AWS 中使用。然而我們都知道,只要使用金鑰,就有外洩風險與輪替負擔。
於是我問了 Google 自家的 Gemini AI,有沒有更好的方式實作跨雲存取?畢竟是自家服務,Gemini 也是很快的給了我回覆,它提到這個案例更推薦使用 Workload Identity Federation(WIF)。
如果想要讓 AWS 存取 GCP 上面的資源,WIF 是 最推薦的無金鑰(Keyless)實踐。它透過 OIDC / AWS STS 建立跨雲身分信任,讓 AWS IAM Role 能直接向 GCP 換取短期 Access Token,完全不需管理長期金鑰。
sequenceDiagram
autonumber
participant AWS as AWS Workload<br/>(Lambda / EC2)
participant AWS_STS as AWS STS<br/>(GetCallerIdentity)
participant GCP_WIP as GCP Workload Identity Pool<br/>(AWS Provider)
participant GCP_IAM as GCP IAM Credentials API<br/>(Service Account)
participant GCP_API as GCP Services<br/>(Resource Manager / BigQuery)
AWS->>AWS_STS: 本地簽署 GetCallerIdentity 請求 (SigV4)
AWS->>GCP_WIP: 發送 AWS 簽章請求至 Workload Identity Pool
Note over GCP_WIP: 驗證 AWS 簽章與 Account ID<br/>確認符合 Provider 信任條件
GCP_WIP-->>AWS: 核發 Federated STS Token
AWS->>GCP_IAM: 以 Federated Token 請求 Impersonate Service Account
Note over GCP_IAM: 檢查 roles/iam.workloadIdentityUser 綁定
GCP_IAM-->>AWS: 回傳 GCP SA 短期 Access Token (有效期 1 小時)
AWS->>GCP_API: 使用 Access Token 呼叫 GCP APIs
GCP_API-->>AWS: 回傳 GCP 資源資料使用 Terraform 建立 WIF 資源
整個跨雲存取所需要的資源,都可以透過 Terraform 建立。首先啟用必要的 GCP API:
- IAM API
- Resource Manager API
- Service Account API
- Security Token Service API
resource "google_project_service" "required_apis" {
for_each = toset([
"iam.googleapis.com",
"cloudresourcemanager.googleapis.com",
"iamcredentials.googleapis.com",
"sts.googleapis.com",
])
project = "my-gcp-project"
service = each.key
disable_on_destroy = false
}接下來建立 Service Account 與賦予對應的權限:
resource "google_service_account" "app_sync" {
account_id = "sa-cloud-sync"
display_name = "Cloud Sync Service Account"
project = "my-gcp-project"
}
# 賦予 SA 操作目標資源的權限(例如專案 Viewer)
resource "google_project_iam_member" "project_viewer" {
project = "my-gcp-project"
role = "roles/viewer"
member = "serviceAccount:${google_service_account.app_sync.email}"
}建立 Workload Identity Pool 與 AWS Provider:
# 1. 建立 Pool
resource "google_iam_workload_identity_pool" "aws_pool" {
project = "my-gcp-project"
workload_identity_pool_id = "my-aws-pool"
display_name = "AWS Workload Identity Pool"
}
# 2. 建立 AWS Provider 並設定屬性映射與條件
resource "google_iam_workload_identity_pool_provider" "aws_provider" {
project = "my-gcp-project"
workload_identity_pool_id = google_iam_workload_identity_pool.aws_pool.workload_identity_pool_id
workload_identity_pool_provider_id = "my-aws-provider"
display_name = "AWS Provider"
attribute_mapping = {
"google.subject" = "assertion.arn"
"attribute.aws_account" = "assertion.account"
"attribute.aws_role" = "assertion.arn.extract('/{role_name}/')"
}
# 限制只有指定的 AWS Account 可以換證
attribute_condition = "assertion.account == '123456789012'"
aws {
account_id = "123456789012"
}
}授權 AWS IAM Role 扮演該 Service Account:
resource "google_service_account_iam_binding" "workload_identity_user" {
service_account_id = google_service_account.app_sync.name
role = "roles/iam.workloadIdentityUser"
members = [
# 授權特定的 AWS IAM Role 扮演該 Service Account
"principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.aws_pool.name}/attribute.aws_role/my-lambda-execution-role"
]
}在 Terraform 使用 Local 產生一組 WIF 設定檔:
locals {
gcp_wif_credentials_config = jsonencode({
type = "external_account"
audience = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.aws_provider.name}"
subject_token_type = "urn:ietf:params:aws:token-type:aws4_request"
service_account_impersonation_url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${google_service_account.app_sync.email}:generateAccessToken"
token_url = "https://sts.googleapis.com/v1/token"
credential_source = {
environment_id = "aws1"
regional_cred_verification_url = "https://sts.${local.region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
}
})
}
output "gcp_wif_credentials_config" {
description = "JSON configuration for GCP Workload Identity Federation with AWS"
value = local.gcp_wif_credentials_config
}WIF 設定檔不包含任何私鑰或密鑰,純粹由公開的資源識別字串組成,因此可安全放在程式碼變數或 Lambda 環境變數中:
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/my-aws-pool/providers/my-aws-provider",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa-cloud-sync@my-gcp-project.iam.gserviceaccount.com:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"regional_cred_verification_url": "https://sts.us-west-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
}
}Lambda 上的 Python SDK 整合
以 Python 為例,只要將對應的 AWS IAM Role 綁定在 Lambda 上,就可以使用 google-auth SDK 存取 GCP 上面的資源:
import json
import os
import google.auth
from google.auth.transport.requests import Request
from google.cloud import resourcemanager_v3
def get_gcp_credentials():
wif_config_str = os.getenv("GCP_WIF_CREDENTIALS_CONFIG")
if wif_config_str:
info = json.loads(wif_config_str)
# 注意:必須明確指定 default_scopes,否則在進行 SA Impersonation 時會因缺少 scope 報錯 400
credentials, _ = google.auth.load_credentials_from_dict(
info,
default_scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
return credentials
# 本地開發 fallback 至 ADC (gcloud auth application-default login)
credentials, _ = google.auth.default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
return credentials
# 呼叫 GCP API
credentials = get_gcp_credentials()
client = resourcemanager_v3.ProjectsClient(credentials=credentials)
project = client.get_project(name="projects/my-gcp-project")
print(f"Project ID: {project.project_id}")每當程式執行,AWS Lambda 都會透過 WIF 取得短期的 Token 來獲得權限,不用再煩惱秘密金鑰該儲存在哪裡,以及輪替上的問題。