FargateのWorkload Identity連携を使う一番良い方法
Google Cloud のリソースを、AWSなど他のクラウドから操作するとき、Google Cloud の Workload Identity 連携という機能を使います。
Workload Identity 連携は、ライブラリから認証を行うだけでGoogle Cloud のAPIを叩くことができるようになります。
アクセスキーを使う方法よりも、セキュリティ面や管理コストなど様々な面で嬉しい機能です。
Workload Identity連携は多くのケースでは特別な対応なく利用できますが、一部環境では工夫が必要になります。
例えば、Amazon ECS on Fargateなどです。Fargate は Workload Identity認証が標準対応していないため、EC2と同様に実行してもエラーが出てしまいます。
そのため少し工夫を行う必要があるのですが、いくつか試す中でうまくいったケースとNGなケースがそれぞれあったので紹介します。
エラーが出る原因と必要な対応
そもそも、なぜエラーが出るのかを確認しておきます。
Workload Identity 連携を使ってAWSからリクエストを行う際、内部でアクセスキーの取得を行っています。
この際、ライブラリは 169.254.169.254/latest/meta-data/iam/security-credentials
へのリクエストを実行しますが、これはEC2用のエンドポイントであり、ECSのエンドポイントとは異なります。ここでエラーが発生します。
この問題を解決するために、別の方法でアクセスキーを取得・設定する必要があります。
NGな方法: 環境変数を上書きする
まず試してみた方法として、以下のようにFargate用のエンドポイントからアクセスキーを取得し、環境変数を上書きする方法を試してみました。
export async function setAwsCredentials() {
const credentialUri = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI;
const endpointUrl = "http://169.254.170.2" + credentialUri;
await fetch(endpointUrl)
.then((res) => res.json())
.then((json) => {
process.env.AWS_ACCESS_KEY_ID = json.AccessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = json.SecretAccessKey;
process.env.AWS_SESSION_TOKEN = json.Token;
})
.catch((e) => {
console.error(e);
throw new InvalidParameterError("AWSのクレデンシャルの取得に失敗しました。");
});
}
この方法でも一見動作はしますが、 この処理を実行した後、6時間以内に動作しなくなります。
6時間以内に動作しなくなる理由
今回使用しているNode.JSのSDKは、以下の優先順位でクレデンシャルを探索・使用します。
- 環境変数
process.env
- SSO認証情報(トークンキャッシュ)
- Web Identity Token
- 共有 config / credentials ファイル
- EC2/ECS インスタンスメタデータ
数が多いですが、要は「環境変数」が最優先であり、「メタデータ」は一番優先度が低いということです。
これによって、メタデータよりも環境変数が優先されることでライブラリにアクセスキーを渡すことができています。
しかしこれによって、別の問題が出てきます。
ECSのメタデータエンドポイントから取得できるアクセスキーは、6時間毎にローテーションされる仕組みになっています。
出力には、シークレットアクセスキー ID で構成されるアクセスキーペアと、アプリケーションが AWS リソースにアクセスするために使用するシークレットキーが含まれます。また、認証情報が有効であることを確認するために AWS が使用するトークンも含まれています。デフォルトでは、タスクロールを使用してタスクに割り当てられた認証情報は6 時間有効です。その後は、Amazon ECS コンテナエージェントによって自動的にローテーションされます。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/security-iam-roles.html#security-iam-task-role
つまり、 環境変数に代入したアクセスキーは6時間以内に期限が切れる ということです。
こうなると、タスクに付与されたロールのアクセスキーが期限切れの状態に陥るため、ロールを使用した操作(AWSのAPIリクエストなど)は全て行えなくなります。
回避方法としては、定期的にアクセスキーを再設定したり、処理毎にアクセスキーを環境変数に設定→処理が終了したら削除 のような方法も不可能ではありませんが、望ましい方法でないことは明らかです。
よって、次の方法で対応を行いました。
うまくいった方法: AwsSecurityCredentialsSupplier をカスタマイズする
公式ライブラリが内部で利用しているクラスは、一部のクラスはカスタマイズして利用することが可能なようです。
READMEに紹介のある AwsSecurityCredentialsSupplier
クラスをカスタマイズする方法を試したところ、うまくいきました。
具体的なコードは以下の通りです。
import { AwsClient, AwsSecurityCredentials, AwsSecurityCredentialsSupplier, ExternalAccountSupplierContext } from 'google-auth-library';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { Storage } from '@google-cloud/storage';
class AwsSupplier implements AwsSecurityCredentialsSupplier {
private readonly region: string
constructor(region: string) {
this.region = options.region;
}
async getAwsRegion(context: ExternalAccountSupplierContext): Promise<string> {
// Return the AWS region i.e. "us-east-2".
return this.region
}
async getAwsSecurityCredentials(
context: ExternalAccountSupplierContext
): Promise<AwsSecurityCredentials> {
// Retrieve the AWS credentails.
const awsCredentialsProvider = fromNodeProviderChain();
const awsCredentials = await awsCredentialsProvider();
// Parse the AWS credentials into a AWS security credentials instance and
// return them.
const awsSecurityCredentials = {
accessKeyId: awsCredentials.accessKeyId,
secretAccessKey: awsCredentials.secretAccessKey,
token: awsCredentials.sessionToken
}
return awsSecurityCredentials;
}
}
const clientOptions = {
audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience.
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type.
aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier.
service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken', // Set the service account impersonation url.
}
// Create a new Auth client and use it to create service client, i.e. storage.
const authClient = new AwsClient(clientOptions);
const storage = new Storage({ authClient });
ざっくり処理を確認してみます。
まず大枠として、認証クラスをカスタマイズし、clientOptions のパラメータに設定することでクレデンシャルの取得方法を上書きしています。
class AwsSupplier implements AwsSecurityCredentialsSupplier {
// ....
}
const clientOptions = {
// ....
aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier.
// ....
}
const authClient = new AwsClient(clientOptions);
const storage = new Storage({ authClient });
AwsSupplier クラスの内部では awsCredentialsProvider()
を利用してアクセスキーを取得し、それを Workload Identity の処理で使えるように返しているようですね。
class AwsSupplier implements AwsSecurityCredentialsSupplier {
// .....
async getAwsSecurityCredentials(
context: ExternalAccountSupplierContext
): Promise<AwsSecurityCredentials> {
// Retrieve the AWS credentails.
const awsCredentialsProvider = fromNodeProviderChain();
const awsCredentials = await awsCredentialsProvider();
// Parse the AWS credentials into a AWS security credentials instance and
// return them.
const awsSecurityCredentials = {
accessKeyId: awsCredentials.accessKeyId,
secretAccessKey: awsCredentials.secretAccessKey,
token: awsCredentials.sessionToken
}
return awsSecurityCredentials;
}
// .....
}
clientOptions の他のについては、Workload Identityの認証情報ファイルに記載のある情報をそのまま指定すればよさそうです。
const clientOptions = {
audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WORKLOAD_POOL_ID/providers/$PROVIDER_ID', // Set the GCP audience.
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', // Set the subject token type.
aws_security_credentials_supplier: new AwsSupplier("AWS_REGION") // Set the custom supplier.
service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken', // Set the service account impersonation url.
}
この方法で、無事Fargate上で動作させることができました。
終わりに
アクセスキーの自動ローテーションに関しては今回の課題をきっかけに始めて認識しました。今回はECSでしたが、EC2でも同様の仕組みが組み込まれている(?)ようでした。
アプリケーションがインスタンスメタデータから一時的な認証情報を取得してキャッシュしている場合、1 時間おき、または少なくとも現在のセットが失効する 15 分前までに、更新された認証情報セットを取得する必要があります。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html#roles-usingrole-ec2instance-roles
アクセスキーをアプリケーション側で利用する場合はこういったローテーションについても意識する必要があるということが勉強になった一件でした。
Fargateや、その他のAWSリソースからWorkload Identity 認証を行いたい方の参考になれば幸いです。
参考リンク