Всем привет! На связи Егор Лазарев, DevOps-инженер компании «Флант». В последнее время WebAssembly (Wasm) набирает популярность благодаря своей высокой производительности и безопасности. Мне стало интересно, что это вообще такое и как работает на практике. Я решил поработать с Wasm в Kubernetes, так можно воспользоваться всеми плюсами кубов: шеринг ресурсов, отказоустойчивость, масштабируемость и прочее.
Но запускать Wasm-приложения в ванильном Kubernetes затруднительно, так как есть неудобства в настройке сред выполнения на рабочих узлах. Штатных средств недостаточно, чтобы легко конфигурировать узлы. Конечно, можно сконфигурировать один узел руками. Но если нужно обкатать различные рантаймы или большое количество приложений, то хочется максимально просто масштабировать кластер и управлять узлами декларативно. Поэтому я решил запустить Wasm-приложение в Deckhouse Kubernetes Platform (DKP). Эта платформа упрощает развёртывание и управление кластерами Kubernetes.
В этой статье я покажу, как запускать Wasm-приложения в Kubernetes с использованием DKP. Мы настроим окружение, установим необходимые компоненты и запустим простой WebAssembly-модуль.
Настройка NodeGroup
Думаю, будет правильно разделить «обычную» нагрузку и Wasm-нагрузку, чтобы был отдельный рабочий узел для экспериментов. Для этого создадим NodeGroup, с помощью которой платформа будет управлять отдельными узлами. При настройке нужно сразу добавить в NodeGroup лейблы, чтобы далее с помощью NodeSelector посадить нагрузку на нужные узлы:
kubectl create -f -<<EOF
apiVersion: deckhouse.io/v1
kind: NodeGroup
metadata:
name: wasm
spec:
cloudInstances:
classReference:
kind: YandexInstanceClass
name: worker
maxPerZone: 1
minPerZone: 1
zones:
- ru-central1-a
disruptions:
approvalMode: Automatic
kubelet:
containerLogMaxFiles: 4
containerLogMaxSize: 50Mi
resourceReservation:
mode: Auto
nodeTemplate:
labels:
node.deckhouse.io/group: wasm
nodeType: CloudEphemeral
EOF
После создания NodeGroup DKP закажет в облаке одну виртуальную машину в зоне ru-central1-a
, соответствующую YandexInstanceClass=worker
, а также добавит на неё лейбл node.deckhouse.io/group=wasm
.
Установка WasmEdge runtime
В Kubernetes для запуска Wasm-приложений нам потребуется специальный runtime — WASI (WebAssembly System Interface). В этой статье установим WasmEdge. А также нам нужно дополнить конфигурацию containerd настройками, связанными с новыми рантаймами. Для установки WasmEdge и дополнительного конфигурирования будем использовать ресурс NodeGroupConfiguration, который позволяет выполнять bash-скрипты на узлах.
Проверяем наличие bin-файла WASI и скачиваем по необходимости. Также с помощью bashbooster получаем мерж основного конфига containerd с конфигом из /etc/containerd/conf.d/*.toml
. При изменении /etc/containerd/config.toml
также будет перезапущен containerd:
kubectl create -f -<<EOF
apiVersion: deckhouse.io/v1alpha1
kind: NodeGroupConfiguration
metadata:
name: wasm-additional-shim.sh
spec:
bundles:
- '*'
content: |
[ -f "/bin/containerd-shim-wasmedge-v1" ] || curl -L https://github.com/containerd/runwasi/releases/download/containerd-shim-wasmedge%2Fv0.3.0/containerd-shim-wasmedge-$(uname -m | sed s/arm64/aarch64/g | sed s/amd64/x86_64/g).tar.gz | tar -xzf - -C /bin
mkdir -p /etc/containerd/conf.d
bb-sync-file /etc/containerd/conf.d/additional_shim.toml - containerd-config-changed << "EOF"
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge]
runtime_type = "io.containerd.wasmedge.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge.options]
BinaryName = "/bin/containerd-shim-wasmedge-v1"
EOF
nodeGroups:
- "wasm"
weight: 30
EOF
Определение новых RuntimeClass
После установки WasmEdge необходимо определить новый RuntimeClass, чтобы мы могли указать, как запускать ту или иную нагрузку: использовать дефолтный рантайм или какой-то другой, если явно в подах будем указывать spec.runtimeClassName
:
kubectl apply -f -<<EOF
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: wasmedge
handler: wasmedge
EOF
Запуск тестового Wasm-приложения
Предварительно, проверяем, что bashible закончил настройку узла и дополнил конфигурацию containerd:
root@test-wasm-75934c42-5956c-l5m7f:~# grep wasm /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge]
runtime_type = "io.containerd.wasmedge.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge.options]
BinaryName = "/bin/containerd-shim-wasmedge-v1"
Теперь можно запустить тестовое Wasm-приложение. Для этого создадим Job с простым WebAssembly-модулем. В джобе укажем NodeSelector и новосозданный RuntimeClass wasmedge
:
kubectl apply -f -<<EOF
apiVersion: batch/v1
kind: Job
metadata:
name: wasm-test
spec:
template:
spec:
containers:
- image: wasmedge/example-wasi:latest
name: wasm-test
resources: {}
restartPolicy: Never
runtimeClassName: wasmedge
nodeSelector:
node.deckhouse.io/group: wasm
backoffLimit: 1
EOF
Проверим статус и логи пода, чтобы убедиться, что всё работает корректно:
root@test-master-0:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
wasm-test-2g5jl 0/1 Completed 0 18s
root@test-master-0:~# kubectl logs wasm-test-2g5jl
Random number: -700610054
Random bytes: [163, 184, 229, 154, 4, 145, 145, 96, 181, 77, 64, 159, 123, 45, 5, 134, 93, 193, 207, 74, 129, 113, 204, 174, 188, 152, 172, 151, 125, 78, 199, 177, 127, 112, 116, 255, 188, 180, 47, 110, 22, 241, 63, 87, 78, 168, 36, 202, 168, 90, 248, 79, 38, 59, 204, 128, 141, 92, 209, 205, 129, 51, 71, 214, 91, 237, 115, 145, 77, 136, 166, 115, 221, 66, 123, 186, 19, 39, 122, 204, 103, 221, 89, 97, 148, 57, 250, 255, 165, 53, 14, 241, 97, 138, 147, 201, 204, 29, 76, 219, 128, 48, 143, 165, 138, 231, 62, 235, 190, 94, 142, 63, 197, 37, 57, 241, 33, 99, 240, 215, 216, 33, 68, 141, 82, 21, 152, 93]
Printed from wasi: This is from a main function
This is from a main function
The env vars are as follows.
KUBERNETES_SERVICE_PORT_HTTPS: 443
KUBERNETES_PORT_443_TCP: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP_ADDR: 10.222.0.1
KUBERNETES_PORT_443_TCP_PROTO: tcp
KUBERNETES_SERVICE_PORT: 443
HOSTNAME: wasm-test-2g5jl
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_SERVICE_HOST: 10.222.0.1
KUBERNETES_PORT: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP_PORT: 443
The args are as follows.
/wasi_example_main.wasm
File content is This is in a file
Под в статусе Completed
, то есть задание выполнилось и под завершил свою работу без ошибок.
В логах видим, что приложение сгенерировало случайное число и множество случайных байтов, выполнилась основная функция приложения. Также видим, что у приложения есть доступ к окружению и файловой системе.
Запуск тестового Wasm-приложения с init-контейнером
Теперь немного усложним задачу. Довольно часто в подах нам нужны init- или sidecar-контейнеры, которые должны запускаться из «обычного» container image, а не как Wasm. Для этого нам нужно определить для каждого контейнера свой рантайм запуска. Но проблема в том, что runtimeClassName
определяется на уровне пода, а не контейнеров.
Containerd поддерживает переключение среды выполнения контейнера, соответственно, нам нужен инструмент, который может определить, какая среда для контейнера нужна. Стандартный runc
, который используется у нас в кластере, это не поддерживает. Но в бета-версии такое есть у crun
. Поэтому я попробую реализовать задачу с его помощью.
Для начала нам нужно собрать crun
, так как при установке пакетным менеджером из официальных репозиториев он не поддерживает WasmEdge. Используем NodeGroupConfiguration
:
kubectl apply -f -<<EOF
apiVersion: deckhouse.io/v1alpha1
kind: NodeGroupConfiguration
metadata:
name: crun-install.sh
spec:
bundles:
- '*'
content: |
if ! [ -x /usr/local/bin/crun ]; then
apt-get update && apt-get install -y make git gcc build-essential pkgconf libtool libsystemd-dev libprotobuf-c-dev libcap-dev libseccomp-dev libyajl-dev go-md2man autoconf python3 automake
cd /root
[ -f "/root/.wasmedge/bin/wasmedge" ] || curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
git clone https://github.com/containers/crun && cd crun
./autogen.sh
source /root/.wasmedge/env && ./configure --with-wasmedge
make
make install
cd .. && rm -rf crun
fi
echo "crun has been installed"
mkdir -p /etc/containerd/conf.d
bb-sync-file /etc/containerd/conf.d/add_crun.toml - containerd-config-changed << "EOF"
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
runtime_type = "io.containerd.runc.v2"
pod_annotations = ["*.wasm.*", "wasm.*", "module.wasm.image/*", "*.module.wasm.image", "module.wasm.image/variant.*"]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
BinaryName = "/usr/local/bin/crun"
EOF
nodeGroups:
- wasm
weight: 30
EOF
Здесь мы устанавливаем непосредственно WasmEdge (ранее мы устанавливали WasmEdge runtime), необходимые зависимости и собираем crun
. Также добавляем в конфигурацию /etc/containerd/config.toml
новый контейнер рантайм, как мы это делали ранее.
Нужно обратить внимание на pod_annotations
. Это список аннотаций, который передается как в среду выполнения, так и в OCI аннотации контейнера. Зачем это нужно, рассмотрим чуть ниже.
Далее создаем новый RuntimeClass:
kubectl apply -f -<<EOF
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: crun
handler: crun
EOF
Теперь попробуем запустить нашу нагрузку:
kubectl apply -f -<<EOF
apiVersion: batch/v1
kind: Job
metadata:
name: wasm-test
spec:
template:
metadata:
annotations:
module.wasm.image/variant: compat-smart
spec:
initContainers:
- name: hello
image: busybox:latest
command: ['sh', '-c', 'echo "Hello, Habr!"']
containers:
- image: wasmedge/example-wasi:latest
name: wasm-test
resources: {}
restartPolicy: Never
runtimeClassName: crun
nodeSelector:
node.deckhouse.io/group: wasm
backoffLimit: 1
EOF
Здесь мы определяем runtimeClassName: crun
, чтобы за запуск контейнеров отвечал crun
, а не дефолтный runc
. Также добавляем аннотацию module.wasm.image/variant: compat-smart
, благодаря которой crun
понимает, в каком режиме работать.
Чтобы это работало, WASM-образ должен быть собран с OCI аннотацией:
...
"annotations": {
"run.oci.handler": "wasm"
},
...
Если у нас есть pod_annotations
в конфигурации containerd и аннотация compat-smart на кубовом объекте, то в таком случае crun
понимает, какую нагрузку запустить самому, а какую передать для запуска в Wasm runtime.
Смотрим состояние пода и логи. В логах увидим то же, что и ранее:
root@test-master-0:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
wasm-test-pn4gv 0/1 Completed 0 32s
root@test-master-0:~# kubectl logs wasm-test-pn4gv
Defaulted container "wasm-test" out of: wasm-test, hello (init)
Random number: -158793507
Random bytes: [210, 246, 181, 132, 184, 214, 110, 71, 198, 68, 154, 182, 253, 103, 116, 207, 5, 205, 185, 81, 19, 28, 61, 61, 85, 26, 222, 111, 239, 110, 21, 68, 119, 245, 153, 190, 105, 175, 191, 163, 48, 198, 41, 207, 155, 30, 122, 166, 23, 56, 59, 168, 91, 57, 103, 213, 145, 10, 130, 224, 28, 5, 73, 176, 206, 111, 37, 241, 38, 57, 98, 158, 150, 115, 249, 233, 194, 156, 13, 109, 85, 130, 232, 91, 253, 16, 8, 233, 92, 162, 237, 197, 151, 112, 52, 140, 83, 179, 31, 48, 233, 56, 54, 75, 43, 239, 233, 169, 169, 81, 36, 52, 59, 66, 102, 40, 52, 202, 34, 56, 167, 229, 197, 25, 72, 136, 147, 254]
Printed from wasi: This is from a main function
This is from a main function
The env vars are as follows.
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME: wasm-test-pn4gv
KUBERNETES_PORT: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP: tcp://10.222.0.1:443
KUBERNETES_PORT_443_TCP_PROTO: tcp
KUBERNETES_PORT_443_TCP_PORT: 443
KUBERNETES_PORT_443_TCP_ADDR: 10.222.0.1
KUBERNETES_SERVICE_HOST: 10.222.0.1
KUBERNETES_SERVICE_PORT: 443
KUBERNETES_SERVICE_PORT_HTTPS: 443
HOME: /
The args are as follows.
/wasi_example_main.wasm
File content is This is in a file
И логи init-контейнера:
root@test-master-0:~# kubectl logs wasm-test-pn4gv -c hello
Hello, Habr!
Заключение
Запуск WebAssembly-приложений в Kubernetes может показаться не совсем удобным, но с помощью Deckhouse это становится достаточно простым процессом. В этой статье я показал, как настроить окружение, установить необходимые компоненты и запустить тестовое Wasm-приложение. Надеюсь, что эта информация будет полезна и поможет в работе.
Deckhouse Kubernetes Platform предоставляет множество возможностей для управления Kubernetes-кластером, и мы обязательно будем делиться новыми практиками и советами в будущих статьях.
P. S.
Читайте также в нашем блоге: