The custom code that you create for Service Extensions plugins must be packaged and published to Artifact Registry before other services can access it. This page describes how to create plugin code, package the code in a container image, and upload it to an Artifact Registry repository.
For information about Service Extensions, see Service Extensions overview.
Before you start, review the best practices for writing plugin code.
For more examples, see Code samples for plugins.
Before you begin
- Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
-
In the Google Cloud console, on the project selector page, select or create a Google Cloud project.
-
Make sure that billing is enabled for your Google Cloud project.
-
Enable the Network Services, Network Actions, Artifact Registry, Cloud Build, Cloud Logging, and Cloud Monitoring APIs.
- Install the Google Cloud CLI.
-
To initialize the gcloud CLI, run the following command:
gcloud init
-
In the Google Cloud console, on the project selector page, select or create a Google Cloud project.
-
Make sure that billing is enabled for your Google Cloud project.
-
Enable the Network Services, Network Actions, Artifact Registry, Cloud Build, Cloud Logging, and Cloud Monitoring APIs.
- Install the Google Cloud CLI.
-
To initialize the gcloud CLI, run the following command:
gcloud init
Set up the toolchain
C++
The Proxy-Wasm C++ SDK let developers use C++ to implement WebAssembly (Wasm) plugins for Service Extensions. The SDK uses the C++ WebAssembly toolchain Emscripten, as well as other libraries, such as protobuf and, optionally, Abseil.
Because building plugins written in C++ depends on specific versions of these tools and libraries, we recommend using the Docker image provided by the Proxy-Wasm C++ SDK. The instructions for C++ on this page use the Docker method. To build C++ plugins without using Docker, see the Proxy-Wasm C++ SDK documentation.
Install Docker if it's not already installed. Docker is included in Cloud Shell, the Google Cloud interactive shell environment.
Download a copy of the Proxy-Wasm C++ SDK. The simplest way to do this is to clone the Git repository:
git clone https://github.com/proxy-wasm/proxy-wasm-cpp-sdk.git
Build the Proxy-Wasm C++ SDK Docker image from the Dockerfile provided by the SDK:
cd proxy-wasm-cpp-sdk docker build -t wasmsdk:v2 -f Dockerfile-sdk .
When the command completes building SDK libraries and dependencies, the resulting Docker image is associated with the specified tag, which is
wasmsdk:v2
in this example.
Rust
The customization capability of Service Extensions is provided through the use of WebAssembly and Proxy-Wasm. WebAssembly supports a number of programming languages. Google recommends Rust because it provides excellent WebAssembly support and Proxy-Wasm provides a full-featured Rust SDK. Rust also provides good performance and strong type safety.
-
At the end of the installation process, follow any instructions printed to the console to finish the configuration process.
Add Wasm support to the Rust toolchain:
rustup target add wasm32-wasi
Create the plugin package
C++
Create a new directory, separate from
proxy-wasm-cpp-sdk
:mkdir myproject
Create a Makefile in the directory to specify the WASM files to build.
Here's a basic example that builds a
myproject.wasm
file from source code inmyproject.cc
. You can modify theall
target to specify other WASM files to be built from CC files with the same primary name.PROXY_WASM_CPP_SDK=/sdk all: myproject.wasm include ${PROXY_WASM_CPP_SDK}/Makefile.base_lite
Add a C++ source file for the plugin in the same directory. The names of C++ source files must match the WASM files that the Makefile targets, with the
.wasm
suffix replaced by.cc
. In this example, the source file must be namedmyproject.cc
.Add your plugin code to the source file.
Here's sample source code for a plugin that logs the request path when request headers are received and also logs a message when the processing of the request and response is complete.
#include "proxy_wasm_intrinsics.h" class MyHttpContext : public Context { public: explicit MyHttpContext(uint32_t id, RootContext* root) : Context(id, root) {} FilterHeadersStatus onRequestHeaders(uint32_t headers, bool end_of_stream) override { LOG_INFO("onRequestHeaders: hello from wasm"); // Route Extension example: host rewrite replaceRequestHeader(":host", "service-extensions.com"); replaceRequestHeader(":path", "/"); return FilterHeadersStatus::Continue; } FilterHeadersStatus onResponseHeaders(uint32_t headers, bool end_of_stream) override { LOG_INFO("onResponseHeaders: hello from wasm"); // Traffic Extension example: add response header addResponseHeader("hello", "service-extensions"); return FilterHeadersStatus::Continue; } }; static RegisterContextFactory register_StaticContext( CONTEXT_FACTORY(MyHttpContext), ROOT_FACTORY(RootContext));
The
ExampleContext::onRequestHeaders
andExampleContext::onDone
methods are callbacks that Service Extensions invokes.
Rust
Create a Rust package directory by using the
cargo new
command from Rust's package manager, Cargo:cargo new --lib my-wasm-plugin
The command creates a directory that contains a
cargo.toml
file that you can update to describe how to build the Rust package and ansrc
directory in which you store the plugin code.Update the
cargo.toml
file to specify the parameters required to build the package:[package] name = "my-wasm-plugin" version = "0.1.0" edition = "2021"
To register the Proxy-Wasm Rust SDK and logging support as dependencies, add the
dependencies
section. For example:[dependencies] proxy-wasm = "0.2" log = "0.4"
To build a dynamic library as required for plugins, add the
lib
section. For example:[lib] crate-type = ["cdylib"]
To reduce the size of the compiled plugin, add the
profile.release
section. For example:[profile.release] lto = true opt-level = 3 codegen-units = 1 panic = "abort" strip = "debuginfo"
Add your plugin code to the
lib.rs
file in thesrc
directory.For example, for your plugin to check that each HTTP request contains an
Authorization
header with the valuesecret
and generate an HTTP403 Forbidden
status code if it doesn't contain such a header and value, add the following lines:use log::info; use proxy_wasm::traits::*; use proxy_wasm::types::*; proxy_wasm::main! { { proxy_wasm::set_log_level(LogLevel::Trace); proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(MyHttpContext) }); } } struct MyHttpContext; impl Context for MyHttpContext {} impl HttpContext for MyHttpContext { fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { info!("onRequestHeaders: hello from wasm"); // Route extension example: host rewrite self.set_http_request_header(":host", Some("service-extensions.com")); self.set_http_request_header(":path", Some("/")); return Action::Continue; } fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { info!("onResponseHeaders: hello from wasm"); // Traffic extension example: add response header self.add_http_response_header("hello", "service-extensions"); return Action::Continue; } }
The
on_http_request_headers
function is a callback that Service Extensions invokes.
Compile the plugin
C++
To compile the plugin, run the following command from within the directory in which the Makefile and C++ plugin source files are located:
docker run -v $PWD:/work -w /work wasmsdk:v2 /build_wasm.sh
This command maps the current directory to the work
directory within the
Docker image, and then runs the build_wasm.sh
script provided by the
Docker image to build the plugin code. When the compile operation completes
successfully, a myproject.wasm
file, which contains the compiled Wasm
bytecode, is created in the current directory.
The first time that plugin code is compiled using the Docker image, Emscripten generates the standard libraries. To cache these in the Docker image so that they don't need to be regenerated each time, commit the image with the standard libraries after the first successful compilation:
docker commit `docker ps -l | grep wasmsdk:v2 | awk '{print $1}'` wasmsdk:v2
For more information about building C++ plugins, see the Proxy-Wasm C++ SDK documentation.
Rust
To compile the plugin code, run the cargo build
command:
cargo build --release --target wasm32-wasi
When the build completes successfully, a Finished release [optimized] target(s)
message appears. If you're familiar with Envoy,
you might want to load the plugin in Envoy and verify its behavior.
Publish the image to Artifact Registry
Publish the compiled plugin to Artifact Registry so that Google Cloud services can access it.
Store the plugin artifact in an image name with the following format:
LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE
Replace the following:
LOCATION
: the regional or multi-regional location of the repository. For increased reliability, specify a multi-regional location.PROJECT_ID
: your Google Cloud console project ID.REPOSITORY
: the name of the repository where you intend to store the image.IMAGE
: the name of the container image in the repository.
For example:
us-docker.pkg.dev/my-project/my-repo/my-wasm-plugin
Create a local
package/
directory and copy your publishable plugin artifact into it. The following sample copies the plugin artifact built by Rust:mkdir -p package && cp -f target/wasm32-wasi/release/my_wasm_plugin.wasm package/plugin.wasm
Create a
package/Dockerfile
file with the following contents:FROM scratch COPY plugin.wasm plugin.wasm
Package the plugin code by using either Cloud Build or Docker.
Cloud Build
Create a
package/cloudbuild.yaml
build config file with the following contents:steps: - name: 'gcr.io/cloud-builders/docker' args: [ 'build', '--no-cache', '--platform', 'wasm', '-t', 'LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod', '.' ] images: [ 'LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod' ]
Trigger the operation to build the plugin container and publish it to Artifact Registry:
gcloud builds submit --config package/cloudbuild.yaml package/
Docker
Install Docker if it's not already installed. Docker is included in Cloud Shell, the Google Cloud interactive shell environment.
Configure Docker to authenticate to Artifact Registry. For example:
gcloud auth login gcloud auth configure-docker LOCATION-docker.pkg.dev gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://LOCATION-docker.pkg.dev
Build the container image:
docker build --no-cache --platform wasm -t my-wasm-plugin package/
To tag the local image with the repository image name, use image tags, such as
prod
in the following example:docker tag my-wasm-plugin LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod
Publish your tagged container image to Artifact Registry.
docker push LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod
To confirm that the image was successfully pushed to Artifact Registry, run the
gcloud artifacts docker images list
command.gcloud artifacts docker images list LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE --include-tags
Prepare and upload the configuration file
Plugins might optionally receive configuration data, which can affect plugin behavior at runtime. Configuration data can be text or binary and in any format accepted by the plugin. If the size of the configuration data is large, you might need to upload the configuration file to Artifact Registry.
Update the plugin code for configuration data
C++
Configuration data is passed to the onConfigure
method of the root context
object, which is instantiated once at plugin startup, and remains active for
the lifetime of the Wasm runtime that hosts the plugin. The root context
object is an instance of the RootContext
class or of a subclass of
RootContext
.
Plugin code can control what class is used for the root context through the
RegisterContextFactory
value. For example, the following plugin code
registers ExampleRootContext
and ExampleContext
as the classes to use
for root and stream context instances. It then reads a secret authorization
value from plugin configuration data, which stream context instances can
access through the root context object.
#include <string>
#include <string_view>
#include "third_party/proxy_wasm_cpp_sdk/proxy_wasm_intrinsics.h"
class ExampleRootContext : public RootContext {
public:
explicit ExampleRootContext(uint32_t id, std::string_view root_id)
: RootContext(id, root_id) {}
bool onConfigure(size_t config_len) override {
secret_ = getBufferBytes(WasmBufferType::PluginConfiguration, 0, config_len)
->toString();
return true;
}
const std::string& secret() const { return secret_; }
private:
std::string secret_;
};
class ExampleContext : public Context {
public:
explicit ExampleContext(uint32_t id, RootContext* root)
: Context(id, root),
secret_(static_cast<ExampleRootContext*>(root)->secret()) {}
FilterHeadersStatus onRequestHeaders(uint32_t headers,
bool end_of_stream) override {
// use secret_
return FilterHeadersStatus::Continue;
}
private:
const std::string& secret_;
};
static RegisterContextFactory register_ExampleContext(
CONTEXT_FACTORY(ExampleContext), ROOT_FACTORY(ExampleRootContext));
Rust
Configuration data is read in from a RootContext
trait, which is
instantiated once at plugin startup, and remains active for the lifetime of
the Wasm runtime that hosts the plugin. RootContext
traits are useful for
performing operations or maintaining state across many requests.
To update the plugin code, implement the on_configure
function to receive the configuration data on startup. For example, add the
following lines of code to your plugin code so that the on_configure
method can read the secret
authorization value from configuration data and
pass it to a newly instantiated HttpContext
trait for request processing.
struct DemoPluginRoot {
secret: Rc<String>,
}
impl RootContext for DemoPluginRoot {
fn on_configure(&mut self, _: usize) -> bool {
if let Some(config_bytes) = self.get_plugin_configuration() {
self.secret = Rc::new(String::from_utf8(config_bytes).unwrap())
}
true
}
fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> {
Some(Box::new(DemoPlugin {
secret: self.secret.clone(),
}))
}
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
}
impl Context for DemoPluginRoot {}
Upload the configuration file
If the size of the data to be delivered to your plugin in on_configure
exceeds 900 KiB, publish it to Artifact Registry by following the method
described in Publish the image to Artifact Registry.
In this case, save the configuration file by the name plugin.config
in the
container image.
The next step is to create a plugin.
While creating the plugin, you need to provide the URI to the uploaded container image.
Create a new version of plugin code
To create a new version of the plugin code, edit the plugin file. Then, as described in the preceding sections, compile the plugin code, repackage it, and upload it to Artifact Registry.
Callbacks
The code that you compile into Wasm can define arbitrary methods or functions, but some of them have a special significance. These are the methods that are defined in the Proxy-Wasm SDK for the language of your choice and map to the Proxy-Wasm Application Binary Interface (ABI) specifications. Service Extensions invokes these callbacks in response to user requests or in response to plugin lifecycle events.
These callbacks are listed in the following table in the order in which they're typically invoked:
Callback name and description | C++ method name | Rust method name |
---|---|---|
START_PLUGIN : Invoked when a plugin is
started. |
RootContext::onStart
|
RootContext::on_vm_start
|
CONFIGURE_PLUGIN : Invoked after a plugin
is started, to provide configuration data
to the plugin. |
RootContext::onConfigure
|
RootContext::on_configure
|
CREATE_CONTEXT : Invoked when a new
stream context is created. Each stream
corresponds to a client HTTP request. |
Context::onCreate
|
RootContext:: create_http_context |
HTTP_REQUEST_HEADERS : Invoked to process
HTTP request headers. |
Context::onRequestHeaders
|
HttpContext:: on_http_request_headers |
HTTP_REQUEST_BODY : Invoked repeatedly
to process HTTP request body chunks. |
Context::onRequestBody
|
HttpContext:: on_http_request_body |
HTTP_RESPONSE_HEADERS : Invoked to
process HTTP response headers. |
Context::onResponseHeaders
|
HttpContext:: on_http_response_headers |
HTTP_RESPONSE_BODY : Invoked repeatedly
to process HTTP response body chunks. |
Context::onResponseBody
|
HttpContext:: on_http_response_body |
DONE : Invoked when the processing of a
plugin has completed. |
Context::onDone
|
Context::on_done
|
DELETE : Invoked when the stream context
object corresponding to a client HTTP
request is deleted. |
Context::onDelete
|
(no callback) |