Cloud Functions で AWS STS 接続するときにつまづいたことまとめ

# Cloud Functions で AWS STS 接続するときにつまづいたことまとめ

## はじめに
Cloud Functions で、AWS SDK を使って AWS STS 接続をする関数を開発していたのですが、解決するのにかなり時間を使ったため、つまづいたこととその解決策をまとめます。
初歩的な内容もあると思いますが、誰かの参考になればと思っています。

## 環境
 - Cloud Functions 第一世代
 - AWS SDK for Java 2.21

## 経緯
Cloud Functions の Cloud Storage トリガーを使って、Cloud Storage のファイルを Amazon S3 へ転送する処理を作成することが目的でした。
初めは AWS のアクセスキーを使って接続していたのですが、アクセスキーの漏洩リスクやローテーション管理コストなどを考えて、AWS STS での接続に変更しました。
大まかな流れは AWS の公式ブログを参考にしています。

https://aws.amazon.com/jp/blogs/security/access-aws-using-a-google-cloud-platform-native-workload-identity/

## つまづいたこと
### ① AWS IAM Role の設定
#### 事象
IAM Role を作るところまでは、以下ブログを参考にして実施できました。

https://qiita.com/hyj624117615/items/ca4a8bc8c269098aaeef

つまづいたことは信頼ポリシーに設定する3項目です。AWS の公式ブログで以下の記述があります。

> accounts.google.com:oaud条件キーは、Google ID トークンのaud (AUDIENCE) フィールドと一致します。
accounts.google.com:aud条件キーは、Google ID トークンのazp (AUTHORIZED_PARTY) フィールドと一致します。
accounts.google.com:sub条件キーは、Google ID トークンのsub (SUBJECT) フィールドと一致します。

Cloud Functions から AWS への接続でエラーが発生したときに、原因の切り分けとしてこれらの項目の入力値が正しいか・過不足がないかを確認したのですが、「そもそもこの値は Google 側のどこで確認できるのか？」がわからず、その調査に時間がかかってしまいました。
ちなみに参考ブログ通りに IAM Role を作成すると、 `accounts.google.com:aud` には作成時に指定した `Audience` の値が入っています。

#### 解決策
今回の場合、ポリシーの内容は IAM Role 作成で自動作成されるポリシーそのままで問題ありませんでした。

3つの項目の値は、後述で取得するトークンの文字列を https://jwt.io/ に入力すると `PAYLOAD:DATA` 欄に表示されたので、ポリシーの設定内容と相違ないかを確認できました。

### ②認証情報の設定
#### 事象
AWS SDK を使う際に認証が必要になります。
AWS の公式ブログでは Compute Engine 環境で構築されており、AWS STS 接続をする前に認証情報を取得していますが、サーバーレス環境でどうすればよいかがわかりませんでした。

#### 解決策
Cloud Functions の環境変数に AWS SDK の認証に必要な環境変数を指定し、ソースコードでは `WebIdentityTokenFileCredentialsProvider` クラスで認証することで解決できました。

Cloud Functions に設定する環境変数は3つです。
 - `AWS_ROLE_SESSION_NAME`
   - IAM ロールへの接続元を特定する文字列を指定、識別するためなので任意の値を入力（わたしは `CloudFunctions` と入力）
 - `AWS_ROLE_ARN`
   - ①で作成した IAM Role の ARN を指定、形式は `arn:aws:iam::<アカウントID>:role/＜IAM Role 名＞`
 - `AWS_WEB_IDENTITY_TOKEN_FILE`
     - 取得したトークンを書くファイルパスを指定

一番悩んだのは `AWS_WEB_IDENTITY_TOKEN_FILE` です。このファイルは SDK ライブラリ取得時にファイルのコンテンツ込みで自動で作成されるものかとはじめ勘違いしていたのですが、SDK 側が参照する場所であるだけでコンテンツは手動で設定する必要がありました。
また Cloud Functions の関数処理からはソースコードが存在する `/workspace` ディレクトリには書き込みはできないですが、その他のディレクトリには書き込めるとのことです。
なので任意のパス、今回は `tmp/awscreds` を指定しました。

