Every application has to deal with configuration data, and applications built using Fermyon Spin are no exception here. Instead of baking configuration data into your source code, you should attach it from the outside to alter behavior of your applications without having to re-compile your source code.

This short article demonstrates how to deal with configuration data when building applications using WebAssembly and Fermyon Spin. You can find the source code of the sample application (spin-to-slack) on GitHub at ThorstenHans/spin-to-slack.



Configuration data in the Spin

Every Spin application has a manifest. You can think of the (Spin) manifest (spin.toml) as the center of gravity. We use spin.toml to define essential metadata about our application, specify and wire up components, and provide configuration data.

As always, we have different kinds of configuration data. We must distinguish between sensitive and non-sensitive configuration data. We can either put our configuration data directly in spin.toml or use a configuration provider to inject configuration data at runtime. Configuration data can be specified on the scope of the application (by using variables), or on the scope of a particular component.

We will now create a simple Spin application for demonstration purposes that forwards messages received via HTTP to Slack. This allows us to use and understand sensitive and non-sensitive configuration data by implementing something more sophisticated than Hello World 😁.

# create a new spin application
spin new -o ./slack-integration http-rust slack-integration-sample

# fire-up your editor
code ./slack-integration

Configure the Slack webhook

We have to configure the receiver side (slack in this case). Check out this article explaining how to configure incoming webhook for your slack network of choice. Grab the incoming webhook URL. We’ll use it in this article as a configuration value (slack_webhook_url).

Layout the configuration struct

Before we dive into specifying all necessary configuration data in the Spin manifest, let’s lay out the actual configuration struct that we will use later in our component implementation:

pub struct Configuration
{
  pub channel: String,
  pub is_markdown: bool,
  pub slack_webhook_url: String,
}

Application variables

Now that we know which configuration data our application requires, we can dive into providing them. We will treat the slack_webhook_url as sensitive configuration data.

First, we will update our Spin manifest and introduce necessary global variables. We’ll set slack_webhook_url as required. For is_markdown and channel, we provide a default value and mark them as non-sensitive using the secert = false property:

# Spin.toml

[variables]
slack_webhook_url = { required = true }
channel = { default = "#blog", secret = false }
is_markdown  = { default = "true", secret = false }

Remember that a variable may not be required when a default value is specified.

Component configuration

Component code can not load configuration data from variables. If we want to use configuration data in a component, we must provide it as part of the component configuration ([component.config]).

On the component scope, developers should be able to configure is_markdown and channel individually. The value of slack_webhook_url will be constructed by referencing the global variable slack_webhook_url (Yes, global variables and component configurations can have the same name.)

Let’s update the component configuration to reflect our requirements:

# Spin.toml

[[component]]
id = "slack-notifier"
source = "target/wasm32-wasi/release/slack_notifier.wasm"
allowed_http_hosts = [ "hooks.slack.com"]

[component.config]
slack_webhook_url = "{{ slack_webhook_url }}"
channel = "{{ channel }}"
is_markdown = "true"

[component.trigger]
route = "/..."
[component.build]
command = "cargo build --target wasm32-wasi --release"

Also, notice allowed_http_hosts which you use to allow outbound HTTP calls to specific hosts. (hooks.slack.com should be the origin of your Slack webhook URL).

Config providers in Spin

Instead of hardcoding out configuration data (especially the sensitive values), we can use configuration providers supported by Spin.

Currently, there are two configuration providers available. First, there is the Environment Variable Config Provider, which can pull configuration data from environment variables following a particular naming convention (which we’ll discover in a few seconds). The second provider is the HashiCorp Vault Config Provider. As its name implies, it allows the pulling of sensitive configuration data from a HashiCorp Vault instance.

Pulling configuration data from environment variables

Spin’s Environment Variable Config Provider pulls configuration data from environment variables passed to the underlying Spin process when spawning up.The provider only pulls data from environment variables having a name that starts with SPIN_APP_. The actual name of the variable must be upper-cased. This means we can pull the value for our is_markdown variable (part of Spin.toml) from the SPIN_APP_IS_MARKDOWN environment variable. To set the SPIN_APP_IS_MARKDOWN environment variable, we use export before starting our application with spin up or spin build --up in a particular terminal session:

# set SPIN_APP_IS_MARKDOWN
export SPIN_APP_IS_MARKDOWN=true

# start our application
spin build --up --follow-all

Pulling configuration data from HashiCorp Vault

Obviously, we must have access to an instance of HashiCorp Vault to pull sensitive configuration data from it. Luckily, we can easily run HashiCorp Vault using the vault Docker Image. Let’s start it in the background and forward the default port (8200) to our host machine:

# Start HashiCorp Vault in the background
docker run -d -e VAULT_DEV_ROOT_TOKEN_ID=foobar \
 -e VAULT_SERVER="http://127.0.0.1:8200" \
 -p 8200:8200 vault

Having our vault instance up and running, we must store the slack_webhook_url in it:

# Install vault CLI (macOS shown here with Homebrew)
brew install vault

# Set Vault URL and token as env vars
# Alternatively, you can provide the token when using vault CLI
export VAULT_ADDR='http://0.0.0.0:8200'
export VAULT_TOKEN=foobar

# Store slack_webhook_url in vault
vault kv put secret/slack_webhook_url value="YOUR slack webhook url"

