こんにちは!バックエンドエンジニアの谷口(@ravineport)です。
Google Cloud上のアプリケーションからAWS内のサービスにアクセスする必要があり、AWSのAssumeRoleという方法を教えていただいたのでそのご紹介です。今回は例としてS3のオブジェクトにアクセスする方法をご紹介しますが、その他にもGoogle CloudにはないソリューションがAWSにあったり、データ連携等でクラウドサービスをまたぐとき等にも活用できるかと思います。
方法はいくつかありますが、今回はAWS Security Token Service (AWS STS)のAssumeRoleという機能をOpenID Connect (OIDC)で使います。より具体的にはAssumeRoleWithWebIdentity
というオペレーションを使います。IDトークンを使ってAssumeRoleを行うオペレーションです(参考:ロールを引き受けるための各種方法 - AWS Identity and Access Management)。
他にもGoogle CloudにAWSのIAM Userのアクセスキーを持たせる方法もありますが、AssumeRoleであればGoogle Cloud側にAWSのクレデンシャル情報を持たずに一時的な認証情報を使うだけで済むため、より安全に実現できます。
今回はCloud Run Jobs内で動くRustアプリケーションからAssumeRoleを行ってみます。
では実際にやってみたいと思います。
Google CloudのService Accoutの作成
まずはService Accountを作成します。
作成したものが以下です(procyon-cloud-run という名前に深い意味はありません
)。
ここでコンソール上のOAuth2 Client IDをメモしておきます。
AWSのIAM Roleの用意
次にAWSでIAM Roleを作成します。
Trusted entity typeでWeb identityを選択し、Identity providerをGoogleにします。AudienceにはService AccountでメモしたOAuth2 Cliend IDの値を入力します。
次にS3の権限を適切に設定しIAM Roleを作成します。
作成したIAM Roleの詳細画面を見ると、以下のような設定になっていることが確認できると思います。
これで、Google CloudのService AccountからAWSのIAM RoleにAssumeRoleするための準備ができました。
次にCloud Run Jobsで動かすRustアプリケーションの実装をします。
Cloud Run Jobs上で動かすRustアプリケーションの実装
使用するクレート
- google-cloud-auth
- aws-sdk-config
- aws-sdk-s3
- S3にアクセスするため
- anyhow
- エラー処理を簡単にするため
- dotenvy
- 環境変数を簡単に扱えるようにするため
実装
Google CloudからIDトークンを取得、それを使ってAWSのIAM RoleにAssumeRoleして、S3の指定のオブジェクトを取得するコードです。
※ 現時点のIAM Roleの設定とコードではローカルでは動かず、Cloud Run Jobs上でのみ動作するのでご注意ください。
use std::path::PathBuf; use anyhow::Result; use aws_config::provider_config::ProviderConfig; use aws_config::web_identity_token::{StaticConfiguration, WebIdentityTokenCredentialsProvider}; use aws_config::{BehaviorVersion, Region}; use google_cloud_auth::idtoken::{create_id_token_source, IdTokenSourceConfig}; #[tokio::main] async fn main() -> Result<()> { let token = create_id_token_source(IdTokenSourceConfig::default(), "https://sts.amazonaws.com/") .await? .token() .await?; let aws_region = Region::from_static("ap-northeast-1"); // AWS SDK で AssumeRole するには ID トークンをファイル経由で渡す必要があるのでファイルに書き込む let path_buf = PathBuf::from("token.jwt"); std::fs::write(path_buf.clone(), token.clone().access_token)?; let provider = WebIdentityTokenCredentialsProvider::builder() .configure(&ProviderConfig::empty().with_region(Some(aws_region.clone()))) .static_configuration(StaticConfiguration { web_identity_token_file: path_buf.clone(), role_arn: "arn:aws:iam::[AWS Account Id]:role/[作成したIAM Role名]".to_string(), session_name: "test-session".to_string(), }) .build(); let sdk_config = aws_config::defaults(BehaviorVersion::latest()) .credentials_provider(provider) .region(aws_region) .load() .await; let s3_client = aws_sdk_s3::Client::new(&sdk_config); let s3_result = s3_client .get_object() .set_bucket(Some("bucket name".to_string())) .set_key(Some("key name".to_string())) .send() .await?; println!("{:#?}", s3_result); Ok(()) }
create_id_token_source
関数でGoogle CloudのIDトークンを取得しています。特別なことをせずにCloud Run Jobs上で動かした場合はGoogle Cloudのメタサーバーから情報を取得します。
AWSのRustの公式SDKは各サービスごとに提供されていますが、基本的にどれもクライアントのインスタンスを作る際にSdkConfig
を渡しています。SdkConfig
のクレデンシャルのプロバイダーとしてWebIdentityTokenCredentialsProvider
を使い、必要な情報を渡しています。
WebIdentityTokenCredentialsProvider
にIDトークンを渡すためにはファイルを経由する必要があるそうなので、Google CloudのIDトークンをファイルに書き込み、それを渡すようにしています。
(ファイルを経由せずに渡す方法をご存知の方がいらっしゃればぜひご教示ください…!)
うまく動けば
GetObjectOutput { body: ByteStream { inner: Inner { body: SdkBody { inner: BoxBody, retryable: false, }, }, }, … }
のような出力がされるはずです。
IAM Roleに適切な権限が設定されていない場合は以下のようなエラーが返ってきます。
以下のエラー例は上記のコードを書き換えて、許可していない ListAllMyBuckets を実行しようとしたときの例です。
Caused by: 0: unhandled error (AccessDenied) 1: Error { code: "AccessDenied", message: "User: arn:aws:iam::[AWS Account Id]:role/[作成したIAM Role名]/test-session is not authorized to perform: s3:ListAllMyBuckets because no identity-based policy allows the s3:ListAllMyBuckets action", aws_request_id: "xxx", s3_extended_request_id: "yyy" }
ローカルで動くようにする
Cloud Run Jobs上でしか動かないと動作確認等で不便なので、ローカルでもAssumeRoleできるようにしてみます。
ローカルのセットアップとコードの修正
まずはGoogle Cloudで作成したService AccountのクレデンシャルJSONをローカルにダウンロードしておきます。
次に .env という名前のファイルを作成して以下の内容を書いておきます。
GOOGLE_APPLICATION_CREDENTIALS="path/to/credential-json-file"
この環境変数をRustのコードから読み取れるように以下のように修正します。
# ... 省略 #[tokio::main] async fn main() -> Result<()> { let _ = dotenvy::dotenv(); # ... 省略 }
create_id_token_source
関数 を呼び出す前に let _ = dotenvy::dotenv();
という行を追加しました。これで .env ファイルの環境変数を読み取ることができます。
create_id_token_source
関数はGOOGLE_APPLICATION_CREDENTIALS
という名前の環境変数がある場合は、それが指すクレデンシャルのファイルを使って認証をしてくれます。
ローカルのセットアップとコードの修正はこれで問題ないのですが、ローカルで実行すると以下のようなエラーが出るかと思います。
Error { code: "AccessDenied", message: "Not authorized to perform sts:AssumeRoleWithWebIdentity", aws_request_id: "xxx" }
AWS IAM Roleの修正
IAM RoleのTrusted entitiesを以下のように修正します。
OAuth2 Cliend ID(13行目)に加えて、Service Account名を追加しています。accounts.google.com:aud
に対して配列で指定するようになっていることにご注意ください。
以上でローカルのService AccountのクレデンシャルJSONを使ってAssumeRoleができるようになります。
おまけ:Google Cloudでの認証方法ごとのIDトークン情報の違い
Cloud Run JobsとローカルどちらでもAssumeRoleができるようになりましたが、折角なのでなぜローカルで動かなかったかをちょっとだけ深堀りしてみます。 IDトークンはJWTでやりとりされますが、その中身を見てみます。
注目すべき箇所はazpクレームです。メタサーバー経由のIDトークンではOAuth2 Cliend IDの値が入っていますがクレデンシャルJSON経由の方ではService Account名が入っています。
AWSのドキュメントには以下の記載があります。
IAM で OpenID Connect (OIDC) ID プロバイダーを作成する - AWS Identity and Access Management
注記 IdP JWT トークンに azp クレームが含まれている場合は、Audience 値としてこの値を入力します。 OIDC ID プロバイダーがトークンに aud と azp の両方のクレームを設定している場合、AWS STS は azp クレーム内の値を aud クレームとして使用します。
はじめに設定したIAM RoleではOAuth2 Cliend IDのみを指定していたために、ローカルからはAssumeRoleできませんでした。ということで、IAM RoleのTrusted entitiesにService Account名を追加してあげればローカルからでもAssumeRoleができるようになりました。
まとめ
Google Cloud 上のRustで書かれたアプリケーションからAWSのIAM RoleにAssumeRoleする方法をご紹介しました。Google Cloud側にAWSのクレデンシャル情報を一切持つ必要がないので、セキュリティ面でも安心できるのがとてもうれしいです。
私はCloud Run Jobsで動かす前にローカルで動作確認をしようとしてうまくAssumeRoleができず四苦八苦したので、同様のことをしようとしている方の一助になれば幸いです。
エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。ご興味のある方はぜひ採用ページからご応募をお願いいたします。 hrmos.co hrmos.co