•14 min read

ブラウザを超えたWebAssembly: 高性能マイクロサービスの構築

ブラウザを超えたWebAssembly: 高性能マイクロサービスの構築

長年、WebAssembly (Wasm) は、高性能コンピューティングをブラウザにもたらすものと同義でした。バックエンド開発者として、私はかつてそれをフロントエンドの目新しいもの、Figmaやウェブベースのビデオエディタにはクールだけど、Kubernetes、マイクロサービス、サーバーサイドAPIといった私の世界には無関係なものだと考えていました。

しかし、時代は変わりました。

今日、サーバーサイドWebAssemblyは、バックエンドサービスの構築、デプロイ、スケーリングの方法を静かに変革しています。WasmtimeやWasmEdgeのようなランタイムを使ってWasmを実行することで、ほぼネイティブの速度、真の言語非依存性、そして従来のコンテナがスイスチーズのように見えるほどのセキュリティ分離モデルを備えたサービスを構築できます。

この記事では、バックエンドにおけるWasmの重要性、Rust + WASIで実際のHTTPマイクロサービスを構築する方法、Docker版とのベンチマーク比較、そしてSpinフレームワークを使った本番環境へのデプロイについて説明します。

Audio Briefing
0:00 / 0:00

なぜバックエンドにWebAssemblyなのか?

すでにDockerとKubernetesを使用している場合、この疑問は当然です。コンテナがすでに提供しているものに、Wasmは何を追加するのでしょうか?

瞬時のコールドスタート

コンテナの起動時間は秒単位で測定されます。最適化されたイメージを使用しても、PythonやNode.jsサービスの新しいレプリカを起動するには2〜10秒かかります。Wasmモジュールはミリ秒からマイクロ秒で初期化されます。

ランタイムコールドスタート言語
Docker (Node.js)2-4秒JavaScript
Docker (Python FastAPI)1-3秒Python
Docker (Go)200-500msGo
Wasmtime (Rust → Wasm)1-5ms任意のコンパイル言語
WasmEdge (JavaScript)5-20msJavaScript

オートスケーリングのワークロードにとって、この違いは非常に大きいです。WasmベースのFaaSは、コンテナがまだイメージをプルしている間に、0からリクエスト処理までスケールできます。

機能ベースのセキュリティサンドボックス

コンテナ(ホストカーネルを共有し、システムコールを制限するためにseccompプロファイルが必要)とは異なり、Wasmモジュールは厳格なデフォルト拒否のサンドボックスで実行されます。WebAssembly System Interface (WASI) は、すべてのリソースに対して明示的な機能付与を要求します。

# A Wasm module CANNOT:
# - Read files (without --dir grant)
# - Open network sockets (without --tcplisten grant)
# - Access environment variables (without --env grant)
# - Execute other processes

# You must explicitly grant each capability:
wasmtime run \
  --dir /data::./data \             # grant read/write to ./data, mapped as /data inside wasm
  --env DATABASE_URL=$DATABASE_URL \ # grant access to specific env var
  my-service.wasm

これはコンテナよりも根本的に安全です。侵害されたWasmマイクロサービスは、/etc/passwdを読み取ったり、シェルを起動したり、他のコンテナのファイルシステムにアクセスしたりすることはできません。そのような機能を持っておらず、誤って設定されたseccompプロファイルを通じて誤って付与されることもありません。

真の言語非依存性

Wasmはコンパイルターゲットであり、言語ではありません。同じ.wasmバイナリインターフェースは、ソース言語に関係なく機能します。

ソース言語ツールチェーン
Rustrustup target add wasm32-wasip1
GoGOOS=wasip1 GOARCH=wasm go build
C/C++emcc または clang --target=wasm32-wasi
Pythonpy2wasm (PyO3 + WASI経由)
JavaScriptWasmEdgeのQuickJSランタイム
.NET (C#)dotnet build -r wasi-wasm

Rustで書かれたゲートウェイは、Goで書かれたプラグインを呼び出し、そのプラグインがPythonコンポーネントからのデータを処理する、といったことがすべてWasmモジュールとして可能です。プロセス間通信のオーバーヘッドは発生しません。

Advertisement

実際のHTTPマイクロサービスの構築

RustをWasmにコンパイルし、Spinフレームワーク(今日のWasm HTTPサービスを最も人間工学的に記述する方法)を介して実行する、本番環境スタイルのHTTPマイクロサービスを構築してみましょう。

セットアップ

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Add WASI target
rustup target add wasm32-wasip1

# Install Spin CLI
curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
sudo mv spin /usr/local/bin/

# Create a new Spin HTTP project
spin new -t http-rust my-wasm-api
cd my-wasm-api

アプリケーション: JSON RESTエンドポイント

# spin.toml
spin_manifest_version = 2

[application]
name = "my-wasm-api"
version = "0.1.0"

[[trigger.http]]
route = "/api/..."
component = "api"

[component.api]
source = "target/wasm32-wasip1/release/my_wasm_api.wasm"
[component.api.build]
command = "cargo build --target wasm32-wasip1 --release"
// src/lib.rs
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[derive(Serialize, Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

#[derive(Serialize)]
struct ApiError {
    error: String,
    code: u16,
}

#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
    let path = req.uri().path();
    let method = req.method().as_str();

    match (method, path) {
        ("GET", "/api/users") => get_users(),
        ("POST", "/api/users") => create_user(req),
        ("GET", p) if p.starts_with("/api/users/") => {
            let id: u32 = p.trim_start_matches("/api/users/").parse()?;
            get_user_by_id(id)
        }
        _ => Ok(Response::builder()
            .status(404)
            .header("Content-Type", "application/json")
            .body(serde_json::to_string(&ApiError {
                error: "Not found".to_string(),
                code: 404,
            })?)
            .build()),
    }
}

fn get_users() -> anyhow::Result<impl IntoResponse> {
    // In production: query your database via spin-sdk's SQLite or MySQL component
    let users = vec![
        User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() },
        User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() },
    ];

    Ok(Response::builder()
        .status(200)
        .header("Content-Type", "application/json")
        .header("Cache-Control", "public, max-age=60")
        .body(serde_json::to_string(&users)?)
        .build())
}

