Comment Apache APISIX permet la fonctionnalité Wasm

Xinxin Zhu

April 7, 2023

Products

Pour permettre aux applications hautes performances de s'exécuter dans les navigateurs web, Apache APISIX prend en charge Wasm au niveau de la passerelle, un format d'instructions binaires qui permet une exécution efficace et sécurisée du code sur le web. Par conséquent, les développeurs peuvent utiliser des langages de programmation de haut niveau tels que C/C++/Go/Rust et respecter la spécification proxy-wasm pour créer des plugins Wasm. La spécification proxy-wasm garantit la compatibilité et l'interopérabilité avec d'autres systèmes. Plongeons dans cet article pour en apprendre plus de détails.

Qu'est-ce que Wasm

WebAssembly (abrégé Wasm) est un format d'instructions binaires pour une machine virtuelle basée sur une pile.

Avant l'émergence de Wasm, seul Javascript pouvait être exécuté dans les navigateurs web. Cependant, avec l'introduction de Wasm, des langages de haut niveau comme C/C++/Golang peuvent désormais être exécutés dans les navigateurs web. Les principaux navigateurs tels que Chrome, Firefox et Safari supportent désormais tous Wasm. De plus, grâce aux avancées du projet WASI (WebAssembly System Interface), les environnements côté serveur peuvent également supporter l'exécution d'instructions Wasm.

Actuellement, Apache APISIX prend en charge Wasm au niveau de la passerelle. Les développeurs peuvent utiliser des langages de programmation de haut niveau tels que C/C++/Go/Rust et respecter la spécification proxy-wasm pour créer des plugins Wasm.

wasm

Pourquoi APISIX prend-il en charge les plugins Wasm ?

Comparés aux plugins Lua natifs, les plugins Wasm offrent plusieurs avantages :

  • Évolutivité : En prenant en charge Wasm, APISIX peut utiliser le SDK fourni par proxy-wasm pour développer des plugins dans des langages de programmation de haut niveau tels que C++, Golang et Rust. Ces langages ont souvent des écosystèmes plus riches, permettant aux développeurs de créer des plugins plus fonctionnels.

  • Sécurité : Comme l'interaction entre APISIX et Wasm repose sur l'Application Binary Interface (ABI) fournie par proxy-wasm, cet accès est plus sécurisé. Les plugins Wasm ne peuvent apporter que des modifications spécifiques aux requêtes, garantissant qu'ils ne peuvent pas effectuer d'actions malveillantes. De plus, comme les plugins Wasm s'exécutent dans une machine virtuelle séparée, même si le plugin plante, cela n'affectera pas le processus principal d'APISIX.

Comment APISIX prend-il en charge Wasm ?

Maintenant que nous comprenons ce qu'est Wasm, examinons de haut en bas comment APISIX prend en charge les plugins Wasm.

apisix-wasm

Plugins Wasm d'APISIX

APISIX permet aux développeurs de créer des plugins en utilisant des langages de programmation de haut niveau populaires tels que C/C++, Go et Rust. Ces plugins peuvent être construits en utilisant le SDK correspondant et en suivant la spécification proxy-wasm.

proxy-wasm est une spécification pour les ABI entre les proxies L4/L7, introduite par Envoy. Cette spécification définit des ABI incluant la gestion de la mémoire, les extensions de proxy L4 et L7. Par exemple, dans HTTP(L7), la spécification proxy-wasm définit des ABI telles que proxy_on_http_request_headers, proxy_on_http_request_body, proxy_on_http_request_trailers et proxy_on_http_response_headers, permettant aux modules de récupérer et de modifier le contenu des requêtes à différentes étapes.

Par exemple, nous utiliserons Golang et proxy-wasm-go-sdk pour développer ce plugin :

Le proxy-wasm-go-sdk est un SDK pour la spécification proxy-wasm qui aide les développeurs à créer des plugins proxy-wasm plus facilement en utilisant Golang. Cependant, il est important de noter qu'en raison de certains problèmes avec le support natif de Golang pour WASI, ce SDK est implémenté sur la base de TinyGo. Pour plus d'informations, vous pouvez cliquer ici pour voir les détails.

La fonction principale de ce plugin est de modifier le code de statut de la réponse HTTP et le corps de la réponse d'une requête de modification HTTP, comme référencé dans le lien APISIX.

