들어가며
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 {
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는 세 가지 핵심 컴포넌트로 구성된다.
1. **Spin Operator**: Kubernetes 커스텀 컨트롤러로, SpinApp CRD를 감시하고 관리
2. **SpinApp CRD**: Spin 애플리케이션을 선언적으로 정의하는 커스텀 리소스
3. **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) 루프를 실행한다.
1. **SpinApp 감지**: Watch를 통해 SpinApp 리소스의 생성/수정/삭제를 감지
2. **실행 전략 결정**: `spec.executor` 필드에 따라 containerd-shim-spin 또는 SpinKube 커스텀 executor를 선택
3. **Deployment 생성**: RuntimeClass가 설정된 Deployment를 생성
4. **Service 생성**: HTTP 트리거가 있는 경우 Kubernetes Service를 자동 생성
5. **오토스케일링 설정**: `enableAutoscaling: true`인 경우 HPA(Horizontal Pod Autoscaler) 또는 KEDA ScaledObject를 생성
6. **상태 업데이트**: 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
참고자료
- [SpinKube 공식 문서](https://www.spinkube.dev/docs/)
- [Fermyon Spin 공식 문서](https://developer.fermyon.com/spin/v2/)
- [containerd-wasm-shims GitHub](https://github.com/containerd/runwasi)
- [WASI 공식 사이트](https://wasi.dev/)
- [Bytecode Alliance - Component Model](https://component-model.bytecodealliance.org/)
- [Kubernetes RuntimeClass 문서](https://kubernetes.io/docs/concepts/containers/runtime-class/)
- [WebAssembly Specification](https://webassembly.github.io/spec/core/)
- [Solomon Hykes의 WASM 관련 트윗](https://twitter.com/saborsh/status/1111004913222324225)
- [CNCF Wasm Landscape](https://landscape.cncf.io/wasm)
현재 단락 (1/353)
Solomon Hykes(Docker 창시자)는 2019년 이렇게 말했다. "만약 2008년에 WASM+WASI가 존재했다면, Docker를 만들 필요가 없었을 것이다." 이 발언...