# Verify that slack_webhook_url is persisted
vault kv get secret/slack_webhook_url

vault kv get secret/slack_webhook_url
======== Secret Path ========
secret/data/slack_webhook_url

======= Metadata =======
Key                Value
---                -----
created_time       2022-12-15T22:11:11.791431757Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

==== Data ====
Key      Value
---      -----
value    https://here-is-my-secret-slack-webhook-url

Setup HashiCorp Vault Config Provider

Having the sensitive configuration data stored in HashiCorp Vault, we must tell our Spin application about our intent to use the HashiCorp Vault Config Provider. The config provider configuration is not stored in the Spin manifest, instead, add a new .toml file (I called mine vault.toml) with the following content:

# vault.toml

[[config_provider]]
type = "vault"
url = "http://0.0.0.0:8200"
token = "foobar"
mount = "secret"

Now that we’ve both config providers in place, we can move on and implement the actual component.

Reading configuration data in a WebAssembly component

Within the context of a particular component, we can easily access configuration data using the Spin SDK. To do so, we use the spin_skd::config::get(key: &str) -> Result<String, Error>, which is just a wrapper around the actual get_config function that is specified as part of spin-config.wit. That said, let’s take a quick look at how to pull configuration data in the most basic form:

let webhook_url = config::get("slack_webhook_url").unwrap();
let is_markdown = config::get("is_markdown").unwrap();
let channel = config::get("channel").unwrap();

Although this works, I would highly recommend encapsulating the configuration access from the actual component implementation. We can implement a simple constructor for our Configuration struct and provide a way cleaner API for component developers:

impl Configuration {
  pub fn new() -> Result<Self, anyhow::Error> {
    let channel = config::get("channel")?;
    let is_markdown = config::get("is_markdown")?.trim().parse()?;
    let slack_url = config::get("slack_webhook_url")?;

    Ok(Configuration {
      channel,
      is_markdown,
      slack_webhook_url: slack_url
    })
  }
}

Within the actual component function (the one that is decorated with #[http_component], we can now access our configuration data by simply invoking the constructor for Configuration:

#[http_component]
fn configuration_in_spin(req: Request) -> Result<Response> {
  let c = Configuration::new();
  match c {
    Ok(cfg) => {
        let body = req.body().clone().unwrap_or_default();
        let inbound_message : Message = serde_json::from_slice(&body)?;
        println!("Sending message to the {} channel", &cfg.channel);
        send_to_slack(inbound_message, &cfg)
    },
    Err(error) => {
      println!("Error while reading configuration: {}", error);
      Ok(http::Response::builder().status(500).body(None)?)
    }
  }
}

Invoking the actual webhook is encapsulated in the send_to_slack function here:

fn send_to_slack(inbound: Message, cfg: &Configuration) -> Result<Response>
{
  let msg = SlackMessage::new(
    cfg.channel.clone(), 
    inbound.message,
    cfg.is_markdown);
  let payload = serde_json::to_string(&msg)?;

  let res = spin_sdk::http::send(
    http::Request::builder()
      .uri(cfg.slack_webhook_url.clone())
      .method(http::Method::POST)
      .header(http::header::CONTENT_TYPE, "application/json")
      .body(Some(payload.into()))?,
  )?;
  Ok(res)
}

Both structs Message and SlackMessage are just plain DTOs to ensure inbound and outbound payloads have a proper structure.

Taming the HashiCorp Vault provider

When writing this article, I used the canary version of spin (spin 0.6.0 (268ae0a 2022-12-15)). I ran into the issue that Spin ignored variable default values (only when using the Vault configuration provider). That said, you have to provide SPIN_APP_CHANNEL and SPIN_APP_IS_MARKDOWN as environment variables to get this app up and running as expected.

Consult this issue to check if you are still affected by this misbehavior.

Testing Spin-to-Slack

Now that we’re finished with the implementation, we can test our Spin-to-Slack application using the steps:

  1. Ensure Vault is running
  2. Set necessary Environment Variables
  3. Start the Spin application
  4. Send an HTTP POST request to the Spin application
# 1. Ensure vault is running (or start it as described earlier and set the secret)
docker ps
# find the vault and verify 8200 is mapped to your host

# 2. Set necessary Environment Variables
export SPIN_APP_CHANNEL="#spin-to-slack"
export SPIN_APP_IS_MARKDOWN="true"

# 3. Start the Spin application
spin build --up --follow-all \
 --runtime-config-file vault.toml

# Send an HTTP POST request to the Spin application
curl -X POST --json '{
  "message": "Hello! This is Spin *speaking*\r\n:beverage_box:"
}' http://localhost:3000

Finally, you should see your message appear in the Slack channel of your choice:

Spin-to-Slack: Custom Inbound Webhook with WebAssembly

Conclusion

Being able to alter the behavior of an application from the outside without having to re-compile or re-deploy the app is super important (not just when building for the cloud). Luckily, we can deal with both sensitive and non-sensitive configuration data in Spin. Having a distinction between global configuration data (variables) and the actual component configuration ([component.config]) is super handy when you want to prevent specific components from accessing sensitive configuration data being specified in the outer application context.

I’m pretty sure we’ll see more and more configuration providers finding their way into Spin, which would be really beneficial, especially for those of you that consider running Spin applications in the public cloud (Maybe someone at Azure will contribute a provider for Azure Key Vault :D)