...
func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
        data, err := proxywasm.GetPluginConfiguration()
        if err != nil {
                proxywasm.LogErrorf("error reading plugin configuration: %v", err)
                return types.OnPluginStartStatusFailed
        }

        var p fastjson.Parser
        v, err := p.ParseBytes(data)
        if err != nil {
                proxywasm.LogErrorf("error decoding plugin configuration: %v", err)
                return types.OnPluginStartStatusFailed
        }
        ctx.Body = v.GetStringBytes("body")
        ctx.HttpStatus = uint32(v.GetUint("http_status"))
        if v.Exists("percentage") {
                ctx.Percentage = v.GetInt("percentage")
        } else {
                ctx.Percentage = 100
        }

        // vérification du schéma
        if ctx.HttpStatus < 200 {
                proxywasm.LogError("bad http_status")
                return types.OnPluginStartStatusFailed
        }
        if ctx.Percentage < 0 || ctx.Percentage > 100 {
                proxywasm.LogError("bad percentage")
                return types.OnPluginStartStatusFailed
        }

        return types.OnPluginStartStatusOK
}

func (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
        plugin := ctx.parent
        if !sampleHit(plugin.Percentage) {
                return types.ActionContinue
        }

        err := proxywasm.SendHttpResponse(plugin.HttpStatus, nil, plugin.Body, -1)
        if err != nil {
                proxywasm.LogErrorf("failed to send local response: %v", err)
                return types.ActionContinue
        }
        return types.ActionPause
}
...

Ensuite, nous utilisons TinyGo pour compiler le code Golang ci-dessus et générer un fichier .wasm.

tinygo build -o wasm_fault_injection.go.wasm -scheduler=none -target=wasi ./main.go

Une fois la compilation terminée, nous obtenons le fichier fault_injection.go.wasm.

Si vous êtes intéressé par le contenu du fichier wasm, vous pouvez utiliser wasm-tool pour voir le contenu spécifique du fichier wasm. wasm-tools dump hello.go.wasm

Configurez wasm_fault_injection.go.wasm dans le fichier config.yaml d'APISIX et nommez le plugin wasm_fault_injection.

apisix:
        ...
wasm:
  plugins:
    - name: wasm_fault_injection
      priority: 7997
      file: wasm_fault_injection.go.wasm

Ensuite, nous démarrons APISIX et créons une route qui référence le plugin Wasm :

curl  http://127.0.0.1:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{
    "uri":"/*",
    "upstream":{
        "type":"roundrobin",
        "timeout":{
            "connect":1,
            "read":1,
            "send":1
        },
        "nodes":{
            "httpbin.org:80":1
        }
    },
    "plugins":{
        "wasm_fault_injection":{
            "conf":"{\"http_status\":200, \"body\":\"Hello WebAssembly!\n\"}"
        }
    },
    "name":"wasm_fault_injection"
}'

Après avoir effectué un test d'accès, nous constatons que le corps de la réponse a été modifié en "Hello WebAssembly", ce qui indique que le plugin Wasm est maintenant effectif.

