1 - Packaging and deploying apps

Learn how to package and distribute Spin Apps using either public or private OCI compliant registries.

This article explains how Spin Apps are packaged and distributed via both public and private registries. You will learn how to:

  • Package and distribute Spin Apps
  • Deploy Spin Apps
  • Scaffold Kubernetes Manifests for Spin Apps
  • Use private registries that require authentication

Prerequisites

For this tutorial in particular, you need

Creating a new Spin App

You use the spin CLI, to create a new Spin App. The spin CLI provides different templates, which you can use to quickly create different kinds of Spin Apps. For demonstration purposes, you will use the http-go template to create a simple Spin App.

# Create a new Spin App using the http-go template
spin new --accept-defaults -t http-go hello-spin

# Navigate into the hello-spin directory
cd hello-spin

The spin CLI created all necessary files within hello-spin. Besides the Spin Manifest (spin.toml), you can find the actual implementation of the app in main.go:

package main

import (
	"fmt"
	"net/http"

	spinhttp "github.com/fermyon/spin/sdk/go/v2/http"
)

func init() {
	spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain")
		fmt.Fprintln(w, "Hello Fermyon!")
	})
}

func main() {}

This implementation will respond to any incoming HTTP request, and return an HTTP response with a status code of 200 (Ok) and send Hello Fermyon as the response body.

You can test the app on your local machine by invoking the spin up command from within the hello-spin folder.

Packaging and Distributing Spin Apps

Spin Apps are packaged and distributed as OCI artifacts. By leveraging OCI artifacts, Spin Apps can be distributed using any registry that implements the Open Container Initiative Distribution Specification (a.k.a. “OCI Distribution Spec”).

The spin CLI simplifies packaging and distribution of Spin Apps and provides an atomic command for this (spin registry push). You can package and distribute the hello-spin app that you created as part of the previous section like this:

# Package and Distribute the hello-spin app
spin registry push --build ttl.sh/hello-spin:24h

It is a good practice to add the --build flag to spin registry push. It prevents you from accidentally pushing an outdated version of your Spin App to your registry of choice.

Deploying Spin Apps

To deploy Spin Apps to a Kubernetes cluster which has Spin Operator running, you use the kube plugin for spin. Use the spin kube deploy command as shown here to deploy the hello-spin app to your Kubernetes cluster:

# Deploy the hello-spin app to your Kubernetes Cluster
spin kube deploy --from ttl.sh/hello-spin:24h

spinapp.core.spinoperator.dev/hello-spin created

Scaffolding Spin Apps

In the previous section, you deployed the hello-spin app using the spin kube deploy command. Although this is handy, you may want to inspect, or alter the Kubernetes manifests before applying them. You use the spin kube scaffold command to generate Kubernetes manifests:

spin kube scaffold --from ttl.sh/hello-spin:24h
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: hello-spin
spec:
  image: "ttl.sh/hello-spin:24h"
  replicas: 2

By default, the command will print all Kubernetes manifests to STDOUT. Alternatively, you can specify the out argument to store the manifests to a file:

# Scaffold manifests to spinapp.yaml
spin kube scaffold --from ttl.sh/hello-spin:24h \
    --out spinapp.yaml

# Print contents of spinapp.yaml
cat spinapp.yaml
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: hello-spin
spec:
  image: "ttl.sh/hello-spin:24h"
  replicas: 2

You can then deploy the Spin App by applying the manifest with the kubectl CLI:

kubectl apply -f spinapp.yaml

Distributing and Deploying Spin Apps via private registries

It is quite common to distribute Spin Apps through private registries that require some sort of authentication. To publish a Spin App to a private registry, you have to authenticate using the spin registry login command.

For demonstration purposes, you will now distribute the Spin App via GitHub Container Registry (GHCR). You can follow this guide by GitHub to create a new personal access token (PAT), which is required for authentication.

# Store PAT and GitHub username as environment variables
export GH_PAT=YOUR_TOKEN
export GH_USER=YOUR_GITHUB_USERNAME

# Authenticate spin CLI with GHCR
echo $GH_PAT | spin registry login ghcr.io -u $GH_USER --password-stdin

Successfully logged in as YOUR_GITHUB_USERNAME to registry ghcr.io

Once authentication succeeded, you can use spin registry push to push your Spin App to GHCR:

# Push hello-spin to GHCR
spin registry push --build ghcr.io/$GH_USER/hello-spin:0.0.1

Pushing app to the Registry...
Pushed with digest sha256:1611d51b296574f74b99df1391e2dc65f210e9ea695fbbce34d770ecfcfba581

In Kubernetes you store authentication information as secret of type docker-registry. The following snippet shows how to create such a secret with kubectl leveraging the environment variables, you specified in the previous section:

# Create Secret in Kubernetes
kubectl create secret docker-registry ghcr \
    --docker-server ghcr.io \
    --docker-username $GH_USER \
    --docker-password $CR_PAT

secret/ghcr created

Scaffold the necessary SpinApp Custom Resource (CR) using spin kube scaffold:

# Scaffold the SpinApp manifest
spin kube scaffold --from ghcr.io/$GH_USER/hello-spin:0.0.1 \
    --out spinapp.yaml

Before deploying the manifest with kubectl, update spinapp.yaml and link the ghcr secret you previously created using the imagePullSecrets property. Your SpinApp manifest should look like this:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: hello-spin
spec:
  image: ghcr.io/$GH_USER/hello-spin:0.0.1
  imagePullSecrets:
    - name: ghcr
  replicas: 2
  executor: containerd-shim-spin

$GH_USER should match the actual username provided while running through the previous sections of this article

Finally, you can deploy the app using kubectl apply:

# Deploy the spinapp.yaml using kubectl
kubectl apply -f spinapp.yaml
spinapp.core.spinoperator.dev/hello-spin created

2 - Making HTTPS Requests

Configure Spin Apps to allow HTTPS requests.

To enable HTTPS requests, the executor must be configured to use certificates. SpinKube can be configured to use either default or custom certificates.

If you make a request without properly configured certificates, you’ll encounter an error message that reads: error trying to connect: unexpected EOF (unable to get local issuer certificate).

Using default certificates

SpinKube can generate a default CA certificate bundle by setting installDefaultCACerts to true. This creates a secret named spin-ca populated with curl’s default bundle. You can specify a custom secret name by setting caCertSecret.

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinAppExecutor
metadata:
  name: containerd-shim-spin
spec:
  createDeployment: true
  deploymentConfig:
    runtimeClassName: wasmtime-spin-v2
    installDefaultCACerts: true

Apply the executor using kubectl:

kubectl apply -f myexecutor.yaml

Using custom certificates

Create a secret from your certificate file:

kubectl create secret generic my-custom-ca --from-file=ca-certificates.crt

Configure the executor to use the custom certificate secret:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinAppExecutor
metadata:
  name: containerd-shim-spin
spec:
  createDeployment: true
  deploymentConfig:
    runtimeClassName: wasmtime-spin-v2
    caCertSecret: my-custom-ca

Apply the executor using kubectl:

kubectl apply -f myexecutor.yaml

3 - Assigning variables

Configure Spin Apps using values from Kubernetes ConfigMaps and Secrets.

By using variables, you can alter application behavior without recompiling your SpinApp. When running in Kubernetes, you can either provide constant values for variables, or reference them from Kubernetes primitives such as ConfigMaps and Secrets. This tutorial guides your through the process of assigning variables to your SpinApp.

Note: If you’d like to learn how to configure your application with an external variable provider like Vault or Azure Key Vault, see the External Variable Provider guide

Build and Store SpinApp in an OCI Registry

We’re going to build the SpinApp and store it inside of a ttl.sh registry. Move into the apps/variable-explorer directory and build the SpinApp we’ve provided:

# Build and publish the sample app
cd apps/variable-explorer
spin build
spin registry push ttl.sh/variable-explorer:1h

Note that the tag at the end of ttl.sh/variable-explorer:1h indicates how long the image will last e.g. 1h (1 hour). The maximum is 24h and you will need to repush if ttl exceeds 24 hours.

For demonstration purposes, we use the variable explorer sample app. It reads three different variables (log_level, platform_name and db_password) and prints their values to the STDOUT stream as shown in the following snippet:

let log_level = variables::get("log_level")?;
let platform_name = variables::get("platform_name")?;
let db_password = variables::get("db_password")?;

println!("# Log Level: {}", log_level);
println!("# Platform name: {}", platform_name);
println!("# DB Password: {}", db_password);

Those variables are defined as part of the Spin manifest (spin.toml), and access to them is granted to the variable-explorer component:

[variables]
log_level = { default = "WARN" }
platform_name = { default = "Fermyon Cloud" }
db_password = { required = true }

[component.variable-explorer.variables]
log_level = "{{ log_level }}"
platform_name = "{{ platform_name }}"
db_password = "{{ db_password }}"

For further reading on defining variables in the Spin manifest, see the Spin Application Manifest Reference.

Configuration data in Kubernetes

In Kubernetes, you use ConfigMaps for storing non-sensitive, and Secrets for storing sensitive configuration data. The deployment manifest (config/samples/variable-explorer.yaml) contains specifications for both a ConfigMap and a Secret:

kind: ConfigMap
apiVersion: v1
metadata:
  name: spinapp-cfg
data:
  logLevel: INFO
---
kind: Secret
apiVersion: v1
metadata:
  name: spinapp-secret
data:
  password: c2VjcmV0X3NhdWNlCg==

Assigning variables to a SpinApp

When creating a SpinApp, you can choose from different approaches for specifying variables:

  1. Providing constant values
  2. Loading configuration values from ConfigMaps
  3. Loading configuration values from Secrets

The SpinApp specification contains the variables array, that you use for specifying variables (See kubectl explain spinapp.spec.variables).

The deployment manifest (config/samples/variable-explorer.yaml) specifies a static value for platform_name. The value of log_level is read from the ConfigMap called spinapp-cfg, and the db_password is read from the Secret called spinapp-secret:

kind: SpinApp
apiVersion: core.spinoperator.dev/v1alpha1
metadata:
  name: variable-explorer
spec:
  replicas: 1
  image: ttl.sh/variable-explorer:1h
  executor: containerd-shim-spin
  variables:
    - name: platform_name
      value: Kubernetes
    - name: log_level
      valueFrom:
        configMapKeyRef:
          name: spinapp-cfg
          key: logLevel
          optional: true
    - name: db_password
      valueFrom:
        secretKeyRef:
          name: spinapp-secret
          key: password
          optional: false

As the deployment manifest outlines, you can use the optional property - as you would do when specifying environment variables for a regular Kubernetes Pod - to control if Kubernetes should prevent starting the SpinApp, if the referenced configuration source does not exist.

You can deploy all resources by executing the following command:

kubectl apply -f config/samples/variable-explorer.yaml

configmap/spinapp-cfg created
secret/spinapp-secret created
spinapp.core.spinoperator.dev/variable-explorer created

Inspecting runtime logs of your SpinApp

To verify that all variables are passed correctly to the SpinApp, you can configure port forwarding from your local machine to the corresponding Kubernetes Service:

kubectl port-forward services/variable-explorer 8080:80

Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

When port forwarding is established, you can send an HTTP request to the variable-explorer from within an additional terminal session:

curl http://localhost:8080
Hello from Kubernetes

Finally, you can use kubectl logs to see all logs produced by the variable-explorer at runtime:

kubectl logs -l core.spinoperator.dev/app-name=variable-explorer

# Log Level: INFO
# Platform Name: Kubernetes
# DB Password: secret_sauce

4 - External Variable Providers

Configure external variable providers for your Spin App.

In the Assigning Variables guide, you learned how to configure variables on the SpinApp via its variables section, either by supplying values in-line or via a Kubernetes ConfigMap or Secret.

You can also utilize an external service like Vault or Azure Key Vault to provide variable values for your application. This guide will show you how to use and configure both services in tandem with corresponding sample applications.

Prerequisites

To follow along with this tutorial, you’ll need:

Supported providers

Spin currently supports Vault and Azure Key Vault as external variable providers. Configuration is supplied to the application via a Runtime Configuration file.

In SpinKube, this configuration file can be supplied in the form of a Kubernetes secret and linked to a SpinApp via its runtimeConfig.loadFromSecret section.

Note: loadFromSecret takes precedence over any other runtimeConfig configuration. Thus, all runtime configuration must be contained in the Kubernetes secret, including SQLite, Key Value and LLM options that might otherwise be specified via their dedicated specs.

Let’s look at examples utilizing specific provider configuration next.

Vault provider

Vault is a popular choice for storing secrets and serving as a secure key-value store.

This guide assumes you have:

Build and publish the Spin application

We’ll use the variable explorer app to test this integration.

First, clone the repository locally and navigate to the variable-explorer directory:

git clone git@github.com:spinkube/spin-operator.git
cd apps/variable-explorer

Now, build and push the application to a registry you have access to. Here we’ll use ttl.sh:

spin build
spin registry push ttl.sh/variable-explorer:1h

Create the runtime-config.toml file

Here’s a sample runtime-config.toml file containing Vault provider configuration:

[[config_provider]]
type = "vault"
url = "https://my-vault-server:8200"
token = "my_token"
mount = "admin/secret"

To use this sample, you’ll want to update the url and token fields with values applicable to your Vault cluster. The mount value will depend on the Vault namespace and kv-v2 secrets engine name. In this sample, the namespace is admin and the engine is named secret, eg by running vault secrets enable --path=secret kv-v2.

Create the secrets in Vault

Create the log_level, platform_name and db_password secrets used by the variable-explorer application in Vault:

vault kv put secret/log_level value=INFO
vault kv put secret/platform_name value=Kubernetes
vault kv put secret/db_password value=secret_sauce

Create the SpinApp and Secret

Next, scaffold the SpinApp and Secret resource (containing the runtime-config.toml data) together in one go via the kube plugin:

spin kube scaffold -f ttl.sh/variable-explorer:1h -c runtime-config.toml -o scaffold.yaml

Deploy the application

kubectl apply -f scaffold.yaml

Test the application

You are now ready to test the application and verify that all variables are passed correctly to the SpinApp from the Vault provider.

Configure port forwarding from your local machine to the corresponding Kubernetes Service:

kubectl port-forward services/variable-explorer 8080:80

Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

When port forwarding is established, you can send an HTTP request to the variable-explorer from within an additional terminal session:

curl http://localhost:8080
Hello from Kubernetes

Finally, you can use kubectl logs to see all logs produced by the variable-explorer at runtime:

kubectl logs -l core.spinoperator.dev/app-name=variable-explorer

# Log Level: INFO
# Platform Name: Kubernetes
# DB Password: secret_sauce

