Rust + rusoto でAWSのサービスを操作する

Rust言語でAWSのサービスにアクセスするためのライブラリ Rusoto を使ってS3を操作してみました。デフォルトプロファイルを使った認証・AssumeRoleを使った認証、APIの同期実行・非同期実行を試してみました。
2018.09.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

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と同様の構成になっていて、コアライブラリと各サービスを使用するためのライブラリに分かれています。

futurestokio は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_bucketslist_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だけでしたが、引き続きいろいろなサービスを試してみたいと思います。