curl 127.0.0.1:9080/get -v
*   Trying 127.0.0.1:9080...
* Connected to 127.0.0.1 (127.0.0.1) port 9080 (#0)
> GET /get HTTP/1.1
> Host: 127.0.0.1:9080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 09 Feb 2023 07:46:50 GMT
< Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: APISIX/3.1.0
<
Hello WebAssembly!

Wasm-nginx-module

Après avoir appris comment Apache APISIX utilise les plugins Wasm, approfondissons la question : "Pourquoi pouvons-nous accéder et modifier le contenu des requêtes dans les plugins Wasm ?"

Comme APISIX utilise OpenResty comme framework sous-jacent, pour pouvoir accéder et modifier le contenu des requêtes dans les plugins Wasm, nous devons interagir avec les API fournies par OpenResty ou NGINX. C'est exactement ce que fait wasm-nginx-module.

wasm-nginx-module est un module NGINX qui supporte Wasm et a été développé par API7. Ce module tente d'implémenter proxy-wasm-abi sur la base de NGINX et encapsule l'API Lua, nous permettant de compléter les appels proxy-wasm-abi au niveau Lua. Pour plus d'informations, veuillez consulter wasm-nginx-module.

Par exemple, lorsque APISIX atteint la phase access, il appelle la méthode Lua on_http_request_headers fournie par wasm-nginx-module.

-- apisix/wasm.lua
...
local ok, err = wasm.on_http_request_headers(plugin_ctx)
   if not ok then
       core.log.error(name, ": failed to run wasm plugin: ", err)
       return 503
   end
end
...

Plus tard dans cette méthode, la méthode ngx_http_wasm_on_http fournie par wasm-nginx-module sera appelée.

ngx_int_t
ngx_http_wasm_on_http(ngx_http_wasm_plugin_ctx_t *hwp_ctx, ngx_http_request_t *r,
                      ngx_http_wasm_phase_t type, const u_char *body, size_t size,
                      int end_of_body)
{
    ...

    ctx = ngx_http_wasm_get_module_ctx(r);

    if (type == HTTP_REQUEST_HEADERS) {
        cb_name = &proxy_on_request_headers;
    } else if (type == HTTP_REQUEST_BODY) {
        cb_name = &proxy_on_request_body;
    } else if (type == HTTP_RESPONSE_HEADERS) {
        cb_name = &proxy_on_response_headers;
    } else {
        cb_name = &proxy_on_response_body;
    }

    if (type == HTTP_REQUEST_HEADERS || type == HTTP_RESPONSE_HEADERS) {
        if (hwp_ctx->hw_plugin->abi_version == PROXY_WASM_ABI_VER_010) {
            rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                                   cb_name,
                                   true, NGX_WASM_PARAM_I32_I32, http_ctx->id, 0);
        } else {
            rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                                   cb_name,
                                   true, NGX_WASM_PARAM_I32_I32_I32, http_ctx->id,
                                   0, 1);
        }

    } else {
        rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                              cb_name,
                              true, NGX_WASM_PARAM_I32_I32_I32, http_ctx->id,
                              size, end_of_body);
    }
    ...
}

Dans wasm-nginx-module, nous définissons le cb_name en fonction des différentes étapes, par exemple HTTP_REQUEST_HEADERS correspond à proxy_on_request_headers, puis nous appelons la méthode dans la VM via ngx_wasm_vm->call, qui est la méthode OnHttpRequestHeaders du plugin wasm mentionné précédemment dans cet article.

Avec cela, la chaîne d'appel complète d'APISIX appelant le plugin wasm et exécutant Golang est terminée. La chaîne d'appel est la suivante :

Wasm VM

Wasm VM est une machine virtuelle utilisée pour exécuter du code Wasm. wasm-nginx-module implémente deux types de machines virtuelles, "wasmtime" et "wasmedge", pour exécuter du code Wasm. Dans APISIX, "wasmtime" est celui par défaut pour exécuter du code Wasm.

Wasmtime est un runtime rapide et sécurisé pour WebAssembly et WASI, open-source par Bytecode Alliance. Il peut exécuter du code WebAssembly en dehors de l'environnement web et peut être utilisé comme un outil en ligne de commande ou intégré comme un moteur de runtime WebAssembly dans d'autres programmes en tant que bibliothèque. Wasmedge est une machine virtuelle WebAssembly (Wasm) légère, haute performance et évolutive, optimisée pour le calcul en périphérie. Elle peut être utilisée pour des applications cloud-native, en périphérie et décentralisées.

Tout d'abord, dans la VM Wasm, nous utilisons la méthode load pour charger le fichier .wasm en mémoire. Ensuite, nous pouvons utiliser la méthode call de la VM pour invoquer ces fonctions. La VM est basée sur l'implémentation de l'interface WASI, ce qui permet au code Wasm de non seulement s'exécuter côté navigateur, mais aussi de supporter l'exécution côté serveur.

Résumé

Nous avons compris ce qu'est Wasm et comment APISIX prend en charge les plugins Wasm. En offrant un support pour les plugins Wasm, APISIX améliore non seulement ses capacités à supporter plusieurs langages, tels que C++, Rust, Golang et AssemblyScript pour le développement de plugins, mais bénéficie également de l'écosystème étendu et des cas d'utilisation de WebAssembly, qui s'étend au-delà du navigateur vers des environnements cloud-native.

En conséquence, APISIX peut tirer parti de Wasm pour offrir des fonctionnalités plus avancées côté passerelle API, lui permettant de répondre à un plus large éventail de scénarios d'utilisation.

Tags: