DEV Community

Zach S. for Google Cloud

Posted on

Detecting WasmPlugin version drift in Cloud Load Balancing Service Extensions

When you attach a WebAssembly (Wasm) plugin to an Application Load Balancer with Service Extensions, the load balancer's extension chain (LbEdgeExtension, LbRouteExtension, or LbTrafficExtension) doesn't reference a specific plugin version. Instead, the extension chain points to the parent WasmPlugin resource, and the plugin's mainVersionId field controls which WasmPluginVersion actually executes on the proxy path.

That indirection makes rolling back a bad release easy, but it also makes it easy for a CI/CD job to stage a new build without ever serving it. If a pipeline pushes a new Wasm image to Artifact Registry and runs gcloud service-extensions wasm-plugin-versions create | Google Cloud SDK, Google Cloud creates the new version resource, yet mainVersionId stays pinned to the previous version until you update the parent plugin.

What the default CLI output leaves out

If you check your plugins with gcloud service-extensions wasm-plugins list | Google Cloud SDK, the default table shows the active MAINVERSION for each plugin, but it doesn't tell you if a newer version is sitting unpromoted:

~  gcloud service-extensions wasm-plugins list \
    --project="my-gcp-project" \
    --location="global"

NAME                DESCRIPTION                         MAINVERSION  UPDATETIME
auth-header-plugin  Injects tenant auth headers         v1           2026-10-14T16:20:11.402198Z
bot-score-plugin    Tags requests with bot score header v3           2026-11-05T09:12:44.118004Z
Enter fullscreen mode Exit fullscreen mode

To see whether auth-header-plugin has a newer version waiting, you have to query its version history separately with gcloud service-extensions wasm-plugin-versions list | Google Cloud SDK:

~  gcloud service-extensions wasm-plugin-versions list \
    --project="my-gcp-project" \
    --location="global" \
    --wasm-plugin="auth-header-plugin"

NAME  WASM_IMAGE                                                              WASM_IMAGE_DIGEST    CONFIG_SIZE  CONFIG_IMAGE  CONFIG_IMAGE_DIGEST  UPDATE_TIME
v1    us-south1-docker.pkg.dev/my-gcp-project/wasm-plugins/auth-header:1.0.0  sha256:4a8b91e0...   -                                               2026-10-14
v2    us-south1-docker.pkg.dev/my-gcp-project/wasm-plugins/auth-header:1.1.0  sha256:7c3d2e1f...   -                                               2026-11-08
Enter fullscreen mode Exit fullscreen mode

Here, v2 was uploaded on 2026-11-08, while the plugin is still serving v1 from October. Even after running both commands, neither table shows which load balancer extensions are actually calling auth-header-plugin.

How the API links extension chains to plugin versions

If you pull the raw JSON for a plugin from Method: projects.locations.wasmPlugins.get | Service Extensions (or pass --format=json to gcloud), the response includes a usedBy array listing every load balancer extension attached to that plugin:

