Início Tecnologia WebAssembly em Kubernetes: O que você precisa saber

WebAssembly em Kubernetes: O que você precisa saber

19
0

Como algumas tecnologias inovadoras, pessoas diferentes têm pontos de vista diferentes sobre onde a WebAssembly se encaixa no cenário da tecnologia.

O WebAssembly (também chamado WASM) é certamente o assunto de muito hype agora. Mas o que é? É o assassino de JavaScript? É uma nova linguagem de programação para a web? É (como gostamos de dizer) a próxima onda de computação em nuvem? Ouvimos isso chamado muitas coisas: um EBPF melhor, a alternativa ao RISC V, um concorrente do Java (ou Flash), um impulsionador de desempenho para navegadores, um substituto para o Docker.

– Como pensar em WebAssembly

Neste post, ficarei longe desses debates e focarei apenas em como usar o WebAssembly no Kubernetes.

Minha abordagem e o caso de uso

Diferentemente das linguagens de programação regulares, você não escreve o WebAssembly diretamente: você escreve código que gera o WebAssembly. No momento, vá e a ferrugem são os principais idiomas da fonte. Eu sei que Kotlin e Python estão trabalhando para esse objetivo. Pode haver outros idiomas de que não estou ciente.

Eu decidi a Rust para este post por causa da minha familiaridade com o idioma. Em particular, vou manter o mesmo código em três arquiteturas diferentes:

  • Código de ferrugem para nativo regular como linha de base
  • Rusto-to-webassembly usando um tempo de execução incorporado a Wasmedge
  • Rusto-to-Webassembly usando um tempo de execução externo

Não se preocupe; Vou explicar a diferença entre as duas últimas abordagens mais tarde.

O caso de uso deve ser mais avançado do que o Hello World para destacar as capacidades do WebAssembly. Eu implementei um servidor HTTP imitando um único terminal do excelente utilitário de teste de API HTTPBIN. O código em si não é essencial, pois a postagem não é sobre ferrugem, mas, caso você esteja interessado, você pode encontrá -lo no Github. Eu adiciono um campo à resposta para retornar explicitamente a abordagem subjacente, respectivamente nativeAssim, embedou runtime.

Linha de base: ferrugem regular à nativa

Para a compilação nativa regular, estou usando um arquivo do Docker de vários estágios:

FROM rust:1.84-slim AS build                                             #1

RUN <
  1. Comece a partir da última imagem de ferrugem
  2. Heredocs para a vitória
  3. Instale a cadeia de ferramentas necessária para cruzar o compile
  4. Compilar estaticamente
  5. Eu poderia usar FROM scratchmas depois de ler isso, prefiro usar distritos
  6. Copie o executável da fase de compilação anterior

O final wasm-kubernetes:native A imagem pesa 8,71m, com sua imagem base distroless/static levando 6,03m deles.

Adaptando -se ao WebAssembly

A principal idéia por trás da WebAssembly é que ela é segura porque não pode acessar o sistema host. No entanto, devemos abrir um soquete para ouvir solicitações de entrada para executar um servidor HTTP. WebAssembly não pode fazer isso. Precisamos de um tempo de execução que forneça esse recurso e outros recursos dependentes do sistema. É o objetivo da interface do sistema WebAssembly.

A interface do sistema WebAssembly (WASI) é um grupo de especificações da API de padrões-track para software compilado com o padrão W3C WebAssembly (WASM). O WASI foi projetado para fornecer uma interface padrão segura para aplicativos que podem ser compilados em WASM de qualquer idioma e que possa ser executado em qualquer lugar – de navegadores a nuvens a dispositivos incorporados.

– Introdução a Wasi

A especificação v0.2 define as seguintes interfaces do sistema:

  • Relógios
  • Aleatório
  • FileSystem
  • Soquetes
  • CLI
  • Http

Alguns tempos de execução já implementam a especificação:

  • Wasmtime, desenvolvido pela ByteCode Alliance
  • Wasmer
  • Wazero, baseado em Go
  • Wasmedge, projetado para aplicativos de nuvem, computação de borda e IA
  • Girar para cargas de trabalho sem servidor

