🍣

Workload Identity連携でAWS(EC2/ECS/EKS)からサービスアカウントキーなしでBigQueryにアクセスする

2022/11/22に公開

背景

オンプレミスやAWSなどのクラウドからGoogle Cloud StorageやBigQueryなどのGCPサービス(API)を利用したい場合、サービスアカウントキーを使用していました。サービスアカウントキーはサービスアカウントから払い出された秘密鍵ファイルであり、使用する場合は環境変数 GOOGLE_APPLICATION_CREDENTIALSにサービスアカウントキーのファイルパスを指定します。

$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account_key.json

サービスアカウントキーの利用について次の懸念があるため、システム提供者側は発行を極力避けたいと考えます。

  1. キーファイルの漏洩リスク:サービスアカウントキーを使用する権限を持っていることを、サービスアカウントキーを利用される側は知ることができない。つまり、外部に漏洩すると誰でもサービスアカウントに付与されている権限が実行できてしまう

  2. 管理の煩雑さ:サービスアカウントキーには有効期限がないため、発行者側でローテートする必要がある。GCPドキュメントにはキーファイルのローテートを推奨されているが面倒くさい

この問題に対して、Workload Identity連携を用いることでサービスアカウントキーを使うことなく、オンプレミスやAWSなどのクラウドからGCPサービスを利用できます。

Workload Identity連携についてはGoogle公式ブログで紹介されています。

本記事では、Workload Identity連携を設定して、AWS EC2/Amazon ECS/Amazon EKSからBigQueryにアクセスする方法について紹介します。

Workload Identity連携とは

有効期限がないサービスアカウントキーをトークンとして一時的な認証情報に置き換えることができる機能です。GCPリソースにアクセスできるまでの流れは以下のようになります。

image
YouTuebe「What is Workload Identity Federation?」より

  1. AWSなどGCP外の環境で、アプリケーション(Workload)は自環境のIDプロバイダ(Identity Provider : IdP)に対して認証しクレデンシャルを取得する
  2. アプリケーションはGoogleのSecurity Token Service(STS)に1のクレデンシャルを渡して妥当性をチェックし、問題がなければアクセストークンが発行される
    • STSはWorkload Identity Poolにクレデンシャルを渡して信頼できるIdPからであることを確認してからアクセストークンを発行する
  3. 2のアクセストークンを使って、GCPのサービスアカウントになりすましてGCPリソースにアクセスできるようになる

GCP公式動画に詳しく説明されています。

https://www.youtube.com/watch?v=4vajaXzHN08

設定の流れ

EC2/ECS/EKSいずれの場合も以下のように設定を行っていきます。

  1. AWS側:IAMロールを作成する。ECSの場合はECSタスクロール、EKSの場合はPod単位のアクセス制御をするために、IAMロールとKubernetesのサービスアカウント(KSA : Kubernetes Service Account)を紐づける(IRSA : IAM Roles for Service Accounts)
  2. GCP側:Workload Identity PoolとWorkload Identity Providerを作成する
    • Workload Identity Providerは、Workload Identity Poolが外部IdPを信頼するための設定
  3. GCP側:サービスアカウントを作成し、Workload Identity Poolの権限を借用するための権限とGCPリソースを利用するために必要な権限を付与(今回の場合はBigQuery関連)
  4. GCP側:認証の構成ファイルを取得する
  5. AWS側:5で取得したファイルのファイルパスを環境変数 GOOGLE_APPLICATION_CREDENTIALSに指定してアプリケーションを実行

EC2インスタンスからアクセスする場合

(AWS) IAMロールの作成

access2bqというIAMロールを作成します。必要に応じてAWSリソースにアクセスするためのIAMポリシーをアタッチしてください。

$ cat << EOF > assume.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

$ IAM_ROLE="access2bq"
$ aws iam create-role --role-name $IAM_ROLE \
  --assume-role-policy-document file://assume.json
