rust(axum)+shuttleの実装
前提
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"
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以外の通常のサーバー上でも稼働できるよう、分岐を準備してみます。
前提
shuttle用のmain関数には、
#[shuttle_runtime::main]
をつけます。
これは、関数の名前がmainじゃなくても、自動的にmain関数を作成しますので
通常のmain関数があると、名称重複でコンパイルエラーになってしまいます。
例:
//関数名が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")]
が使えるので、とりあえず書いてみる。
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がデフォルトになるので、若干気持ち悪さが残ります。
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
公式サイトを調査したり、
cargo shuttle run のときだけ判定できるよう、
mainモジュールや、build.rs上で、shuttle実行に関連する環境変数や引数の変化があるか確認してみたが、
結論としてはほぼ変化がなく、なかなか難しそう。
確認できたことは、プログラム実行時の第一引数が絶対パスになってる、第2引数に--version という文字が入っていることぐらいだが、明示的なものではなく今後変更される可能性もあり、微妙。
ということで、取り急ぎは通常実行のときにfeaturesをセットする方法を取ることにしました。
その他の注意点(shuttleのversion: 0.30.1 時点)
shuttleは、マクロ内で自動的にtracing(ロギング)の設定を行っているようです。※将来色々機能追加されそう
なので、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に任せることにして自分では設定しないで稼働させてます