{
  "description": "Injects tenant auth headers",
  "logConfig": {
    "enable": true,
    "minLogLevel": "INFO",
    "sampleRate": 1.0
  },
  "mainVersionId": "v1",
  "name": "projects/my-gcp-project/locations/global/wasmPlugins/auth-header-plugin",
  "updateTime": "2026-10-14T16:20:11.402198Z",
  "usedBy": [
    {
      "name": "projects/my-gcp-project/locations/global/lbRouteExtensions/prod-alb-route-ext"
    },
    {
      "name": "projects/my-gcp-project/locations/global/lbEdgeExtensions/prod-alb-edge-ext"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

On the load balancer side, each extension chain binds to the parent wasmPlugins/auth-header-plugin path in its service field rather than a versioned URL, as covered in Create a plugin | Service Extensions - Google Cloud Documentation:

"extensionChains": [
  {
    "extensions": [
      {
        "failOpen": false,
        "forwardHeaders": [
          "x-tenant-id",
          "authorization"
        ],
        "name": "normalize-tenant-auth",
        "service": "projects/my-gcp-project/locations/global/wasmPlugins/auth-header-plugin",
        "supportedEvents": [
          "REQUEST_HEADERS"
        ],
        "timeout": "0.100s"
      }
    ],
    "matchCondition": {
      "celExpression": "request.path.startsWith('/api/v1/')"
    },
    "name": "api-v1-chain"
  }
]
Enter fullscreen mode Exit fullscreen mode

Meanwhile, the individual WasmPluginVersion objects returned by Method: projects.locations.wasmPlugins.versions.list | Service Extensions record the createTime and the exact imageDigest resolved from Artifact Registry:

{
  "createTime": "2026-11-08T21:04:18.892310Z",
  "imageDigest": "sha256:7c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d",
  "imageUri": "us-south1-docker.pkg.dev/my-gcp-project/wasm-plugins/auth-header:1.1.0",
  "name": "projects/my-gcp-project/locations/global/wasmPlugins/auth-header-plugin/versions/v2",
  "updateTime": "2026-11-08T21:04:18.892310Z"
}
Enter fullscreen mode Exit fullscreen mode

Checking for unpromoted versions across a project

Instead of inspecting each plugin one at a time, we can join the wasm-plugins list and wasm-plugin-versions list JSON responses with jq to compare mainVersionId against the newest version by createTime and print the attached load balancer extensions:

#!/bin/bash

PROJECT="<YOUR_PROJECT_ID>"
LOCATION="global"

gcloud service-extensions wasm-plugins list \
    --project="$PROJECT" \
    --location="$LOCATION" \
    --format=json | \
jq -c '.[]' | while read -r plugin; do
  PLUGIN_NAME=$(echo "$plugin" | jq -r '.name | split("/") | last')

  gcloud service-extensions wasm-plugin-versions list \
      --project="$PROJECT" \
      --location="$LOCATION" \
      --wasm-plugin="$PLUGIN_NAME" \
      --format=json | \
  jq -r --argjson plugin "$plugin" '
    $plugin as $p |
    ($p.name | split("/") | last) as $name |
    $p.mainVersionId as $active_ver |
    (sort_by(.createTime) | last) as $latest |
    ($latest.name | split("/") | last) as $latest_ver |
    (map(select((.name | split("/") | last) == $active_ver))[0]) as $active |
    ([($p.usedBy // [])[] | .name | split("/") | last] | if length > 0 then join(", ") else "none" end) as $exts |
    "Plugin: \($name) | Active: \($active_ver) (\($active.imageDigest[0:19])) | Latest: \($latest_ver) (\($latest.imageDigest[0:19])) | Drift: \($active_ver != $latest_ver) | Used By: \($exts)"
  '
done
Enter fullscreen mode Exit fullscreen mode

Here is what the output looks like against our project:

~  bash wasm-plugin-version-check.sh

Plugin: auth-header-plugin | Active: v1 (sha256:4a8b91e0c3f2) | Latest: v2 (sha256:7c3d2e1f0a9b) | Drift: true | Used By: prod-alb-route-ext, prod-alb-edge-ext
Plugin: bot-score-plugin | Active: v3 (sha256:8e9f0a1b2c3d) | Latest: v3 (sha256:8e9f0a1b2c3d) | Drift: false | Used By: prod-alb-traffic-ext
Enter fullscreen mode Exit fullscreen mode

Promoting versions and a few sharp edges

When Drift: true shows up, you can promote the version or adjust your deployment workflow with a few rules in mind:

  • Uploading vs. promoting in one command: Running wasm-plugin-versions create only stages the version. If you want your CI/CD job to create a new version and switch mainVersionId in one step, use gcloud service-extensions wasm-plugins update | Google Cloud SDK and pass both --main-version and --image. If the version already exists (like v2 above), run gcloud service-extensions wasm-plugins update auth-header-plugin --main-version=v2 --location=global to roll all attached usedBy extensions forward without touching your load balancer configs.
  • Mutable container tags don't auto-update: Service Extensions resolves the container tag to its immutable sha256 imageDigest when the WasmPluginVersion is created, as described in Manage plugins | Service Extensions - Google Cloud Documentation. Pushing a new image to the same :latest tag in Artifact Registry won't change what an existing WasmPluginVersion runs - you still need to create a new version and update mainVersionId.
  • Cleaning up old versions: The API won't let you delete the WasmPluginVersion currently referenced by mainVersionId. Promote the new version first before pruning older versions.

Top comments (0)