Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

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를 만들 필요가 없었을 것이다." 이 발언...

작성 글자: 0원문 글자: 12,660작성 단락: 0/353