Azure Key Vault provider

Azure Key Vault is a secure secret store for distributed applications hosted on the Azure platform.

This guide assumes you have:

Build and publish the Spin application

We’ll use the Azure Key Vault Provider sample application for this exercise.

First, clone the repository locally and navigate to the azure-key-vault-provider directory:

git clone git@github.com:fermyon/enterprise-architectures-and-patterns.git
cd enterprise-architectures-and-patterns/application-variable-providers/azure-key-vault-provider

Now, build and push the application to a registry you have access to. Here we’ll use ttl.sh:

spin build
spin registry push ttl.sh/azure-key-vault-provider:1h

The next steps will guide you in creating and configuring an Azure Key Vault and populating the runtime configuration file with connection credentials.

Deploy Azure Key Vault

# Variable Definition
KV_NAME=spinkube-keyvault
LOCATION=westus2
RG_NAME=rg-spinkube-keyvault

# Create Azure Resource Group and Azure Key Vault
az group create -n $RG_NAME -l $LOCATION
az keyvault create -n $KV_NAME \
  -g $RG_NAME \
  -l $LOCATION \
  --enable-rbac-authorization true

# Grab the Azure Resource Identifier of the Azure Key Vault instance
KV_SCOPE=$(az keyvault show -n $KV_NAME -g $RG_NAME -otsv --query "id")

Add a Secret to the Azure Key Vault instance

# Grab the ID of the currently signed in user in Azure CLI
CURRENT_USER_ID=$(az ad signed-in-user show -otsv --query "id")

# Make the currently signed in user a Key Vault Secrets Officer
# on the scope of the new Azure Key Vault instance
az role assignment create --assignee $CURRENT_USER_ID \
  --role "Key Vault Secrets Officer" \
  --scope $KV_SCOPE

# Create a test secret called 'secret` in the Azure Key Vault instance
az keyvault secret set -n secret --vault-name $KV_NAME --value secret_value -o none

Create a Service Principal and Role Assignment for Spin

SP_NAME=sp-spinkube-keyvault
SP=$(az ad sp create-for-rbac -n $SP_NAME -ojson)

CLIENT_ID=$(echo $SP | jq -r '.appId')
CLIENT_SECRET=$(echo $SP | jq -r '.password')
TENANT_ID=$(echo $SP | jq -r '.tenant')

az role assignment create --assignee $CLIENT_ID \
  --role "Key Vault Secrets User" \
  --scope $KV_SCOPE

Create the runtime-config.toml file

Create a runtime-config.toml file with the following contents, substituting in the values for KV_NAME, CLIENT_ID, CLIENT_SECRET and TENANT_ID from the previous steps.

[[config_provider]]
type = "azure_key_vault"
vault_url = "https://<$KV_NAME>.vault.azure.net/"
client_id = "<$CLIENT_ID>"
client_secret = "<$CLIENT_SECRET>"
tenant_id = "<$TENANT_ID>"
authority_host = "AzurePublicCloud"

Create the SpinApp and Secret

Scaffold the SpinApp and Secret resource (containing the runtime-config.toml data) together in one go via the kube plugin:

spin kube scaffold -f ttl.sh/azure-key-vault-provider:1h -c runtime-config.toml -o scaffold.yaml

Deploy the application

kubectl apply -f scaffold.yaml

Test the application

Now you are ready to test the application and verify that the secret resolves its value from Azure Key Vault.

Configure port forwarding from your local machine to the corresponding Kubernetes Service:

kubectl port-forward services/azure-key-vault-provider 8080:80

Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

When port forwarding is established, you can send an HTTP request to the azure-key-vault-provider app from within an additional terminal session:

curl http://localhost:8080
Loaded secret from Azure Key Vault: secret_value

5 - Connecting to your app

Learn how to connect to your application.

This topic guide shows you how to connect to your application deployed to SpinKube, including how to use port-forwarding for local development, or Ingress rules for a production setup.

Run the sample application

Let’s deploy a sample application to your Kubernetes cluster. We will use this application throughout the tutorial to demonstrate how to connect to it.

Refer to the quickstart guide if you haven’t set up a Kubernetes cluster yet.

kubectl apply -f https://raw.githubusercontent.com/spinkube/spin-operator/main/config/samples/simple.yaml

When SpinKube deploys the application, it creates a Kubernetes Service that exposes the application to the cluster. You can check the status of the deployment with the following command:

kubectl get services

You should see a service named simple-spinapp with a type of ClusterIP. This means that the service is only accessible from within the cluster.

NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
simple-spinapp   ClusterIP   10.43.152.184    <none>        80/TCP    1m

We will use this service to connect to your application.

Port forwarding

This option is useful for debugging and development. It allows you to forward a local port to the service.

Forward port 8083 to the service so that it can be reached from your computer:

kubectl port-forward svc/simple-spinapp 8083:80

You should be able to reach it from your browser at http://localhost:8083:

curl http://localhost:8083

You should see a message like “Hello world from Spin!”.

This is one of the simplest ways to test your application. However, it is not suitable for production use. The next section will show you how to expose your application to the internet using an Ingress controller.

Ingress

Ingress exposes HTTP and HTTPS routes from outside the cluster to services within the cluster. Traffic routing is controlled by rules defined on the Ingress resource.

Here is a simple example where an Ingress sends all its traffic to one Service:

Ingress

(source: Kubernetes documentation)

An Ingress may be configured to give applications externally-reachable URLs, load balance traffic, terminate SSL / TLS, and offer name-based virtual hosting. An Ingress controller is responsible for fulfilling the Ingress, usually with a load balancer, though it may also configure your edge router or additional frontends to help handle the traffic.

Prerequisites

You must have an Ingress controller to satisfy an Ingress rule. Creating an Ingress rule without a controller has no effect.

Ideally, all Ingress controllers should fit the reference specification. In reality, the various Ingress controllers operate slightly differently. Make sure you review your Ingress controller’s documentation to understand the specifics of how it works.

ingress-nginx is a popular Ingress controller, so we will use it in this tutorial:

helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

Wait for the ingress controller to be ready:

kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=120s

Check the Ingress controller’s external IP address

If your Kubernetes cluster is a “real” cluster that supports services of type LoadBalancer, it will have allocated an external IP address or FQDN to the ingress controller.

Check the IP address or FQDN with the following command:

kubectl get service ingress-nginx-controller --namespace=ingress-nginx

It will be the EXTERNAL-IP field. If that field shows <pending>, this means that your Kubernetes cluster wasn’t able to provision the load balancer. Generally, this is because it doesn’t support services of type LoadBalancer.

Once you have the external IP address (or FQDN), set up a DNS record pointing to it. Refer to your DNS provider’s documentation on how to add a new DNS record to your domain.

You will want to create an A record that points to the external IP address. If your external IP address is <EXTERNAL-IP>, you would create a record like this:

A    myapp.spinkube.local    <EXTERNAL-IP>

Once you’ve added a DNS record to your domain and it has propagated, proceed to create an ingress resource.

Create an Ingress resource

Create an Ingress resource that routes traffic to the simple-spinapp service. The following example assumes that you have set up a DNS record for myapp.spinkube.local:

kubectl create ingress simple-spinapp --class=nginx --rule="myapp.spinkube.local/*=simple-spinapp:80"

A couple notes about the above command:

  • simple-spinapp is the name of the Ingress resource.
  • myapp.spinkube.local is the hostname that the Ingress will route traffic to. This is the DNS record you set up earlier.
  • simple-spinapp:80 is the Service that SpinKube created for us. The application listens for requests on port 80.

