Rust + rusoto でAWSのサービスを操作する
はじめに
Rust言語はいまのところ公式のAWS SDKが提供されていません。が、 Rusoto というライブラリを利用して各種サービスのAPIを実行できます。 今回はRusotoを使ってS3のAPIを実行してみます。
前提条件
- macOS: 10.13.6
- Rust: rustc 1.29.1 (b801ae664 2018-09-20)
- rusoto: 0.34.0
プロジェクトを作る
まず、プロジェクトを作りましょう。 cargo コマンドで1発です。
$ cargo new rusoto-example-app $ exa --tree rusoto-example-app rusoto-example-app ├── Cargo.toml └── src └── main.rs
依存ライブラリを追加
次に、Cargo.tomlに依存ライブラリを指定しましょう。
[dependencies] rusoto_core = "0.34" rusoto_s3 = "0.34" # AssumeRoleする場合 rusoto_sts = "0.34" # 非同期実行する場合 futures = "0.1" tokio = "0.1"
Rusotoは他言語のAWS SDKと同様の構成になっていて、コアライブラリと各サービスを使用するためのライブラリに分かれています。
futures
と tokio
はRust言語で非同期実行するためのライブラリです。
依存関係を追加したら、 src/main.rs
に外部クレートの宣言を追加しましょう。
extern crate rusoto_core;
extern crate rusoto_s3;
extern crate rusoto_sts;
extern crate futures;
extern crate tokio;
fn main() {
println!("Hello, world!");
}
S3のクライアントを作る
デフォルトのプロファイルで実行する場合は、 S3Client::new
にリージョンを指定するだけです。簡単ですね。
use rusoto_core::{Region};
use rusoto_s3::{S3, S3Client};
fn default_profile_client(region: Region) -> S3Client {
S3Client::new(region)
}
クロスアカウントなどでAssumeRoleする場合は少し複雑です。STSのAssumeRoleを利用するクレデンシャルプロバイダーを使ってS3クライアントを作ります。
use rusoto_core::{Region, HttpClient}; // HttpClientを追加
use rusoto_sts::{StsAssumeRoleSessionCredentialsProvider, StsClient};
fn assume_role_client(region: Region, role_arn: &str, session_name: &str) -> S3Client {
let sts = StsClient::new(Region::ApNortheast1);
let provider = StsAssumeRoleSessionCredentialsProvider::new(
sts,
role_arn.to_string(),
session_name.to_string(),
None, // external id
None, // session duration
None, // scope down policy
None, // mfa serial
);
S3Client::new_with(HttpClient::new().unwrap(), provider, region)
}
ここまででS3のクライアントを作る準備ができました。せっかくなのでAssumeRoleするクライアントを作ってみましょう。
fn main() {
let s3 = assume_role_client(
Region::ApNortheast1,
"arn:aws:iam::[ACCOUNT_ID]:role/[ROLE_NAME]",
"rusoto-example-app-session",
);
}
実際に動かしてみる場合は、リージョン・ロール・セッション名を環境にあわせて変更してください。また、以降の操作を実行する場合はロールから s3:ListBucket
を実行できるようにポリシーを設定してください。
S3のAPIを実行する
ここまでに用意したS3クライアントを使ってS3のAPIを実行してみます。
RusotoでS3のAPIを実行すると、 RusotoFuture
というFuture型で結果を取得します。結果を同期的に取得する方法と、非同期で取得する方法が用意されているので両方とも試してみましょう。今回はS3のListBucketでバケットを列挙してみます。
同期的に結果を取得する場合
RusotoFuture::sync()
を実行すると、処理結果を受け取るまでブロックします。これを使って同期的に結果を取得します。
use rusoto_s3::{ListBucketsError, ListBucketsOutput};
fn list_bucket_sync(s3: &S3) {
match s3.list_buckets().sync() {
Ok(out) => show_buckets(out),
Err(e) => eprintln!("{:?}", e),
}
}
show_buckets
は list_buckets
の結果を受け取って表示するだけの関数です。
fn show_buckets(out: ListBucketsOutput) {
if let Some(buckets) = out.buckets {
for b in buckets.iter() {
println!("{}", b.name.as_ref().unwrap());
}
}
}
main関数から実行してみましょう。
fn main() {
let s3 = assume_role_client(
Region::ApNortheast1,
"arn:aws:iam::[ACCOUNT_ID]:role/[ROLE_NAME]",
"rusoto-example-app-session",
);
list_bucket_sync(&s3);
}
cargoコマンドで実行します。
$ cargo run
バケットの一覧が表示されれば成功です。
非同期実行する場合
Rusotoを非同期実行する場合、 tokio のランタイムが必要なようです。
上記Issueのコメントを参考に非同期実行してみます。
use tokio::runtime::Runtime;
fn list_bucket_async(runtime: &Runtime, s3: &S3) {
let f = s3
.list_buckets()
.map(show_buckets)
.map_err(|e| eprintln!("{}", e));
let h = spawn(f, &runtime.executor());
h.wait();
}
fn main() {
let s3 = assume_role_client(
Region::ApNortheast1,
"arn:aws:iam::[ACCOUNT_ID]:role/[ROLE_NAME]",
"rusoto-example-app-session",
);
let runtime = Runtime::new().unwrap();
list_bucket_async(&runtime, &s3);
runtime.shutdown_now().wait();
}
このままだと実行順序を確認しにくいので、各ステップで標準出力にメッセージを表示してみます。
fn list_bucket_async(runtime: &Runtime, s3: &S3) {
println!("## Step-1: Call ListBucket API");
let f = s3
.list_buckets()
.map(show_buckets)
.map_err(|e| eprintln!("{}", e));
println!("## Step-2: Spawn future");
let h = spawn(f, &runtime.executor());
println!("## Step-3: Wait future completion");
h.wait();
println!("## Step-4: Done!");
}
cargoコマンドで実行します。 環境に依存するかもしれませんが、おそらく以下の順序で表示されると思います。
$ cargo run ## Step-1: Call ListBucket API ## Step-2: Spawn future ## Step-3: Wait future completion バケット名1 バケット名2 ... ## Step-4: Done!
この結果だけ見ると非同期実行しているように見えないですよね。 wait
の前で数秒スレッドをスリープさせてみます。
println!("## Step-2: Spawn future");
let h = spawn(f, &runtime.executor());
// ここにスリープを追加
std::thread::sleep_ms(3000);
println!("## Step-3: Wait future completion");
h.wait();
再度実行してみます。
$ cargo run ## Step-1: Call ListBucket API ## Step-2: Spawn future バケット名1 バケット名2 ... ## Step-3: Wait future completion ## Step-4: Done!
今度は wait
する前にバケット名が表示されるようになりましたね!
私の環境だとStep-2のメッセージが表示してからバケット名を表示するまでに少し待ち時間がありましたが、おそらく tokio::Runtime
の作り方の問題な気がしています。今回は無理やりtokioのランタイムを使いましたが、tokioのアプリケーションで実装するとまた違ってきそうですね。
おわりに
Rusotoを使ってAWSのサービスにアクセスすることができました。今回はS3だけでしたが、引き続きいろいろなサービスを試してみたいと思います。