# インスタンスプロファイルの追加とロールのアタッチ
$ aws iam create-instance-profile --instance-profile-name access2bq
$ aws iam add-role-to-instance-profile \
  --instance-profile-name access2bq \
  --role-name access2bq

(GCP)Workload Identity Poolの作成

作成するために必要なAPI(IAM、Resource Manager、Service Account Credentials、Security Token Service APIを)有効にします。GCPドキュメントにあるAPI有効化リンクをクリックすると一気に有効化できます。

bq-poolというWorkload Identity Poolを作成します。

$ POOL="bq-pool"
$ gcloud iam workload-identity-pools create $POOL --location="global"

(GCP)Workload Identity Providerの作成

aws-providerというWorkload Identity Providerを作成します。属性マッピング(--attribute-mapping)のattribute.aws_roleは特定のIAMロールからしか許可しないように指定します。今回はドキュメントに記載されているものを使います。

$ AWS_ACCOUND_ID="<AWSアカウントID>"
$ POOL="bq-pool"
$ PROVIDER="aws-provider"
$ gcloud iam workload-identity-pools providers create-aws $PROVIDER \
  --location="global" \
  --workload-identity-pool=$POOL \
  --account-id=$AWS_ACCOUND_ID \
  --attribute-mapping='google.subject=assertion.arn,attribute.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'

(GCP)サービスアカウントの作成

access-from-awsというサービスアカウントを作成します。

$ SERVICE_ACCOUNT="access-from-aws"
$ gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --description="Access from AWS with workload identity federation"

今回はBigQueryにクエリ実行するため、以下の権限も付与します。BigQueryを利用しない場合はこの作業は不要です。

$ PROJECT_ID="foo-project"
$ SERVICE_ACCOUNT="access-from-aws"

# BigQueryデータ閲覧者(BigQuery Data Viewer)の権限を付与する
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/bigquery.dataViewer"

# クエリ実行するための権限(BigQueryジョブユーザー、BigQuery読み取りセッションユーザー)を付与する
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/bigquery.jobUser"
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/bigquery.readSessionUser"

(GCP)Workload Identity Poolとサービスアカウントの連携

サービスアカウントに対して、Workload Identity Poolの権限を借用するための権限を付与します。プロジェクト番号(PROJECT_NUMBER)はGCPの「IAM & Adminの設定画面」から確認できます(プロジェクトID、プロジェクト名とは異なるので注意)。

$ SERVICE_ACCOUNT="access-from-aws"
$ PROJECT_NUMBER="123456789012"
$ PROJECT_ID="foo-project"
$ POOL="bq-pool"
$ AWS_ACCOUND_ID="<AWSアカウントID>"
$ IAM_ROLE="access2bq"
$ gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL}/attribute.aws_role/arn:aws:sts::${AWS_ACCOUND_ID}:assumed-role/${IAM_ROLE}"

(GCP)認証構成ファイルの取得

認証に必要な構成ファイルを取得します。

$ SERVICE_ACCOUNT="access-from-aws"
$ PROJECT_ID="foo-project"
$ PROJECT_NUMBER="123456789012"
$ POOL="bq-pool"
$ PROVIDER="aws-provider"
$ gcloud iam workload-identity-pools create-cred-config \
  "projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL}/providers/${PROVIDER}" \
  --service-account="${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --aws \
  --output-file="config-aws-provider.json"

--output-fileで指定したファイル名(config-aws-provider.json)に生成されます。このファイルの中身は機密情報が含まれていないため、サービスアカウントキーよりも管理を厳密にする必要はありません。

$ cat config-aws-provider.json
{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/bq-pool/providers/aws-provider",
  "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "environment_id": "aws1",
    "region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
    "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
    "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
  },
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/access-from-aws@foo-project.iam.gserviceaccount.com:generateAccessToken"

アプリケーションの実行