Assuming DNS has propagated correctly, you should see a message like “Hello world from Spin!” when you connect to http://myapp.spinkube.local/.

Congratulations, you are serving a public website hosted on a Kubernetes cluster! 🎉

Connecting with kubectl port-forward

This is a quick way to test your Ingress setup without setting up DNS records or on clusters without support for services of type LoadBalancer.

Open a new terminal and forward a port from localhost port 8080 to the Ingress controller:

kubectl port-forward --namespace=ingress-nginx service/ingress-nginx-controller 8080:80

Then, in another terminal, test the Ingress setup:

curl --resolve myapp.spinkube.local:8080:127.0.0.1 http://myapp.spinkube.local:8080/hello

You should see a message like “Hello world from Spin!”.

If you want to see your app running in the browser, update your /etc/hosts file to resolve requests from myapp.spinkube.local to the ingress controller:

127.0.0.1       myapp.spinkube.local

6 - Monitoring your app

How to view telemetry data from your Spin apps running in SpinKube.

This topic guide shows you how to configure SpinKube so your Spin apps export observability data. This data will export to an OpenTelemetry collector which will send it to Jaeger.

Prerequisites

Please ensure you have the following tools installed before continuing:

About OpenTelemetry Collector

From the OpenTelemetry documentation:

The OpenTelemetry Collector offers a vendor-agnostic implementation of how to receive, process and export telemetry data. It removes the need to run, operate, and maintain multiple agents/collectors. This works with improved scalability and supports open source observability data formats (e.g. Jaeger, Prometheus, Fluent Bit, etc.) sending to one or more open source or commercial backends.

In our case, the OpenTelemetry collector serves as a single endpoint to receive and route telemetry data, letting us to monitor metrics, traces, and logs via our preferred UIs.

About Jaeger

From the Jaeger documentation:

Jaeger is a distributed tracing platform released as open source by Uber Technologies. With Jaeger you can: Monitor and troubleshoot distributed workflows, Identify performance bottlenecks, Track down root causes, Analyze service dependencies

Here, we have the OpenTelemetry collector send the trace data to Jaeger.

Deploy OpenTelemetry Collector

First, add the OpenTelemetry collector Helm repository:

helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update

Next, deploy the OpenTelemetry collector to your cluster:

helm upgrade --install otel-collector open-telemetry/opentelemetry-collector \
    --set image.repository="otel/opentelemetry-collector-k8s" \
    --set nameOverride=otel-collector \
    --set mode=deployment \
    --set config.exporters.otlp.endpoint=http://jaeger-collector.default.svc.cluster.local:4317 \
    --set config.exporters.otlp.tls.insecure=true \
    --set config.service.pipelines.traces.exporters\[0\]=otlp \
    --set config.service.pipelines.traces.processors\[0\]=batch \
    --set config.service.pipelines.traces.receivers\[0\]=otlp \
    --set config.service.pipelines.traces.receivers\[1\]=jaeger

Deploy Jaeger

Next, add the Jaeger Helm repository:

helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update

Then, deploy Jaeger to your cluster:

helm upgrade --install jaeger jaegertracing/jaeger \
    --set provisionDataStore.cassandra=false \
    --set allInOne.enabled=true \
    --set agent.enabled=false \
    --set collector.enabled=false \
    --set query.enabled=false \
    --set storage.type=memory

Configure the SpinAppExecutor

The SpinAppExecutor resource determines how Spin applications are deployed in the cluster. The following configuration will ensure that any SpinApp resource using this executor will send telemetry data to the OpenTelemetry collector. To see a comprehensive list of OTel options for the SpinAppExecutor, see the API reference.

Create a file called executor.yaml with the following content:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinAppExecutor
metadata:
  name: otel-shim-executor
spec:
  createDeployment: true
  deploymentConfig:
    runtimeClassName: wasmtime-spin-v2
    installDefaultCACerts: true
    otel:
      exporter_otlp_endpoint: http://otel-collector.default.svc.cluster.local:4318

To deploy the executor, run:

kubectl apply -f executor.yaml

Deploy a Spin app to observe

With everything in place, we can now deploy a SpinApp resource that uses the executor otel-shim-executor.

Create a file called app.yaml with the following content:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: otel-spinapp
spec:
  image: ghcr.io/spinkube/spin-operator/cpu-load-gen:20240311-163328-g1121986
  executor: otel-shim-executor
  replicas: 1

Deploy the app by running:

kubectl apply -f app.yaml

Congratulations! You now have a Spin app exporting telemetry data.

Next, we need to generate telemetry data for the Spin app to export. Use the below command to port-forward the Spin app:

kubectl port-forward svc/otel-spinapp 3000:80

In a new terminal window, execute a curl request:

curl localhost:3000

The request will take a couple of moments to run, but once it’s done, you should see an output similar to this:

fib(43) = 433494437

Interact with Jaeger

To view the traces in Jaeger, use the following port-forward command:

kubectl port-forward svc/jaeger-query 16686:16686

Then, open your browser and navigate to localhost:16686 to interact with Jaeger’s UI.

7 - Using a key value store

Connect your Spin App to a key value store

Spin applications can utilize a standardized API for persisting data in a key value store. The default key value store in Spin is an SQLite database, which is great for quickly utilizing non-relational local storage without any infrastructure set-up. However, this solution may not be preferable for an app running in the context of SpinKube, where apps are often scaled beyond just one replica.

Thankfully, Spin supports configuring an application with an external key value provider. External providers include Redis or Valkey and Azure Cosmos DB.

Prerequisites

To follow along with this tutorial, you’ll need:

Build and publish the Spin application

For this tutorial, we’ll use a Spin key/value application written with the Go SDK. The application serves a CRUD (Create, Read, Update, Delete) API for managing key/value pairs.

First, clone the repository locally and navigate to the examples/key-value directory:

git clone git@github.com:fermyon/spin-go-sdk.git
cd examples/key-value

Now, build and push the application to a registry you have access to. Here we’ll use ttl.sh:

export IMAGE_NAME=ttl.sh/$(uuidgen):1h
spin build
spin registry push ${IMAGE_NAME}

Configure an external key value provider

Since we have access to a Kubernetes cluster already running SpinKube, we’ll choose Valkey for our key value provider and install this provider via Bitnami’s Valkey Helm chart. Valkey is swappable for Redis in Spin, though note we do need to supply a URL using the redis:// protocol rather than valkey://.

helm install valkey --namespace valkey --create-namespace oci://registry-1.docker.io/bitnamicharts/valkey

As mentioned in the notes shown after successful installation, be sure to capture the valkey password for use later:

export VALKEY_PASSWORD=$(kubectl get secret --namespace valkey valkey -o jsonpath="{.data.valkey-password}" | base64 -d)

Create a Kubernetes Secret for the Valkey URL

The runtime configuration will require the Valkey URL so that it can connect to this provider. As this URL contains the sensitive password string, we will create it as a Secret resource in Kubernetes:

kubectl create secret generic kv-secret --from-literal=valkey-url="redis://:${VALKEY_PASSWORD}@valkey-master.valkey.svc.cluster.local:6379"

Prepare the SpinApp manifest

You’re now ready to assemble the SpinApp custom resource manifest for this application.

  • All of the key value config is set under spec.runtimeConfig.keyValueStores. See the keyValueStores reference guide for more details.
  • Here we configure the default store to use the redis provider type and under options supply the Valkey URL (via its Kubernetes secret)

