Rust | Axum プロジェクトを Shuttle にデプロイする
Shuttle とは
Shuttle は、すべてのインフラストラクチャを管理しながらアプリをデプロイできる Rust ネイティブのクラウド開発プラットフォームです🚀
アプリケーションの構築とデプロイを簡単にすること を目標としています。
Shuttle の Infrastructure from Code
は、リソースのプロビジョニングがシンプルかつ手間のかからない仕組みです。
PostgreSQL をプロビジョニングしたい場合は、アプリケーションコードに以下のようなマクロを追加します。
#[shuttle_runtime::main]
async fn main(
// automatic db provisioning + hands you back an authenticated connection pool
#[shuttle_shared_db::Postgres] pool: PgPool,
) -> ShuttleRocket<...> {
// application code
}
Migrating from Render to Shuttle
codemountains/mountix というプロジェクトは現在 Render で動作しています。
そのプロジェクトを Shuttle に移行してみたいと思います!
Shuttle では examples が充実していますが、
Cargo Workspaces を使ったケースがありませんでした。
以下のドキュメントに沿って、マイグレーションを進めていきます。
マイグレーションされたソースコードは Github でも確認できます。
cargo-shuttle をインストール
まずは cargo-shuttle
をインストールします。
cargo install cargo-shuttle
次にログインします。
ログインには API Key の入力が必要です。
cargo shuttle login
クレートの追加
Shuttle 環境で Axum 実行させるために必要なクレートを追加していきます。
cargo add shuttle-runtime shuttle-axum
cargo add shuttle-secrets
ソースコードのマイグレーション
src/main.rs
src
直下に main.rs
を配置します。
mountix-driver
├── Cargo.toml
├── Secrets.dev.toml
├── Secrets.toml
├── Secrets.toml.example
└── src
├── lib.rs
├── main.rs (contains #[shuttle_runtime::main])
├── model
│ ├── information.rs
│ ├── mod.rs
│ ├── mountain.rs
│ └── surrounding_mountain.rs
├── module
│ └── mod.rs
├── routes
│ ├── health.rs
│ ├── information.rs
│ ├── mod.rs
│ ├── mountain.rs
│ └── surrounding_mountain.rs
└── startup
└── mod.rs
codemountains/mountix のプロジェクト構成の状態で、
mountix-driver/src/bin/bootstrap.rs
のままでは実行時にエラーで動きませんでした。
ドキュメントを見ても、src
直下に main.rs (contains #[shuttle_runtime::main])
があることが分かります。
.
├── .gitignore
├── Cargo.toml
├── Shuttle.toml (optional)
├── backend
│ ├── Cargo.toml
│ └── src
│ └── main.rs (contains #[shuttle_runtime::main])
├── frontend
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── shared
├── Cargo.toml
└── src
└── lib.rs
main.rs の修正
主な変更点です。
-
#[shuttle_runtime::main]
に修正 -
#[shuttle_shared_db::MongoDb]
で MongoDB をプロビジョニング -
shuttle_secrets::Secrets
でシークレット情報を取得して環境変数に書き込む- Axum の State で引き回せるようにするなど、改善の余地あり?
use mountix_driver::module::Modules;
use mountix_driver::startup::startup;
use shuttle_secrets::SecretStore;
use std::sync::Arc;
use tracing::info;
#[shuttle_runtime::main]
pub async fn axum(
#[shuttle_secrets::Secrets] secret_store: SecretStore,
#[shuttle_shared_db::MongoDb] db: Database,
) -> shuttle_axum::ShuttleAxum {
let _ = write_env(&secret_store);
let modules = Modules::new(db).await;
let router = startup(Arc::new(modules)).await;
Ok(router.into())
}
// Write to environment variables
// See more: https://docs.shuttle.rs/resources/shuttle-secrets#caveats
fn write_env(secret_store: &SecretStore) {
info!("Write to environment variables");
let mountains_url = secret_store
.get("MOUNTAINS_URL")
.expect("MOUNTAINS_URL is undefined.");
std::env::set_var("MOUNTAINS_URL", mountains_url);
let documents_url = secret_store
.get("DOCUMENTS_URL")
.expect("DOCUMENTS_URL is undefined.");
std::env::set_var("DOCUMENTS_URL", documents_url);
let default_distance = secret_store
.get("DEFAULT_DISTANCE")
.expect("DEFAULT_DISTANCE is undefined.");
std::env::set_var("DEFAULT_DISTANCE", default_distance);
let max_distance = secret_store
.get("MAX_DISTANCE")
.expect("MAX_DISTANCE is undefined.");
std::env::set_var("MAX_DISTANCE", max_distance);
}
.env
で環境変数として管理していた部分は、SecretStore
をうまく扱うことで改善できそうです。
今回はドキュメントに記載があった「シークレットをロードした後 (ライブラリをロードする前) に変数を手動で設定する」という回避策で対応しています。
この部分に関しては、Axum の State で引き回せるようにする方が良いのかもしれません。
dotenv と tracing
以下のような、dotenv
の初回の読み取りや tracing
の初期化は不要です。
pub fn init_app() {
dotenv().ok();
tracing_subscriber::fmt::init();
}
dotenv
は shuttle_secrets::Secrets
が変わりとなります。
また、デフォルト機能として tracing
の初期化が shuttle-runtime
に含まれています。
詳細は以下のドキュメントを確認してください。
初期データのインサート
マスタデータとして、1059 件の山岳情報一覧を登録しておく必要があります。
ただ、1059 件の配列やベクタを生成しようとした際に以下のエラーに遭遇しました。
thread 'tokio-runtime-worker' has overflowed its stack
fatal runtime error: stack overflow
おそらく、規定のスタックサイズである 2 MB をオーバーしているものと思われます。
苦肉の策として、初期データを 4 つに分割しています。(init_data_1
~ init_data_4
)
use std::sync::Arc;
use crate::model::mountain::MountainDocument;
use crate::persistence::{init_data_1, init_data_2, init_data_3, init_data_4};
use mongodb::Database;
#[derive(Clone)]
pub struct Db(pub(crate) Arc<Database>);
impl Db {
pub async fn new(db: Database) -> Db {
// insert initial data
let _ = init(db.clone()).await;
Db(Arc::new(db))
}
}
async fn init(db: Database) {
let count = db
.collection::<MountainDocument>("mountains")
.count_documents(None, None)
.await
.expect("Failed to connect database.");
if count > 0 {
return;
}
// MEMO: To prevent overflow, `mountains` was divided
//
// ```
// thread 'tokio-runtime-worker' has overflowed its stack
// fatal runtime error: stack overflow
// ```
for idx in 1..=4 {
let mountains = match idx {
1 => init_data_1(),
2 => init_data_2(),
3 => init_data_3(),
_ => init_data_4(),
};
let _ = db
.collection("mountains")
.insert_many(mountains, None)
.await
.expect("Failed to insert initial data.");
}
}
デバッグ(ローカル実行)
以下のコマンドでローカル実行することができます。
cargo shuttle run
なお、MongoDB は Docker 上で起動します。
If you do not specify a local_uri, then cargo-shuttle will attempt to spin up a Docker container and launch the database inside of it. For this to succeed, you must have Docker installed and you must also have started the Docker engine. If you have not used Docker before, the easiest way is to install the desktop app and then launch it in order to start the Docker engine.
デプロイ
Shuttle.toml
を作成し、name
を設定します。
name = "my-unique-app-name-here"
あとは、次のコマンドを実行するだけです。
cargo shuttle project start
cargo shuttle deploy
これでプロジェクトがデプロイされます🚀🚀🚀
注意
MongoDB Atlas 上に構築したデータベースに Shuttle から接続する場合、0.0.0.0/0
をホワイトリストに追加する必要があります。
0.0.0.0/0
を許容するのはちょっと... という思いもあり、
MongoDB も Shuttle Shared Databases に移行させてみました。
ちなみに、Render ではダッシュボードから IP アドレスを確認することができます。
まとめ
非常に簡単にデプロイまで進めることができました!
Rust に特化したプラットフォームというのも面白いですよね。
ドキュメントを見ていく中で、Shuttle-next という WASM Web フレームワークがあることも知りました...
気になる...👀
また触ってみようと思います🫣✨
Discussion