IAM Roles for Service Accountsの認証情報をAWS SDKで利用する方法

## はじめに

最近EKSでは「IAM Roles for Service Accounts」が正式にサポートされ、EKSクラスターでこの設定をONにしてPodベースでのIAM roleの割り当てが可能になりました。
kube2iamなどを使ったノード(EC2)ベースのロール割り当ての場合は、AWS SDKが良しなに認証情報を読み込んでくれますが、2019/11/01現在のAWS SDK for Java(ver2)ではPodに割り当てられた認証情報をそのままのコードで読み取ることができませんでした。
その時の対応を含めて、AWS SDKでの認証情報の使用方法をまとめます。

## IAM Roles for Service Accountsとは

その名の通り、KubernetesリソースであるService AccountにIAM Roleを割り当てる仕組みです。
Kubernetes 1.3以降を実行するEKSクラスターで有効にすることができます。

[AWSのブログ記事](https://aws.amazon.com/jp/blogs/news/introducing-fine-grained-iam-roles-service-accounts/)がとても分かりやすいので、そちらを参照していただければと思います。

この機能を有効化したEKSクラスターはOpenID Connectの認可サーバーのエンドポイントが発行され、Pod及びAWS SDKはこの認可サーバーにトークンを検証してもらいIAM Roleの割り当てを受けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262177/60003dc2-ece1-9ed0-4c4f-0aa3354c5c5f.png)

### Podに割り当てられる情報

(具体的な記述方法については省略しますが、)アノテーションを利用してIAM Roles for Service Accountsを使用したPodには、以下の情報が割り当てられます。
`kubectl describe pods` 等で確認できると思います。

* 環境変数 `AWS_ROLE_ARN`
    * `arn:aws:iam::000000000000:role/xxxxxxxxx-role`
* 環境変数 `AWS_WEB_IDENTITY_TOKEN_FILE`
    * `/var/run/secrets/eks.amazonaws.com/serviceaccount/token`
* OIDC用のサービスアカウントファイルのボリュームマウント
    * `/var/run/secrets/eks.amazonaws.com/serviceaccount`

これらの情報を使いEKSクラスターのOIDCエンドポイントからroleの認証情報の引き受けをAWS SDKは行う必要があります。

## AWS SDK for Java(ver2)

上述したOIDCエンドポイントから認証情報を取得する実装は、古いAWS SDKではサポートされておらず、対応しているバージョンまで引き上げる必要があります。
公式のドキュメントを参照すると**AWS SDK for Java(ver2)**は**2.7.36**以上が必要であるようなのでバージョンを上げます。

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/iam-roles-for-service-accounts-minimum-sdk.html

```diff:build.gradle.kts
- val awsSdkVersion = "2.7.23"
+ val awsSdkVersion = "2.10.4"

dependencies {
    implementation(platform("software.amazon.awssdk:bom:$awsSdkVersion"))
    implementation("software.amazon.awssdk:s3")
}
```

WebIdentityTokenFileCredentialsProviderというクラスがIAM Roles for Service Accountでの認証方式のProviderのようです。
環境変数 `AWS_ROLE_ARN` や `AWS_WEB_IDENTITY_TOKEN_FILE` 、またそれに対応するJavaのシステムプロパティを読んで処理をしているみたいです。

https://github.com/aws/aws-sdk-java-v2/blob/master/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/WebIdentityTokenFileCredentialsProvider.java

ドキュメントを参照する限り対応はこれだけで良いのかと思ったのですが、以下の二点の対応をしないと動作しませんでした。

### Security Token Serviceモジュールの追加

AWS SDKがIAM Roleを引き受けるときに、Security Token Serviceの実装が必要になります。

```diff:build.gradle.kts
val awsSdkVersion = "2.10.4"

dependencies {
    implementation(platform("software.amazon.awssdk:bom:$awsSdkVersion"))
    implementation("software.amazon.awssdk:s3")
+   implementation("software.amazon.awssdk:sts")
}
```

実装を見てみるとわかりますが、 `Class.forName()` しているので、モジュールが不足していてもコンパイルエラーにはならず実行時エラーになる点に注意が必要です。
https://github.com/aws/aws-sdk-java-v2/blob/f3265c681e68dbbadca9d8794617a067dda9959d/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/WebIdentityCredentialsUtils.java#L43-L55

### credentialsProviderの明示的な指定

[Issue](https://github.com/aws/aws-sdk-java-v2/issues/1470)にも挙がっていますが、2019/11/01現在の最新バージョン( `2.10.4` )でも、デフォルトの認証情報プロバイダチェーンに上述の**WebIdentityTokenFileCredentialsProvider**は含まれておらず、Service AccountのIAM roleを引き受けるには明示的にcredentialsProviderを指定する必要があります。
(Issueをぱっと見た感じAWS SDK for Java(ver2)以外でもこの問題はあるようです)

```diff:modules.kt
single {
    S3AsyncClient.builder()
+       .credentialsProvider(WebIdentityTokenFileCredentialsProvider.create())
        .endpointOverride(URI(s3Config.endpoint))
        .region(Region.of(s3Config.region))
        .build()
}
```

#### ローカル環境

credentialsProviderを明示的に指定した場合デフォルトの認証情報取得の処理は行われないため、ローカル環境での処理が動かなくなってしまいました。
仕方がないのでとりあえず以下の形で対応しました。
時間があれば[この辺り](https://github.com/aws/aws-sdk-java-v2/blob/master/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java)を読んでPRでも出せたら良いな、と思っています。

```kotlin:modules.kt
single {
    if (!System.getenv("AWS_ROLE_ARN").isNullOrEmpty()) {
        S3AsyncClient.builder()
            .credentialsProvider(WebIdentityTokenFileCredentialsProvider.create())
            .endpointOverride(URI(s3Config.endpoint))
            .region(Region.of(s3Config.region))
            .build()
    } else {
        S3AsyncClient.builder()
            .endpointOverride(URI(s3Config.endpoint))
            .region(Region.of(s3Config.region))
            .build()
    }
}
```

## まとめ

* AWS SDK for Java(ver2)では2019/11/01現在最新バージョンに上げるだけではIAM Roles for Service Accountsの認証情報を引き受けることができない
    * stsの依存の追加と、credentialsProviderの明示的な指定が必要
    * 他のSDKでも同様の実装になっているかもしれない(未確認)
* デフォルトの認証情報プロバイダチェーンへの追加は、Issueにもあるので今後対応されると思われる

## 参考

* [Amazon EKS が Kubernetes サービスアカウントに IAM アクセス許可を割り当てるサポートを追加](https://aws.amazon.com/jp/about-aws/whats-new/2019/09/amazon-eks-adds-support-to-assign-iam-permissions-to-kubernetes-service-accounts/)
* [Kubernetes サービスアカウントに対するきめ細やかな IAM ロール割り当ての紹介 | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/introducing-fine-grained-iam-roles-service-accounts/)
* [サポートされる AWS SDK の使用 - Amazon EKS](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/iam-roles-for-service-accounts-minimum-sdk.html)
* [AWS 認証情報の使用 - AWS SDK for Java](
https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/credentials.html)