Plug the $IMAGE_NAME and $DB_URL values into the manifest below and save as spinapp.yaml:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: kv-app
spec:
  image: "$IMAGE_NAME"
  replicas: 1
  executor: containerd-shim-spin
  runtimeConfig:
    keyValueStores:
      - name: "default"
        type: "redis"
        options:
          - name: "url"
            valueFrom:
              secretKeyRef:
                name: "kv-secret"
                key: "valkey-url"

Create the SpinApp

Apply the resource manifest to your Kubernetes cluster:

kubectl apply -f spinapp.yaml

The Spin Operator will handle the creation of the underlying Kubernetes resources on your behalf.

Test the application

Now you are ready to test the application and verify connectivity and key value storage to the configured provider.

Configure port forwarding from your local machine to the corresponding Kubernetes Service:

kubectl port-forward services/kv-app 8080:80

Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

When port forwarding is established, you can send HTTP requests to the application from within an additional terminal session. Here are a few examples to get you started.

Create a test key with value ok!:

$ curl -i -X POST -d "ok!" localhost:8080/test
HTTP/1.1 200 OK
content-length: 0
date: Mon, 29 Jul 2024 19:58:14 GMT

Get the value for the test key:

$ curl -i -X GET localhost:8080/test
HTTP/1.1 200 OK
content-length: 3
date: Mon, 29 Jul 2024 19:58:39 GMT

ok!

Delete the value for the test key:

$ curl -i -X DELETE localhost:8080/test
HTTP/1.1 200 OK
content-length: 0
date: Mon, 29 Jul 2024 19:59:18 GMT

Attempt to get the value for the test key:

$ curl -i -X GET localhost:8080/test
HTTP/1.1 500 Internal Server Error
content-type: text/plain; charset=utf-8
x-content-type-options: nosniff
content-length: 12
date: Mon, 29 Jul 2024 19:59:44 GMT

no such key

8 - Connecting to a SQLite database

Connect your Spin App to an external SQLite database

Spin applications can utilize a standardized API for persisting data in a SQLite database. A default database is created by the Spin runtime on the local filesystem, which is great for getting an application up and running. However, this on-disk solution may not be preferable for an app running in the context of SpinKube, where apps are often scaled beyond just one replica.

Thankfully, Spin supports configuring an application with an external SQLite database provider via runtime configuration. External providers include any libSQL databases that can be accessed over HTTPS.

Prerequisites

To follow along with this tutorial, you’ll need:

Build and publish the Spin application

For this tutorial, we’ll use the HTTP CRUD Go SQLite sample application. It is a Go-based app implementing CRUD (Create, Read, Update, Delete) operations via the SQLite API.

First, clone the repository locally and navigate to the http-crud-go-sqlite directory:

git clone git@github.com:fermyon/enterprise-architectures-and-patterns.git
cd enterprise-architectures-and-patterns/http-crud-go-sqlite

Now, build and push the application to a registry you have access to. Here we’ll use ttl.sh:

export IMAGE_NAME=ttl.sh/$(uuidgen):1h
spin build
spin registry push ${IMAGE_NAME}

Create a LibSQL database

If you don’t already have a LibSQL database that can be used over HTTPS, you can follow along as we set one up via Turso.

Before proceeding, install the turso CLI and sign up for an account, if you haven’t done so already.

Create a new database and save its HTTP URL:

turso db create spinkube
export DB_URL=$(turso db show spinkube --http-url)

Next, create an auth token for this database:

export DB_TOKEN=$(turso db tokens create spinkube)

Create a Kubernetes Secret for the database token

The database token is a sensitive value and thus should be created as a Secret resource in Kubernetes:

kubectl create secret generic turso-auth --from-literal=db-token="${DB_TOKEN}"

Prepare the SpinApp manifest

You’re now ready to assemble the SpinApp custom resource manifest.

  • Note the image value uses the reference you published above.
  • All of the SQLite database config is set under spec.runtimeConfig.sqliteDatabases. See the sqliteDatabases reference guide for more details.
  • Here we configure the default database to use the libsql provider type and under options supply the database URL and auth token (via its Kubernetes secret)

Plug the $IMAGE_NAME and $DB_URL values into the manifest below and save as spinapp.yaml:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: http-crud-go-sqlite
spec:
  image: "$IMAGE_NAME"
  replicas: 1
  executor: containerd-shim-spin
  runtimeConfig:
    sqliteDatabases:
      - name: "default"
        type: "libsql"
        options:
          - name: "url"
            value: "$DB_URL"
          - name: "token"
            valueFrom:
              secretKeyRef:
                name: "turso-auth"
                key: "db-token"

Create the SpinApp

Apply the resource manifest to your Kubernetes cluster:

kubectl apply -f spinapp.yaml

The Spin Operator will handle the creation of the underlying Kubernetes resources on your behalf.

Test the application

Now you are ready to test the application and verify connectivity and data storage to the configured SQLite database.

Configure port forwarding from your local machine to the corresponding Kubernetes Service:

kubectl port-forward services/http-crud-go-sqlite 8080:80

Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

When port forwarding is established, you can send HTTP requests to the http-crud-go-sqlite app from within an additional terminal session. Here are a few examples to get you started.

Get current items:

$ curl -X GET http://localhost:8080/items
[
  {
    "id": "8b933c84-ee60-45a1-848d-428ad3259e2b",
    "name": "Full Self Driving (FSD)",
    "active": true
  },
  {
    "id": "d660b9b2-0406-46d6-9efe-b40b4cca59fc",
    "name": "Sentry Mode",
    "active": true
  }
]

Create a new item:

$ curl -X POST -d '{"name":"Engage Thrusters","active":true}' localhost:8080/items
{
  "id": "a5efaa73-a4ac-4ffc-9c5c-61c5740e2d9f",
  "name": "Engage Thrusters",
  "active": true
}

Get items and see the newly added item:

$ curl -X GET http://localhost:8080/items
[
  {
    "id": "8b933c84-ee60-45a1-848d-428ad3259e2b",
    "name": "Full Self Driving (FSD)",
    "active": true
  },
  {
    "id": "d660b9b2-0406-46d6-9efe-b40b4cca59fc",
    "name": "Sentry Mode",
    "active": true
  },
  {
    "id": "a5efaa73-a4ac-4ffc-9c5c-61c5740e2d9f",
    "name": "Engage Thrusters",
    "active": true
  }
]

9 - Autoscaling your apps

Guides on autoscaling your applications with SpinKube.

9.1 - Using the `spin kube` plugin

A tutorial to show how autoscaler support can be enabled via the spin kube command.

Horizontal autoscaling support

In Kubernetes, a horizontal autoscaler automatically updates a workload resource (such as a Deployment or StatefulSet) with the aim of automatically scaling the workload to match demand.

Horizontal scaling means that the response to increased load is to deploy more resources. This is different from vertical scaling, which for Kubernetes would mean assigning more memory or CPU to the resources that are already running for the workload.

If the load decreases, and the number of resources is above the configured minimum, a horizontal autoscaler would instruct the workload resource (the Deployment, StatefulSet, or other similar resource) to scale back down.

The Kubernetes plugin for Spin includes autoscaler support, which allows you to tell Kubernetes when to scale your Spin application up or down based on demand. This tutorial will show you how to enable autoscaler support via the spin kube scaffold command.

