Deep Dive into Envoy Dynamic Module: Principles, Development, and End-to-End Demo

A comprehensive guide to Envoy Proxy Dynamic Module—principles, configuration, development, and hands-on demo—with official Rust SDK and multi-language capabilities. Step by step, learn how to achieve high-performance native extensions.

In my previous post, Understanding Envoy’s Extension and Integration Mechanisms: From Built-in Filters to Dynamic Modules, I systematically reviewed the various extension and integration mechanisms of Envoy and specifically recommended the emerging Dynamic Module mechanism. This post focuses on the principles and full workflow of the Dynamic Module, helping you efficiently master native extension development for Envoy.

What is a Dynamic Module?

Dynamic Module is an experimental feature introduced in Envoy Proxy v1.34, allowing developers to load custom extensions as shared libraries (.so) dynamically. It does not depend on a virtual machine or out-of-process integration—extensions are connected directly to the main process via C ABI, achieving extremely high performance and modern SDK support (such as the official Rust SDK).

Technical Principles

A Dynamic Module is essentially a shared library that implements specific ABI entry points, interacting with Envoy through interfaces defined in a C header file. The official SDK primarily supports Rust (for type safety and better developer experience), but Go, C++, and other languages that can compile to a C ABI-compatible dynamic library are also viable. The community already offers an experimental Go implementation.

Key Points of Dynamic Module Loading Mechanism
  • Set the search path for dynamic modules via the ENVOY_DYNAMIC_MODULES_SEARCH_PATH environment variable.
  • .so files must be named according to the lib{name}.so convention, where {name} matches the dynamic_module_config.name in your Envoy config.
  • Once loaded, the module is directly inserted into the HTTP filter chain and enjoys equal status to native filters.

Note: Dynamic Modules run within the main process and share memory and privileges—there is no isolation or sandboxing. Always load only trusted modules.

Advantages and Limitations

As summarized in my previous article, Dynamic Modules offer several advantages:

  • Ultra-high performance: No intermediate VM, no cross-process or language serialization, suitable for high-throughput scenarios.
  • Modern development: The Rust SDK is type-safe, well-documented, and offers a much better developer experience than Wasm.
  • Highly flexible: You can implement virtually any custom logic at the HTTP layer.
  • Multi-language potential: While officially Rust is supported, Go, C++, etc., are all theoretically possible.

Limitations:

  • No security sandbox—only suitable for controlled environments.
  • Tight coupling with Envoy version; any upgrade requires module recompilation.

Hands-on: Complete Dynamic Module Demo with Official Examples

Next, let’s walk through a one-click experience of Envoy Dynamic Modules (Rust/Go versions) from source to container, based on the dynamic-modules-examples official repo.

This lab includes several typical modules to help you understand the extension capabilities of the Dynamic Module:

  1. access_logger
    • Structured logging for all request/response headers; outputs JSON for observability and debugging.
  2. header_mutation
    • Supports adding/removing headers and policy injection—useful for canary/A-B testing and policy rollout.
  3. passthrough (Rust/Go implementations)
    • Demonstrates multi-language compatibility and filter chain loading (no logic).

1. Clone the source repo

git clone https://github.com/envoyproxy/dynamic-modules-examples.git
cd dynamic-modules-examples
git checkout 74e9dbf80c0ea056e0ee5fc1b8f1c35d3a4c9a98
Note
Pinning to a specific commit ensures consistency with official docs and experiments, avoiding compatibility issues caused by changes on the main branch.

2. Build all dynamic modules and the Envoy image with Docker

docker build -t envoy-with-dynamic-modules:latest .
  • This command will:
    • Compile the Rust/Go dynamic modules, producing .so shared libraries.
    • Build a custom Envoy binary.
    • Package everything into a runnable Docker image.
  • By default, only the host architecture is supported (e.g., x86_64 or ARM64). For cross-platform, you must adapt the Dockerfile manually.