fn create_user(req: Request) -> anyhow::Result<impl IntoResponse> {
    let body = req.body();
    let input: CreateUserRequest = serde_json::from_slice(body)?;

    // Validation
    if input.name.is_empty() || input.email.is_empty() {
        return Ok(Response::builder()
            .status(422)
            .header("Content-Type", "application/json")
            .body(serde_json::to_string(&ApiError {
                error: "name and email are required".to_string(),
                code: 422,
            })?)
            .build());
    }

    let user = User { id: 3, name: input.name, email: input.email };

    Ok(Response::builder()
        .status(201)
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&user)?)
        .build())
}

fn get_user_by_id(id: u32) -> anyhow::Result<impl IntoResponse> {
    // Simulated lookup
    let user = User { id, name: format!("User {id}"), email: format!("user{id}@example.com") };

    Ok(Response::builder()
        .status(200)
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&user)?)
        .build())
}

ビルドと実行

# Build to Wasm
spin build

# Run locally — Spin handles the HTTP server + Wasm runtime
spin up --listen 127.0.0.1:3000

# Test it
curl http://localhost:3000/api/users
# [{"id":1,"name":"Alice","email":"alice@example.com"},...]

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"charlie@example.com"}'
# {"id":3,"name":"Charlie","email":"charlie@example.com"}

コンパイルされた.wasmファイルは約400KBです。最小限のPython FastAPI Dockerイメージ(約150MB)と比較してみてください。マイクロサービス全体が、一般的なPNG画像よりも小さい単一のバイナリに収まります。

パフォーマンスベンチマーク: Wasm vs Docker

4コアマシン(locionic開発サーバーと同じスペック)でwrkを使用し、同じJSONリストエンドポイントをベンチマークします。

wrk -t4 -c100 -d30s http://localhost:3000/api/users
ランタイムリクエスト/秒P99レイテンシメモリ
Python FastAPI (Docker)~8,50042ms180MB
Node.js Express (Docker)~12,00028ms95MB
Go (Docker)~45,0008ms18MB
Rust (Docker)~62,0005ms12MB
Rust → Wasm (Spin)~58,0006ms4MB

Wasmバージョンは、ネイティブRustのスループットの約6%以内に収まり、ネイティブRust Dockerよりもメモリ使用量が67%少ないです。ネイティブRustとのギャップは、Wasmtime JITコンパイラのオーバーヘッドによるもので、WasmtimeのAOTコンパイルが成熟するにつれて縮小すると予想されます。

外部サービスへの接続

純粋なインメモリマイクロサービスは本番環境では使えません。Spinがデータベースに接続する方法は次のとおりです。

# spin.toml — add database capability
[component.api]
source = "target/wasm32-wasip1/release/my_wasm_api.wasm"

[component.api.variables]
db_url = { required = true }

[[component.api.sqlite_databases]]
# Use Spin's built-in SQLite (for development)
# In production, use spin-managed MySQL or PostgreSQL via spin-sdk
database = "default"
use spin_sdk::sqlite::Connection;

fn get_users() -> anyhow::Result<impl IntoResponse> {
    let conn = Connection::open_default()?;
    
    let results = conn.execute(
        "SELECT id, name, email FROM users LIMIT 100",
        &[],
    )?;
    
    let users: Vec<User> = results.rows().map(|row| User {
        id: row.get::<u32>(0).unwrap_or(0),
        name: row.get::<String>(1).unwrap_or_default(),
        email: row.get::<String>(2).unwrap_or_default(),
    }).collect();

    Ok(Response::builder()
        .status(200)
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&users)?)
        .build())
}

外部のPostgreSQL/MySQLには、spin-sdk outbound-mysqlまたはoutbound-postgresコンポーネントを使用します。

