🥨

【AWS ECS】Workload Identity連携でGCPへサービスアカウントなしでアクセスする

2025/01/28に公開

Workload Identityとは

簡単にいうと、AWSの認証情報を使ってGCPのリソースにアクセスする機能です。 メリット としては、サービスアカウントの秘密鍵を不要にできるため、プライベートキーの管理がいらなくなります。

詳しくは、以下の記事が参考になるので参照してみてください。

https://zenn.dev/ohsawa0515/articles/gcp-workload-identity-federation#workload-identity連携とは

ECSからGCPのリソースにアクセスする

GCP側の設定は先ほどの記事を参考にしてください。

ここでは AWS の ECS 側の設定方法 について解説します。

元記事にも ECS のサンプルコードが掲載されていますが、そこで紹介されている方法に問題があったので、別の方法を提案します。

元記事では自前で認証情報をリクエストし、環境変数に代入していました。しかし、このやり方だと2点の問題があります。

# 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']
  1. 自前でアクセスキーをローテーションする必要がある

    アクセスキーの有効期限(約 6 時間程度)が切れると更新が必要になるため、認証ロジックを自前で記述しなければなりません。

  2. 他の AWS リソースを利用する際に、意図せぬ認証トラブルが起こる

    たとえば、Node.js の aws-sdk は次の優先順位で認証情報を取得します。ソース

    • 環境変数(process.env
    • SSO のトークンキャッシュ
    • Web Identity Token
    • Shared credentials / config ファイル
    • EC2/ECS メタデータサービス

    そのため、他の AWS サービスを利用するときに、意図せず環境変数上の認証情報が使われてしまい、不具合を引き起こす可能性があります。

    実際、GCP の API を呼び出してから S3 にアップロードするようなケースで、認証情報が期限切れを起こすと S3 にアクセスできなくなる問題が発生しました。

解決策

以下では、元記事の方法とは異なり、@aws-sdk/credential-provider-nodeが提供する自動的なクレデンシャルローテーションを利用するアプローチを紹介します。

下記のGithubのサンプルコードを参考にアレンジしてます。

GitHub - googleapis/google-auth-library-nodejs: 🔑 Google Auth Library for Node.js

import {
  AwsClient,
  AwsSecurityCredentials,
  AwsSecurityCredentialsSupplier,
  ExternalAccountSupplierContext,
  auth,
} from 'google-auth-library';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
import { JSONClient } from 'google-auth-library/build/src/auth/googleauth';

class AwsSupplier implements AwsSecurityCredentialsSupplier {
  private readonly region: string;

  constructor(region: string) {
    this.region = region;
  }

  async getAwsRegion(_context: ExternalAccountSupplierContext): Promise<string> {
    return this.region;
  }

  async getAwsSecurityCredentials(
    _context: ExternalAccountSupplierContext,
  ): Promise<AwsSecurityCredentials> {
    // Retrieve the AWS credentials.
    const awsCredentialsProvider = defaultProvider();
    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;
  }
}

// 認証クライアントの設定を環境に応じて切り替え
let authClient: JSONClient;
if ((process.env.NODE_ENV || '') === 'production') {
  const configPath = process.env.CREDENTIALS_PATH || '';
  const config = JSON.parse(readFileSync(configPath, 'utf-8'));

  const clientOptions = {
    audience: config.audience,
    subject_token_type: config.subject_token_type,
    aws_security_credentials_supplier: new AwsSupplier(process.env.AWS_REGION || 'ap-northeast-1'),
    service_account_impersonation_url: config.service_account_impersonation_url,
  };

  authClient = new AwsClient(clientOptions);
} else {
  // ローカル環境用の認証設定
  const localServiceAccountPath = process.env.CREDENTIALS_PATH || '';
  const credentials = JSON.parse(readFileSync(localServiceAccountPath, 'utf-8'));
  authClient = auth.fromJSON(credentials);
}

const storage = new Storage({ authClient })

上記のコードのポイントは以下のとおりです。

  • AWSSupplierクラス
    AWSの認証情報をGCPのへ渡すためのクラスです。
    @aws-sdk/credential-provider-nodedefaultProvider() を用いて、AWS クレデンシャルを取得します。これにより ローテーション も自動管理されます。

  • ローカル環境と本番環境で認証方法を切り替え

    NODE_ENVCREDENTIALS_PATH などの環境変数を利用し、本番環境では Workload Identity(外部アカウント)用の設定を、ローカル開発環境ではサービスアカウントの JSON ファイルを直接読み込みます。

  • let authClient: JSONClient; と型定義することで同じ変数に代入可能

    本番向けとローカル向けの両方の認証方法を、同じ authClient に代入して扱えます。

credential.jsonは以下のような感じ。 "credential_source"は使用しないので、削除しています。ローカルでは、プライベートキーありのサービスアカウントの JSONファイルを配置して、柔軟に対応しています。

credential.json の例

以下はWorkload Identity連携用のクレデンシャルJSONファイルの例です。"credential_source" は ECS を利用する場合は不要のため削除しています。

ローカルでは、プライベートキーありのサービスアカウントの JSONファイルを配置して、柔軟に対応しています。

{
  "universe_domain": "googleapis.com",
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/xxxxxx",
  "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/xxxxxx",
  "token_url": "https://sts.googleapis.com/v1/token"
}

これで、AWS の ECS から GCP のリソースにアクセスする際に、サービスアカウントの秘密鍵を管理する必要がなく、かつAWS のクレデンシャルが期限切れを起こす問題にも対応可能になります。ぜひ試してみてください。

採用情報

架電特化のAI電話SaaSを展開しているnocall.ai株式会社では、エンジニアを積極採用中です!
ジュニア層のエンジニアからリーダー職まで幅広く募集しています。

https://passionategeniushq.notion.site/cec8d81619524d218643e34d5e7431c6?pvs=4

https://passionategenius.com/

Discussion