Project Radius

Photo of author

Dan Rios

📅

🔄

17 minute read

Radius is an open-source, cloud-native application platform from the Azure Incubations team (Dapr, KEDA, Drasi) and a CNCF sandbox project. It works across Azure and AWS, bridging what developers declare with what operators provision. I’ll use ‘operators’ to mean platform or cloud engineering teams throughout the rest of the blog, as this is how Radius frames things.

TLDR: Radius separates application declarations from infrastructure implementations. This blog shows how to build Dapr based applications where the same code deploys to local containers (dev) or managed Azure services (prod) without changing a line.

In this blog, we’ll cover:

  • The core problem Radius solves: decoupling app definitions from infrastructure implementations
  • How recipes let operators enforce standards without touching app code
  • A hands-on walk through of building a Dapr app incrementally with Radius and Bicep
  • Understanding portable resources and environments
  • Writing and publishing your own recipes with Bicep
  • How Radius integrates with Kubernetes and Azure

The Problem

Dapr applications need infrastructure: pub/sub, secrets, state, dbs. These can be hardcoded implementations, be cloud-specific, and/or tangled with your app code. So when the infrastructure changes, the app may break with it.

The result of this is that resource definition and resource deployment are coupled together. A dev may hardcode “I need Azure Service Bus” in Bicep. The operator can’t swap it to a different service or even cloud provider. There’s no standard way to describe what an application actually needs independently from how to deliver it. This is where Radius comes into play.

How Radius Solves It

Radius separates what you need from how to deliver it:

  • Resource Types – Abstraction: “I need a pub/sub broker” instead of “I need Service Bus with these settings”
  • Recipes – A containerised message broker for dev, Azure Service Bus for prod. They have the same type but different recipe.
  • Environments – Collections of recipes and infrastructure configuration for a deployment target.

Recipes can be written in Bicep or Terraform. Applications and environments are always defined in Bicep. Bicep types for AWS exist (radius-project/bicep-types-aws), so your Radius recipes can deploy AWS resources too (pretty cool.).

Your workspace’s bicepconfig.json (created during rad initialize) tells Bicep where to find Radius type definitions:

{
    "extensions": {
        "radius": "br:biceptypes.azurecr.io/radius:latest",
        "aws": "br:biceptypes.azurecr.io/aws:latest"
    }
}
JSON

Understanding Dapr

Dapr is a distributed application runtime that handles state management, pub/sub, service invocation, secrets and more. It runs as a sidecar in your containers. Radius manages the infrastructure that Dapr depends on. You’ll see Dapr references throughout the walkthrough (e.g. Applications.Dapr/pubSubBrokers, Applications.Dapr/stateStores, daprSidecar).

Radius doesn’t replace Dapr rather, it orchestrates it. When you declare “I need a pub/sub broker”, Radius provisions the infrastructure (container, Azure Service Bus, etc.) and connects it to Dapr as a component. Radius handles the operational complexity. Your application just uses Dapr’s runtime APIs.


Install

You’ll need a Kubernetes cluster. Docker Desktop works for local development; I’m using Azure Kubernetes Service (AKS) for a production / Azure scenario in this walk through. Radius runs on your cluster and manages infrastructure for your Dapr applications.

# Install Radius
winget install Radius.Radius

# Verify
rad version
Bash

Initialise the workspace:

mkdir Radius
cd Radius
rad initialize
rad bicep download
Bash

This scaffolds your workspace with project files. To install Radius to your cluster:

rad install kubernetes
Bash

Walk through: Building Up Incrementally

This hands-on walkthrough shows how Radius lets developers and operators split work naturally. Starting with a UI, then add backing infrastructure incrementally.

Before you start, you’ll need:

  • Docker Desktop (or any local Kubernetes cluster)
  • rad CLI installed (docs)
This walkthrough is dev/local-focused. Optionally: to follow the any of the ‘prod’ deployment section, you’ll also need an Azure subscription, ACR, and an AKS cluster.

Environment setup

The operator deploys these two files once. The developer never edits them.

Dev environment (dev.bicep) local Kubernetes with public recipes:

extension radius

param namespace string = 'dev'

resource devEnvironment 'Applications.Core/environments@2023-10-01-preview' = {
  name: 'dev'
  properties: {
    compute: { kind: 'kubernetes', namespace: namespace }
    recipes: {
      'Applications.Dapr/stateStores': {
        default: { templateKind: 'bicep', templatePath: 'ghcr.io/radius-project/recipes/local-dev/statestores:latest' }
      }
      'Applications.Datastores/sqlDatabases': {
        default: { templateKind: 'bicep', templatePath: 'ghcr.io/radius-project/recipes/local-dev/sqldatabases:latest' }
      }
      'Applications.Dapr/pubSubBrokers': {
        default: { templateKind: 'bicep', templatePath: 'ghcr.io/radius-project/recipes/local-dev/pubsubbrokers:latest' }
      }
    }
  }
}

