Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

ECS/Fargateからサービスアカウントキーを使わずにGoogle CloudのAPIにアクセスする方法

はじめに

こんにちは!SREの三橋です。
今回はECS/Fargateからサービスアカウントキーを使わずにGoogle CloudのAPIにアクセスする方法をご紹介いたします。

ECS/FargateなどAWSリソースからGoogle Cloud上のAPIへアクセスする際にはサービスアカウントキーを利用することが最も簡単です。しかし、サービスアカウントキーを正しく管理できていない場合、そのサービスアカウントの権限を利用して悪意のある操作を実行される恐れがあります。
YappliのバックエンドでもECS/FargateからGoogle Cloud上のAPIへアクセスすることがあり悩みの種の一つになっていましたが、今回ご紹介するWorkload Identity連携を使うことでこの問題を解決することができました。

想定する読者

  • AWSからGoogle Cloud APIにアクセスする際にサービスアカウントキーを使用している方
  • 認証情報の安全な取り扱い方に興味がある方
  • Workload Identity連携に興味がある方
  • ECS/FargateでWorkload Identity連携を利用する方法について興味がある方
  • Yappliの裏側に興味がある方

結論

はじめに結論を書いておくと以下のようになります。

  • Workload Identityを使うことでサービスアカウントキーの管理が不要となり、一時的な認証情報でGoogle CloudのAPIを叩くことができる
  • Workload Identityまたはサービスアカウントの設定で認証元に制限をかけることができる
  • Workload Identityが発行するクライアントライブラリ設定ファイルはEC2インスタンス前提であるが、認証情報を環境変数に入れ込むことでECS/Fargateでも動作する

Workload Identity連携とは

Workload Identity連携とはGoogle Cloudが提供する仕組みで、キーなしの新しいアプリケーション認証メカニズムであると記載されています。この仕組みを利用することで外部IDプロバイダ(IdP)と連携してサービスアカウントを使用せずにGoogle Cloudリソースを呼び出すことができます。

cloud.google.com

ECS/Fargateと連携した場合の認証処理の流れは以下のようになります。

  1. AWSリソース(ECS/Fargate)に付与されたロールの一時認証情報を利用しGoogle Cloud Security Token Service(STS)にアクセスする
  2. Google Cloud側でAWSの認証情報を確認しSTSトークンを払い出す
  3. 払い出されたSTSトークンを利用してサービスアカウントにアクセスする
  4. Google Cloud側でSTSトークンを確認しサービスアカウントのアクセストークンを払い出す
  5. 払い出されたアクセストークンを利用し、サービスアカウントとしてGoole Cloudの各種APIを利用することが可能になる

Workload Identity連携の事前設定

Workload Identity連携を利用するには事前にIdPをWorkload Identityプールに接続し、サービスアカウントにアクセス権限を付与する必要があります。

設定概要は以下の通りです。

  1. AWSリソースから利用するサービスアカウントを発行し、サービスアカウントに対しAPI実行に必要な権限を付与する
  2. Workload Identityプールとプロバイダを構成し、信頼するAWSアカウントIDを指定する
  3. Workload Identityプールにサービスアカウントへのアクセス権限を付与し、クライアントライブラリの設定ファイルをダウンロードする

