With the 1.2.1 release of Fermyon Spin, we can now use Azure Cosmos DB as a key-value store for persisting data when building serverless applications. In this article, I guide you through setting up the necessary infrastructure and creating a fairly simple app to store and retrieve arbitrary values using Azure Cosmos DB as the key-value store.

All code shown in this article is also available on GitHub at https://github.com/ThorstenHans/spin-kv-azure-cosmosdb.

Provision necessary Azure Infrastructure

To use Azure Cosmos DB as a key-value store for a Spin application, we have to provision the following infrastructure components:

  • 1 Azure Resource Group
  • 1 Azure Cosmos DB Account
  • 1 Azure Cosmos DB Database
  • 1 Azure Cosmos DB Container

Currently, Spin requires the Azure Cosmos DB Container’s partition key to be set to /id. Besides this, we can configure all services according to our needs.

The infra folder of the sample repository contains a Terraform project that you can use to provision the entire infrastructure to your preferred Azure Subscription. Although it is recommended to run Terraform projects using a Service Principal (SP), you can also re-use existing Azure CLI (az) authentication information for demonstration purposes.

Please verify that you’ve logged in with Azure CLI (az login) and ensure the desired Azure Subscription is selected (az account set -s <YOUR_SUBSCRIPTION_NAME_OR_ID>).

You can get a preview of all cloud resources before actually deploying them using terraform plan. To provision the infrastructure, you can use terraform apply as shown here:

# move to the infra folder
cd infra

# init the terraform project
terraform init

# preview cloud modifications
terraform plan

# deploy the infra
terraform apply -auto-approve

Provisioning an Azure Cosmos DB account can take several minutes. A great chance to refill your cup of coffee ☕️. Once the infrastructure is provisioned, we will see non-sensitive configuration data of our Azure Cosmos DB instance being printed to stdout as shown here:

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

account_name = "cosmos-spin-1233"
container_name = "kv"
database_name = "cosmos-sql-spin"
key = <sensitive>

We can use terraform output- raw key to receive the sensitive data (key). We need all these values in a few minutes to configure the key-value store for our Spin application.

The Spin application

For demonstration purposes, we will create a simple HTTP trigger in Rust 🦀, which provides two routes:

  • GET /:key to receive a value from the key-value store
  • POST /:key to store a value in the key-value store

Starting from scratch, you can create a new Spin application using spin new:

# go to your dev/sources folder
cd ~/dev

# Create a new Spin app using the http-rust template
spin new http-rust cosmos-showcase -a

# Open the app in the editor (Helix here)
hx .

Configuring Azure Cosmos DB as the default key-value store for Spin

First, we must enable the key-value store for the bootstrapped component of our Spin application. To do so, we must update the spin.toml file and add the key_value_stores = ["default"] configuration to the [[component]] block. Your spin.toml should now look like this:

spin_manifest_version = "1"
authors = ["Thorsten Hans <[email protected]>"]
description = ""
name = "cosmos-showcase"
trigger = { type = "http", base = "/" }
version = "0.1.0"

[[component]]
id = "cosmos-showcase"
source = "target/wasm32-wasi/release/cosmos_showcase.wasm"
allowed_http_hosts = []
key_value_stores = ["default"]
[component.trigger]
route = "/..."
[component.build]
command = "cargo build --target wasm32-wasi --release"
watch = ["src/**/*.rs", "Cargo.toml"]

With the manifest being updated, we can move on and provide our runtime configuration file, which tells Spin to use our Azure Cosmos DB instance as the default key-value store. Create a new file called rt-config.toml in the folder of the Spin app and add the values we received from Terraform. rt-config.toml should look similar to this:

[key_value_store.default]
type = "azure_cosmos"
key = "UmVhbGx5LCB5b3UgdGhvdWdodCBJIHByZXNlbnQgbXkgYWN0dWFsIGtleSBoZXJlPz8gQ29tZSBvbiEgOkQK"
account = "cosmos-spin-1233"
database = "cosmos-sql-spin"
container = "kv"

Now that everything is configured, we can move on to the last part and implement our Spin application.

Use Azure Cosmos DB as a key-value store in Spin

To interact with the key-value store from within a Spin application, we can leverage the functionality provided by the Spin SDK. The following snippet is also using the built-in Router to expose both routes, as mentioned earlier (If you haven’t used the built-in Router in Rust, consider reading this article.):