Prerequisites

Regardless of what type of autoscaling is used, you must determine how you want your application to scale by answering the following questions:

  1. Do you want your application to scale based upon system metrics (CPU and memory utilization) or based upon events (like messages in a queue or rows in a database)?
  2. If you application scales based on system metrics, how much CPU and memory each instance does your application need to operate?

Choosing an autoscaler

The Kubernetes plugin for Spin supports two types of autoscalers: Horizontal Pod Autoscaler (HPA) and Kubernetes Event-driven Autoscaling (KEDA). The choice of autoscaler depends on the requirements of your application.

Horizontal Pod Autoscaling (HPA)

Horizontal Pod Autoscaler (HPA) scales Kubernetes pods based on CPU or memory utilization. This HPA scaling can be implemented via the Kubernetes plugin for Spin by setting the --autoscaler hpa option. This page deals exclusively with autoscaling via the Kubernetes plugin for Spin.

spin kube scaffold --from user-name/app-name:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi

Horizontal Pod Autoscaling is built-in to Kubernetes and does not require the installation of a third-party runtime. For more general information about scaling with HPA, please see the Spin Operator’s Scaling with HPA section

Kubernetes Event-driven Autoscaling (KEDA)

Kubernetes Event-driven Autoscaling (KEDA) is an extension of Horizontal Pod Autoscaling (HPA). On top of allowing to scale based on CPU or memory utilization, KEDA allows for scaling based on events from various sources like messages in a queue, or the number of rows in a database.

KEDA can be enabled by setting the --autoscaler keda option:

spin kube scaffold --from user-name/app-name:latest --autoscaler keda --cpu-limit 100m --memory-limit 128Mi -replicas 1 --max-replicas 10

Using KEDA to autoscale your Spin applications requires the installation of the KEDA runtime into your Kubernetes cluster. For more information about scaling with KEDA in general, please see the Spin Operator’s Scaling with KEDA section

Setting min/max replicas

The --replicas and --max-replicas options can be used to set the minimum and maximum number of replicas for your application. The --replicas option defaults to 2 and the --max-replicas option defaults to 3.

spin kube scaffold --from user-name/app-name:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi -replicas 1 --max-replicas 10

Setting CPU/memory limits and CPU/memory requests

If the node where an application is running has enough of a resource available, it’s possible (and allowed) for that application to use more resource than its resource request for that resource specifies. However, an application is not allowed to use more than its resource limit.

For example, if you set a memory request of 256 MiB, and that application is scheduled to a node with 8GiB of memory and no other appplications, then the application can try to use more RAM.

If you set a memory limit of 4GiB for that application, the webassembly runtime will enforce that limit. The runtime prevents the application from using more than the configured resource limit. For example: when a process in the application tries to consume more than the allowed amount of memory, the webassembly runtime terminates the process that attempted the allocation with an out of memory (OOM) error.

The --cpu-limit, --memory-limit, --cpu-request, and --memory-request options can be used to set the CPU and memory limits and requests for your application. The --cpu-limit and --memory-limit options are required, while the --cpu-request and --memory-request options are optional.

It is important to note the following:

  • CPU/memory requests are optional and will default to the CPU/memory limit if not set.
  • CPU/memory requests must be lower than their respective CPU/memory limit.
  • If you specify a limit for a resource, but do not specify any request, and no admission-time mechanism has applied a default request for that resource, then Kubernetes copies the limit you specified and uses it as the requested value for the resource.
spin kube scaffold --from user-name/app-name:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi --cpu-request 50m --memory-request 64Mi

Setting target utilization

Target utilization is the percentage of the resource that you want to be used before the autoscaler kicks in. The autoscaler will check the current resource utilization of your application against the target utilization and scale your application up or down based on the result.

Target utilization is based on the average resource utilization across all instances of your application. For example, if you have 3 instances of your application, the target CPU utilization is 50%, and each application is averaging 80% CPU utilization, the autoscaler will continue to increase the number of instances until all instances are averaging 50% CPU utilization.

To scale based on CPU utilization, use the --autoscaler-target-cpu-utilization option:

spin kube scaffold --from user-name/app-name:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi --autoscaler-target-cpu-utilization 50

To scale based on memory utilization, use the --autoscaler-target-memory-utilization option:

spin kube scaffold --from user-name/app-name:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi --autoscaler-target-memory-utilization 50

9.2 - Scaling Spin App With Horizontal Pod Autoscaling (HPA)

This tutorial illustrates how one can horizontally scale Spin Apps in Kubernetes using Horizontal Pod Autscaling (HPA).

Horizontal scaling, in the Kubernetes sense, means deploying more pods to meet demand (different from vertical scaling whereby more memory and CPU resources are assigned to already running pods). In this tutorial, we configure HPA to dynamically scale the instance count of our SpinApps to meet the demand.

Prerequisites

Ensure you have the following tools installed:

  • Docker - for running k3d
  • kubectl - the Kubernetes CLI
  • k3d - a lightweight Kubernetes distribution that runs on Docker
  • Helm - the package manager for Kubernetes
  • Bombardier - cross-platform HTTP benchmarking CLI

We use k3d to run a Kubernetes cluster locally as part of this tutorial, but you can follow these steps to configure HPA autoscaling on your desired Kubernetes environment.

Setting Up Kubernetes Cluster

Run the following command to create a Kubernetes cluster that has the containerd-shim-spin pre-requisites installed: If you have a Kubernetes cluster already, please feel free to use it:

k3d cluster create wasm-cluster-scale \
  --image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.15.1 \
  -p "8081:80@loadbalancer" \
  --agents 2

Deploying Spin Operator and its dependencies

First, you have to install cert-manager to automatically provision and manage TLS certificates (used by Spin Operator’s admission webhook system). For detailed installation instructions see the cert-manager documentation.

# Install cert-manager CRDs
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.3/cert-manager.crds.yaml

# Add and update Jetstack repository
helm repo add jetstack https://charts.jetstack.io
helm repo update

# Install the cert-manager Helm chart
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.14.3

Next, run the following commands to install the Spin Runtime Class and Spin Operator Custom Resource Definitions (CRDs):

Note: In a production cluster you likely want to customize the Runtime Class with a nodeSelector that matches nodes that have the shim installed. However, in the K3d example, they’re installed on every node.

# Install the RuntimeClass
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.runtime-class.yaml

# Install the CRDs
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.crds.yaml

Lastly, install Spin Operator using helm and the shim executor with the following commands:

# Install Spin Operator
helm install spin-operator \
  --namespace spin-operator \
  --create-namespace \
  --version 0.3.0 \
  --wait \
  oci://ghcr.io/spinkube/charts/spin-operator

# Install the shim executor
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.shim-executor.yaml

Great, now you have Spin Operator up and running on your cluster. This means you’re set to create and deploy SpinApps later on in the tutorial.

Set Up Ingress

Use the following command to set up ingress on your Kubernetes cluster. This ensures traffic can reach your SpinApp once we’ve created it in future steps:

# Setup ingress following this tutorial https://k3d.io/v5.4.6/usage/exposing_services/
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
  annotations:
    ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: hpa-spinapp
            port:
              number: 80
EOF

Hit enter to create the ingress resource.

Deploy Spin App and HorizontalPodAutoscaler (HPA)

Next up we’re going to deploy the Spin App we will be scaling. You can find the source code of the Spin App in the apps/cpu-load-gen folder of the Spin Operator repository.

