Rust から GCP のマネージド・サービスを使う

2018-05-18

GCPに限らず、Cloud のマネージド・サービスを使おうと思うと、そのクライアント・ライブラリが提供されているか言語を選択することが重要になってきます。
もちろん、クライアント・ライブラリが提供されていなくても、接続仕様が公開されていれば自力実装しても良いです。
が、普通に考えればサービスを提供するベンダーが提供しているものの方が信頼度が高そうなのは言うまでもありません。

Google の CloudサービスであるGCPのマネージド・サービスのクライアントですと、Go, Java, JavaScript(Node.js), Python, Ruby においてはクライアント・ライブラリが提供されています。
また、APIs のクライアント・ライブラリでは、上記にプラスして .NET, PHP, Objective-C, Dart などが提供されています。

想像通りですが、Rustなんて影も形もありません。
そこで敢えてRustでGCPマネージド・サービスに挑もうとするとすると茨の道に見えたのですが、やってみたら意外とあっさりできてしまったのでこのブログに書くことにしました。

ライブラリを使おう

Byron/google-apis-rs
Google謹製ではないですが、全Google APIを網羅した API クライアント・ライブラリになっています。
というのも、google discovery service と mako スクリプトによって生成されているからのようです。
2018年5月現在、まだポツポツと開発は続いているようです。
このリポジトリの中に70+のクレートが入っています。

dermesser/yup-oauth2
APIクライアントで何が面倒かって、認証ですよね。
このライブラリは、なんと GoogleのJSON型鍵ファイルを使ったOAuth2.0を可能としてくれます。
難しいことを考える必要はありません。
サービス・アカウントを作って、ただyup-oauth2を使うだけです。

hyperium/hyper
有名な、RustのHTTPライブラリhyperです。
これを使って作られているWebフレームワークが多数あります。

この3つがあればほぼDONEです。

準備

API認証用のサービス・アカウントと鍵を作成します。
作り方は公式のページをご覧ください。
サービス アカウントの作成と管理

秘密鍵は JSON ウェブキー(JWK) で作成します。

動くコード

Cloud Pub/Sub にメッセージをPublishします。
GithubにあげているコードにはSubscribeも載せています。
yup-oauth2のexample を多分に参考にさせて頂いており、ほぼこれの解説と言っても過言ではありません。

Cargo.toml

Cargo.toml はこちらを御覧ください。

main.rs

全貌はこちらです。
fn main から見ていきましょう。

fn main

fn main() {

    let client_secret = oauth::service_account_key_from_file(&"auth.json".to_string()).unwrap();
    let client = hyper::Client::with_connector(HttpsConnector::new(hyper_rustls::TlsClient::new()));

    let mut access = oauth::ServiceAccountAccess::new(client_secret, client);

    use oauth::GetToken;
    println!("{:?}",
             access.token(&vec!["https://www.googleapis.com/auth/pubsub"]).unwrap());

    let client = hyper::Client::with_connector(HttpsConnector::new(hyper_rustls::TlsClient::new()));
    let hub = pubsub::Pubsub::new(client, access);
    let methods = hub.projects();

    let args: Vec<String> = env::args().skip(1).collect();

    if args[0] == "pub" {
        let message = "pubsub test by rust.".to_string();
        publish(&methods, &message);
    } else if args[0] == "sub" {
        subscribe(&methods);
    }

    println!("completed {:?}.", args[0]);
}

まず、下記でJSON鍵ファイルの読み込みを行っています。

let client_secret
    = oauth::service_account_key_from_file(&"auth.json".to_string())
        .unwrap();

次に、hyperのHTTPSクライアントを生成します。

let client =
    hyper::Client::with_connector(
        HttpsConnector::new(hyper_rustls::TlsClient::new())
    );

ServiceAccountAccessに読み込んだ鍵ファイルと生成したclientを渡します。

let mut access =
    oauth::ServiceAccountAccess::new(client_secret, client);

hyperのHTTPSクライアントをもう1回作成してPubsubに渡せば、Pubsub操作クライアントが生成されます。
※ ServiceAccountAccessに渡してしまったhyperクライアントを使うことはできません。

let client =
    hyper::Client::with_connector(
        HttpsConnector::new(hyper_rustls::TlsClient::new())
    );
let hub = pubsub::Pubsub::new(client, access);

hubからPubsub操作オブジェクトを取得します。

let methods = hub.projects();

fn publish

Pubsubにメッセージをpublishするpublishメソッドです。

fn publish(methods: &PubsubMethods, message: &str) {

    let message = pubsub::PubsubMessage {
        data: Some(base64::encode(message.as_bytes())),
        ..Default::default()
    };
    let request = pubsub::PublishRequest { messages: Some(vec![message]) };
    let result = methods.topics_publish(request.clone(), TOPIC_NAME).doit();

    match result {
        Err(e) => {
            println!("Publish error: {}", e);
        }
        Ok((_response, response)) => {
            for msg in response.message_ids.unwrap_or(Vec::new()) {
                println!("Published message #{}", msg);
            }
        }
    }
}

引数methodsの型が &PubsubMethods になっています。
PubsubMethods はtypeエイリアスでmain.rsの15行目に定義しています。

type PubsubMethods<'a> = pubsub::ProjectMethods<'a,
    hyper::Client,
    oauth::ServiceAccountAccess<hyper::Client>>;

Pubsubのprojectsメソッドの戻り値は ProjectMethods<‘a, C, A&rt; です。

この C と A は Pubsub::new 時に決定するので new 時に渡した、
C -> hyper::Client
A -> oauth::ServiceAccountAccess>
なので、
要は PubsubMethods はPubsubのprojectsメソッドの戻り値の型を表しているわけです。

publish メソッドでは PubsubMessage と PublishRequest を生成します。

let message = pubsub::PubsubMessage {
    data: Some(base64::encode(message.as_bytes())),
    ..Default::default()
};
let request = pubsub::PublishRequest { messages: Some(vec![message]) };

生成したPublishRequestを Pubsubのトピックに投げます。

let result = methods.topics_publish(request.clone(), TOPIC_NAME).doit();

レスポンスを受け取ってエラー処理をします。
このあたりはアプリケーション仕様になってきます。

let result =
    methods.topics_publish(request.clone(), TOPIC_NAME).doit();

match result {
    Err(e) => {
        println!("Publish error: {}", e);
    }
    Ok((_response, response)) => {
        for msg in response.message_ids.unwrap_or(Vec::new()) {
            println!("Published message #{}", msg);
        }
    }
}

実行結果

publish した内容をsubscribeすると下記のように出力されます。

補足

Rust から GCP のマネージド・サービスを使う というタイトルにしたのですが、API経由でアクセスしているので、Rust から Google APIs を使う、とかの方が適切だったかもしれません、、、