AWS 访问 Google Cloud 的 Workload Identity Federation, 中文文档称为工作负载身份联合. 可以在 aws 的机器上通过 aws 的身份认证并访问 Google Cloud 服务. 相关文档:

  1. 介绍: Workload Identity Federation
  2. 配置: Configure Workload Identity Federation with AWS or Azure
  3. 配置: Configure Workload Identity Federation with other identity providers
  4. 支持与限制: Identity federation: products and limitations
  5. IAM 介绍: IAM overview

这里展示在网页端控制台 Console 配置 Provider 类型为 AWS 的 Workload Identity Federation 不通过服务账号直接访问资源的流程.

准备工作

  1. (可选) Google Cloud 创建专用项目用于管理工作负载身份池.

  2. 需要确保项目开启了 Cloud Billing (文档标注, 我这边直接打开了).

  3. 创建 AWS Role 并绑定给需要访问 Google Cloud 的 EC2.

    找到 AWS IAM / Roles, 点击 Create Role 创建身份, 类型我选择的 AWS Service, Use case 选的 EC2.

    起名字, 下文中用 {AWS_ROLE} 代替. 找到 EC2, 页面右上角 Actions / Security / Modify IAM role 将身份赋给它.

配置 Google Cloud

创建工作负载身份池 & 添加身份提供方

  1. Console 在 IAM & Admin 找到 Workload Identity Federation, 选择 create pool 创建一个工作负载身份池.

    起名字, 如果点出了 Pool ID, 和 Pool Name 保持一致就行, 下文中用 {POOL_ID} 代替. continue 下一步.

  2. 添加 Provider, 类型选择 AWS, 起名字, 下文中用 {PROVIDER_NAME} 代替, 输入 AWS account id, 应为12位数字, 下文中用 {AWS_ACCOUNT} 代替. continue 下一步.

    目前 provider 类型有 AWS, OIDC, SAML, 后两种方法更加通用, 可以配置任意支持 OIDC/SAML 的 IdP.

  3. 配置 Provider Attributes

    工作负载身份池使用 ABAC 的方式授权, 对于 AWS Provider 会调用 GetCallerIdentity 接口, 可以从返回的内容中提取 attribute.

    这里解释下 attribute, 中文文档称为特性, 实际上就是资源的属性, 比如这里取 assertion.arn 映射为 google.subject, 后面配置访问权限的时候就可以说 subject 为给定 arn 的 workload identity, 以此来授权.

    这里可以看到有两条预先填好的映射:

    1google.subject=assertion.arn
    2attribute.aws_role=assertion.arn.contains('assumed-role') ? assertion.arn.extract('{account_arn}assumed-role/') + 'assumed-role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn
    

    第一条 google.subject 是必需的, 两条都是取 arn 作为内容, 后者额外支持 assumed role. attribute.aws_role 这样以 attribute 开头的是自定义特性, 名字可以随便写, 这里就不改了. 可以添加如下映射方便针对 aws 账号级别的权限管理:

    1attribute.aws_account=assertion.account
    
  4. (可选) 定义 Attribute Conditions, 如果条件计算结果不为 true, 则会拒绝凭据. 这个也是 CEL 表达式, 这里就先不填了.

  5. save 完成创建.

授予工作负载身份池直接资源访问权限

  1. Console 在 IAM & Admin 找到 IAM, 点击 grant access 添加权限.

  2. 指定主账号 Principal, 也就是指定对应的 attribute, 根据之前设置的 attribute mapping, 这里可以是:

  • 按 arn, 也就是 google.subject 指定

    1principal://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{POOL_ID}/subject/{ARN}
    

    这里的 {PROJECT_NUMBER} 是数字格式的那个, 在首页 Project Info 可以找到

    这样可以把权限授予指定的 ec2. 关于 {ARN} 的值, 这里我的用法是 assumed role, 所以是 arn:aws:sts::{AWS_ACCOUNT}:assumed-role/{AWS_ROLE}/{EC2_INSTANCE_ID}, 具体可以调用 GetCallerIdentity 查看结果.

  • 按 aws_role 指定

    1principalSet://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{POOL_ID}/attribute.aws_role/arn:aws:sts::{AWS_ACCOUNT}:assumed-role/{AWS_ROLE}
    

    最后这段 arn 是根据前面 mapping 里设置的规则提取出来的. 注意前缀是 principalSet.

  • 按 aws_account 指定, 其实这个和上面那条一样, 都是自定义属性

    1principalSet://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{POOL_ID}/attribute.aws_account/{AWS_ACCOUNT}
    
  1. 直接资源访问的情况下, 不需要添加 OAuth 相关权限, 直接添加对应资源的访问权限即可.

    这里用 Cloud Translation 接口为例, 翻译接口需要 "cloudtranslate.generalModels.predict" 等权限

    把权限输入进去, 可以看到拥有该权限的 Roles, 这里选择 Cloud Translation API Editor 添加即可.

使用凭据配置访问 Google Cloud

  1. Console 在 IAM & Admin 找到 Workload Identity Federation, 点选我们刚创建的 identity pool

    在右侧面板的 connected service accounts 标签下选择 download config.

    会得到一个 clientLibraryConfig-{PROVIDER_NAME}.json 文件, 上传到要访问 Google Cloud 的 EC2 机器上.

  2. 配置环境变量

    1export GOOGLE_APPLICATION_CREDENTIALS=/path/to/clientLibraryConfig-{PROVIDER_NAME}.json
    

    配置文件路径, 记得替换.

  3. 配置完成, 支持读取该配置的库会自动处理访问凭据的获取, 这里以 go 调用 Cloud Translation V3 为例, 编译运行以下代码:

     1package main
     2
     3import (
     4	"context"
     5	"fmt"
     6
     7	translate "cloud.google.com/go/translate/apiv3"
     8	"cloud.google.com/go/translate/apiv3/translatepb"
     9)
    10
    11func translateText(projectID string, sourceLang string, targetLang string, text string) (string, error) {
    12	// 初始化 client
    13	ctx := context.Background()
    14	client, err := translate.NewTranslationClient(ctx)
    15	if err != nil {
    16		return "", fmt.Errorf("NewTranslationClient: %w", err)
    17	}
    18	defer client.Close()
    19
    20	// 调用请求
    21	req := &translatepb.TranslateTextRequest{
    22		Parent:             fmt.Sprintf("projects/%s/locations/global", projectID),
    23		SourceLanguageCode: sourceLang,
    24		TargetLanguageCode: targetLang,
    25		MimeType:           "text/plain",
    26		Contents:           []string{text},
    27	}
    28
    29	resp, err := client.TranslateText(ctx, req)
    30	if err != nil {
    31		return "", fmt.Errorf("TranslateText: %w", err)
    32	}
    33
    34	results := resp.GetTranslations()
    35	if len(results) != 1 {
    36		fmt.Printf("warning: len(results) = %d\n", len(results))
    37	}
    38
    39	if len(results) == 0 {
    40		return "", fmt.Errorf("No result")
    41	}
    42	return results[0].GetTranslatedText(), nil
    43}
    44
    45func main() {
    46	// 这里的 PROJECT_ID 是字符串id, 记得替换
    47	resp, err := translateText("{PROJECT_ID}", "zh", "en", "那你能帮帮我吗")
    48	if err != nil {
    49		panic(err)
    50	}
    51	fmt.Printf("answer: %s\n", resp)
    52}
    

    输出: answer: So can you help me?