EN

Challenges in developing CEL Playground with WebAssembly

This article explores the motivations and challenges behind development of the CEL Playground with WebAssembly.

Software Engineer

Matheus Faria

Introdução

Em postagens recentes no blog, discutimos sobre ValidatingAdmissionPolicy, a nova API do Kubernetes para aplicação de políticas, e apresentamos o CEL Playground, um ambiente web que desenvolvemos para testar rapidamente a linguagem Common Expression Language (CEL).

Você pode acessar os artigos anteriores para obter mais informações:

Este artigo explora as motivações e os desafios por trás do desenvolvimento do CEL Playground com WebAssembly.

Motivação

Houve dois principais fatores motivadores que levaram ao desenvolvimento do CEL Playground. O primeiro foi a necessidade de um ambiente interativo para testar e experimentar o CEL, e o segundo é a crescente popularidade do CEL, especialmente depois que foi adotado e se tornou parte do ecossistema do Kubernetes.

Necessidade

Quando começamos a adotar o CEL para as verificações do Marvin, e também porque ele já era usado no Kubernetes na API ValidatingAdmissionPolicy, pudemos perceber que havia uma falta de um ambiente onde as expressões CEL pudessem ser facilmente testadas e experimentadas. Linguagens de propósito semelhante, como Rego e CUE, já possuíam playgrounds web para essa finalidade.

Um playground simplifica o teste e a experimentação com uma linguagem, pois não requer instalações ou configurações prévias. Os usuários podem escrever e executar expressões diretamente de seus navegadores, inspirar-se em exemplos pré-existentes e também compartilhar seus experimentos.

Popularidade

Como mencionado anteriormente, outra motivação significativa para a criação do CEL Playground foi a crescente popularidade do CEL, especialmente após ser adotado pelo Kubernetes, uma das plataformas de tecnologia mais influentes e amplamente utilizadas, contendo um vasto ecossistema.

O CEL é usado nas APIs do Kubernetes para declarar regras de validação, regras de políticas e outras restrições ou condições. Um dos objetivos do Kubernetes é fornecer essa funcionalidade como uma biblioteca para que outros projetos possam executar as mesmas expressões CEL que o API Server (o componente do Kubernetes responsável por isso).

Já é possível observar novos projetos, como o Marvin e o Kyverno, que adotaram o CEL.

Como o CEL Playground funciona?

Uma das principais preocupações durante o desenvolvimento do CEL Playground era evitar custos com servidores, e é aí que o WebAssembly (WASM) desempenhou um papel crucial, permitindo a execução de código Go diretamente no navegador. Como resultado, as expressões CEL são executadas usando o cel-go.

Sempre que o botão Run é clicado, a função JavaScript run() é chamada. Esta função chama outra função JavaScript eval(exp, data) fornecendo a expressão CEL e os dados de entrada como argumentos.

Observe que você pode inspecionar e ler o código-fonte da função run(). No entanto, não consegue ver a função eval(exp, data) porque ela faz parte do binário WASM, que não é visível diretamente no navegador.


Ao carregar o CEL Playground, um arquivo binário WASM é carregado e executado, com a ajuda do arquivo de suporte JavaScript do Go SDK: $(go env GOROOT)/misc/wasm/wasm_exec.js.

Em sua essência, trata-se de um programa em Go que está em execução desde que o binário WASM foi compilado com o Go.

O programa Go define a função JavaScript eval(exp, data) e continua rodando. Dessa forma, a função permanece disponível para ser acionada quando o botão Run é clicado (através da função run() mostrada acima).

func main() {
    evalFunc := js.FuncOf(evalWrapper)
    js.Global().Set("eval", evalFunc)
    defer evalFunc.Release()
    <-make(chan bool)
}


A função evalWrapper, como o nome sugere, é um wrapper que permite executar uma expressão CEL dentro do contexto de um ambiente JavaScript/WASM. Para mais detalhes, verifique o repositório no GitHub.

Por fim, o comando abaixo é usado para compilar um programa Go em WebAssembly.

GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/assets/main.wasm cmd/wasm/main.go

Tamanho do binário WASM

De acordo com a documentação do Go, atualmente, arquivos WASM grandes são gerados pelo Go, com o menor tamanho possível em torno de ~2MB. Esse tamanho pode aumentar drasticamente ao importar bibliotecas, sendo comuns tamanhos de mais de 10MB.

No caso do CEL Playground, o tamanho do binário WASM chegou a impressionantes 70MB.

Uma das maneiras recomendadas para reduzir o tamanho do arquivo WASM é usar o TinyGo para a compilação.