そして、Cloud Functions のソースコードにて、AWS STS 接続をする前にトークンを取得し `AWS_WEB_IDENTITY_TOKEN_FILE` で指定したファイルに書き込みます。
ソースコード全体は以下にあります。

https://github.com/Yuka-Sawaki/cloud-run-functions-gcs-to-s3

下記はトークン取得のソースコードです。


``` トークンの取得.java
	try {
		GoogleCredentials googleCredentials = GoogleCredentials.getApplicationDefault();
	
		IdTokenCredentials idTokenCredentials = IdTokenCredentials.newBuilder()
				.setIdTokenProvider((IdTokenProvider) googleCredentials)
				.setTargetAudience(TARGET_AUDIENCE)
				.setOptions(Arrays.asList(IdTokenProvider.Option.FORMAT_FULL, IdTokenProvider.Option.LICENSES_TRUE))
				.build();
		token = idTokenCredentials.refreshAccessToken().getTokenValue();
		logger.info("Token: " + token);
	} catch (Exception e) {
		logger.info(e.getMessage());
		System.exit(1);
	}

```

コード内の `TARGET_AUDIENCE` は URL を指定するのですが、多分何を指定してもよいと思われます。今回は `https://sts.amazonaws.com/` を指定しました。
また、①の jwt.io で解析する際に使う文字列は、コード内の `token` です。

トークンを `AWS_WEB_IDENTITY_TOKEN_FILE` で指定したファイルに書き込み後、`WebIdentityTokenFileCredentialsProvider` を使って StsClient を作成、STS から取得した認証情報を使って S3Client を作成します。

``` Client 作成.java
	StsClient stsClient = StsClient.builder()
			.region(Region.AWS_GLOBAL)
			.credentialsProvider(WebIdentityTokenFileCredentialsProvider.create())
			.build();
	AssumeRoleWithWebIdentityRequest roleRequest = AssumeRoleWithWebIdentityRequest.builder()
			.webIdentityToken(token)
			.roleArn(System.getenv("AWS_ROLE_ARN"))
			.roleSessionName(System.getenv("AWS_ROLE_SESSION_NAME"))
			.build();
	StsAssumeRoleWithWebIdentityCredentialsProvider provider = StsAssumeRoleWithWebIdentityCredentialsProvider.builder()
			.stsClient(stsClient)
			.refreshRequest(roleRequest)
			.build();
	S3Client client = S3Client.builder()
			.credentialsProvider(provider)
			.region(Region.US_EAST_1)
			.build();
```

これで AWS STS を使った接続ができました:tada:
その後は S3Client を使ってやりたいこと（今回は S3 へのファイル書き込み）を実装すれば完了です。

今回は Cloud Functions 第一世代で開発しましたが、将来的には第二世代にアップデートする予定です。
アップデートによって設定方法などが変わればまた記事を書きたいと思います。

## 参考
たくさんの記事を参考させていただきました。ありがとうございます。

https://cloud.google.com/docs/authentication/get-id-token?hl=ja#java

https://cloud.google.com/functions/docs/configuring/env-var?hl=ja

https://aws.amazon.com/jp/blogs/security/access-aws-using-a-google-cloud-platform-native-workload-identity/

https://docs.aws.amazon.com/ja_jp/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html

https://qiita.com/hyj624117615/items/ca4a8bc8c269098aaeef

https://qiita.com/munaita_/items/c44b679b9dcf53d7624d

https://qiita.com/oil-mikan/items/32ee181117340cda9fe8

https://qiita.com/toro_ponz/items/15723cd72a260e401a48

https://zenn.dev/yutaro1985/articles/b012f69b49bec095b9f1

https://book.st-hakky.com/hakky/access-aws-from-gcp/