実行するためのEC2インスタンスがなれけば作ります。IAMロールは最初に作成したロール(access2bq)を指定します。OSは何でも良いですが今回はAmazon Linux 2にしました。

EC2インスタンスにSSHログイン後、認証構成ファイル(config-aws-provider.json)を配置します。今回はPythonからBigQueryにクエリ実行するので、必要なパッケージを入れておきます。

$ pip3 install google-cloud-bigquery
$ pip3 install pandas db-dtypes

サンプルコード(query_bq_ec2.py)

認証構成ファイル(config-aws-provider.json)と同じフォルダで記載します。

import os
from google.cloud import bigquery

# 認証構成ファイルのパスを環境変数に入れる。ソースコードの外から入れても良い
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = './config-aws-provider.json'

# Cloud Identity連携が設定されているプロジェクトIDを指定する
project_id = 'foo-project'
bqclient = bigquery.Client(project=project_id)
dryrun = False

sql = """
SELECT *
FROM `foo-project.hoge_dataset.fuga_table`
LIMIT 10
"""

job_config = bigquery.QueryJobConfig(dry_run=dryrun)
query_job = bqclient.query(sql, job_config)
assert query_job.errors is None, f'errors: {query_job.errors}'

# debug
print(f"Bytes processed: {query_job.total_bytes_processed} B")

# 先頭5行分のみ出力
df = query_job.result().to_dataframe()
print(df.head())

クエリ実行してBigQueryからクエリ実行できることを確認します。

$ python ./query_bq_ec2.py

異なるIAMロールからアクセスしてみる

今回作成したものと異なるIAMロールで実行した場合は以下のエラーとなり、アクセスできません。

Traceback (most recent call last):
...(省略)...
google.auth.exceptions.RefreshError: ('Unable to acquire impersonated credentials', '{\n  "error": {\n    "code": 403,\n    "message": "The caller does not have permission",\n  "status": "PERMISSION_DENIED"\n  }\n}\n')

Amazon ECSからアクセスする場合

基本的な流れはEC2インスタンスと同じです。ECSの場合、GCPリソースへのアクセス制御は以下の2パターンがあります。

  1. IAMロールを用いる(データプレーンがEC2の場合)
  2. ECSタスクロールを用いる

1はEC2インスタンスと同じですが、データプレーンがEC2のみ、Fargateでは使えません。また、EC2インスタンス上に動いているECSタスクすべてがGCPリソースにアクセスできてしまいます。

今回は2のECSタスクロールを用いることで、特定のECSタスクのみアクセスできるようにします。

(AWS)ECSタスクロールの作成

ecs-task-roleというECSタスクロールを作成します。

$ cat << EOF > assume.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

$ IAM_ROLE="ecs-task-role"
$ aws iam create-role --role-name $IAM_ROLE \
  --assume-role-policy-document file://assume.json

(GCP)Workload Identity Pool、Workload Identity Provider、サービスアカウントの作成

EC2インスタンスと同じです。今回はEC2インスタンスで作ったものを使います。

(GCP)Workload Identity Poolとサービスアカウントの連携

EC2インスタンスと同じです。今回はIAMロールがECSタスクロールに変わっています。

$ SERVICE_ACCOUNT="access-from-aws"
$ PROJECT_NUMBER="123456789012"
$ PROJECT_ID="foo-project"
$ POOL="bq-pool"
$ AWS_ACCOUND_ID="<AWSアカウントID>"
$ IAM_ROLE="ecs-task-role"
$ gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL}/attribute.aws_role/arn:aws:sts::${AWS_ACCOUND_ID}:assumed-role/${IAM_ROLE}"

アプリケーションの実行

ECSタスクからBigQueryにアクセスする場合、EC2インスタンスのときと同じにできません。なぜなら、EC2インスタンスとECSタスクロールでクレデンシャルを取得するためのメタデータURLが異なるからです。

  • EC2インスタンスhttp://169.254.169.254/latest/meta-data/iam/security-credentials
  • ECSタスクロールhttp://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI

認証構成ファイルの中身をみるとEC2インスタンスのメタデータを見にいくようになっています。この箇所をECSタスクロールの認証エンドポイントに置き換えたとしても、google-auth-library-pythonが対応していないため動作しません。

https://github.com/googleapis/google-auth-library-python/issues/1099

そのため、こちらで提示されているようにECSタスクロールのクレデンシャルを取得して環境変数に入れる必要があります。

平たく書くと以下のようになります。

サンプルコード(query_bq_ecs1.py)

認証構成ファイルのパスはコンテナイメージに同梱し、ECSタスク定義を作成する際に環境変数 GOOGLE_APPLICATION_CREDENTIALSに認証構成ファイルのファイルパスを指定します。

import os
import requests
from google.cloud import bigquery

# ECSタスクロールのクレデンシャルを環境変数に入れている
url_path = os.environ.get('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI')
url = urljoin('http://169.254.170.2', url_path)
res = requests.get(url, timeout=3).json()
os.environ['AWS_ACCESS_KEY_ID'] = res['AccessKeyId']
os.environ['AWS_SECRET_ACCESS_KEY'] = res['SecretAccessKey']
os.environ['AWS_SESSION_TOKEN'] = res['Token']

# Cloud Identity連携が設定されているプロジェクトIDを指定する
project_id = 'foo-project'
bqclient = bigquery.Client(project=project_id)
dryrun = False

sql = """
SELECT *
FROM `foo-project.hoge_dataset.fuga_table`
LIMIT 10
"""

job_config = bigquery.QueryJobConfig(dry_run=dryrun)
query_job = bqclient.query(sql, job_config)
assert query_job.errors is None, f'errors: {query_job.errors}'

# debug
print(f"Bytes processed: {query_job.total_bytes_processed} B")

# 先頭5行分のみ出力
df = query_job.result().to_dataframe()
print(df.head())

サンプルコード(query_bq_ecs2.py)

先ほどのサンプルコード(query_bq_ecs1.py)では、冒頭でクレデンシャルを環境変数に入れるところを毎回書くのが面倒です。また、ECS、EC2、ローカル環境といった複数の環境でアプリケーション実行するためにソースコードの差分をなくしたいので、以下のPyPIを使います(自作しました)。

https://pypi.org/project/aws-ecs-gcp-workload-identity-federation/

これをimportするだけでECS、EC2といった環境によらず認証を通すことができ、コードがスッキリします。

import aws_ecs_gcp_workload_identity  # 追加
from google.cloud import bigquery

# Cloud Identity連携が設定されているプロジェクトIDを指定する
project_id = 'foo-project'
bqclient = bigquery.Client(project=project_id)
dryrun = False

sql = """
SELECT *
FROM `foo-project.hoge_dataset.fuga_table`
LIMIT 10
"""

job_config = bigquery.QueryJobConfig(dry_run=dryrun)
query_job = bqclient.query(sql, job_config)
assert query_job.errors is None, f'errors: {query_job.errors}'

# debug
print(f"Bytes processed: {query_job.total_bytes_processed} B")

# 先頭5行分のみ出力
df = query_job.result().to_dataframe()
print(df.head())

Amazon EKSからアクセスする場合

以下の記事のように、Pod単位で権限付与するためにOIDC プロバイダを用いる必要があります。
Workload Identityを用いてEKSクラスタからGoogle Cloudへアクセスする

(AWS)EKSクラスタのセットアップ、準備

すでにEKSクラスタがあるならこの節は不要です。EKSクラスタがない人はQuick Startで作成します。CloudFormation一発で作成してくれるので楽ですが40分ぐらいかかりました。

  • デプロイ方法: 新規の VPC にデプロイする
  • リージョン: オハイオ(us-east-2)

構築した後、自動作成されて踏み台サーバで作業するため、SSHログインし以下の準備をします。また、踏み台サーバのIAMロールにAdministratorAccessポリシーをアタッチしておきます。

