クラウドIDとしてのKubernetes Pod: EKS IRSA vs. GKE Workload Identity Federation
Kubernetes ServiceAccountがAWSおよびGoogle Cloudでどのように実際のクラウドIDになるか - EKS IRSAとGKE Workload Identity Federationの背後にあるOIDCフェデレーション信頼チェーンを1対1でマッピングし、これまでの常識を覆す違いを解説します。
AWSとGoogle Cloudはどちらも同じ問題を同じ方法で解決します。つまり、PodがKubernetes ServiceAccountをフェデレーションされたクラウドIDに変えることで、短期間のトークンと保存されたシークレットなしで、それ自体としてクラウドAPIに認証します。AWSではこの機能はIRSA (IAM Roles for Service Accounts) と呼ばれ、Google CloudではWorkload Identity Federation for GKE (2024年に単なる「Workload Identity」から改名) と呼ばれます。内部的にはどちらも同じOIDCフェデレーションのトリックですが、GKEは配管の多くを隠しており、それが設定方法、デバッグ方法、そして理解の仕方に違いをもたらします。
もし1つの段落だけ読むなら、要するに次のとおりです。クラスターは各PodのServiceAccountに対して短期間のトークンに署名し、クラウドはそのトークンを1つのIDにスコープされた実際の認証情報と交換します。 AWSでは、クラスターごとにIAM OIDCプロバイダーを登録し、IAMロールを作成し、信頼ポリシーでゲートします。Pod内のSDKはAssumeRoleWithWebIdentityを呼び出します。GKEでは何も登録しません。すべてのプロジェクトにはPROJECT_ID.svc.id.googという永続的なGoogle管理のワークロードIDプールがあり、すべてのServiceAccountは自動的にその中のプリンシパルとなり、ノードローカルのメタデータサーバーがトークン交換を行うため、アプリはまるで通常のVM上にあるかのようにApplication Default Credentialsを使用します。GKEには2つの有効化トグル(クラスターとノードプール)が必要であること、GKEはクラウドIDオブジェクトが全くなくてもKubernetes ServiceAccountにクラウド権限を付与できること、そして-いつものことながら-コンテナイメージのプルはワークロードIDとは異なるIDであること、この3点が大きく異なります。
両者が解決する問題
フェデレーション以前は、Podにクラウド権限を与えることは悪い選択肢を意味しました。静的キーをイメージまたはSecretに焼き付ける(漏洩、ローテーションの苦痛)、またはノード全体に権限を与え、その上のすべてのPodがそれを継承する(分離なし、過剰なスコープ)といったものです。AWS側の応急処置はkube2iamやkiamのようなノードIMDSインターセプターでした。GCP側の応急処置はノードサービスアカウントキーをPodにマウントすることでした。これらはすべて扱いにくく、広範囲に及び、競合状態に陥りやすいものでした。
フェデレーションはそれを暗号化された信頼に置き換えます。クラスターはPodのServiceAccountにスコープされた、署名済みの短期間のOIDCトークンをプロジェクションします。クラウドはそのトークンをクラスターが公開しているキーに対して検証し、トークンのIDが設定したルールと一致すれば、正確に1つのクラウドIDに対する認証情報を返します。シークレットはどこにも保存されません。トークンは自動的にローテーションされ(デフォルトの有効期間は約1時間)、クラスター外では無効です。
共通のメカニズム: OIDCフェデレーション
どちらのクラウドでも、すべてのワークロードID設定は同じ信頼の三角形です。
- クラスターはOIDC発行者です。 ServiceAccountトークンに署名する公開鍵を含むディスカバリドキュメントとJWKSエンドポイントを公開します。この発行者が信頼のアンカーとなります。
- クラウドはその発行者を特定のServiceAccount IDに対して信頼します。 AWSはロールごとに信頼を表現し、GCPはプロジェクトごとに一度、永続的に信頼を表現し、Googleがそれを管理します。
- **ワークロードはプロジェクションされたトークンをクラウド認証情報と交換し、**単一のクラウドIDに対する短期間の認証情報を受け取ります。
それ以外のすべては命名であり、ステップ2のどれだけを自分で構築する必要があるかです。ここで両方のクラウドが最も大きく異なります。
1対1のマッピング
- Kubernetes側。 AWSでは、ServiceAccountに
eks.amazonaws.com/role-arn: <role arn>というアノテーションを付けます。GKEでは、最新の直接モデルでは、ServiceAccountにアノテーションを全く付けません。IAMプリンシパルとして参照するだけです。(古いなりすましモデルではアノテーションを使用します。詳細は後述します。) - 「OIDCプロバイダー」。 AWSでは、これはEKSクラスターのOIDC発行者であり、IAMにOIDC IDプロバイダーとしてクラスターごとに一度登録します。GKEでは、プロジェクトのワークロードIDプール
PROJECT_ID.svc.id.googであり、自動的に作成されGoogleによって管理されます。登録する必要はなく、プロジェクト内のすべてのクラスターのすべてのServiceAccountはすでにその中のプリンシパルです。 - ID。 AWS: IAMロール。GKE 直接モデル: 専用のIDオブジェクトなし - Kubernetes ServiceAccount自体がプリンシパルです。GKE なりすましモデル: KSAがなりすますGoogleサービスアカウント。
- 信頼ルール。 AWS: IAMロールの信頼ポリシーは
sub = system:serviceaccount:<ns>:<sa>とaud = sts.amazonaws.comをピン留めします。GKE 直接: 個別の信頼ドキュメントはありません。名前空間とServiceAccountをエンコードするプリンシパル識別子に直接ロールを付与します。GKE なりすまし: KSAにGoogleサービスアカウントに対するroles/iam.workloadIdentityUserロールを付与します。 - 権限。 AWS: ロールにアタッチされたIAMポリシー。GKE: ターゲットリソース(またはプロジェクト)に対するIAM許可ポリシーロールバインディング -
roles/storage.objectViewer、roles/secretmanager.secretAccessorなど。これがより深い違いです。AWSはポリシーをIDに固定し、GCPはリソースのスコープでロールを付与します。 - 交換。 AWS: Pod内のSDKがプロジェクションされたトークンで
sts:AssumeRoleWithWebIdentityを呼び出します。GKE: ノードのメタデータサーバーが透過的に交換を行い、アプリは交換コードを全く使用せずにApplication Default Credentialsを使用します。
この投稿の残りの部分では、各クラウドを最初から最後まで説明し、人々を実際に困らせる違いに焦点を当てます。
ウォークスルー: AWS EKS IRSA、エンドツーエンド
信頼の流れの順序での構成要素:
1. クラスターOIDC発行者は、https://oidc.eks.<region>.amazonaws.com/id/<hash>のようなURLにあります。IAMにOIDC IDプロバイダーとして一度登録し、IAMが署名されたトークンを信頼するようにします。
2. IAMロールとその信頼ポリシー。 信頼ポリシーは、そのロールを1つのServiceAccountのみが引き受けられるようにし、他は許可しません。
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<hash>" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<region>.amazonaws.com/id/<hash>:sub": "system:serviceaccount:apps:checkout",
"oidc.eks.<region>.amazonaws.com/id/<hash>:aud": "sts.amazonaws.com"
}
}
}
:subと:audの両方をピン留めしてください。発行者のみをピン留めすると、クラスター内の任意のServiceAccountがロールを引き受けることを許可してしまいます。
3. IAMポリシーはロールにアタッチされ、アプリが必要とするもの(特定のバケットに対するs3:GetObjectなど)を許可します。
4. ServiceAccountは1つのアノテーションを持ちます。
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/checkout
5. プロジェクション。 EKSはクラスターに組み込まれたPod Identity Webhookを提供します。Podがアノテーション付きのServiceAccountを使用すると、WebhookはAWS_ROLE_ARNとAWS_WEB_IDENTITY_TOKEN_FILEを注入し、プロジェクションされたトークン(オーディエンスsts.amazonaws.com、自動ローテーション)をマウントします。
6. 交換。 AWS SDKはAWS_WEB_IDENTITY_TOKEN_FILEを検出し、sts:AssumeRoleWithWebIdentityを呼び出し、返された短期間の認証情報をキャッシュします。コードは単にboto3.client("s3")となります。
新しい代替手段: EKS Pod Identity (2023) は、クラスターごとのIAM OIDCプロバイダーとロールごとの信頼ポリシーの代わりに、クラスター内エージェントとアソシエーションAPIを通じて同じジョブを実行します。これは、GKEのマネージドモデルに非常によく似ています。IRSAとPod Identityはどちらも本番環境で共存していますが、IRSAはOIDCフェデレーションのフレームワークにきれいにマッピングされるため、ここではIRSAを念頭に置いてください。
ウォークスルー: GKE Workload Identity Federation、エンドツーエンド
同じ信頼の流れですが、構築するものははるかに少ないです。
1. 2つの有効化トグル。 クラスターでワークロードIDプールを有効にし、ノードプールでメタデータサーバーを有効にします。
gcloud container clusters update <cluster> --workload-pool=<project-id>.svc.id.goog
gcloud container node-pools update <pool> --cluster=<cluster> --workload-metadata=GKE_METADATA
両方が必須です。クラスターフラグはクラスターをプロジェクトのプールにオプトインさせます。ノードプールフラグは、認証情報リクエストを傍受するGKEメタデータサーバー(各ノード上のDaemonSet)をデプロイします。GKE_METADATAが有効になっていないノードプール上のPodは、自身のIDではなくノードのIDを取得します。これはサイレントで広範囲に及ぶフォールバックです。
2. プールはすでに存在します。 <project-id>.svc.id.googはプロジェクトに対して自動的に作成されます。OIDCプロバイダーを登録する必要はなく、アカウントごとのプロバイダー制限に達することもありません。プロジェクト内のすべてのクラスターのすべてのServiceAccountはすでにプリンシパルです。
3a. 直接アクセス(最新のデフォルト)。 IAMプリンシパルとしてアドレス指定されたServiceAccountに直接ロールを付与します。Googleサービスアカウントもアノテーションも不要です。
gcloud storage buckets add-iam-policy-binding gs://<bucket> \
--role=roles/storage.objectViewer \
--member="principal://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<project-id>.svc.id.goog/subject/ns/apps/sa/checkout"
プリンシパルパスはns/apps/sa/checkoutをエンコードします。名前空間とServiceAccountが信頼ルールであり、個別の信頼ドキュメントを作成する必要はありません。
3b. なりすまし(古いモデル、一部のサービスでまだ必要)。 Googleサービスアカウントを作成し、KSAにそれをなりすまさせ、KSAにアノテーションを付けます。
gcloud iam service-accounts add-iam-policy-binding checkout@<project-id>.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:<project-id>.svc.id.goog[apps/checkout]"
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
iam.gke.io/gcp-service-account: checkout@<project-id>.iam.gserviceaccount.com
ここでは、Googleサービスアカウントが権限を保持し(通常のロールバインディングを介して)、KSAがそれらを借用します。
4. 交換。 アプリが認証情報を要求すると、GKEメタデータサーバーがhttp://metadata.google.internalへのリクエストを傍受し、そのPodのKubernetes APIからServiceAccount JWTを取得し、Security Token Serviceを介して短期間のフェデレーションアクセストークン(デフォルト1時間、期限切れ前に積極的に更新)と交換し、それを返します。アプリはApplication Default Credentialsと標準のGoogle Cloudクライアントライブラリを使用します。まるでGCE VM上で実行されているかのように動作します。環境変数を注入するWebhookも、AZURE_*スタイルの変数も、コード内の交換呼び出しもありません。Cloud Storageの読み取りはPythonでは単にstorage.Client()です。
主体は要(かなめ)です(両方のクラウドで)
両方のクラウドで正確に一致しなければならない値は、ServiceAccountのIDです。
- AWSは、ロールの信頼ポリシーでトークンサブジェクト
system:serviceaccount:<namespace>:<serviceaccount>をピン留めします。 - GKE直接は、プリンシパルパス
.../subject/ns/<namespace>/sa/<serviceaccount>に同じペアリングをエンコードします。 - GKEなりすましは、メンバー
serviceAccount:<project-id>.svc.id.goog[<namespace>/<serviceaccount>]にそれをエンコードします。
どちらのクラウドでも最も一般的な「誰も認証しない」という失敗は、ここでの不一致です。チャートが異なる名前空間にデプロイされた、ServiceAccountがアプリ名ではなくリリース名を取得した、またはPodがdefault ServiceAccountにフォールバックした、といったケースです。フェデレーションがサイレントに失敗した場合は、まず名前空間とServiceAccount名を確認してください。
実際に困る違い
1. GKEは「OIDCプロバイダー」をプロジェクトごとに一度、永続的に登録します。 EKSでは、クラスターごとにIAM OIDCプロバイダーを登録し、フリート規模ではAWSアカウントあたりのOIDCプロバイダーのソフトリミット100に達し、回避策を講じる必要があります。GKEでは、プロジェクトごとに正確に1つのプール(PROJECT_ID.svc.id.goog)があり、Googleによって作成および管理され、プロジェクト内のすべてのクラスターで共有されます。登録するものはなく、上限に達することもありません。裏を返せば、信頼はデフォルトでプロジェクト全体に及ぶため、プロバイダーを分割するのではなく、IAMバインディング(どの名前空間とServiceAccountに付与するか)でスコープを設定します。
2. GKEはノードローカルのメタデータサーバーでトークンを交換し、Pod内のSDK呼び出しでは行いません。 IRSAはPodをミューテートし(Webhookが環境変数とプロジェクションされたトークンを注入)、SDKがSTSを呼び出します。GKEはノード上のmetadata.google.internalを傍受し、透過的に認証情報を返すため、アプリはまるで通常のVM上にあるかのようにApplication Default Credentialsを使用します。これには2つの結果があります。アプリはクラウド固有の認証情報コードを全く必要とせず、ノードプールはメタデータサーバーを有効にする(GKE_METADATA)必要があります。そうしないと、Podは自身のIDではなくノードのIDをサイレントに取得してしまいます。
3. GKEはクラウドIDオブジェクトなしでServiceAccountにクラウド権限を付与できます。 IRSAでは常にIAMロールが存在します。GKEの直接モデルでは、ロールバインディング自体を除いて、クラウド側で作成するものは何もありません。Kubernetes ServiceAccountがプリンシパルです(principal://.../subject/ns/NS/sa/SA)。古いなりすましモデルでは、Googleサービスアカウントとiam.gke.io/gcp-service-accountアノテーションを導入し、フェデレーションされたプリンシパルを直接受け入れない少数のサービスではまだ必要ですが、まずは直接バインディングを試してください。
4. GKEはAzureのように2つのトグルが必要です。 クラスター(--workload-pool)とノードプール(--workload-metadata=GKE_METADATA)です。ノードプール側の半分を忘れると、失敗はせず、間違った(ノードの)IDが取得されるだけです。IRSAはプロジェクションをEKS自体に組み込んでいるため、同等の2番目のスイッチはありません。
5. イメージのプルはワークロードIDとは異なるIDです - 両方のクラウドで。 イメージのプルはノードIDの仕事です。GKEでは、ノードプールのGoogleサービスアカウントがArtifact Registryからプルするためにroles/artifactregistry.readerを必要とします。EKSでは、ノードグループのインスタンスロールがAmazonEC2ContainerRegistryReadOnlyを必要とします(またはプルシークレットを使用します)。ワークロードIDはアプリコードがクラウドAPIを呼び出す場合にのみ使用されます。クラウドSDKと全く通信しない純粋なHTTPサービスは、ワークロードIDを全く必要としません。また、ワークロードIDにartifactregistry.readerを付与してもイメージのプルには何の効果もありません。なぜなら、プルはアプリ(とそのフェデレーションされたトークン)が実行される前に発生するからです。
6. 権限の付与方法が異なります。 AWSはIAMポリシーをロールに固定します。権限はIDとともに移動します。GCPはターゲットリソースのスコープ(またはプロジェクト)でIAMロールバインディングを付与します。付与はアクセスされるものに存在し、IDには存在しません。最終的な状態は同じですが、監査する場所が異なります。AWSではロールにアタッチされたポリシーを読み取り、GCPではリソースのIAMポリシーをリスト表示します(またはPolicy Analyzerを使用してプリンシパルが何にアクセスできるかを確認します)。
アプリケーションコードの動作
どちらのクラウドも「シークレットなし、認証情報コードなし」という結果になりますが、そこに到達する方法は異なります。
- AWS: Webhookが
AWS_ROLE_ARNとAWS_WEB_IDENTITY_TOKEN_FILEを設定します。SDKのデフォルトチェーンはAssumeRoleWithWebIdentityを呼び出します。コードはboto3.client("s3")です。 - GKE: Podには何も注入されません。SDKのApplication Default Credentialsチェーンはノードメタデータサーバーにアクセスし、そこで交換が行われます。コードは
storage.Client()です。
どちらの場合も、認証情報は短期間で透過的に更新されます。ローテーションするものも、保存するものも、漏洩するものもありません。
両方のクラウドで機能するデバッグチェックリスト
Podが「認証できない」場合、信頼チェーンを順に確認します。
- クラスター/プロジェクトのフェデレーションは有効か? EKS: IAM OIDCプロバイダーが登録されているか。GKE: クラスターに
--workload-poolが設定されており、かつノードプールにGKE_METADATAが設定されているか。GKEでは、ノードプールのトグルが欠けていると、Podが自身のIDではなくノードのIDを取得するという典型的なサイレント障害が発生します。 - 正しいServiceAccountか? Podが実際に意図したServiceAccountを使用しているか(
defaultではないか)。execで確認します。 - IDは一致しているか? 信頼ルールと実際の名前空間およびServiceAccountを比較します。AWSでは
system:serviceaccount:<ns>:<sa>、GKEでは.../subject/ns/<ns>/sa/<sa>(または[ns/sa])です。これは両方のクラウドで最も頻繁に発生する失敗です。 - 交換は機能しているか? EKS: Podに
AWS_WEB_IDENTITY_TOKEN_FILEがあるか。GKE: Podからcurl -H "Metadata-Flavor: Google" metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/emailを実行すると、期待されるIDが返されるか。 - 権限は? IDが解決された後でのみ確認します。ロール上のIAMポリシー(AWS)またはターゲットリソース上のロールバインディング(GCP)です。ここで空の結果は認証失敗ではなく認可失敗であり、フェデレーション自体は機能していることを示します。
この内容が問われる認定資格
ワークロードIDはKubernetes、クラウドIAM、OIDCフェデレーションが交わる場所に位置するため、3つの試験トラックすべてで出題されます。すでに一方のクラウドの資格を持っている場合、もう一方のクラウドでの同じ概念はすぐに理解できるでしょう。
AWS側では:
- AWS Certified Solutions Architect - Associate (SAA-C03) - IAMロール、EKS、およびIDがクラウド権限を取得する方法。
- AWS Certified Security - Specialty (SCS-C03) - IAM信頼ポリシー、OIDCフェデレーション、および最小権限スコープ(まさにIRSA信頼チェーン)。
Google Cloud側では:
- Google Cloud Associate Cloud Engineer - GKE、IAM、サービスアカウント、およびロールバインディング。
- Google Cloud Professional Cloud Security Engineer - ワークロードID、IAMの詳細、サービスアカウントのセキュリティ、および最小権限。
Kubernetes側では:
- CNCF Certified Kubernetes Administrator (CKA) - ServiceAccounts、プロジェクションされたトークン、およびPodスペック。
- CNCF Certified Kubernetes Security Specialist (CKS) - ServiceAccountトークンのセキュリティ、最小権限、および静的シークレット露出の削減。
まとめ
IRSAとGKE Workload Identity Federationは、配管の量が異なるだけで同じアイデアに基づいています。クラスターがServiceAccountに対して短期間のOIDCトークンに署名し、クラウドがそれを単一のIDにスコープされた認証情報と交換します。AWSでは、OIDCプロバイダーをクラスターごとに、IAMロール、信頼ポリシー、SDKが交換するプロジェクションされたトークンなど、信頼を自分で構築する必要があります。GKEは、Googleが管理するプロジェクト全体のプールを提供し、クラウドIDを全く持たないKubernetes ServiceAccountに直接ロールをバインドでき、ノードメタデータサーバーで交換を行うため、アプリは認証情報を目にすることはありません。信頼の三角形を一度学べば、その変換は機械的です。ロールはプリンシパルバインディング(またはなりすますサービスアカウント)になり、信頼ポリシーはIAMメンバー文字列になり、アタッチされたポリシーはリソース上のロールバインディングになり、AssumeRoleWithWebIdentityは目に見えないメタデータサーバーの交換になります。GKEの2つのトグル、IDオブジェクトなしの直接モデル、そしてイメージのプルが全く異なるIDであるという3つの点を常に念頭に置いておけば、これまでランダムに見えた拒否が、首尾一貫した意図的な設計として理解できるようになるでしょう。