How it works
  • Dynamic modules are compiled as .so shared libraries, dynamically loaded into the Envoy process via C ABI.
  • Ensure your host and container architectures match; otherwise, the module or Envoy binary may not be compatible.

3. Envoy configuration and mounting

The included envoy.yaml is already configured for dynamic module loading. Here’s a typical snippet:

static_resources:
  listeners:
    - name: main
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 1062   # Listening port
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                route_config: ...
                http_filters:
                  - name: envoy.filters.http.dynamic_module
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter
                      dynamic_module_config:
                        name: access_logger  # Must match the .so file name
                      filter_name: access_logger
                  - name: envoy.filters.http.router
  clusters:
    - name: echo_service
      connect_timeout: 0.25s
      type: STATIC
      load_assignment: ...
  • The dynamic_module_config.name field tells Envoy to look for libaccess_logger.so in the directory specified by the ENVOY_DYNAMIC_MODULES_SEARCH_PATH environment variable.
  • For example, name: access_logger requires a libaccess_logger.so file.
  • filter_name is used for filter chain identification; it’s recommended (but not required) to match the module name.
  • In your docker-compose.yaml, you’ll see environment variables and volume mounts to ensure modules and config files are loaded into the container:
services:
  envoy:
    image: envoy-with-dynamic-modules:latest
    volumes:
      - ./integration/envoy.yaml:/etc/envoy/envoy.yaml
      - ./target/x86_64-unknown-linux-gnu/release:/usr/local/lib
      - ./integration/access_logs:/access_logs
    ports:
      - 1062:1062
    environment:
      - ENVOY_DYNAMIC_MODULES_SEARCH_PATH=/usr/local/lib
  echo:
    image: hashicorp/http-echo
    command: ["-text=Hello from echo server"]
    ports:
      - 1234:1234
Details
  • All dynamic modules are stored in /usr/local/lib; mount additional .so files as needed (e.g., header_mutation, passthrough, etc.)
  • Multi-port demonstration: test different modules using different listeners/routes/filters.
  • Access logs are output via mounted directories for local analysis and debugging.

4. Start the lab environment (Envoy + Echo)

docker compose up -d
  • This launches two services:
    • Envoy: Loads all integrated dynamic modules automatically.
    • Echo Server: Simple HTTP backend for verifying traffic and filter effects.
  • Envoy listens on ports 1062, 1063, 1064, each configured with different modules (see envoy.yaml).

5. Send a request to test dynamic module effects

For the access_logger module (listening on 1062):

curl localhost:1062/uuid

You should receive:

Hello from echo server

Likewise, try ports 1063/1064 for other modules (header_mutation, passthrough, etc.).

6. Check structured logs and header changes

Access logs help you analyze module behavior (such as access_logger). For header_mutation, check the response headers for injected values.

cat integration/access_logs/access_log_0.log
  • access_logger outputs structured headers for every request and response.
  • header_mutation injects custom fields like foo: bar, foo2: bar2 into response headers.
  • Compare the logs and responses to verify the full effect of your filter chain.

7. Tear down and clean up

docker compose down
  • This stops and cleans up all containers and mounts.
  • To retest, modify the source, rebuild the module, and launch again.

Important Notes

  • Ensure that all .so files are compiled for the exact Envoy version you are running (same Git SHA/Release Tag), or ABI incompatibilities will arise. Every Envoy upgrade requires recompilation of all dynamic modules.
  • The dynamic_module_config.name field in the config must match the module filename.
  • Rust is officially supported; Go is experimental; C/C++ support depends on ABI compatibility.
  • Only load modules that are fully controlled and trusted by your organization.

Summary

Dynamic Modules bridge the gap between C++/Wasm/Lua/out-of-process extension methods, offering both high performance and developer friendliness. For enterprise, self-managed scenarios, Dynamic Modules are highly recommended. If you wish to discuss implementation or filter chain details, feel free to leave a comment.

References

Post Navigation

Comments