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
- Application variables
- Component configuration
- Config providers in Spin
- Reading configuration data in a WebAssembly component
- Taming the HashiCorp Vault provider
- Testing Spin-to-Slack
- Conclusion
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:
- Ensure Vault is running
- Set necessary Environment Variables
- Start the Spin application
- 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:
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)