Skip to content
Published on

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

Authors
  • Name
    Twitter

Kubernetes WebAssembly

들어가며

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 1WASI Preview 2 (0.2.x)
인터페이스 정의witx(텍스트 형식)WIT(Wasm Interface Type)
모듈 모델Core ModuleComponent 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/SpinWasm Module
                           → containerd-shim-slight-v2 → Wasmtime/SpiderLightningWasm Module
                           → containerd-shim-wasmedge-v1 → WasmEdgeWasm 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 Server450ms1,200ms12MB
Docker (Distroless+Go)HTTP Server380ms980ms8MB
gVisorHTTP Server520ms1,500ms12MB
Spin (Wasm)HTTP Handler1.2ms3.8ms680KB
WasmEdgeHTTP Handler2.1ms5.2ms720KB
Wasmtime (standalone)HTTP Handler0.8ms2.5ms650KB

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.4GB320MB20x
인스턴스당 메모리64MB3.2MB20x
인스턴스당 시작 시간450ms1.2ms375x
이미지 크기12MB680KB18x

처리량 (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 InferenceContainer (GPU)GPU 접근 필요
레거시 앱Container마이그레이션 비용 대비 이점 부족
Edge/IoTWasm극소형 바이너리, 이식성

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

참고자료