Dockerで最小かつ安全なRust Webアプリケーションを構築する
Olivia Novak
Dev Intern · Leapcell

はじめに
クラウドネイティブ開発の世界では、Dockerはアプリケーションのパッケージ化とデプロイに不可欠なツールとなっています。RustベースのWebアプリケーションにおいては、軽量で安全、かつ効率的なデプロイの追求が特に重要です。Rustはそのパフォーマンスとメモリ安全性で高く評価されていますが、結果として生成されるDockerイメージは、予想よりも大きくなることがあり、攻撃対象領域の拡大やデプロイ時間の遅延につながる可能性があります。本稿では、高度なDockerテクニック、特にマルチステージビルドとDistrolessイメージを活用することで、Rust Webアプリケーションコンテナのフットプリントを劇的に縮小し、セキュリティを強化する方法を探ります。これにより、リソース利用率を最適化するだけでなく、真に「本番稼働可能」なアプリケーションという理想に近づき、より回復力があり信頼性の高いデプロイメント環境を構築します。
基本概念の理解
実践に入る前に、最小かつ安全なRust Dockerイメージを構築するための戦略の基盤となるコアコンセプトについて、共通の理解を確立しましょう。
Dockerイメージとレイヤー: Dockerイメージは、コード、ランタイム、システムツール、ライブラリ、設定など、アプリケーションの実行に必要なすべてを含む、軽量でスタンドアロンの実行可能なソフトウェアパッケージです。イメージは一連のレイヤーから構築され、Dockerfile内の各命令が新しいレイヤーを作成します。レイヤーの再利用は、ビルドの高速化やイメージサイズの削減につながります。
Rustツールチェーン: Rustエコシステムは、rustc(コンパイラ)、cargo(ビルドシステムおよびパッケージマネージャー)、およびその他のさまざまなツールを提供します。アプリケーションのビルドにはこれらのツールが不可欠ですが、実行時に必須ではありません。
マルチステージビルド: このDocker機能により、Dockerfile内で複数のFROMステートメントを使用できます。各FROM命令は、FROM stagesという名前の新しいビルドステージを開始します。アーティファクトをステージ間で選択的にコピーでき、それ以外はすべて破棄できます。これは、ビルド時の依存関係と実行時の依存関係を分離するための強力なテクニックです。
Distrolessイメージ: Googleによって開発されたDistrolessイメージは、「アプリケーションとそのランタイム依存関係のみを含む、言語固有のベースイメージ」です。これには、UbuntuやAlpineのような標準的なベースイメージに一般的に見られるオペレーティングシステムコンポーネント、パッケージマネージャー、シェル、その他のユーティリティはほとんど含まれていません。主な利点は、イメージサイズの劇的な縮小と、はるかに小さい攻撃対象領域です。
攻撃対象領域: これは、不正なユーザーが環境にデータを入力したり、環境からデータを抽出したりできるすべてのポイントの合計を指します。Dockerイメージ内の不要なコンポーネントの数を減らすことで、本質的に攻撃対象領域を縮小し、悪意のあるアクターが悪名高い脆弱性をシステムライブラリやユーティリティで悪用することをより困難にします。
最小かつ安全なDockerイメージの作成
Actix Webフレームワークを使用したシンプルなRust WebアプリケーションのDockerイメージを構築することを目指します。目的達成のために、マルチステージビルドとDistrolessイメージの両方を実演します。
Rust Webアプリケーション
基本的なActix Webアプリケーションから始めましょう。新しいRustプロジェクトを作成します。
cargo new --bin my-rust-app cd my-rust-app
Cargo.tomlにactix-webを追加します。
# Cargo.toml [package] name = "my-rust-app" version = "0.1.0" edition = "2021" [dependencies] actix-web = "4" tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
そして、src/main.rsの内容をシンプルな「Hello, world!」サーバーに置き換えます。
// src/main.rs use actix_web::{get, App, HttpServer, Responder}; #[get("/")] async fn hello() -> impl Responder { "Hello from Rust Web App!" } #[actix_web]::main async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(hello) }) .bind(("0.0.0.0", 8080))? .run() .await }
このアプリケーションはポート8080でリッスンし、ルートパスで「Hello from Rust Web App!」という応答を返します。
マルチステージビルド:最小化への第一歩
マルチステージビルドは、重いビルド環境と軽量なランタイム環境を分離するために重要です。
プロジェクトのルートにDockerfileを作成しましょう。
# Dockerfile # ステージ1:ビルダー # 特定のRustイメージをビルド環境として使用します。 # コンパイル中の広範な互換性のために、しばしば 'buster' または 'bullseye' バリアントを選択しますが、 # 依存関係が最小限であれば 'slim' または 'alpine' も使用できます。 FROM rust:1.75-bookworm AS builder # コンテナ内の作業ディレクトリを設定します WORKDIR /app # Dockerが依存関係をキャッシュできるように、まずCargo.tomlとCargo.lockをコピーします # これらのファイルが変更されない場合、後続のビルドはキャッシュされたレイヤーを再利用できます COPY Cargo.toml Cargo.lock ./ # 依存関係のビルド(依存関係ビルドのみを強制するためにmain.rsを空にします) # このステップは、キャッシュレイヤーの最適化にとって重要です。 # 依存関係が変更されていない場合、このレイヤーは再利用されます。 RUN mkdir src \ && echo "fn main() {}" > src/main.rs \ && cargo build --release \ && rm -rf src # 実際のソースコードをコピーします COPY src ./src # リリースバイナリをビルドします # cargo build --release はバイナリをそこに入れるため、target/releaseを使用します RUN cargo build --release # ステージ2:ランナー # これは実行時ステージです。Debianの'buster-slim'イメージを使用します。 # これはフルRustイメージよりもはるかに小さいですが、基本的なlibcは含まれています。 FROM debian:bookworm-slim AS runner # 作業ディレクトリを設定します WORKDIR /app # コンパイルされたバイナリを'builder'ステージからコピーします # バイナリ名がプロジェクト名(my_rust_app)と一致していることを確認してください COPY /app/target/release/my-rust-app ./my-rust-app # アプリケーションがリッスンするポートを公開します EXPOSE 8080 # アプリケーションを実行します CMD ["./my-rust-app"]
説明:
builderステージ:FROM rust:1.75-bookworm AS builder: 公式Rustイメージから開始します。これにはrustc、cargo、およびすべての必要なビルドツールが含まれています。このステージをbuilderと名付けます。WORKDIR /app: 作業ディレクトリを設定します。COPY Cargo.toml Cargo.lock ./: マニフェストファイルのみをコピーします。この重要なステップにより、DockerはCargo.tomlとCargo.lockが変更されていない場合に依存関係のcargo build --releaseコマンドをキャッシュでき、リビルドが大幅に高速化されます。- `RUN mkdir src && echo