$ aws configure
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
Default region name [None]: us-east-2
Default output format [None]:

# eksctlのインストール
$ curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
$ sudo mv /tmp/eksctl /usr/local/bin
$ eksctl version
0.115.0

(AWS)EKSクラスタのIAM OIDCプロバイダーの確認

Quick Startで構築した場合はすでに作成されているため確認作業のみです。

# CloudFormationで自動作成されているため、確認しておく
$ CLUSTER="EKS-HOGEHOGE"
$ OIDC_ID=$(aws eks describe-cluster \
  --name $CLUSTER \
  --query "cluster.identity.oidc.issuer" --output text | cut -d '/' -f 5)
$ aws iam list-open-id-connect-providers | grep $OIDC_ID
"Arn": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"

(AWS)IAMロールの作成、Kubernetesのサービスアカウントとの関連付け

eksctlコマンドで簡単につくれます。今回は検証のためAmazonS3FullAccessポリシーを付与します。Kubernetesのサービスアカウント(KSA)の名前はeks-access-to-gcpとしています。

$ CLUSTER="EKS-HOGEHOGE"
$ KSA_NAME="eks-access-to-gcp"
$ eksctl create iamserviceaccount \
  --name $KSA_NAME \
  --cluster $CLUSTER \
  --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
  --approve

KSAの中身を確認します。

$ kubectl get serviceaccounts
NAME                SECRETS   AGE
default             1         78m
eks-access-to-gcp   1         3m1s

$ KSA_NAME="eks-access-to-gcp"
$ kubectl get serviceaccounts $KSA_NAME --output yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eksctl-EKS-HOGEHOGE-addon-iamserviceaccount-Role1-1LMY230X5R55Z
  creationTimestamp: "2022-10-27T12:16:57Z"
  labels:
    app.kubernetes.io/managed-by: eksctl
  name: eks-access-to-gcp
  namespace: default
  resourceVersion: "15574"
  uid: 619f3b0c-61a9-4996-9b9e-2d2b4672a450
secrets:
- name: eks-access-to-gcp-token-2tdsg

eksctl-${CLUSTER}-addon-iamserviceaccount-Role1-(ランダム文字列) という命名規則でIAMロールが作成されます。今回の場合はeksctl-EKS-HOGEHOGE-addon-iamserviceaccount-Role1-1LMY230X5R55Zが作成されました。

IAMロールのポリシーを見ると、ロールとKSAがマッピングされていることが確認できます。

$ ROLE_NAME="eksctl-EKS-HOGEHOGE-addon-iamserviceaccount-Role1-1LMY230X5R55Z"
$ aws iam get-role \
  --role-name $ROLE_NAME \
  --query Role.AssumeRolePolicyDocument
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Effect": "Allow",
            "Condition": {
                "StringEquals": {
                    "oidc.eks.us-east-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud": "sts.amazonaws.com",
                    "oidc.eks.us-east-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:default:eks-access-to-gcp"
                }
            },
            "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
            }
        }
    ]

確認作業

KSAに付与された権限が正しく機能しているかを確認します。AWS CLI用のPodを立ててS3 APIを実行してみます。awscli-pod.yamlというYAMLファイルを作成します。

apiVersion: v1
kind: Pod
metadata:
  name: awscli
  labels:
    app: awscli
spec:
  serviceAccountName: eks-access-to-gcp
  containers:
  - image: amazon/aws-cli
    command:
      - "sleep"
      - "604800"
    imagePullPolicy: IfNotPresent
    name: awscli
  restartPolicy: Always

Podを作成した後、Podに入ってAWS CLIを実行してみます。

$ kubectl apply -f ./awscli-pod.yaml -n default