use anyhow::Result;
use spin_sdk::{
  http::{Params, Request, Response, Router},
  http_component,
  key_value::Store,
};

#[http_component]
fn handle_cosmos_showcase(req: Request) -> Result<Response> {
  let mut router = Router::default();
   
  router.post(":key", set_value);
  router.get("/:key", get_value);
  router.handle(req)
}

/// handler to get the value at key from the store
/// If the key does not exist, it returns a 404
fn get_value(_req: Request, params: Params) -> Result<Response> {
  let store = Store::open_default()?;
  let Some(key) = params.get("key") else {
    return bad_request();
  };
  match store.exists(key) {
    Ok(true) => (),
    Ok(false) => {
      return Ok(http::Response::builder()
        .status(http::StatusCode::NOT_FOUND)
        .body(None)?)
    }
    Err(_) => return err()
  }
  match store.get(key) {
    Ok(value) => Ok(http::Response::builder()
      .status(http::StatusCode::OK)
      .body(Some(value.into()))?),
    Err(_) => err(),
  }
}

/// Handler to store a value in the key-value store
fn set_value(req: Request, params: Params) -> Result<Response> {
  let store = Store::open_default()?;
  let Some(key) = params.get("key") else {
    return bad_request()
  };
  match store.set(key, req.body().as_deref().unwrap_or(&[])) {
    Ok(_) => Ok(http::Response::builder()
      .status(http::StatusCode::OK)
      .body(None)?),
    Err(_) => err(),
  }
}

/// helper function to quickly return a bad request HTTP Response
fn bad_request() -> Result<Response> {
  Ok(http::Response::builder()
    .status(http::StatusCode::BAD_REQUEST)
    .body(None)?)
}

/// helper function to quickly return an internal server error HTTP Response
fn err() -> Result<Response> {
  Ok(http::Response::builder()
    .status(http::StatusCode::INTERNAL_SERVER_ERROR)
    .body(None)?)
}

The implementation of the component should be self-explaining. Both routes use the default store, which could be retrieved via spin_sdk::key_value::Store::open_default(). When reading values from the key-value store, we first check if the key exists using the store.exists function. If the provided key exists, we grab and return the value using store.get(key). If the key doesn’t exist, we return early and respond with an HTTP 404.

Storing values in the key-value store is even simpler. We grab the key from the route parameter using the Params struct and store the entire body of the incoming request in Azure Cosmos DB using store.set.

Verify Azure Cosmos DB key-value storage

At this point, we can verify if our app works as expected. First, let’s build and run the application. I added a small Makefile to the repository to simplify the process. The Makefile executes the following commands:

# Build the Spin app
spin build

# Run the Spin app and provide the runtime configuration file
spin up --runtime-config-file ./rt-config.toml

You can use those commands directly in your terminal or invoke make run. Once the app has been compiled and is running, you can access both routes at http://localhost:3000. As always, let’s use curl to test our application:

# store some data in the key-value store
curl -iX POST --data 'hello' http://localhost:3000/test
HTTP/1.1 200 OK
content-length: 0
date: Thu, 25 May 2023 19:36:40 GMT

# retrieve data from the key-value store
curl -iX GET http://localhost:3000/test
HTTP/1.1 200 OK
content-length: 5
date: Thu, 25 May 2023 19:36:58 GMT

hello

# Try to retrieve a non-existing key
curl -iX GET http://localhost:3000/foobar
HTTP/1.1 404 Not Found
content-length: 0
date: Thu, 25 May 2023 19:37:09 GMT

Finally, we can browse Azure Cosmos DB using the Azure Portal. Locate your Azure Cosmos DB Account and use the Data Explorer to inspect all data stored in the kv container. You will immediately spot the test key in the collection. The actual value is stored in Azure Cosmos DB as byte-Array.

Our custom key in Azure Cosmos DB

Our custom key in Azure Cosmos DB

Conclusion

With Spin 1.2.1, we finally understand how Spin will continue integrating with popular services provided by major cloud vendors such as Microsoft. It’s just a matter of time until we see more cloud services supported by Spin. IMO, these integrations are mission-critical to drive the adoption of Spin in the coming months and years. Combining those integrations with a secure connection to Fermyon Cloud or hosting Fermyon Platform in your cloud of choice looks promising.

From a technical point of view, I like that I can swap the actual key-value store without changing the actual implementation of my Spin application. This helps with becoming more cloud-agnostic in the long run.