We can take a look at the SpinApp and HPA definitions in our deployment file below/. As you can see, we have set our resources -> limits to 500m of cpu and 500Mi of memory per Spin application and we will scale the instance count when we’ve reached a 50% utilization in cpu and memory. We’ve also defined support a maximum replica count of 10 and a minimum replica count of 1:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: hpa-spinapp
spec:
  image: ghcr.io/spinkube/spin-operator/cpu-load-gen:20240311-163328-g1121986
  enableAutoscaling: true
  resources:
    limits:
      cpu: 500m
      memory: 500Mi
    requests:
      cpu: 100m
      memory: 400Mi
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spinapp-autoscaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hpa-spinapp
  minReplicas: 1
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50

For more information about HPA, please visit the following links:

Below is an example of the configuration to scale resources:

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: hpa-spinapp
spec:
  image: ghcr.io/spinkube/spin-operator/cpu-load-gen:20240311-163328-g1121986
  executor: containerd-shim-spin
  enableAutoscaling: true
  resources:
    limits:
      cpu: 500m
      memory: 500Mi
    requests:
      cpu: 100m
      memory: 400Mi
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spinapp-autoscaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hpa-spinapp
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

Let’s deploy the SpinApp and the HPA instance onto our cluster (using the above .yaml configuration). To apply the above configuration we use the following kubectl apply command:

# Install SpinApp and HPA
kubectl apply -f https://raw.githubusercontent.com/spinkube/spin-operator/main/config/samples/hpa.yaml

You can see your running Spin application by running the following command:

kubectl get spinapps
NAME          AGE
hpa-spinapp   92m

You can also see your HPA instance with the following command:

kubectl get hpa
NAME                 REFERENCE                TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
spinapp-autoscaler   Deployment/hpa-spinapp   6%/50%    1         10        1          97m

Please note: The Kubernetes Plugin for Spin is a tool designed for Kubernetes integration with the Spin command-line interface. The Kubernetes Plugin for Spin has a scaling tutorial that demonstrates how to use the spin kube command to tell Kubernetes when to scale your Spin application up or down based on demand).

Generate Load to Test Autoscale

Now let’s use Bombardier to generate traffic to test how well HPA scales our SpinApp. The following Bombardier command will attempt to establish 40 connections during a period of 3 minutes (or less). If a request is not responded to within 5 seconds that request will timeout:

# Generate a bunch of load
bombardier -c 40 -t 5s -d 3m http://localhost:8081

To watch the load, we can run the following command to get the status of our deployment:

kubectl describe deploy hpa-spinapp
...
---

Available      True    MinimumReplicasAvailable
Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   hpa-spinapp-544c649cf4 (1/1 replicas created)
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  11m    deployment-controller  Scaled up replica set hpa-spinapp-544c649cf4 to 1
  Normal  ScalingReplicaSet  9m45s  deployment-controller  Scaled up replica set hpa-spinapp-544c649cf4  to 4
  Normal  ScalingReplicaSet  9m30s  deployment-controller  Scaled up replica set hpa-spinapp-544c649cf4  to 8
  Normal  ScalingReplicaSet  9m15s  deployment-controller  Scaled up replica set hpa-spinapp-544c649cf4  to 10

9.3 - Scaling Spin App With Kubernetes Event-Driven Autoscaling (KEDA)

This tutorial illustrates how one can horizontally scale Spin Apps in Kubernetes using Kubernetes Event-Driven Autoscaling (KEDA).

KEDA extends Kubernetes to provide event-driven scaling capabilities, allowing it to react to events from Kubernetes internal and external sources using KEDA scalers. KEDA provides a wide variety of scalers to define scaling behavior base on sources like CPU, Memory, Azure Event Hubs, Kafka, RabbitMQ, and more. We use a ScaledObject to dynamically scale the instance count of our SpinApp to meet the demand.

Prerequisites

Please ensure the following tools are installed on your local machine:

  • kubectl - the Kubernetes CLI
  • Helm - the package manager for Kubernetes
  • Docker - for running k3d
  • k3d - a lightweight Kubernetes distribution that runs on Docker
  • Bombardier - cross-platform HTTP benchmarking CLI

We use k3d to run a Kubernetes cluster locally as part of this tutorial, but you can follow these steps to configure KEDA autoscaling on your desired Kubernetes environment.

Setting Up Kubernetes Cluster

Run the following command to create a Kubernetes cluster that has the containerd-shim-spin pre-requisites installed: If you have a Kubernetes cluster already, please feel free to use it:

k3d cluster create wasm-cluster-scale \
  --image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.15.1 \
  -p "8081:80@loadbalancer" \
  --agents 2

Deploying Spin Operator and its dependencies

First, you have to install cert-manager to automatically provision and manage TLS certificates (used by Spin Operator’s admission webhook system). For detailed installation instructions see the cert-manager documentation.

# Install cert-manager CRDs
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.3/cert-manager.crds.yaml

# Add and update Jetstack repository
helm repo add jetstack https://charts.jetstack.io
helm repo update

# Install the cert-manager Helm chart
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.14.3

Next, run the following commands to install the Spin Runtime Class and Spin Operator Custom Resource Definitions (CRDs):

Note: In a production cluster you likely want to customize the Runtime Class with a nodeSelector that matches nodes that have the shim installed. However, in the K3d example, they’re installed on every node.

# Install the RuntimeClass
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.runtime-class.yaml

# Install the CRDs
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.crds.yaml

Lastly, install Spin Operator using helm and the shim executor with the following commands:

# Install Spin Operator
helm install spin-operator \
  --namespace spin-operator \
  --create-namespace \
  --version 0.3.0 \
  --wait \
  oci://ghcr.io/spinkube/charts/spin-operator

# Install the shim executor
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.shim-executor.yaml

Great, now you have Spin Operator up and running on your cluster. This means you’re set to create and deploy SpinApps later on in the tutorial.

Set Up Ingress

Use the following command to set up ingress on your Kubernetes cluster. This ensures traffic can reach your Spin App once we’ve created it in future steps:

# Setup ingress following this tutorial https://k3d.io/v5.4.6/usage/exposing_services/
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
  annotations:
    ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: keda-spinapp
            port:
              number: 80
EOF

Hit enter to create the ingress resource.

Setting Up KEDA

Use the following command to setup KEDA on your Kubernetes cluster using Helm. Different deployment methods are described at Deploying KEDA on keda.sh:

# Add the Helm repository
helm repo add kedacore https://kedacore.github.io/charts

# Update your Helm repositories
helm repo update

# Install the keda Helm chart into the keda namespace
helm install keda kedacore/keda --namespace keda --create-namespace

Deploy Spin App and the KEDA ScaledObject

Next up we’re going to deploy the Spin App we will be scaling. You can find the source code of the Spin App in the apps/cpu-load-gen folder of the Spin Operator repository.

We can take a look at the SpinApp and the KEDA ScaledObject definitions in our deployment files below. As you can see, we have explicitly specified resource limits to 500m of cpu (spec.resources.limits.cpu) and 500Mi of memory (spec.resources.limits.memory) per SpinApp:

# https://raw.githubusercontent.com/spinkube/spin-operator/main/config/samples/keda-app.yaml
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: keda-spinapp
spec:
  image: ghcr.io/spinkube/spin-operator/cpu-load-gen:20240311-163328-g1121986
  executor: containerd-shim-spin
  enableAutoscaling: true
  resources:
    limits:
      cpu: 500m
      memory: 500Mi
    requests:
      cpu: 100m
      memory: 400Mi