# Podに環境変数がセットされていることが確認できる
$ kubectl exec -n default awscli env | grep AWS
AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
AWS_DEFAULT_REGION=us-east-2
AWS_REGION=us-east-2
AWS_ROLE_ARN=arn:aws:iam::123456789012:role/eksctl-EKS-HOGEHOGE-addon-iamserviceaccount-Role1-1LMY230X5R55Z

# Podに入ってAWS CLIを実行、S3 APIが叩けていることを確認
$ kubectl exec -it awscli -n default -- /bin/bash

bash-4.2# aws s3 ls
2022-10-27 10:45:41 foo-bucket
2022-10-26 08:27:36 bar-bucket

(GCP)Workload Identity Poolの作成

EC2インスタンスと同じです。今回はEC2インスタンスで作ったものを使います。

(GCP)Workload Identity Providerの作成

EKSクラスタのIAM OIDC プロバイダのURLを取得しておきます。

$ CLUSTER="EKS-HOGEHOGE"
$ aws eks describe-cluster \
  --name $CLUSTER \
  --query "cluster.identity.oidc.issuer" --output text
https://oidc.eks.us-east-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E

aws-oidc-providerというWorkload Identity Providerを作成します。属性マッピング(--attribute-mapping)のattribute.ksa_nameは特定のKSAからしか許可しないように指定しています。

$ PROVIDER="aws-oidc-provider"
$ POOL="bq-pool"
$ ISSUER_URL="https://oidc.eks.us-east-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
$ gcloud iam workload-identity-pools providers create-oidc $PROVIDER \
  --location="global" \
  --workload-identity-pool=$POOL \
  --attribute-mapping="google.subject=assertion.sub,attribute.ksa_name=assertion.sub.extract('system:serviceaccount:{ksa_name}')" \
  --issuer-uri=$ISSUER_URL \
  --allowed-audiences=sts.amazonaws.com

(GCP)サービスアカウントの作成

access-from-aws-eks-podというサービスアカウントを作成します。

$ SERVICE_ACCOUNT="access-from-aws-eks-pod"
$ gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --description="Access from Amazon EKS with workload identity federation"

今回はBigQueryにクエリ実行するため、以下の権限も付与します。BigQueryを利用しない場合はこの作業は不要です。

$ PROJECT_ID="foo-project"
$ SERVICE_ACCOUNT="access-from-aws-eks-pod"

# BigQueryデータ閲覧者(BigQuery Data Viewer)の権限を付与する
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/bigquery.dataViewer"

# クエリ実行するための権限(BigQueryジョブユーザー、BigQuery読み取りセッションユーザー)を付与する
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/bigquery.jobUser"
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/bigquery.readSessionUser"

(GCP)Workload Identity Poolとサービスアカウントの連携

EC2のときと同じですが、--memberオプションが少し異なっている点に注意してください。

$ PROJECT_NUMBER="123456789012"
$ PROJECT_ID="foo-project"
$ SERVICE_ACCOUNT="access-from-aws-eks-pod"
$ POOL="bq-pool"
$ KSA_NAME="default:eks-access-to-gcp"
$ gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL}/attribute.ksa_name/${KSA_NAME}"

(GCP)認証構成ファイルの取得

認証に必要な構成ファイルを取得します。

$ PROJECT_ID="foo-project"
$ PROJECT_NUMBER="123456789012"
$ SERVICE_ACCOUNT="access-from-aws-eks-pod"
$ POOL="bq-pool"
$ PROVIDER="aws-oidc-provider"
$ gcloud iam workload-identity-pools create-cred-config \
  "projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL}/providers/${PROVIDER}" \
  --subject-token-type="urn:ietf:params:oauth:token-type:jwt" \
  --service-account="${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --credential-source-file="/var/run/secrets/eks.amazonaws.com/serviceaccount/token" \
  --credential-source-type="text" \
  --output-file="config-aws-eks-provider.json"

--output-fileで指定したファイル名(config-aws-eks-provider.json)に生成されます。このファイルの中身は機密情報が含まれていないため、サービスアカウントキーよりも管理を厳密にする必要はありません。生成されたconfig-aws-eks-provider.jsonは以下のようになります。