具体的な設定は方法は公式サイト(https://cloud.google.com/iam/docs/configuring-workload-identity-federation#aws)をご参照ください。

以下では設定の際の悩みどころを記載します。

サービスアカウント/Workload Identityをどのプロジェクトに作成すべきか

まずサービスアカウントの作成するプロジェクトについてですが、こちらは公式サイト(https://cloud.google.com/iam/docs/service-accounts?hl=ja#locations)に管理方法の記載があります。
手法は以下の2つのいずれかになります。

  • APIを利用するプロジェクトと同じプロジェクトに作成する
    • メリット: 導入が簡単
    • デメリット: 多数プロジェクトにまたがるサービスアカウントを追跡することが困難
  • APIを利用するプロジェクトとは別の場所で作成する
    • メリット: サービスアカウントの作成場所をまとめることで管理が楽になる
    • デメリット: アクセス権の付与が煩雑になる

Workload Identityを作成するプロジェクトについてはは公式サイトでドキュメントを見つけられませんでしたが、サービスアカウントと同様の考え方になると思います。

作業自体は煩雑になりますが、単一プロジェクトに閉じることが約束されていない限りは今後の拡張性やセキュリティガバナンスの観点から共通的なプロジェクトでこれらのリソースを管理するのがよいと考えます。

認証元の制限はどのように行うべきか

Workload Identityプールとプロバイダを何も考えず属性条件なしで構成すると、認証元のアカウントの制限はAWSアカウントIDのみで行われることになります。信頼されたAWSアカウントID内で発行された認証情報を利用すると誰でもGoogle Cloud APIにアクセスできてしまう(=混乱した代理問題(Confused Deputy Problem)が発生する)ため、もう少し認証元に制限を加えたいところです。

認証元の制限は以下の2箇所で設定することができます。

  1. Workload Identityプールで認証ができるIDを制限する (Workload Identityプールの手前で制限をかける)
  2. サービスアカウントを利用できるWorkload Identityプール内のIDを制限する (Service Accountの手前で制限をかける)

柔軟性とセキュリティガバナンスのトレードオフになるのでどちらで制限をかけるのがよいかは一概には言えないですが、少なくともどちらかには設定すべきであると考えます。それぞれの設定箇所ではロールレベルやインスタンスレベルでのアクセス制限をかけることが可能です。

参考:

tips
サービスアカウントで制限を行う場合は「Workload Identity IDプールの詳細画面」の上部にある「アクセスを許可」ボタンから設定すると楽です。

IAMロールレベルで制限をかける場合は以下のように属性値を設定します。

arn:aws:sts::${AWS_ACCOUNT_ID}:assumed-role/${ECS_TASK_ROLE}

ECS/FargateでのWorkload Identityの利用

ECS/FargateでWorkload Identityを利用する際の全体構成と処理の流れは以下のようになります。

  1. プログラム内からS3バケットに保存されたクライアントライブラリの設定ファイルを取得する (※)
  2. クライアントライブラリの設定ファイルを使用してWorkload Identity経由でサービスアカウントトークンを取得する
  3. 取得したサービスアカウントトークンを使用してGoogle CloudのAPIにアクセスする

※ Workload Identity連携の事前設定で払い出されたクライアントライブラリ設定ファイルはS3に保存しています。

ECS/Fargateでクライアントライブラリ設定ファイル利用する際の注意点

ECS/Fargateでクライアントライブラリ設定ファイルを使用する際は、少し注意が必要であるためその点について補足します。
Workload Identity連携の事前設定で払い出された設定ファイルを利用することでAWS、GoogleCloud間で認証プロセスが動くようになっているのですが、2022年12月現在この設定ファイルの region_url, urlの値にはインスタンスエンドポイントのURL http://169.254.169.254/latest/meta-data が記述されており認証元がEC2インスタンス前提となっています。

  "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"
  }

ECSタスクの認証情報エンドポイントは https://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI であり、環境変数の値がエンドポイントに含まれているため単純に設定ファイルの値を書き換えるだけでは動作しません。

参考: https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-iam-roles.html

ECSタスクで動作するさせるには、以下のように認証情報エンドポイントから取得した値を環境変数に入れてあげる必要があります。
↓ Goで実装する場合の例

func setCredentialECS(ctx context.Context) error {
    res, err := http.Get("http://169.254.170.2" + os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"))
    if err != nil {
        return errors.WithStack(err)
    }
    defer res.Body.Close()
    b, err := io.ReadAll(res.Body)
    if err != nil {
        return errors.WithStack(err)
    }

    awsRes := new(awsResponseBody)
    err = json.Unmarshal(b, awsRes)
    if err != nil {
        return errors.WithStack(err)
    }

    os.Setenv("AWS_ACCESS_KEY_ID", awsRes.AccessKeyID)
    os.Setenv("AWS_SECRET_ACCESS_KEY", awsRes.SecretAccessKey)
    os.Setenv("AWS_SESSION_TOKEN", awsRes.Token)

    return nil
}

これはGoogle Cloudクライアントライブラリの認証プロセスの実装で環境変数に認証情報が設定されている場合、認証情報エンドポイントへのアクセスをスキップするようになっているからです。

func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result awsSecurityCredentials, err error) {
    if canRetrieveSecurityCredentialFromEnvironment() {
        return awsSecurityCredentials{
            AccessKeyID:     getenv(awsAccessKeyId),
            SecretAccessKey: getenv(awsSecretAccessKey),
            SecurityToken:   getenv(awsSessionToken),
        }, nil
    }

    roleName, err := cs.getMetadataRoleName(headers)
    if err != nil {
        return
    }

引用: https://cs.opensource.google/go/x/oauth2/+/master:google/internal/externalaccount/aws.go;drc=b177c21ac9b48a8e3b2a6824b49de2397bd9e721;l=503

他の言語でも同じようにしてECSタスクからWorkload Identityを利用することができるようです。

zenn.dev blog.studysapuri.jp

あとは環境変数 GOOGLE_APPLICATION_CREDENTIALS にローカルにダウンロードしたクライアントライブラリ設定ファイルのパスを指定してあげればクライアント生成時に自動的に認証処理を行なってくれます。
参考: https://cloud.google.com/docs/authentication/application-default-credentials

APIを直接呼び出す場合はDefaultClientを利用することで、各種エンドポイントにリクエストを投げることができます。
参考: https://pkg.go.dev/golang.org/x/oauth2/google#DefaultClient

まとめ

Workload Identity連携は現在EC2インスタンス前提でクライアントライブラリ設定ファイルを払い出しますが、認証情報を環境変数に設定することでECS/Fargateでもその仕組みを利用することができました。 Workload Identityを利用することでサービスアカウントキーの管理が不要となり、よりセキュアな環境を実現することができました。
この記事がAWSからGoogle Cloudへのアクセス方法に悩んでいる方の一助となれますと幸いです。