No entanto, não tivemos sucesso ao tentar usar o TinyGo com duas versões diferentes.

A primeira tentativa foi com a versão v0.27.0, e o problema foi com o uso do pacote reflect pelo cel-go:

# github.com/google/cel-go/common/types
../../../../../home/tinygo/go/pkg/mod/github.com/google/cel-go@v0.16.0/common/types/map.go:187:24: undefined: reflect.MakeMapWithSize
../../../../../home/tinygo/go/pkg/mod/github.com/google/cel-go@v0.16.0/common/types/map.go:625:20: undefined: reflect.MakeMapWithSize


A versão v0.28.0 melhorou o suporte ao pacote reflect e resolveu o problema que tivemos na versão anterior.

Mas desta vez, enfrentamos um problema diferente:

# k8s.io/utils/net
../../../../../home/tinygo/go/pkg/mod/k8s.io/utils@v0.0.0-20230209194617-a36077c30491/net/port.go:116:20: undefined: net.ResolveUDPAddr
../../../../../home/tinygo/go/pkg/mod/k8s.io/utils@v0.0.0-20230209194617-a36077c30491/net/port.go:120:20: undefined: net.ListenUDP


A dependência indireta k8s.io/utils utiliza o pacote net da biblioteca padrão, que não está disponível no TinyGo. Esse módulo é importado indiretamente devido a algumas bibliotecas CEL do Kubernetes disponíveis no Playground.

Considerando que este código não é utilizado, tentamos criar um fork, remover o código desnecessário e utilizar o replace no go.mod. No entanto, ainda enfrentamos problemas com dependências indiretas do Kubernetes:

# k8s.io/klog/v2
../../../../../home/tinygo/go/pkg/mod/k8s.io/klog/v2@v2.90.1/klog_file.go:113:7: undefined: os.Symlink


Quando comentamos o código que importa as bibliotecas CEL do Kubernetes, foi possível compilar com o TinyGO, resultando em um binário de apenas 1.6MB.

Neste ponto, enfrentamos um dilema: manter as bibliotecas CEL do Kubernetes disponíveis no playground ou carregar um binário (muito) menor?

A decisão não foi difícil e escolhemos a primeira opção, permitindo que os usuários testem no Playground as mesmas expressões que são usadas nas APIs do Kubernetes.

Você pode encontrar os testes com o TinyGo no repositório do GitHub na ramo tinygo.

Se você tiver alguma sugestão quanto ao desafio de reduzir o tamanho do binário e/ou usar o TinyGo, sinta-se à vontade para abrir uma issue.

Embora o uso do TinyGo não tenha sido possível, outras ações foram tomadas para reduzir o tamanho do binário.
Conseguimos obter um arquivo de 10.8MB compactando o binário com gzip e usando algumas ldflags no comando de build:

GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/assets/main.wasm cmd/wasm/main.go
gzip --best -f web/assets/main.wasm

Por fim, o front-end teve que ser adaptado para descompactar o arquivo antes de instanciar o WASM, o que foi feito usando a biblioteca pako.

Compartilhando Experimentos

Compartilhar experimentos é um recurso útil e comum em playgrounds web.

A primeira versão do CEL Playground não tinha essa opção. Mas foi, na verdade, a primeira sugestão de melhoria que surgiu juntamente com o desafio de evitar custos com servidores.

Muitos playgrounds salvam experimentos para serem compartilhados no lado do servidor. Como nosso objetivo é manter a aplicação estática, a primeira ideia que testamos foi codificar as informações a serem compartilhadas em base64 e colocá-las na URL. Isso funcionou muito bem.

Também adicionamos compactação com gzip para encurtar o tamanho da URL. Não encontramos informações claras sobre o limite de tamanho de URLs nos navegadores. Por isso, o CEL Playground não o limita.

Conclusão

O objetivo deste artigo foi compartilhar as experiências e desafios que enfrentamos no desenvolvimento do CEL Playground com WebAssembly e Go. Também queríamos compartilhar um pouco sobre as motivações por trás e que levaram à criação deste projeto.

Esperamos que este conteúdo contribua de alguma forma com a comunidade. O código-fonte do projeto está disponível no GitHub, e qualquer contribuição é muito bem-vinda.

Além disso, não deixe de visitar nossos outros projetos de código aberto para Kubernetes: Zora e Marvin.

Newsletter Getup.

Atualizações sobre Kubernetes e Software Supply Chain Security todos os meses.

Operating Kubernetes in production for more than 13 years. With Quor, this experience extends to software supply chain security as well.