output environmentId string = devEnvironment.id
BICEP

Prod environment (prod.bicep) Azure with private ACR recipes. This uses Azure workload identity to authenticate to your ACR – no passwords. See docs.radapp.io/guides/operations/providers/azure-provider for the Azure provider overview, and howto-azure-provider-wi for the workload identity setup guide.

extension radius

param azureScope string = '/subscriptions/<your-sub>/resourceGroups/<your-rg>'
param namespace string = 'prod'
param acrClientId string = '<your-acr-client-id>'
param acrTenantId string = '<your-tenant-id>'
@secure() 
param sqlAdminPassword string

resource acrSecret 'Applications.Core/secretStores@2023-10-01-preview' = {
  name: 'acr-secret-prod'
  properties: {
    resource: 'radius-system/acr-secret-prod'
    type: 'azureWorkloadIdentity'
    data: { clientId: { value: acrClientId }, tenantId: { value: acrTenantId } }
  }
}

resource prodEnvironment 'Applications.Core/environments@2023-10-01-preview' = {
  name: 'prod'
  properties: {
    compute: { kind: 'kubernetes', namespace: namespace }
    providers: { azure: { scope: azureScope } }
    recipeConfig: {
      bicep: {
        authentication: { '<your-acr>': { secret: acrSecret.id } }
      }
    }
    recipes: {
      'Applications.Dapr/stateStores': {
        default: { templateKind: 'bicep', templatePath: '<your-acr>/recipes/redis:1.0.0' }
      }
      'Applications.Datastores/sqlDatabases': {
        default: { templateKind: 'bicep', templatePath: '<your-acr>/recipes/sql:1.0.0', parameters: { adminPassword: sqlAdminPassword } }
      }
      'Applications.Dapr/pubSubBrokers': {
        default: { templateKind: 'bicep', templatePath: '<your-acr>/recipes/servicebus:1.0.0' }
      }
    }
  }
}

output environmentId string = prodEnvironment.id
BICEP

Deploy both:

rad deploy environments/dev.bicep
rad deploy environments/prod.bicep --parameters sqlAdminPassword='...'
BICEP

These environment definitions register different infrastructure recipes for the same resource types. The app deployment comes next.

Note: Radius doesn’t yet support bicepparam files. GitHub issue #11045.

Stage 1: Deploy the UI shell

Start with just a frontend container and a gateway. No Dapr, no recipes added yet:

extension radius

param environment string
param application string

resource frontend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'frontend'
  properties: {
    application: application
    container: {
      image: 'ghcr.io/radius-project/samples/dapr-frontend:latest'
      env: {
        ASPNETCORE_URLS: { value: 'http://*:8080' }
      }
      ports: { ui: { containerPort: 8080 } }
    }
  }
}

resource gateway 'Applications.Core/gateways@2023-10-01-preview' = {
  name: 'gateway'
  properties: {
    application: application
    routes: [{ path: '/', destination: 'http://frontend:8080' }]
  }
}

output url string = gateway.properties.url
BICEP

Deploy it:

rad deploy app.bicep --environment dev
Bash

At this point, no recipes fired because no portable resources were declared. The frontend and gateway deployed as regular Kubernetes containers, not through the recipe system. The environment files are registered but waiting to be used.


Stage 2: Add a pub/sub backend

Now add a backend service that publishes/subscribes to events. Add three things to app.bicep:

  1. A pubSubBrokers portable resource that declares “I need a pub/sub broker”
  2. A backend container wired to the broker via connections
  3. Updated frontend that gains a Dapr sidecar and CONNECTION_BACKEND_APPID
extension radius

param environment string
param application string

// MARK: Pub/Sub
resource pubsub 'Applications.Dapr/pubSubBrokers@2023-10-01-preview' = {
  name: 'pubsub'
  properties: {
    environment: environment
    application: application
    resourceProvisioning: 'recipe'
  }
}

// MARK: Backend
resource backend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'backend'
  properties: {
    application: application
    container: {
      image: 'ghcr.io/radius-project/samples/dapr-backend:latest'
      ports: { api: { containerPort: 3000 } }
    }
    connections: {
      pubsub: { source: pubsub.id }  // Radius injects DAPR_PUBSUB_COMPONENTNAME etc.
    }
    extensions: [{ kind: 'daprSidecar', appId: 'backend', appPort: 3000 }]
  }
}

