デプロイメント
Rustアプリケーションのビルドとデプロイ方法を学びます。
リリースビルド
開発ビルド vs リリースビルド
# 開発ビルド(デバッグ情報あり、最適化なし)
cargo build
# リリースビルド(最適化あり、高速)
cargo build --release
| 開発ビルド | リリースビルド | |
|---|---|---|
| コマンド | cargo build | cargo build --release |
| 出力先 | target/debug/ | target/release/ |
| 最適化 | なし | あり |
| ビルド時間 | 短い | 長い |
| 実行速度 | 遅い | 速い |
| バイナリサイズ | 大きい | 小さい |
実行
# 開発
cargo run
# リリース
cargo run --release
# または直接バイナリを実行
./target/release/my-app
バイナリの最適化
Cargo.toml の設定
[profile.release]
# 最適化レベル(0-3、sはサイズ優先、zは最小サイズ)
opt-level = 3
# リンク時最適化(LTO)
lto = true
# コード生成ユニット(1が最も最適化される)
codegen-units = 1
# パニック時の動作(abort = バイナリ小さくなる)
panic = "abort"
# デバッグシンボルを削除
strip = true
バイナリサイズの確認
# サイズ確認
ls -lh target/release/my-app
# さらに小さくする(stripコマンド)
strip target/release/my-app
環境変数と設定
環境変数の読み込み
#![allow(unused)]
fn main() {
use std::env;
struct Config {
database_url: String,
port: u16,
environment: String,
}
impl Config {
fn from_env() -> Self {
Self {
database_url: env::var("DATABASE_URL")
.expect("DATABASE_URL must be set"),
port: env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse()
.expect("PORT must be a number"),
environment: env::var("RUST_ENV")
.unwrap_or_else(|_| "development".to_string()),
}
}
}
}
.env ファイル(開発用)
# .env
DATABASE_URL=sqlite:./database.db
PORT=3000
RUST_ENV=development
// dotenvy クレートを使用
fn main() {
dotenvy::dotenv().ok(); // .envファイルを読み込み
let config = Config::from_env();
// ...
}
Docker
Dockerfile(マルチステージビルド)
# ビルドステージ
FROM rust:1.75 AS builder
WORKDIR /app
# 依存関係をキャッシュするため、先にCargo.tomlだけコピー
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# ソースコードをコピーしてビルド
COPY . .
RUN touch src/main.rs # タイムスタンプ更新
RUN cargo build --release
# 実行ステージ
FROM debian:bookworm-slim
# 必要なライブラリをインストール
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# 非rootユーザーで実行
RUN useradd -m appuser
USER appuser
WORKDIR /app
# ビルド成果物をコピー
COPY --from=builder /app/target/release/my-app /app/my-app
# 静的ファイルをコピー(必要な場合)
COPY --from=builder /app/static /app/static
ENV PORT=3000
EXPOSE 3000
CMD ["./my-app"]
.dockerignore
target/
.git/
.env
*.md
Dockerfile
.dockerignore
Dockerコマンド
# イメージのビルド
docker build -t my-app .
# コンテナの実行
docker run -p 3000:3000 \
-e DATABASE_URL=sqlite:/app/data/database.db \
-v $(pwd)/data:/app/data \
my-app
# バックグラウンドで実行
docker run -d -p 3000:3000 --name my-app my-app
Docker Compose
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=sqlite:/app/data/database.db
- RUST_ENV=production
volumes:
- ./data:/app/data
restart: unless-stopped
# PostgreSQLを使う場合
# db:
# image: postgres:15
# environment:
# POSTGRES_USER: user
# POSTGRES_PASSWORD: password
# POSTGRES_DB: myapp
# volumes:
# - postgres_data:/var/lib/postgresql/data
# volumes:
# postgres_data:
Docker Composeコマンド
# 起動
docker-compose up
# バックグラウンドで起動
docker-compose up -d
# 停止
docker-compose down
# ログ確認
docker-compose logs -f app
# 再ビルド
docker-compose up --build
クラウドデプロイ
Railway
# Railway CLIをインストール
npm install -g @railway/cli
# ログイン
railway login
# プロジェクト作成
railway init
# デプロイ
railway up
Fly.io
# Fly CLIをインストール
curl -L https://fly.io/install.sh | sh
# ログイン
fly auth login
# アプリ作成
fly launch
# デプロイ
fly deploy
fly.toml
app = "my-app"
primary_region = "nrt" # 東京
[build]
[http_service]
internal_port = 3000
force_https = true
[env]
RUST_ENV = "production"
Shuttle(Rust専用)
# Shuttle CLIをインストール
cargo install cargo-shuttle
# プロジェクト作成
cargo shuttle init
# ローカル実行
cargo shuttle run
# デプロイ
cargo shuttle deploy
ヘルスチェック
ヘルスチェックエンドポイント
#![allow(unused)]
fn main() {
async fn health_check() -> &'static str {
"OK"
}
// より詳細なチェック
async fn health_detailed(State(pool): State<SqlitePool>) -> Json<HealthStatus> {
let db_ok = sqlx::query("SELECT 1")
.execute(&pool)
.await
.is_ok();
Json(HealthStatus {
status: if db_ok { "healthy" } else { "unhealthy" },
database: db_ok,
version: env!("CARGO_PKG_VERSION"),
})
}
#[derive(Serialize)]
struct HealthStatus {
status: &'static str,
database: bool,
version: &'static str,
}
}
ルートに追加
#![allow(unused)]
fn main() {
let app = Router::new()
.route("/health", get(health_check))
.route("/health/detailed", get(health_detailed))
// ... 他のルート
}
ログ設定
tracing クレート
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
use tracing::{info, warn, error};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
fn init_logging() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "my_app=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
}
#[tokio::main]
async fn main() {
init_logging();
info!("Starting server...");
// ...
}
リクエストログ
#![allow(unused)]
fn main() {
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/", get(handler))
.layer(TraceLayer::new_for_http());
}
デプロイチェックリスト
セキュリティ
- 環境変数でシークレットを管理
- HTTPSを強制(本番)
- CORSを適切に設定
- SQLインジェクション対策(パラメータバインド)
パフォーマンス
- リリースビルドを使用
- データベース接続プール設定
- 適切なタイムアウト設定
運用
- ヘルスチェックエンドポイント
- ログ出力の設定
- エラーハンドリング
- グレースフルシャットダウン
グレースフルシャットダウン
use tokio::signal;
#[tokio::main]
async fn main() {
let app = Router::new()
// ... routes ...
;
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
println!("Shutting down gracefully...");
}
まとめ
| コマンド | 用途 |
|---|---|
cargo build --release | リリースビルド |
docker build | Dockerイメージ作成 |
docker-compose up | Docker Compose起動 |
| デプロイ先 | 特徴 |
|---|---|
| Railway | シンプル、自動デプロイ |
| Fly.io | グローバル、高速 |
| Shuttle | Rust専用、簡単 |
確認テスト
Q1. cargo build --release の特徴として正しいものは?
正解: B) リリースビルドは最適化が有効になるため実行速度が速くなります。ただし、ビルド時間は長くなります。
Q2. Dockerのマルチステージビルドを使う主な理由は?
正解: B) マルチステージビルドでは、ビルドに必要なツール(Rustコンパイラなど)を含まず、実行に必要なバイナリのみを最終イメージに含めることで、イメージサイズを大幅に削減できます。
Q3. マルチステージビルドなしのDockerfileの主な問題点は?
正解: A) マルチステージビルドを使わないと、Rustコンパイラや依存関係すべてが最終イメージに含まれ、イメージサイズが数GBになることがあります。
Q4. std::env::var("PORT")の戻り値の型は?
正解: C)
env::varはResult<String, VarError>を返します。環境変数が存在しない場合はErrになるため、適切に処理する必要があります。
Q5. ヘルスチェックでデータベース接続失敗時に返すべきHTTPステータスコードは?
正解: D) 503 Service Unavailableは、サーバーが一時的にリクエストを処理できない状態を示します。データベース接続失敗などの場合に適切で、監視システムが異常を検知できます。
Phase 5 完了!
おめでとうございます!Phase 5を完了しました。
学んだこと:
- HTTP/RESTの基礎
- Axumによるウェブアプリケーション開発
- SQLxによるデータベース操作
- 認証の実装
- フロントエンド連携
- デプロイメント
次は最終プロジェクトに挑戦しましょう!
次のドキュメント: 最終プロジェクト