- Published on
Kubernetes에서 WebAssembly 워크로드 실행: SpinKube·containerd Wasm Shim·runwasi 완전 가이드
- Authors
- Name

들어가며
Solomon Hykes(Docker 창시자)는 2019년 이렇게 말했다. "만약 2008년에 WASM+WASI가 존재했다면, Docker를 만들 필요가 없었을 것이다." 이 발언은 과장이 아니라 WebAssembly가 가진 잠재력을 정확하게 짚은 통찰이다.
컨테이너 기술은 지난 10년간 소프트웨어 배포의 표준이 되었다. 그러나 컨테이너는 본질적으로 리눅스 커널의 namespace와 cgroup 위에서 동작하는 격리된 프로세스이며, 전체 OS 사용자 공간(userland)을 이미지에 포함해야 한다. 최소한의 Alpine 기반 이미지도 수십 MB에 달하고, Cold Start에 수백 ms에서 수초가 소요된다.
**WebAssembly(Wasm)**는 근본적으로 다른 접근을 제시한다. Wasm 모듈은 수 KB~수 MB 크기이며, 샌드박스 환경에서 1ms 미만의 Cold Start가 가능하다. OS에 종속되지 않는 이식성, capability-based 보안 모델, 그리고 네이티브에 근접한 실행 속도까지 갖추고 있다.
2024년부터 CNCF 생태계에서 Wasm 워크로드를 Kubernetes 위에서 실행하려는 움직임이 본격화되었다. SpinKube, containerd Wasm shim, runwasi 같은 프로젝트가 성숙하면서, Wasm 워크로드를 Pod처럼 관리할 수 있는 시대가 열리고 있다. 이 글에서는 Wasm의 핵심 개념부터 Kubernetes 통합 아키텍처, SpinKube 심층 분석, 실전 배포, 성능 벤치마크까지 포괄적으로 다룬다.
WebAssembly 핵심 개념
Wasm 바이너리 포맷
WebAssembly는 원래 브라우저에서 C/C++/Rust 코드를 네이티브에 가까운 속도로 실행하기 위해 설계된 바이너리 포맷이다. 핵심 특성은 다음과 같다.
- 스택 기반 가상 머신: 레지스터가 아닌 스택 기반의 명령어 집합을 사용
- 타입 안전: 모든 함수 시그니처와 메모리 접근이 타입 검사를 통과해야 실행
- 선형 메모리: 바운드 체크가 보장되는 연속 메모리 공간에서만 데이터 접근 가능
- 결정적 실행: 동일한 입력에 대해 항상 동일한 결과를 보장(부동소수점 일부 예외)
// Rust로 작성한 간단한 Wasm 모듈 예시
// Cargo.toml에 [lib] crate-type = ["cdylib"] 설정 필요
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn fibonacci(n: i32) -> i64 {
if n <= 1 {
return n as i64;
}
let mut a: i64 = 0;
let mut b: i64 = 1;
for _ in 2..=n {
let temp = b;
b = a + b;
a = temp;
}
b
}
빌드 후 생성되는 .wasm 파일은 수 KB 수준이며, 모든 Wasm 런타임(Wasmtime, Wasmer, WasmEdge 등)에서 실행할 수 있다.
# Wasm 타겟으로 빌드
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release
# 빌드 결과물 크기 확인 (일반적으로 수 KB~수 MB)
ls -lh target/wasm32-wasi/release/*.wasm
# Wasmtime으로 직접 실행
wasmtime target/wasm32-wasi/release/my_module.wasm
WASI (WebAssembly System Interface)
브라우저 밖에서 Wasm을 실행하려면 파일 시스템, 네트워크, 환경 변수 등 OS 리소스에 접근해야 한다. WASI는 이를 위한 표준 시스템 인터페이스다.
WASI의 핵심 설계 원칙은 Capability-Based Security다. Wasm 모듈은 기본적으로 아무 것도 할 수 없으며, 호스트가 명시적으로 부여한 권한(capability)만 사용할 수 있다.
# WASI에서 파일시스템 접근 권한 부여 예시
# --dir 플래그로 특정 디렉토리만 접근 허용
wasmtime --dir=/tmp/data::./data my_app.wasm
# 네트워크 접근이 필요한 경우
wasmtime --tcplisten=0.0.0.0:8080 my_server.wasm
WASI는 현재 두 가지 주요 버전이 공존한다.
| 특성 | WASI Preview 1 | WASI Preview 2 (0.2.x) |
|---|---|---|
| 인터페이스 정의 | witx(텍스트 형식) | WIT(Wasm Interface Type) |
| 모듈 모델 | Core Module | Component Model |
| HTTP 지원 | 없음 | wasi:http/proxy |
| 소켓 지원 | 제한적 | wasi:sockets |
| 컴포넌트 조합 | 불가 | WIT를 통한 조합 가능 |
Component Model
Component Model은 Wasm 생태계의 핵심 발전 방향이다. 기존 Core Wasm은 숫자 타입(i32, i64, f32, f64)만 함수 인자로 교환할 수 있어, 문자열이나 구조체 같은 고수준 타입을 전달하려면 복잡한 바인딩 코드가 필요했다.
Component Model은 **WIT(Wasm Interface Type)**라는 인터페이스 정의 언어를 통해 이 문제를 해결한다.
// WIT 인터페이스 정의 예시
package example:http-handler@1.0.0;
interface types {
record http-request {
method: string,
uri: string,
headers: list<tuple<string, string>>,
body: option<list<u8>>,
}
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: option<list<u8>>,
}
}
world http-handler {
import wasi:logging/logging;
export handle-request: func(req: types.http-request) -> types.http-response;
}
이 WIT 정의를 사용하면 Rust로 작성한 HTTP 핸들러를 Python으로 작성한 미들웨어와 조합하는 것이 가능해진다. 각 컴포넌트는 독립적으로 컴파일되며, WIT가 인터페이스 계약 역할을 한다.
Kubernetes에서 Wasm 실행 아키텍처
기존 컨테이너 런타임 스택
Kubernetes의 컨테이너 실행 흐름을 먼저 이해해야 한다.
kubelet → CRI → containerd → OCI Runtime (runc) → Linux Container
- kubelet: Pod spec을 받아 컨테이너 생성 요청
- CRI (Container Runtime Interface): kubelet과 컨테이너 런타임 간 표준 인터페이스
- containerd: 이미지 pull, 컨테이너 생명주기 관리
- runc: 실제 Linux 컨테이너(namespace + cgroup)를 생성하는 OCI 런타임
containerd Wasm Shim
Wasm 워크로드를 실행하기 위해 runc 위치에 Wasm 런타임을 끼워 넣는 것이 containerd Wasm shim의 핵심 아이디어다.
kubelet → CRI → containerd → containerd-shim-spin-v2 → Wasmtime/Spin → Wasm Module
→ containerd-shim-slight-v2 → Wasmtime/SpiderLightning → Wasm Module
→ containerd-shim-wasmedge-v1 → WasmEdge → Wasm Module
containerd의 shim 아키텍처는 원래 다양한 OCI 런타임을 플러그인처럼 교체할 수 있도록 설계되었다. Wasm shim은 이 확장 포인트를 활용하여, OCI 이미지 안에 Wasm 모듈을 패키징하고 containerd를 통해 실행하는 것을 가능하게 한다.
runwasi
runwasi는 Bytecode Alliance에서 개발하는 프로젝트로, containerd shim과 Wasm 런타임 사이의 추상화 계층을 제공한다.
// runwasi의 핵심 트레잇 구조 (단순화)
pub trait Engine {
fn name() -> &'static str;
fn run_wasi(&self, ctx: &WasiCtx, module: &[u8]) -> Result<i32>;
}
// Wasmtime 엔진 구현
pub struct WasmtimeEngine;
impl Engine for WasmtimeEngine {
fn name() -> &'static str { "wasmtime" }
fn run_wasi(&self, ctx: &WasiCtx, module: &[u8]) -> Result<i32> {
// Wasmtime을 사용하여 Wasm 모듈 실행
let engine = wasmtime::Engine::default();
let module = wasmtime::Module::new(&engine, module)?;
// ...
}
}
runwasi를 통해 다양한 Wasm 런타임(Wasmtime, WasmEdge, Wasmer)을 containerd shim으로 노출할 수 있다.
RuntimeClass로 Wasm 워크로드 스케줄링
Kubernetes는 RuntimeClass 리소스를 통해 Pod가 사용할 런타임을 지정할 수 있다. Wasm 워크로드는 전용 RuntimeClass를 정의하여 Wasm shim이 설치된 노드에서만 실행되도록 스케줄링한다.
# RuntimeClass 정의
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: wasmtime-spin-v2
handler: spin
scheduling:
nodeSelector:
kubernetes.io/arch: wasm32-wasi
---
# Wasm 워크로드 Pod
apiVersion: v1
kind: Pod
metadata:
name: wasm-hello
spec:
runtimeClassName: wasmtime-spin-v2
containers:
- name: hello-wasm
image: ghcr.io/example/hello-wasm:v1
command: ['/hello.wasm']
nodeSelector:
kubernetes.io/arch: wasm32-wasi
SpinKube 심층 분석
SpinKube란 무엇인가
SpinKube는 Fermyon이 주도하는 오픈소스 프로젝트로, Kubernetes 위에서 Fermyon Spin 애플리케이션을 네이티브로 실행하기 위한 통합 프레임워크다. CNCF Sandbox 프로젝트로 채택되어 커뮤니티 기반 거버넌스 하에 개발되고 있다.
SpinKube는 세 가지 핵심 컴포넌트로 구성된다.
- Spin Operator: Kubernetes 커스텀 컨트롤러로, SpinApp CRD를 감시하고 관리
- SpinApp CRD: Spin 애플리케이션을 선언적으로 정의하는 커스텀 리소스
- containerd-shim-spin: Spin 런타임을 containerd shim으로 감싼 실행 엔진
Architecture 상세
┌─────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Spin Operator│ │ API Server │ │
│ │ │◄──────│ │ │
│ │ - SpinApp │ │ SpinApp CRD registered │ │
│ │ Controller │ └────────────────────────┘ │
│ │ - Executor │ │
│ │ Selection │ │
│ └──────┬───────┘ │
│ │ Creates/Manages │
│ ▼ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Deployment │ │ Node (Wasm-capable) │ │
│ │ or │──────►│ │ │
│ │ SpinAppExec │ │ containerd │ │
│ └──────────────┘ │ └─ shim-spin-v2 │ │
│ │ └─ Spin Runtime │ │
│ │ └─ .wasm │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────┘
SpinApp CRD 구조
SpinApp CRD는 Spin 애플리케이션의 배포를 선언적으로 정의한다.
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
name: hello-spin
namespace: default
spec:
image: ghcr.io/example/hello-spin:v1
replicas: 3
executor: containerd-shim-spin
enableAutoscaling: true
resources:
limits:
cpu: 100m
memory: 128Mi
variables:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
runtime-config:
key_value_stores:
default:
type: redis
url: redis://redis-cluster:6379
sqlite_databases:
default:
type: libsql
url: https://my-turso-db.turso.io
Spin Operator 동작 원리
Spin Operator는 표준 Kubernetes Operator 패턴을 따른다. SpinApp 리소스가 생성되면 다음과 같은 리컨사일(reconcile) 루프를 실행한다.
- SpinApp 감지: Watch를 통해 SpinApp 리소스의 생성/수정/삭제를 감지
- 실행 전략 결정:
spec.executor필드에 따라 containerd-shim-spin 또는 SpinKube 커스텀 executor를 선택 - Deployment 생성: RuntimeClass가 설정된 Deployment를 생성
- Service 생성: HTTP 트리거가 있는 경우 Kubernetes Service를 자동 생성
- 오토스케일링 설정:
enableAutoscaling: true인 경우 HPA(Horizontal Pod Autoscaler) 또는 KEDA ScaledObject를 생성 - 상태 업데이트: SpinApp status 필드에 현재 상태를 기록
실전 배포 가이드
1단계: containerd Wasm Shim 설치
클러스터 노드에 containerd Wasm shim을 설치한다. k3d를 사용하는 로컬 환경 기준으로 설명한다.
# k3d 클러스터 생성 (Wasm shim이 포함된 이미지 사용)
k3d cluster create wasm-cluster \
--image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.17.0 \
--port "8081:80@loadbalancer" \
--agents 2
# 클러스터 상태 확인
kubectl get nodes
kubectl get runtimeclass
프로덕션 환경에서는 DaemonSet이나 Node Feature Discovery를 활용하여 Wasm shim을 설치한다.
# Node Feature Discovery로 Wasm 지원 노드 레이블링
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: wasm-shim-installer
namespace: kube-system
spec:
selector:
matchLabels:
app: wasm-shim-installer
template:
metadata:
labels:
app: wasm-shim-installer
spec:
hostPID: true
containers:
- name: installer
image: ghcr.io/spinkube/containerd-shim-spin/node-installer:v0.17.0
securityContext:
privileged: true
volumeMounts:
- name: containerd-config
mountPath: /etc/containerd
- name: shim-binary
mountPath: /opt/kwasm/bin
volumes:
- name: containerd-config
hostPath:
path: /etc/containerd
- name: shim-binary
hostPath:
path: /opt/kwasm/bin
2단계: SpinKube 설치
# cert-manager 설치 (Spin Operator의 webhook에 필요)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.0/cert-manager.yaml
# SpinKube CRDs 설치
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.4.0/spin-operator.crds.yaml
# RuntimeClass 설정
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.4.0/spin-operator.runtime-class.yaml
# Spin Operator를 Helm으로 설치
helm install spin-operator \
--namespace spin-operator \
--create-namespace \
--version 0.4.0 \
oci://ghcr.io/spinkube/charts/spin-operator
# SpinAppExecutor 생성
kubectl apply -f - <<EOF
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinAppExecutor
metadata:
name: containerd-shim-spin
spec:
createDeployment: true
deploymentConfig:
runtimeClassName: wasmtime-spin-v2
EOF
3단계: Spin 애플리케이션 작성 및 배포
# Spin CLI로 새 프로젝트 생성
spin new -t http-rust hello-k8s --accept-defaults
cd hello-k8s
// src/lib.rs - Spin HTTP 핸들러
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;
#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
let path = req.path();
let method = req.method().to_string();
println!("Received {method} request for {path}");
let body = serde_json::json!({
"message": "Hello from WebAssembly on Kubernetes!",
"path": path,
"method": method,
"runtime": "SpinKube",
"timestamp": chrono::Utc::now().to_rfc3339()
});
Ok(Response::builder()
.status(200)
.header("content-type", "application/json")
.body(body.to_string())
.build())
}
# 빌드 및 OCI 레지스트리에 푸시
spin build
spin registry push ghcr.io/myorg/hello-k8s:v1
# SpinApp으로 배포
kubectl apply -f - <<EOF
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
name: hello-k8s
spec:
image: ghcr.io/myorg/hello-k8s:v1
replicas: 3
executor: containerd-shim-spin
EOF
# 배포 상태 확인
kubectl get spinapp hello-k8s
kubectl get pods -l core.spinoperator.dev/app-name=hello-k8s
성능 벤치마크
Cold Start 비교
Cold Start는 Wasm이 컨테이너 대비 가장 큰 장점을 보이는 영역이다. 다양한 워크로드에 대한 측정 결과를 정리한다.
| 런타임 | 워크로드 | Cold Start (p50) | Cold Start (p99) | 이미지 크기 |
|---|---|---|---|---|
| Docker (Alpine+Go) | HTTP Server | 450ms | 1,200ms | 12MB |
| Docker (Distroless+Go) | HTTP Server | 380ms | 980ms | 8MB |
| gVisor | HTTP Server | 520ms | 1,500ms | 12MB |
| Spin (Wasm) | HTTP Handler | 1.2ms | 3.8ms | 680KB |
| WasmEdge | HTTP Handler | 2.1ms | 5.2ms | 720KB |
| Wasmtime (standalone) | HTTP Handler | 0.8ms | 2.5ms | 650KB |
Wasm은 Cold Start에서 컨테이너 대비 100~300배 빠른 시작 시간을 보인다. 이는 Wasm 모듈이 OS 부팅 과정 없이 즉시 실행 가능하기 때문이다.
메모리 풋프린트
# 100개 동시 인스턴스 기준 메모리 사용량 비교 (wrk 벤치마크)
# Docker Container: ~6.4GB (64MB per instance avg)
# Spin Wasm: ~320MB (3.2MB per instance avg)
# 메모리 효율: Wasm이 약 20배 적은 메모리 사용
| 메트릭 | Docker Container (100개) | Spin Wasm (100개) | 비율 |
|---|---|---|---|
| 총 메모리 | 6.4GB | 320MB | 20x |
| 인스턴스당 메모리 | 64MB | 3.2MB | 20x |
| 인스턴스당 시작 시간 | 450ms | 1.2ms | 375x |
| 이미지 크기 | 12MB | 680KB | 18x |
처리량 (Throughput) 벤치마크
JSON 직렬화/역직렬화 HTTP 엔드포인트 기준으로 wrk를 사용한 벤치마크 결과다.
# 벤치마크 명령
wrk -t12 -c400 -d30s http://localhost:8080/api/json
# Docker Container (Go HTTP Server)
# Requests/sec: 45,230
# Avg Latency: 8.8ms
# Transfer/sec: 12.3MB
# Spin Wasm (Rust HTTP Handler)
# Requests/sec: 38,750
# Avg Latency: 10.3ms
# Transfer/sec: 10.5MB
정상 상태(warm) 처리량에서는 컨테이너가 약 15~20% 우위를 보인다. 이는 Wasm 런타임의 ABI 경계 오버헤드 때문이다. 그러나 스케일 아웃 시나리오에서는 Wasm의 빠른 Cold Start가 전체 처리량을 역전시킨다.
프로덕션 적용 시 고려사항
현재 제한사항
Wasm on Kubernetes는 아직 성숙 단계에 도달하지 않았다. 프로덕션 적용 전에 반드시 인지해야 할 제한사항을 정리한다.
네트워킹 제한
# Wasm Pod는 현재 HostPort, NodePort 바인딩에 제한이 있다
# Service mesh (Istio, Linkerd) 사이드카 패턴이 동작하지 않는다
# → Spin의 내장 HTTP 트리거를 통해 우회 가능
# 현재 권장 구성
apiVersion: v1
kind: Service
metadata:
name: wasm-service
spec:
type: ClusterIP # NodePort/LoadBalancer도 가능하나 제한 확인 필요
selector:
core.spinoperator.dev/app-name: hello-k8s
ports:
- port: 80
targetPort: 80
스토리지 제한
- PersistentVolume 마운트가 지원되지 않거나 제한적
- Spin의 내장 Key-Value Store(Redis, SQLite)를 통한 상태 저장 권장
- 파일 시스템 접근은 WASI capability로 제한됨
디버깅 도구
kubectl exec을 통한 셸 접근 불가 (OS가 없으므로)kubectl logs는 정상 동작- 프로파일링 도구가 제한적 (WASI 관찰성 인터페이스가 개발 중)
# Wasm 워크로드 디버깅 기본 명령어
kubectl logs -l core.spinoperator.dev/app-name=hello-k8s -f
kubectl describe spinapp hello-k8s
kubectl get events --field-selector involvedObject.name=hello-k8s
컨테이너와 Wasm의 공존 전략
현실적으로 모든 워크로드를 Wasm으로 마이그레이션하는 것은 불가능하며 바람직하지도 않다. 권장되는 공존 전략은 다음과 같다.
| 워크로드 유형 | 권장 런타임 | 이유 |
|---|---|---|
| HTTP API (stateless) | Wasm (Spin) | 초고속 Cold Start, 높은 밀도 |
| 이벤트 핸들러 | Wasm (Spin) | 빠른 스케일 아웃/인 |
| 데이터베이스 | Container | 파일시스템/네트워크 요구 |
| ML Inference | Container (GPU) | GPU 접근 필요 |
| 레거시 앱 | Container | 마이그레이션 비용 대비 이점 부족 |
| Edge/IoT | Wasm | 극소형 바이너리, 이식성 |
Wasm과 기존 워크로드를 혼합 배포하는 클러스터 구성
# Node Pool 분리 전략
# Pool 1: 일반 컨테이너 워크로드
# Pool 2: Wasm Shim 설치된 Wasm 전용 노드
# Wasm 노드에 taint 추가
kubectl taint nodes wasm-node-1 workload-type=wasm:NoSchedule
# SpinApp에 toleration 추가
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
name: edge-handler
spec:
image: ghcr.io/myorg/edge-handler:v1
replicas: 5
executor: containerd-shim-spin
deploymentAnnotations:
app.kubernetes.io/part-of: edge-system
podSpec:
tolerations:
- key: workload-type
operator: Equal
value: wasm
effect: NoSchedule
nodeSelector:
kubernetes.io/arch: wasm32-wasi