// MARK: Frontend
resource frontend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'frontend'
  properties: {
    application: application
    container: {
      image: 'ghcr.io/radius-project/samples/dapr-frontend:latest'
      env: {
        CONNECTION_BACKEND_APPID: { value: 'backend' }
        ASPNETCORE_URLS:          { value: 'http://*:8080' }
      }
      ports: { ui: { containerPort: 8080 } }
    }
    extensions: [{ kind: 'daprSidecar', appId: 'frontend' }]
  }
}

// MARK: Gateway
resource gateway 'Applications.Core/gateways@2023-10-01-preview' = {
  name: 'gateway'
  properties: {
    application: application
    routes: [{ path: '/', destination: 'http://frontend:8080' }]
  }
}

output url string = gateway.properties.url
BICEP

Deploy to dev:

rad deploy app.bicep --environment dev
Bash

What happens:

  1. Radius sees resourceProvisioning: 'recipe' on pubsub
  2. It looks up Applications.Dapr/pubSubBrokers in the dev environment’s recipe catalogue
  3. dev.bicep registered: ghcr.io/radius-project/recipes/local-dev/pubsubbrokers:latest
  4. A container spins up in the cluster
  5. Radius creates the Dapr Component and injects connection info into the backend pod

Now I’ll deploy the exact same app.bicep to prod with no changes:

rad deploy app.bicep --environment prod
Bash

This time:

  1. Radius sees the same resourceProvisioning: 'recipe' on pubsub
  2. It looks up Applications.Dapr/pubSubBrokers in the prod environment’s recipe catalogue
  3. prod.bicep registered: <your-acr>.azurecr.io/recipes/servicebus:1.0.0
  4. Radius calls ARM, and an Azure Service Bus namespace is provisioned
  5. Radius creates the Dapr Component with the Service Bus connection string

The developer just declared what they needed. The environment decided what got created.


Stage 3: Add state store and SQL database

Now the backend needs to persist state and read from a database. Add two more portable resources and wire them to the backend:

extension radius

param environment string
param application string

// MARK: Pub/Sub
resource pubsub 'Applications.Dapr/pubSubBrokers@2023-10-01-preview' = {
  name: 'pubsub'
  properties: {
    environment: environment
    application: application
    resourceProvisioning: 'recipe'
  }
}

// MARK: State
resource statestore 'Applications.Dapr/stateStores@2023-10-01-preview' = {
  name: 'statestore'
  properties: {
    environment: environment
    application: application
    resourceProvisioning: 'recipe'
  }
}

// MARK: SQL DB
resource db 'Applications.Datastores/sqlDatabases@2023-10-01-preview' = {
  name: 'db'
  properties: {
    environment: environment
    application: application
    resourceProvisioning: 'recipe'
  }
}

// MARK: Backend
resource backend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'backend'
  properties: {
    application: application
    container: {
      image: 'ghcr.io/radius-project/samples/dapr-backend:latest'
      ports: { api: { containerPort: 3000 } }
    }
    connections: {
      pubsub:  { source: pubsub.id }      // ← Radius injects Dapr pub/sub component
      orders:  { source: statestore.id }  // ← Radius injects Dapr state store component
      db:      { source: db.id }          // ← Radius injects CONNECTION_DB_* env vars
    }
    extensions: [{ kind: 'daprSidecar', appId: 'backend', appPort: 3000 }]
  }
}

// MARK: Frontend
resource frontend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'frontend'
  properties: {
    application: application
    container: {
      image: 'ghcr.io/radius-project/samples/dapr-frontend:latest'
      env: {
        CONNECTION_BACKEND_APPID: { value: 'backend' }
        ASPNETCORE_URLS:          { value: 'http://*:8080' }
      }
      ports: { ui: { containerPort: 8080 } }
    }
    connections: {
      backend: { source: 'http://backend:3000' }
    }
    extensions: [{ kind: 'daprSidecar', appId: 'frontend' }]
  }
}

// MARK: Gateway
resource gateway 'Applications.Core/gateways@2023-10-01-preview' = {
  name: 'gateway'
  properties: {
    application: application
    routes: [{ path: '/', destination: 'http://frontend:8080' }]
  }
}

output url string = gateway.properties.url
BICEP

The frontend still calls the backend through Dapr using CONNECTION_BACKEND_APPID. The extra Radius service connection is there so the frontend -> backend dependency appears in Radius’ application graph (I’ll show this later in the blog).

