Всем привет! На связи Егор Лазарев, 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.

Читайте также в нашем блоге:

Комментарии (0)