---

We will scale the instance count when we’ve reached a 50% utilization in cpu (spec.triggers[cpu].metadata.value). We’ve also instructed KEDA to scale our SpinApp horizontally within the range of 1 (spec.minReplicaCount) and 20 (spec.maxReplicaCount).:

# https://raw.githubusercontent.com/spinkube/spin-operator/main/config/samples/keda-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: cpu-scaling
spec:
  scaleTargetRef:
    name: keda-spinapp
  minReplicaCount: 1
  maxReplicaCount: 20
  triggers:
    - type: cpu
      metricType: Utilization
      metadata:
        value: "50"

The Kubernetes documentation is the place to learn more about limits and requests. Consult the KEDA documentation to learn more about ScaledObject and KEDA’s built-in scalers.

Let’s deploy the SpinApp and the KEDA ScaledObject instance onto our cluster with the following command:

# Deploy the SpinApp
kubectl apply -f https://raw.githubusercontent.com/spinkube/spin-operator/main/config/samples/keda-app.yaml
spinapp.core.spinoperator.dev/keda-spinapp created

# Deploy the ScaledObject
kubectl apply -f https://raw.githubusercontent.com/spinkube/spin-operator/main/config/samples/keda-scaledobject.yaml
scaledobject.keda.sh/cpu-scaling created

You can see your running Spin application by running the following command:

kubectl get spinapps

NAME          READY REPLICAS   EXECUTOR
keda-spinapp  1                containerd-shim-spin

You can also see your KEDA ScaledObject instance with the following command:

kubectl get scaledobject

NAME          SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   TRIGGERS   READY   ACTIVE   AGE
cpu-scaling   apps/v1.Deployment   keda-spinapp      1     20    cpu        True    True     7m

Generate Load to Test Autoscale

Now let’s use Bombardier to generate traffic to test how well KEDA scales our SpinApp. The following Bombardier command will attempt to establish 40 connections during a period of 3 minutes (or less). If a request is not responded to within 5 seconds that request will timeout:

# Generate a bunch of load
bombardier -c 40 -t 5s -d 3m http://localhost:8081

To watch the load, we can run the following command to get the status of our deployment:

kubectl describe deploy keda-spinapp
...
---

Available      True    MinimumReplicasAvailable
Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   keda-spinapp-76db5d7f9f (1/1 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  84s   deployment-controller  Scaled up replica set hpa-spinapp-76db5d7f9f  to 2 from 1
  Normal  ScalingReplicaSet  69s   deployment-controller  Scaled up replica set hpa-spinapp-76db5d7f9f  to 4 from 2
  Normal  ScalingReplicaSet  54s   deployment-controller  Scaled up replica set hpa-spinapp-76db5d7f9f  to 8 from 4
  Normal  ScalingReplicaSet  39s   deployment-controller  Scaled up replica set hpa-spinapp-76db5d7f9f  to 16 from 8
  Normal  ScalingReplicaSet  24s   deployment-controller  Scaled up replica set hpa-spinapp-76db5d7f9f  to 20 from 16

10 - SpinKube at a glance

A high level overview of the SpinKube sub-projects.

spin-operator

Spin Operator is a Kubernetes operator which empowers platform engineers to deploy Spin applications as custom resources to their Kubernetes clusters. Spin Operator provides an elegant solution for platform engineers looking to improve efficiency without compromising on performance while maintaining workload portability.

Why Spin Operator?

By bringing the power of the Spin framework to Kubernetes clusters, Spin Operator provides application developers and platform engineers with the best of both worlds. For developers, this means easily building portable serverless functions that leverage the power and performance of Wasm via the Spin developer tool. For platform engineers, this means using idiomatic Kubernetes primitives (secrets, autoscaling, etc.) and tooling to manage these workloads at scale in a production environment, improving their overall operational efficiency.

How Does Spin Operator Work?

Built with the kubebuilder framework, Spin Operator is a Kubernetes operator. Kubernetes operators are used to extend Kubernetes automation to new objects, defined as custom resources, without modifying the Kubernetes API. The Spin Operator is composed of two main components:

  • A controller that defines and manages Wasm workloads on k8s.
  • The “SpinApps” Custom Resource Definition (CRD).

spin-operator diagram

SpinApps CRDs can be composed manually or generated automatically from an existing Spin application using the spin kube scaffold command. The former approach lends itself well to CI/CD systems, whereas the latter is a better fit for local testing as part of a local developer workflow.

Once an application deployment begins, Spin Operator handles scheduling the workload on the appropriate nodes (thanks to the Runtime Class Manager, previously known as Kwasm) and managing the resource’s lifecycle. There is no need to fetch the containerd-shim-spin binary or mutate node labels. This is all managed via the Runtime Class Manager, which you will install as a dependency when setting up Spin Operator.

containerd-shim-spin

The containerd-shim-spin is a containerd shim implementation for Spin, which enables running Spin workloads on Kubernetes via runwasi. This means that by installing this shim onto Kubernetes nodes, we can add a runtime class to Kubernetes and schedule Spin workloads on those nodes. Your Spin apps can act just like container workloads!

The containerd-shim-spin is specifically designed to execute applications built with Spin (a developer tool for building and running serverless Wasm applications). The shim ensures that Wasm workloads can be managed effectively within a Kubernetes environment, leveraging containerd’s capabilities.

In a Kubernetes cluster, specific nodes can be bootstrapped with Wasm runtimes and labeled accordingly to facilitate the scheduling of Wasm workloads. RuntimeClasses in Kubernetes are used to schedule Pods to specific nodes and target specific runtimes. By defining a RuntimeClass with the appropriate NodeSelector and handler, Kubernetes can direct Wasm workloads to nodes equipped with the necessary Wasm runtimes and ensure they are executed with the correct runtime handler.

Overall, the Containerd Shim Spin represents a significant advancement in integrating Wasm workloads into Kubernetes clusters, enhancing the versatility and capabilities of container orchestration.

runtime-class-manager

The Runtime Class Manager, also known as the Containerd Shim Lifecycle Operator, is designed to automate and manage the lifecycle of containerd shims in a Kubernetes environment. This includes tasks like installation, update, removal, and configuration of shims, reducing manual errors and improving reliability in managing WebAssembly (Wasm) workloads and other containerd extensions.

The Runtime Class Manager provides a robust and production-ready solution for installing, updating, and removing shims, as well as managing node labels and runtime classes in a Kubernetes environment.

By automating these processes, the runtime-class-manager enhances reliability, reduces human error, and simplifies the deployment and management of containerd shims in Kubernetes clusters.

spin-plugin-kube

The Kubernetes plugin for Spin is designed to enhance Spin by enabling the execution of Wasm modules directly within a Kubernetes cluster. Specifically a tool designed for Kubernetes integration with the Spin command-line interface. This plugin works by integrating with containerd shims, allowing Kubernetes to manage and run Wasm workloads in a way similar to traditional container workloads.

The Kubernetes plugin for Spin allows developers to use the Spin command-line interface for deploying Spin applications; it provides a seamless workflow for building, pushing, deploying, and managing Spin applications in a Kubernetes environment. It includes commands for scaffolding new components as Kubernetes manifests, and deploying and retrieving information about Spin applications running in Kubernetes. This plugin is an essential tool for developers looking to streamline their Spin application deployment on Kubernetes platforms.