Deploy to dev:

rad deploy app.bicep --environment dev
Bash

Same deployment but different recipe implementations:

  • Dev: Redis container for state store, SQL Server container for database
  • Prod: Azure Cache for Redis and Azure SQL Database

The environment files explained

The two environment files are a recipe catalogue:

environments/dev.bicep is deployed once by the operator against the dev cluster (e.g., Docker Desktop or an AKS cluster):

recipes: {
  'Applications.Dapr/pubSubBrokers': {
    default: {
      templateKind: 'bicep'
      templatePath: 'ghcr.io/radius-project/recipes/local-dev/pubsubbrokers:latest'
    }
  }
}
BICEP

environments/prod.bicep is deployed once by the operator to AKS:

providers: { azure: { scope: azureScope } }

recipeConfig: {
  bicep: {
    authentication: {
      '<your-acr>': { secret: acrSecret.id }
    }
  }
}

recipes: {
  'Applications.Dapr/pubSubBrokers': {
    default: {
      templateKind: 'bicep'
      templatePath: '<your-acr>/recipes/servicebus:1.0.0'
    }
  }
}
BICEP

Same resource type defined but a different value on a different cluster. The same app.bicep file.

When a dev writes environment: environment in their Bicep, they’re saying: use the environment I’m deploying to. Radius then looks up what recipes that environment has registered.


Understanding Recipes

A Recipe is the implementation for a Resource Type. Devs declare the resource and the operator decides what gets provisioned.

Dev recipes vs prod recipes

For local development, the Radius project publishes ready made community recipes as local-dev recipes. These are public, no credentials required. The dev recipe for pub/sub spins up a containerised message broker and registers it as a Dapr component. You don’t have to write or maintain these, they’re maintained by the Radius team.

For production, you can write your own recipes. Your prod recipe for pub/sub provisions Azure Service Bus instead of a container for example, or Azure SQL Database for database. You publish these to your private ACR and only your prod environment references them. Dev and prod point to completely different template paths in your environment files, but your app.bicep doesn’t know or care about this.

Discovering Recipes

Check what recipes are available in an environment:

rad recipe list --environment dev
Bash

To see parameters:

rad recipe show default --resource-type 'Applications.Dapr/pubSubBrokers' --environment dev
Bash

Override Recipe Parameters

Pass parameters directly in app.bicep:

resource pubsub 'Applications.Dapr/pubSubBrokers@2023-10-01-preview' = {
  name: 'pubsub'
  properties: {
    environment: environment
    application: application
    resourceProvisioning: 'recipe'
    recipe: {
      name: 'default'
      parameters: { skuName: 'Standard' }
    }
  }
}
BICEP

Writing a Recipe

Recipes are Bicep templates. Radius also supports Terraform modules as recipes (set templateKind: 'terraform' in your environment file: see rad-bank’s operations.bicep for an example). Radius always injects a context parameter that contains the application metadata and runtime information. Here’s a production demo recipe for Azure Service Bus:

// RECIPE: servicebus
// Provisions an Azure Service Bus namespace and registers it as a
// Dapr pub/sub broker Component in Kubernetes.

param context object
param location string = resourceGroup().location

@allowed(['Standard', 'Premium'])
param skuName string = 'Standard'

var namespaceName = 'sb-${uniqueString(context.resource.id, resourceGroup().id)}'

// MARK: Azure Service Bus Namespace
resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2024-01-01' = {
  name: namespaceName
  location: location
  sku: {
    name: skuName
    tier: skuName
  }
  properties: {
    minimumTlsVersion: '1.2'
  }
}

// MARK: Shared access rule used by Dapr to connect (Send + Listen + Manage).
resource daprAuthRule 'Microsoft.ServiceBus/namespaces/AuthorizationRules@2024-01-01' = {
  parent: serviceBusNamespace
  name: 'DaprAccess'
  properties: {
    rights: ['Send', 'Listen', 'Manage']
  }
}

extension kubernetes with {
  kubeConfig: ''
  namespace: context.runtime.kubernetes.namespace
} as kubernetes

// MARK: Dapr Component - uses the topics-based pub/sub component
resource daprComponent 'dapr.io/Component@v1alpha1' = {
  metadata: { name: context.resource.name }
  spec: {
    type: 'pubsub.azure.servicebus.topics'
    version: 'v1'
    metadata: [
      { name: 'connectionString', value: daprAuthRule.listKeys().primaryConnectionString }
    ]
  }
}

output result object = {
  resources: [serviceBusNamespace.id]
}
BICEP

The recipe creates two things:

  • Azure Service Bus: provisioned in your subscription (because prod.bicep sets the Azure scope)
  • Dapr Component: registered in Kubernetes so the backend pod can publish/subscribe

The context parameter contains the resource name, the app name, the namespace the resource belongs to, and the Azure subscription. You use these to generate unique names, link up connections, and ensure isolation between teams.

Publishing and Using Your Recipe

Publish to your registry:

az acr create --resource-group rg-radius-demo --name <your-acr> --sku Basic
az acr login --name <your-acr>
rad bicep publish --file recipes/servicebus.bicep --target br:<your-acr>.azurecr.io/recipes/servicebus:1.0.0
Bash
Note: Always use version tags for production, not latest. If you deploy v1.0.0 and the operator later publishes v2.0.0 with breaking changes, teams using v1.0.0 aren’t affected until they explicitly update.

Register it in your prod environment:

// environments/prod.bicep
resource prodEnvironment 'Applications.Core/environments@2023-10-01-preview' = {
  name: 'prod'
  properties: {
    compute: { kind: 'kubernetes', namespace: 'prod' }
    providers: { azure: { scope: azureScope } }
    recipes: {
      'Applications.Dapr/pubSubBrokers': {
        default: { templateKind: 'bicep', templatePath: '<your-acr>/recipes/servicebus:1.0.0' }
      }
    }
  }
}
BICEP

Deploy the environment:

$sub = az account show --query id -o tsv 
rad deploy environments/prod.bicep --parameters azureScope="/subscriptions/$sub/resourceGroups/your-rg"
Bash

Now when you deploy your app to prod, Radius uses your recipe. The app code doesn’t change.

For production ready recipe examples, check zachcasper/recipes.

One Recipe, Many Teams

Your platform team writes one Service Bus recipe with enterprise standards. When security policy requires a change, such as a higher minimum TLS version, the operator publishes a new recipe version, updates prod.bicep to reference that version, and redeploys the environment. Every team’s next deployment to that environment then uses the new recipe version. Update the environment once, every team benefits automatically.


How Radius Works Internally

When you deploy, bicep-de (Bicep Deployment Engine) processes your Bicep templates, resolves dependencies, and calls back to Radius for each resource through the appropriate provider(s).

The whole environment config gets stored as a Kubernetes custom resource, backed by the Radius environment CRD. The recipes catalogue, Azure scope, credential refs, everything:

This is the prod environment custom resource in AKS after rad deploy environments/prod.bicep, showing the Azure scope and recipe catalogue.


See What’s Running

The app is running with everything connected through Dapr.

Visualise the app graph with the Dashboard

The Radius Dashboard (built on Backstage) shows your entire application topology, resource dependencies, and which recipes provisioned what.

kubectl port-forward -n radius-system deployment/dashboard 7007:80
Bash

Then open http://localhost:7007.

From the dashboard you can see:

  • Application graph: Visual dependency diagram (frontend -> backend -> state store -> database)
  • Resource types: Portable resources you declared (pubSubBrokers, stateStores, sqlDatabases)
  • Resources: Each container and infrastructure component with live status
  • Recipes: Which recipe provisioned which resource

To discover what recipes are available and what parameters they accept, use the CLI:

rad recipe list --environment dev
rad recipe show default --resource-type 'Applications.Dapr/pubSubBrokers' --environment dev
Bash

You can also query application data and logs via the CLI:

rad app graph 
rad resource list Applications.Core/containers
rad resource logs Applications.Core/containers backend --application <your-app-name>
Bash

Azure resources created by recipes

When you deploy to prod, Radius provisions real Azure infrastructure via ARM. Here’s what gets created:

Each resource corresponds to a recipe. Service Bus for pub/sub, SQL Database for persistence, Redis for state store. Radius automatically creates Dapr Components in the cluster that connect these Azure resources as sidecar bindings, so your backend pod connects via localhost:3500 to Dapr, which handles authentication and routing to the actual Azure services.


Conclusion

I had a really fun time deploying and learning how Radius works, and I hope this breakdown was useful. Radius is a powerful orchestration tool for platform teams managing multiple application teams with standardised infrastructure needs.

For small teams or projects, traditional IaC will be more straightforward. It’s not a tool for every scenario, but for the right organisation and maturity level, it’s a genuine step forward in separating what developers declare from what operators implement. I’m keen to see how the project evolves as more features get added!

Check the GitHub repo, rad-bank, and zachcasper/recipes for reference examples, or join the Radius Discord if you want to explore it further. Happy Radius..ing? 😁

Leave a comment