Open4

rust(axum)+shuttleの実装

yunayuna

前提

rustのサーバーモジュールをaxum開発しています。

・もともとはクラウドにlinuxサーバーを置いて、そこで稼働させる事を前提にしている
・rustに特化したサーバレスのshuttleにモジュールをデプロイして使ってみる(Herokuのrust特化版みたいなイメージ?)
・shuttle以外にもデプロイできるよう、コードを分岐できるようにしておく

shuttle+axumのサンプル

shuttleのコマンドで、cargo shuttle initを行い、
axumを利用するオプションでプロジェクトを作成すると、以下のようなコードが生成されます。

[package]
name = "shuttle-sample"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.6.20"
shuttle-axum = "0.30.0"
shuttle-runtime = "0.30.0"
tokio = "1.28.2"

main.rs
use axum::{routing::get, Router};

async fn hello_world() -> &'static str {
    "Hello, world!"
}

#[shuttle_runtime::main]
async fn main() -> shuttle_axum::ShuttleAxum {
    let router = Router::new().route("/", get(hello_world));

    Ok(router.into())
}

これだと、通常のcargo runはできず、cargo shuttle runでローカル稼働させることになります。
cargo buildもできますが、これでビルドしたモジュールは、shuttleで稼働させる前提のものになってしまいます。

環境ロックインはできる限り除外したいので、
shuttle以外の通常のサーバー上でも稼働できるよう、分岐を準備してみます。

yunayuna

前提

shuttle用のmain関数には、
#[shuttle_runtime::main] をつけます。
これは、関数の名前がmainじゃなくても、自動的にmain関数を作成しますので
通常のmain関数があると、名称重複でコンパイルエラーになってしまいます。

例:

main.rs

//関数名がmainじゃなくても、重複エラー
#[shuttle_runtime::main]
async fn axum() -> shuttle_axum::ShuttleAxum {
    let router = Router::new().route("/", get(hello_world));
    Ok(router.into())
}

#[tokio::main]
async fn main() {
    let (router, conf) = app::router::build_server().await;
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let service = router.into_make_service();
    let server = axum::Server::try_bind(&address)
        .unwrap_or_else(|e| panic!("Error binding to '{}' - {}", address, e))
        .serve(service);

    match server.await {
        Ok(_) => {
            info!("server finished.");
        },
        Err(err) => {
            error!("server err: {:?}", err);
        }
    }
}

error[E0428]: the name `main` is defined multiple times
  --> src/main.rs:42:7
   |
31 | #[shuttle_runtime::main]
   | ------------------------ previous definition of the value `main` here

featureを使って、main関数をshuttle用と通常用に分ける

cargo run や cargo buildのオプションである、"feature" を使うと、
コード内で#[cfg(feature = "shuttle")]が使えるので、とりあえず書いてみる。

main.rs
use axum::{routing::get, Router};
use std::net::SocketAddr;

async fn hello_world() -> &'static str {
    "Hello, world!"
}

#[cfg(feature = "shuttle")]
#[shuttle_runtime::main]
async fn main() -> shuttle_axum::ShuttleAxum {
    let router = Router::new().route("/", get(hello_world));

    Ok(router.into())
}


#[cfg(not(feature = "shuttle"))]
#[tokio::main]
async fn main() {
    let router = Router::new().route("/", get(hello_world));
    let service = router.into_make_service();
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    match axum::Server::try_bind(&addr)
        .unwrap_or_else(|e| panic!("Error binding to '{}' - {}", addr, e))
        .serve(service).await {
        Ok(_) => {}
        Err(err) => {
            println!("Error: {}", err);
        }
    }
}

これだと、cargo runは問題無いが、
cargo shuttle run --features shuttle でエラーが出る。cargo shuttle runに、featuresオプションが用意されていないためです。

妥協案

featureの名前を変えて、
shuttleを使わない場合だけfeatureオプションをつける、とすれば、ビルド時のオプション調整でshuttleと通常版の場合分けが可能になります。
ただ、shuttleがデフォルトになるので、若干気持ち悪さが残ります。

main.rs
use axum::{routing::get, Router};
use std::net::SocketAddr;

async fn hello_world() -> &'static str {
    "Hello, world!"
}

#[cfg(not(feature = "normal"))]
#[shuttle_runtime::main]
async fn main() -> shuttle_axum::ShuttleAxum {
    let router = Router::new().route("/", get(hello_world));

    Ok(router.into())
}


#[cfg(feature = "normal")]
#[tokio::main]
async fn main() {
    let router = Router::new().route("/", get(hello_world));
    let service = router.into_make_service();
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    match axum::Server::try_bind(&addr)
        .unwrap_or_else(|e| panic!("Error binding to '{}' - {}", addr, e))
        .serve(service).await {
        Ok(_) => {}
        Err(err) => {
            println!("Error: {}", err);
        }
    }
}

これで、一応問題はなく扱えるようになる

#shuttle環境で開発
cargo shuttle run

#通常の開発
cargo run --features normal
yunayuna

公式サイトを調査したり、

cargo shuttle run のときだけ判定できるよう、
mainモジュールや、build.rs上で、shuttle実行に関連する環境変数や引数の変化があるか確認してみたが、
結論としてはほぼ変化がなく、なかなか難しそう。

確認できたことは、プログラム実行時の第一引数が絶対パスになってる、第2引数に--version という文字が入っていることぐらいだが、明示的なものではなく今後変更される可能性もあり、微妙。

ということで、取り急ぎは通常実行のときにfeaturesをセットする方法を取ることにしました。

yunayuna

その他の注意点(shuttleのversion: 0.30.1 時点)

shuttleは、マクロ内で自動的にtracing(ロギング)の設定を行っているようです。※将来色々機能追加されそう
https://docs.shuttle.rs/tutorials/send-your-logs-to-datadog

なので、tracingの設定を自分で実装している場合、

例:

tracing::subscriber::set_global_default(subscriber)
            .expect("Unable to set a global subscriber");

重複設定により、エラーが出ます

thread 'tokio-runtime-worker' panicked at src/app/logging.rs:118:14:
Unable to set a global subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set")

あまり深く掘ってませんが、取り急ぎshuttle使うときはロギングのレベル、フォーマット等、設定はshuttleに任せることにして自分では設定しないで稼働させてます