{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/bq-pool/providers/aws-oidc-provider",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "file": "/var/run/secrets/eks.amazonaws.com/serviceaccount/token",
    "format": {
      "type": "text"
    }
  },
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/access-from-aws-eks-pod@foo-project.iam.gserviceaccount.com:generateAccessToken"
}

アプリケーションの実行

サンプルコード(query_bq_eks.py)

query_bq_ec2.pyと同じです。

import os
from google.cloud import bigquery

project_id = 'ohsawa0515-cloud-identity'
bqclient = bigquery.Client(project=project_id)
dryrun = False

sql = """
SELECT
  *
FROM
  `ohsawa0515-bq-nyumon.my_open_data.covid19_open_data_japan`
WHERE
  date = "2022-08-11"
LIMIT
  1000
"""

job_config = bigquery.QueryJobConfig(dry_run=dryrun)
query_job = bqclient.query(sql, job_config)
assert query_job.errors is None, f'errors: {query_job.errors}'

# debug
print(f"Bytes processed: {query_job.total_bytes_processed} B")

# 先頭5行分のみ出力
df = query_job.result().to_dataframe()
print(df.head())

config-aws-eks-provider.jsonとサンプルコード(query_bq_eks.py)を同梱したコンテナイメージ(bq-test)をAmazon ECRにプッシュしておきます。

以下のようなマニフェストファイルを作ります(oidc-test.yaml)。serviceAccountNameに作成したKSA、環境変数GOOGLE_APPLICATION_CREDENTIALSに認証構成ファイルのパスを指定します。

apiVersion: v1
kind: Pod
metadata:
  name: oidc-test
  labels:
    app: oidc-test
spec:
  serviceAccountName: eks-access-to-gcp
  containers:
  - image: 123456789012.dkr.ecr.us-east-2.amazonaws.com/bq-test:latest
    env:
    - name: GOOGLE_APPLICATION_CREDENTIALS
      value: "/app/config-aws-eks-provider.json"
    imagePullPolicy: IfNotPresent
    name: bq-test
  restartPolicy: Always

kubectl applyで実行した後、実行結果からBigQueryにアクセスできていればOKです。

$ kubectl apply -f ./oidc-test.yaml -n default

異なるKubernetes Service Accountからアクセスしてみる

今回作成したKSAのと別のを作成し、BigQueryへのアクセスを試みてアクセスできないことを確認します。

$ CLUSTER="EKS-HOGEHOGE"
$ KSA_NAME="hogehoge-ksa"
$ eksctl create iamserviceaccount \
  --name $KSA_NAME \
  --cluster $CLUSTER \
  --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
  --approve

# 確認
$ kubectl get serviceaccounts
NAME                SECRETS   AGE
default             1         2d
eks-access-to-gcp   1         47h
hogehoge-ksa        1         37s

$ kubectl get serviceaccounts hogehoge-ksa --output yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eksctl-EKS-HOGEHOGE-addon-iamserviceaccount-Role1-JK04WG7AF9ZK
  creationTimestamp: "2022-10-29T11:24:16Z"
  labels:
    app.kubernetes.io/managed-by: eksctl
  name: hogehoge-ksa
  namespace: default
  resourceVersion: "563691"
  uid: 574c9f9e-57ee-4a22-9b62-a845d92caea4
secrets:

先ほど作成したマニフェストファイル(oidc-test.yaml)のserviceAccountNamehogehoge-ksaに差し替えてkubectl applyするとPermission Deniedになることが確認できます。

Traceback (most recent call last):
...(省略)
google.auth.exceptions.RefreshError: ('Unable to acquire impersonated credentials', '{\n  "error": {\n    "code": 403,\n    "message": "The caller does not have permission",\n    "status": "PERMISSION_DENIED"\n  }\n}\n')

参考URLリンク

Discussion