Eu tive que escolher sem ser especialista em nenhum deles. Finalmente decidi por Wasmedge por causa de seu foco na nuvem.

Devemos interceptar o código que chama as APIs do sistema e redirecioná -las para o tempo de execução. Em vez de interceptação de tempo de execução, o ecossistema de ferrugem fornece um mecanismo de patch: substituímos o código que chama APIs do sistema pelo código que chama APIs WASI. Devemos saber quais dependência chama qual API do sistema e esperamos que exista um patch para nossa versão de dependência.

[patch.crates-io]
tokio = { git = " branch = "v1.36.x" }  #1-2
socket2 = { git = " branch = "v0.5.x" }    #1

[dependencies]
tokio = { version = "1.36", features = ["rt", "macros", "net", "time", "io-util"] }     #2
axum = "0.8"
serde = { version = "1.0.217", features = ["derive"] }
  1. Patch o tokio e socket2 caixas com chamadas relacionadas a Wasi
  2. O mais recente tokio A caixa é 1,43, mas o patch mais recente (e único) é v1.36. Não podemos usar a versão mais recente porque não há patch.

Devemos alterar o DockerFile para compilar o código WebAssembly em vez de nativo:

FROM --platform=$BUILDPLATFORM rust:1.84-slim AS build

RUN <<3>
  1. Instale o alvo WASM
  2. Compilar com WASM
  3. Devemos ativar o wasmedge bandeira, bem como o tokio_unstable um, para compilar com êxito com o WebAssembly

Nesta fase, temos duas opções para o segundo estágio:

  • Use o tempo de execução do Wasmedge como uma imagem base:

    FROM --platform=$BUILDPLATFORM wasmedge/slim-runtime:0.13.5
    
    COPY --from=build /wasm/target/wasm32-wasip1/release/httpbin.wasm /httpbin.wasm
    
    CMD ["wasmedge", "--dir", ".:/", "/httpbin.wasm"]
    

    Do ponto de vista do uso, é bem semelhante à abordagem nativa.

  • Copie o arquivo WebAssembly e faça dele uma responsabilidade de tempo de execução:

    FROM scratch
    
    COPY --from=build /wasm/target/wasm32-wasip1/release/httpbin.wasm /httpbin.wasm
    
    ENTRYPOINT ["/httpbin.wasm"]
    

    É onde as coisas ficam interessantes.

O native a abordagem é um pouco melhor que a embed um, mas o runtime é o mais magro, pois contém apenas um único arquivo WebAssembly.

Executando a imagem WASM no Docker

Nem todos os tempos de execução do Docker são iguais e, para executar cargas de trabalho do WASM, precisamos nos aprofundar um pouco no nome do Docker. Enquanto o Docker, a empresa, criou o Docker como o produto, a realidade atual é que os contêineres evoluíram além do Docker e agora respondem às especificações.

O Iniciativa de contêiner aberto é uma estrutura de governança aberta com o objetivo expresso de criar padrões abertos do setor em torno dos formatos de contêineres e tempos de execução.

Fundada em junho de 2015 por Docker e outros líderes da indústria de contêineres, a OCI atualmente contém três especificações: a especificação de tempo de execução (especificação de tempo de execução), a especificação da imagem (especificação da imagem) e a especificação de distribuição (especificação de distribuição). A especificação de tempo de execução descreve como executar um “pacote de sistemas de arquivos” que não é embalado no disco. Em um nível de alto nível, uma implementação da OCI baixaria uma imagem OCI e descompacte essa imagem em um pacote de sistema de arquivos de tempo de execução da OCI. Nesse ponto, o pacote de tempo de execução da OCI seria executado por um tempo de execução da OCI.

– Iniciativa de contêiner aberto

A partir de então, usarei a terminologia adequada para imagens e contêineres da OCI. Nem todos os tempos de execução da OCI são iguais, e longe de todos eles podem executar cargas de trabalho do WASM: Orbstack, meu tempo de execução da OCI atual, não pode, mas o Docker Desktop pode, como um experimental recurso. De acordo com a documentação, devemos:

  • Usar containerd Para puxar e armazenar imagens
  • Ativar Wasm

Por fim, podemos executar a imagem OCI acima que contém o arquivo WASM selecionando um tempo de execução do WASM, Wasmedge, no meu caso. Vamos fazê-lo:

docker run --rm -p3000:3000 --runtime=io.containerd.wasmedge.v1 ghcr.io/ajavageek/wasm-kubernetes:runtime

io.containerd.wasmedge.v1 é a versão atual do tempo de execução do Wasmedge. Você deve ser autenticado com o GitHub se quiser experimentá -lo.

curl localhost:3000/get\?foo=bar | jq

O resultado é o mesmo da versão nativa:

{
  "flavor": "runtime",
  "args": {
    "foo": "bar"
  },
  "headers": {
    "accept": "*/*",
    "host": "localhost:3000",
    "user-agent": "curl/8.7.1"
  },
  "url": "/get?foo=bar"
}

O Wasi On Docker Desktop permite que você gire um servidor HTTP que se comporte como uma imagem nativa comum! Melhor ainda, o tamanho da imagem é tão pequeno quanto o arquivo WebAssembly que ele contém:

Executando a imagem WASM em Kubernetes

Agora vem a parte divertida: seu (s) provedor (s) de nuvem favorito não está usando o Docker Desktop. Apesar disso, ainda podemos executar cargas de trabalho no WebAssembly em Kubernetes. Para isso, precisamos entender um pouco sobre os níveis não muito baixos do que acontece quando você executa um contêiner, independentemente de ser de um tempo de execução da OCI ou Kubernetes.

Este último executa um processo; No nosso caso, é containerd. Ainda, containerd é apenas um orquestrador de outros processos de contêiner. Ele detecta o “sabor” do contêiner e chama o executável relevante. Por exemplo, para contêineres “regulares”, ele chama runc via a calço. O bom é que podemos instalar outros calços dedicados a outros tipos de contêineres, como o WASM. A ilustração a seguir, retirada do site de Wasmedge, resume o fluxo:

Apesar de alguns dos principais provedores de nuvem que oferecem integração do WASM, nenhum deles fornece um nível tão baixo. Continuarei no meu laptop, mas o Docker Desktop também não oferece uma integração direta: é hora de ser criativo. Por exemplo, o Minikube é uma distribuição de Kubernetes completa que cria uma máquina virtual Linux intermediária dentro de um ambiente do Docker. Podemos ssh na VM e configurá -la para o conteúdo do nosso coração. Vamos começar instalando minikube.

brew install minikube

Agora, começamos minikube com o containerd driver e especifique um perfil para ativar VMs configuradas de maneira diferente. Nós chamamos de maneira sem imaginação este perfil wasm.

minikube start --driver=docker --container-runtime=containerd -p=wasm

Dependendo de você já instalou minikube E se já baixou suas imagens, o início pode levar alguns segundos a dezenas de minutos. Ser paciente. A saída deve ser algo semelhante a:

😄  [wasm] minikube v1.35.0 on Darwin 15.1.1 (arm64)
✨  Using the docker driver based on user configuration
📌  Using Docker Desktop driver with root privileges
👍  Starting "wasm" primary control-plane node in "wasm" cluster
🚜  Pulling base image v0.0.46 ...
❗  minikube was unable to download gcr.io/k8s-minikube/kicbase:v0.0.46, but successfully downloaded docker.io/kicbase/stable:v0.0.46@sha256:fd2d445ddcc33ebc5c6b68a17e6219ea207ce63c005095ea1525296da2d1a279 as a fallback image
🔥  Creating docker container (CPUs=2, Memory=12200MB) ...
📦  Preparing Kubernetes v1.32.0 on containerd 1.7.24 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring CNI (Container Networking Interface) ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use "wasm" cluster and "default" namespace by default

Neste ponto, nosso objetivo é instalar na VM subjacente:

  • Wasmedge de executar cargas de trabalho de Wasm
  • Um calço para a ponte entre containerd e wasmedge
minikube ssh -p wasm

Podemos instalar o Wasmedge, mas não encontrei nenhum lugar para baixar o Shim. Na próxima etapa, construiremos os dois. Primeiro precisamos instalar a ferrugem:

curl --proto '=https' --tlsv1.2 -sSf  | sh

O script provavelmente reclama que não pode executar o binário baixado:

Cannot execute /tmp/tmp.NXPz8utAQx/rustup-init (likely because of mounting /tmp as noexec).
Please copy the file to a location where you can execute binaries and run ./rustup-init.

Siga as instruções:

cp /tmp/tmp.NXPz8utAQx/rustup-init .
./rustup-init

Prosseguir com a instalação padrão pressionando o ENTER botão. Quando terminar, obtenha seu shell atual.

. "$HOME/.cargo/env"

O sistema está pronto para construir Wasmedge e o calço.

sudo apt-get update
sudo apt-get install -y git

git clone 

cd runwasi
./scripts/setup-linux.sh

make build-wasmedge
INSTALL="sudo install" LN="sudo ln -sf" make install-wasmedge

A última etapa requer configurar o containerd Processar com o calço. Insira o seguinte trecho no [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] seção do /etc/containerd/config.toml arquivo:

        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedgev1]
          runtime_type = "io.containerd.wasmedge.v1"

Reiniciar containerd Para carregar a nova configuração.

sudo systemctl restart containerd

Nosso sistema está finalmente pronto para aceitar cargas de trabalho WebAssembly. Os usuários podem implantar uma Wasmedge pod Com o seguinte manifesto:

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: wasmedge                                                         #1
handler: wasmedgev1                                                      #2
---
apiVersion: v1
kind: Pod
metadata:
  name: runtime
  labels:
    arch: runtime
spec:
  containers:
    - name: runtime
      image: ghcr.io/ajavageek/wasm-kubernetes:runtime
  runtimeClassName: wasmedge                                             #3
  1. Cargas de trabalho de Wasmedge devem usar este nome
  2. Manipulador para usar. Deve ser o último segmento da seção adicionado no arquivo Toml, ou sejaAssim, containerd.runtimes.wasmedgev2
  3. Aponte para o nome da classe de tempo de execução, definimos logo acima

Eu usei um único Pod em vez de um pleno de pleno direito Deployment Para manter as coisas simples.

Observe os muitos níveis de indireção:

  1. O pod refere -se ao wasmedge Nome da classe de tempo de execução
  2. O wasmedge A classe de tempo de execução aponta para o wasmedgev1 manipulador
  3. O wasmedgev1 manipulador no arquivo Toml especifica o io.containerd.wasmedge.v1 Tipo de tempo de execução

Etapas finais

Para comparar as abordagens e testar nosso trabalho, podemos usar o minikube ingress addon e vcluster. O primeiro oferece um único ponto de acesso para todas as três cargas de trabalho, nativeAssim, embede runtimeenquanto o Vcluster isola as cargas de trabalho umas das outras em seu cluster virtual.

Vamos começar instalando o addon:

minikube -p wasm addons enable ingress

Ele implanta um controlador de entrada nginx no ingress-nginx namespace:

💡  ingress is an addon maintained by Kubernetes. For any concerns contact minikube on GitHub.
You can view the list of minikube maintainers at: 
💡  After the addon is enabled, please run "minikube tunnel" and your ingress resources would be available at "127.0.0.1"
    ▪ Using image registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.4
    ▪ Using image registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.4
    ▪ Using image registry.k8s.io/ingress-nginx/controller:v1.11.3
🔎  Verifying ingress addon...
🌟  The 'ingress' addon is enabled

Devemos criar um cluster virtual dedicado para implantar o Pod mais tarde.

helm upgrade --install runtime vcluster/vcluster --namespace runtime --create-namespace  --values vcluster.yaml

Vamos definir o Ingresso Servicee seus relacionados Pod em cada cluster virtual. Precisamos vcluster para sincronizar o Ingress com o controlador de entrada. Aqui está a configuração para conseguir isso:

sync:
  toHost:
    ingresses:
      enabled: true

A saída deve ser semelhante a:

Release "runtime" does not exist. Installing it now.
NAME: runtime
LAST DEPLOYED: Thu Jan 30 11:53:14 2025
NAMESPACE: runtime
STATUS: deployed
REVISION: 1
TEST SUITE: None

Podemos alterar o manifesto acima com o Service e Ingress para expor o Pod:

apiVersion: v1
kind: Service
metadata:
  name: runtime
spec:
  type: ClusterIP                                                        #1
  ports:
    - port: 3000                                                         #1
  selector:
    arch: runtime
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: runtime
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"                        #2
    nginx.ingress.kubernetes.io/rewrite-target: /$2                      #2
spec:
  ingressClassName: nginx
  rules:
    - host: localhost
      http:
        paths:
          - path: /runtime(/|$)(.*)                                      #4
            pathType: ImplementationSpecific                             #4
            backend:
              service:
                name: runtime
                port:
                  number: 3000
  1. Exponha o Pod dentro do cluster
  2. Anotações específicas para o nginx para lidar com a expressão regular de caminho e reescrever
  3. Caminho regex

Nginx encaminhará todas as solicitações começando com /runtime para o runtime serviço, removendo o prefixo. Para aplicar o manifesto, primeiro nos conectamos ao cluster virtual criado anteriormente:

vcluster connect runtime
11:53:21 info Waiting for vcluster to come up...
11:53:39 done vCluster is up and running
11:53:39 info Starting background proxy container...
11:53:39 done Switched active kube context to vcluster_embed_embed_vcluster_runtime_runtime_wasm
- Use `vcluster disconnect` to return to your previous kube context
- Use `kubectl get namespaces` to access the vcluster

Agora, aplique o manifesto:

kubectl apply -f runtime.yaml

Fazemos o mesmo com o embed e o native vagens, exceto o runtimeClassName como são imagens “regulares”.

O diagrama de implantação final é o seguinte:

O toque final é o túnel para expor os serviços:

minikube -p wasm tunnel
✅  Tunnel successfully started

📌  NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...

❗  The service/ingress runtime-x-default-x-runtime requires privileged ports to be exposed: [80 443]
🔑  sudo permission will be asked for it.
🏃  Starting tunnel for service runtime-x-default-x-runtime.
Password:

Vamos solicitar o contêiner leve que usa o tempo de execução do Wasmedge:

curl localhost/runtime/get\?foo=bar | jq

Temos a saída esperada:

{
  "flavor": "runtime",
  "args": {
    "foo": "bar"
  },
  "headers": {
    "user-agent": "curl/8.7.1",
    "x-forwarded-host": "localhost",
    "x-request-id": "dcbdfde4715fbfc163c7c9098cbdf077",
    "x-scheme": "http",
    "x-forwarded-for": "10.244.0.1",
    "x-forwarded-scheme": "http",
    "accept": "*/*",
    "x-real-ip": "10.244.0.1",
    "x-forwarded-proto": "http",
    "host": "localhost",
    "x-forwarded-port": "80"
  },
  "url": "/get?foo=bar"
}

Devemos obter resultados semelhantes com as outras abordagens, com diferentes flavor valores.

Conclusão

Neste post, mostrei como usar o WebAssembly em Kubernetes com o tempo de execução do Wasmedge. Eu criei três sabores para fins de comparação: nativeAssim, embede runtime. Os dois primeiros são imagens “regulares” do Docker, enquanto o último contém apenas um único arquivo WASM, o que o torna muito leve e seguro. No entanto, precisamos de um tempo de execução dedicado para executá -lo.

Os serviços regulares de Kubernetes gerenciados não permitem configurar um calço adicional, como o Shim Wasmedge. Mesmo no meu laptop, eu tinha que ser criativo para fazer isso acontecer. Eu tive que usar o Minikube e fazer muito esforço para configurar sua máquina virtual intermediária para executar cargas de trabalho do WASM em Kubernetes. No entanto, consegui executar todas as três imagens dentro de seu cluster virtual, expostas fora do cluster por um controlador de entrada nginx.

Agora, cabe a você decidir se o esforço extra vale a redução de 10x do tamanho da imagem e a segurança aprimorada. Espero que o futuro melhore o suporte para que os profissionais superassem os contras.

O código -fonte completo para esta postagem pode ser encontrado no GitHub.

Vá mais longe:

fonte