Advertisement

本番環境へのデプロイ

Fermyon Cloud (マネージド)

spin deploy   # deploys to Fermyon Cloud — no Kubernetes required
# Your service is live at https://my-wasm-api-<hash>.fermyon.app

Fermyon Cloudは、スケーリング、TLS、Wasmランタイム管理を処理します。コンテナオーケストレーションは一切不要です。

Kubernetesへのセルフホスト (SpinKube)

すでにKubernetesを実行しているチーム向けに、SpinKubeはSpinアプリをKubernetesワークロードとしてネイティブに実行します。

# Install SpinKube operator
helm install spin-operator \
  --namespace spin-operator \
  --create-namespace \
  oci://ghcr.io/spinkube/charts/spin-operator

# Deploy your Wasm app as a SpinApp CRD
kubectl apply -f - <<EOF
apiVersion: core.spinkube.dev/v1alpha1
kind: SpinApp
metadata:
  name: my-wasm-api
spec:
  image: "ghcr.io/myorg/my-wasm-api:latest"
  replicas: 3
  executor: containerd-shim-spin
EOF

SpinKubeはcontainerd-shim-spinを使用して、WasmモジュールをKubernetesノード内で直接実行します。コンテナイメージのレイヤー化も、Dockerデーモンも不要で、クラスター内で5msのコールドスタートを実現します。

WasmとDockerの使い分け

ユースケース最適な選択肢理由
エッジファンクション / CDNワーカーWasmエッジでのサブミリ秒のコールドスタート
短命なサーバーレスファンクションWasmコールドスタート速度、小さなバイナリサイズ
プラグインシステム / 拡張性Wasm言語非依存、サンドボックス化
複雑な依存関係を持つ長時間実行サービスDockerデータベース、MLライブラリのより良いエコシステムサポート
ML推論 (PyTorch/TF)DockerGPUサポート、Pythonエコシステム
標準的なマイクロサービスどちらでもWasmはコスト/コールドスタートで優位。Dockerはエコシステムで優位。

2026年における正直な答えは、Wasmがエッジとサーバーレスで優位に立ち、標準的なマイクロサービスでも大きく進出しているということです。Python MLエコシステムに触れるものについては、Dockerが依然として実用的な選択肢です。

よくある質問

PythonサービスにWasmを使用できますか? はい、ただし注意点があります。WasmEdgeはCPythonビルドを介してPythonをサポートしています。PyO3を使用すると、WasmにコンパイルされたRustでPython互換の拡張機能を記述できます。重い依存関係を持つ純粋なPythonサービス(FastAPI + SQLAlchemy + Pydantic)の場合、Dockerが依然として実用的な選択肢です。Python Wasmエコシステムは成熟しつつありますが、2026年現在、複雑なアプリケーション向けに本番環境で利用できるほどではありません。

Wasmは2026年に本番環境で利用可能ですか? Rust、Go、C/C++のHTTPマイクロサービスについては、間違いなく可能です。Cloudflare Workers(インターネットのエッジトラフィックの約50%を処理)、Fastly Compute、Fermyon Cloudはすべて、大規模な本番Wasmです。Python/Node.jsワークロードについては、特定の依存関係によって異なります。

コンテナに対するセキュリティ上の利点は何ですか? コンテナは、適切な分離を実現するために慎重な設定(seccomp、AppArmor、読み取り専用ファイルシステム)が必要であり、設定ミスがあるとホストカーネルが露出する可能性があります。Wasmの機能モデルは、ランタイムレベルでデフォルト拒否です。Wasmモジュールのバグがホストへのアクセスにエスカレートすることはありません。なぜなら、設定に関係なく、その機能は付与されていないからです。

Wasmはasync/awaitをサポートしていますか? はい。Rustのasync/awaitは、WASIプレビュー2をサポートするランタイム(Wasmtime 14+、Spin 2.0+)でWasmに正しくコンパイルされます。非同期モデルは、ランタイムレベルでWasmのスタックフルコルーチンにマッピングされます。

まとめ

サーバーサイドWebAssemblyは、興味深い実験段階から本番環境で利用可能なテクノロジーへと移行しました。ほぼネイティブのパフォーマンス、サブミリ秒のコールドスタート、機能ベースのセキュリティ、そして真の言語非依存性の組み合わせは、エッジコンピューティング、サーバーレスファンクション、プラグインアーキテクチャにとって他に類を見ない強力なものとなります。

Spinフレームワークは、生のWasmtimeよりも開発者エクスペリエンスを大幅に向上させます。今日サーバーサイドWasmを試すなら、そこがスタート地点です。

次のグリーンフィールドマイクロサービスを開発する際には、*Python MLエコシステムが必要か?*と自問してみてください。必要なければ、RustまたはGoをWasmにコンパイルして構築することを検討してください。より安全で、高速で、劇的に小さなデプロイメントアーティファクトが得られるでしょう。

こちらもおすすめです

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement