LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Radius: Microsoft's Open-Source Application Platform for Cloud-Native Teams

radiusplatform-engineeringkubernetesdevopscloud-nativebicepterraformawsazurecncfidp

Platform engineering has a recurring problem: the gap between what developers want (self-service infrastructure, consistent environments, no cloud-specific knowledge required) and what platform teams can deliver (compliant, secure, cost-controlled infrastructure that follows organizational policy). Kubernetes made deploying containers easier, but it widened that gap — every developer now needs to understand Deployments, Services, Ingresses, ConfigMaps, secrets, namespace hygiene, and cloud-specific integrations just to run an application.

Radius is Microsoft’s attempt to close that gap with a different abstraction layer. Rather than making Kubernetes easier to use, Radius adds an application-centric model on top of it: developers describe what their application needs (a container, a Redis cache, a message queue), platform teams describe how those needs are implemented in each environment (Terraform modules, Bicep templates, cloud-specific configurations), and Radius wires them together at deploy time.

This is the core idea. Developers write cloud-agnostic application definitions. Platform teams write infrastructure recipes. Radius connects the two without either team needing to know the details of the other’s work.


What Problem Radius Solves

Consider a typical cloud-native application: a frontend container, a backend API, a Redis cache for sessions, a PostgreSQL database, and a message queue. In a vanilla Kubernetes environment, deploying this requires:

  • Kubernetes manifests for each container (Deployment, Service, ConfigMap)
  • A Helm chart or Kustomize overlay for each environment
  • Cloud provider-specific resources (Azure Cache for Redis in prod, a local Redis container in dev)
  • Manually injected connection strings and secrets, varying by environment
  • Documentation or tribal knowledge about which environment variable means what

When a developer moves this from dev to staging to production, the environment variables change, the backing services change, the cloud providers might change, and the Kubernetes namespaces definitely change. The developer has to know all of this. Or the platform team has to maintain bespoke Helm values files for every environment.

Radius proposes a different model: the application definition doesn’t change between environments. What changes is the environment configuration — specifically, which Recipes implement each resource type and which cloud credentials are available. A developer’s app.bicep is identical whether deploying to a local kind cluster or production AKS. The environment, defined by the platform team, determines what actually gets provisioned.

This is application-centric infrastructure: the application is the unit of deployment, not individual Kubernetes resources.


Architecture

Radius runs as a control plane on top of Kubernetes. It does not replace Kubernetes — it extends it.

┌─────────────────────────────────────────────────────────────────┐
│                    Kubernetes Cluster                            │
│                                                                  │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │               radius-system namespace                     │  │
│  │                                                           │  │
│  │  ┌─────────────────────────┐  ┌────────────────────────┐ │  │
│  │  │ Universal Control Plane │  │   Applications RP      │ │  │
│  │  │        (UCP)            │  │  (Resource Provider)   │ │  │
│  │  │                         │  │                        │ │  │
│  │  │  - REST API             │  │  - App graph tracking  │ │  │
│  │  │  - Kubernetes API agg.  │  │  - Recipe execution    │ │  │
│  │  │  - Multi-plane routing  │  │  - Connection inject.  │ │  │
│  │  └─────────────────────────┘  └────────────────────────┘ │  │
│  │                                                           │  │
│  │  ┌─────────────────────────┐  ┌────────────────────────┐ │  │
│  │  │   Deployment Engine     │  │   Radius Dashboard     │ │  │
│  │  │                         │  │   (Backstage-based)    │ │  │
│  │  │  - Dependency ordering  │  │                        │ │  │
│  │  │  - Bicep/TF execution   │  │  - App graph UI        │ │  │
│  │  │  - State tracking       │  │  - Resource explorer   │ │  │
│  │  └─────────────────────────┘  └────────────────────────┘ │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                  │
│  CRDs: resources.ucp.dev                                         │
│  DeploymentTemplate / DeploymentResource                         │
│  Contour (for gateways)                                          │
└─────────────────────────────────────────────────────────────────┘
           │
           ▼
    Azure / AWS / Kubernetes (in-cluster)

Universal Control Plane (UCP)

The UCP is the core of Radius. It is a REST API written in Go, exposed through the Kubernetes API Aggregation Layer — meaning you interact with it through the standard Kubernetes API server, and Kubernetes RBAC controls access to all Radius resources. There is no separate API endpoint to secure.

The UCP handles routing between different planes:

  • The Radius plane for application resources (containers, connections, environments)
  • The Azure plane for Azure resources provisioned through Radius
  • The AWS plane for AWS resources

This multi-plane design means the UCP can act as a unified control plane across cloud providers while still delegating actual provisioning to the appropriate backend.

Applications Resource Provider (Applications RP)

The Applications RP is the component that understands Radius-specific resource types: Applications.Core/containers, Applications.Datastores/redisCaches, Applications.Core/environments, and so on. It:

  • Maintains the application graph (tracking which resources belong to which application and how they connect)
  • Triggers Recipe execution when portable resources are deployed
  • Handles Connection injection — computing what environment variables to inject into containers based on declared connections

Deployment Engine

When you rad deploy app.bicep, the CLI sends the Bicep template to the Deployment Engine. This component:

  1. Parses the ARM JSON produced by Bicep compilation
  2. Builds a dependency graph of all resources in the template
  3. Deploys resources in dependency order (secrets before databases, databases before containers)
  4. Tracks in-progress operations and reports status

The Deployment Engine also supports a Kubernetes-native mode via the DeploymentTemplate CRD. Using rad bicep generate-kubernetes-manifest, you can convert a Bicep template into a DeploymentTemplate Kubernetes resource, enabling GitOps workflows where ArgoCD or Flux manage Radius deployments like any other Kubernetes resource.

How Resources Map to Kubernetes

When Radius deploys a container to Kubernetes, it creates standard Kubernetes objects:

Radius Resource Kubernetes Objects Created
Applications.Core/containers apps/Deployment@v1 + core/Service@v1 (if ports defined)
Applications.Core/gateways projectcontour.io/HTTPProxy@v1 via Contour
Connections core/Secret@v1 (environment variables injected into pods)
Dapr components dapr.io/Component@v1alpha1

Application-scoped resources deploy by default into a new namespace named <envNamespace>-<appname>. This is configurable via the kubernetesNamespace application extension.

Radius uses Contour as its ingress controller for gateway resources. If you already have an ingress controller, this is worth noting — Contour gets installed alongside your existing setup.


The Application Model

Radius uses Bicep as its primary configuration language. Bicep is Microsoft’s ARM template DSL, but Radius extends it with a custom extension providing Radius-specific resource types. YAML is supported for some configuration, but Bicep is the primary authoring experience.

The central model has four main concepts: Applications, Environments, Connections, and Recipes. They compose cleanly and that composition is what makes the platform work.

Applications

An Application in Radius is a logical grouping of all the resources that make up your service. It is a first-class resource — not a label, not a namespace, but an actual object that Radius tracks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// app.bicep
import radius as radius

@description('The Radius Application ID. Injected automatically by the rad CLI.')
param application string

@description('The ID of your Radius Environment. Set automatically by the rad CLI.')
param environment string

// Container definition
resource backend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'backend'
  properties: {
    application: application
    container: {
      image: 'myorg/backend-api:latest'
      ports: {
        http: {
          containerPort: 8080
        }
      }
      env: {
        LOG_LEVEL: {
          value: 'info'
        }
      }
    }
    connections: {
      cache: {
        source: cache.id
      }
      db: {
        source: database.id
      }
    }
  }
}

// Portable Redis resource — implementation determined by the environment's Recipe
resource cache 'Applications.Datastores/redisCaches@2023-10-01-preview' = {
  name: 'cache'
  properties: {
    application: application
    environment: environment
    // No recipe name = uses the environment's 'default' recipe
  }
}

// Portable MongoDB resource
resource database 'Applications.Datastores/mongoDatabases@2023-10-01-preview' = {
  name: 'database'
  properties: {
    application: application
    environment: environment
  }
}

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

Notice what is not in this file: no cloud-specific configuration, no AWS ARNs, no Azure resource IDs, no Kubernetes namespace references, no connection strings. This file is completely portable.

The application and environment parameters are automatically injected by the rad CLI at deploy time — you never need to hardcode them.

Portable Resource Types

Radius ships with a set of built-in portable resource types — abstract resource definitions that platform teams can implement with any backing service:

Resource Type Description
Applications.Datastores/redisCaches Redis-compatible cache
Applications.Datastores/mongoDatabases MongoDB-compatible database
Applications.Datastores/sqlDatabases SQL database
Applications.Messaging/rabbitMQQueues RabbitMQ message queue
Applications.Dapr/stateStores Dapr state store
Applications.Dapr/secretStores Dapr secret store
Applications.Dapr/pubSubBrokers Dapr pub/sub broker

A developer who declares Applications.Datastores/redisCaches doesn’t know or care whether the implementation is a local Redis container, Azure Cache for Redis, or ElastiCache. That decision belongs to the environment and its Recipes.

Custom Resource Types

Beyond the built-in types, platform teams can define their own custom Resource Types using a YAML definition file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# postgreSqlDatabases.yaml
namespace: Myorg.Data
types:
  postgreSqlDatabases:
    description: |
      A PostgreSQL database for use in Myorg applications.
    apiVersions:
      '2024-01-01':
        schema:
          type: object
          properties:
            environment:
              type: string
              description: "The Radius Environment ID"
            application:
              type: string
              description: "The Radius Application ID"
            size:
              type: string
              enum: [S, M, L]
              description: "Instance size"
          required: [environment]

Register it:

1
2
3
4
rad resource-type create postgreSqlDatabases -f postgreSqlDatabases.yaml

# Publish a Bicep extension so the IDE understands the type
rad bicep publish-extension -f postgreSqlDatabases.yaml --target radiusResources.tgz

This is how platform teams extend Radius with organization-specific resource types — a Myorg.Data/postgreSqlDatabases instead of a generic SQL type, with org-specific properties like size tiers.


Environments

An Environment in Radius is a deployment landing zone. It specifies:

  • Where workloads run (Kubernetes namespace, cloud region)
  • Which cloud provider credentials are available
  • Which Recipes implement each Resource Type

The same application definition deploys unchanged to any environment. What differs between dev, staging, and production is the environment’s configuration.

Environment Bicep Definition

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// environments.bicep
extension radius

// Development environment — uses local-dev recipes (Docker containers in-cluster)
resource devEnv 'Applications.Core/environments@2023-10-01-preview' = {
  name: 'dev'
  properties: {
    compute: {
      kind: 'kubernetes'
      resourceId: 'self'
      namespace: 'dev-workloads'
    }
    recipes: {
      'Applications.Datastores/redisCaches': {
        default: {
          templateKind: 'bicep'
          templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:latest'
        }
      }
      'Applications.Datastores/mongoDatabases': {
        default: {
          templateKind: 'bicep'
          templatePath: 'ghcr.io/radius-project/recipes/local-dev/mongodatabases:latest'
        }
      }
    }
  }
}

// Production environment — uses Azure-backed recipes with organization compliance
resource prodEnv 'Applications.Core/environments@2023-10-01-preview' = {
  name: 'prod'
  properties: {
    compute: {
      kind: 'kubernetes'
      resourceId: 'self'
      namespace: 'prod-workloads'
    }
    providers: {
      azure: {
        scope: '/subscriptions/SUB_ID/resourceGroups/prod-rg'
      }
    }
    recipes: {
      'Applications.Datastores/redisCaches': {
        default: {
          templateKind: 'bicep'
          templatePath: 'ghcr.io/myorg/recipes/azure/rediscaches:1.0.0'
          parameters: {
            sku: 'Standard'
            capacity: 2
          }
        }
      }
      'Applications.Datastores/mongoDatabases': {
        default: {
          templateKind: 'terraform'
          templatePath: 'myorg/recipes/cosmosdb'
          templateVersion: '1.2.0'
        }
      }
    }
  }
}

The developer’s app.bicep is identical in both environments. The environment definition controls what gets provisioned. This is the key separation of concerns.

Multiple Named Recipes Per Type

An environment can register multiple named Recipes for the same resource type — not just a default. Developers can then choose which implementation to use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Environment with multiple Redis options
recipes: {
  'Applications.Datastores/redisCaches': {
    default: {
      // Free-tier Azure Cache for Redis
      templateKind: 'bicep'
      templatePath: 'ghcr.io/myorg/recipes/azure/redis-basic:latest'
    }
    'redis-premium': {
      // Premium tier with clustering, used by high-throughput services
      templateKind: 'bicep'
      templatePath: 'ghcr.io/myorg/recipes/azure/redis-premium:latest'
    }
  }
}

A developer who needs premium Redis explicitly requests it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
resource cache 'Applications.Datastores/redisCaches@2023-10-01-preview' = {
  name: 'cache'
  properties: {
    application: application
    environment: environment
    recipe: {
      name: 'redis-premium'
    }
  }
}

Initializing an Environment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Quick init: installs Radius and creates a dev environment with local-dev recipes
rad init

# Full interactive init: configure cloud providers, namespaces, recipes
rad init --full

# Manual creation
rad install kubernetes
rad group create myapp-group
rad env create production --group myapp-group --namespace prod-workloads
rad env switch production

Recipes

Recipes are the platform team’s contribution to the Radius model. They are IaC templates — Bicep or Terraform — that implement a Resource Type. A Recipe for Applications.Datastores/redisCaches knows how to provision a Redis instance; the application definition just asks for one.

The key design principle: Recipes are not tightly coupled to Radius. An existing Terraform module for Redis can be adapted into a Recipe with minimal changes. The only requirement is accepting a context variable that Radius injects.

The context Object

When Radius invokes a Recipe, it injects a context object containing:

  • context.resource.id — the unique ID of the resource requesting the Recipe
  • context.resource.name — the resource name
  • context.runtime.kubernetes.namespace — the Kubernetes namespace for the deployment
  • context.environment.id — the environment ID
  • context.application.id — the application ID

This context is how Recipes generate unique, deterministic resource names and target the correct namespace.

Writing a Bicep Recipe

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// redis-recipe.bicep — Kubernetes-based Redis for dev/staging

@description('Radius-provided object containing information about the resource calling the Recipe')
param context object

@description('The port Redis is offered on. Defaults to 6379.')
param port int = 6379

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

var redisName = 'redis-${uniqueString(context.resource.id)}'

resource redisDeployment 'apps/Deployment@v1' = {
  metadata: {
    name: redisName
    labels: {
      app: 'redis'
      'radius.app': context.application.id
    }
  }
  spec: {
    selector: {
      matchLabels: {
        app: 'redis'
        resource: context.resource.name
      }
    }
    template: {
      metadata: {
        labels: {
          app: 'redis'
          resource: context.resource.name
        }
      }
      spec: {
        containers: [
          {
            name: 'redis'
            image: 'redis:7-alpine'
            ports: [{ containerPort: 6379 }]
            resources: {
              requests: { memory: '64Mi', cpu: '50m' }
              limits:   { memory: '128Mi', cpu: '100m' }
            }
          }
        ]
      }
    }
  }
}

resource redisSvc 'core/Service@v1' = {
  metadata: {
    name: redisName
  }
  spec: {
    type: 'ClusterIP'
    selector: {
      app: 'redis'
      resource: context.resource.name
    }
    ports: [{ port: port, targetPort: '6379' }]
  }
}

// The result object tells Radius how to connect consumers to this resource
output result object = {
  values: {
    host: '${redisSvc.metadata.name}.${redisSvc.metadata.namespace}.svc.cluster.local'
    port: redisSvc.spec.ports[0].port
    username: ''
  }
  secrets: {
    #disable-next-line outputs-should-not-contain-secrets
    password: ''
  }
  resources: [
    '/planes/kubernetes/local/namespaces/${redisSvc.metadata.namespace}/providers/core/Service/${redisSvc.metadata.name}'
    '/planes/kubernetes/local/namespaces/${redisDeployment.metadata.namespace}/providers/apps/Deployment/${redisDeployment.metadata.name}'
  ]
}

The output result block is critical. The values and secrets in this output are what Radius uses to populate connection environment variables in consuming containers.

Writing a Terraform Recipe

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# main.tf — Azure Cache for Redis recipe

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.0"
    }
  }
}

variable "context" {
  description = "Radius-provided context object"
  type        = any
}

variable "sku" {
  description = "Redis SKU (Basic, Standard, Premium)"
  type        = string
  default     = "Standard"
}

variable "capacity" {
  description = "Redis cache size"
  type        = number
  default     = 1
}

resource "azurerm_redis_cache" "main" {
  name                = "redis-${substr(sha256(var.context.resource.id), 0, 8)}"
  location            = "eastus"
  resource_group_name = element(split("/", var.context.environment.id), 4)
  capacity            = var.capacity
  family              = var.sku == "Premium" ? "P" : "C"
  sku_name            = var.sku

  enable_non_ssl_port = false
  minimum_tls_version = "1.2"

  tags = {
    radius-application = var.context.application.id
    radius-environment = var.context.environment.id
  }
}

output "result" {
  sensitive = true
  value = {
    values = {
      host     = azurerm_redis_cache.main.hostname
      port     = azurerm_redis_cache.main.ssl_port
      username = ""
    }
    secrets = {
      password = azurerm_redis_cache.main.primary_access_key
    }
    resources = [
      "/planes/azure/local/subscriptions/SUBSCRIPTION/resourceGroups/RG/providers/Microsoft.Cache/Redis/${azurerm_redis_cache.main.name}"
    ]
  }
}

Publishing and Registering Recipes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Publish a Bicep recipe to an OCI registry
rad bicep publish --file redis-recipe.bicep --target br:ghcr.io/myorg/recipes/redis-dev:1.0.0

# Register it with an environment
rad recipe register redis-dev \
  --environment dev \
  --resource-type Applications.Datastores/redisCaches \
  --template-kind bicep \
  --template-path ghcr.io/myorg/recipes/redis-dev:1.0.0

# Register a Terraform recipe (from a Terraform registry module)
rad recipe register redis-azure \
  --environment prod \
  --resource-type Applications.Datastores/redisCaches \
  --template-kind terraform \
  --template-path myorg/recipes/azure-redis \
  --template-version "1.2.0"

# List registered recipes for an environment
rad recipe list --environment prod

# Show recipe details including parameters
rad recipe show redis-azure --environment prod --resource-type Applications.Datastores/redisCaches

Manual Provisioning

When you need to connect to pre-existing infrastructure (a database managed outside of Radius), use resourceProvisioning: 'manual':

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
resource existingRedis 'Applications.Datastores/redisCaches@2023-10-01-preview' = {
  name: 'redis'
  properties: {
    environment: environment
    application: application
    resourceProvisioning: 'manual'
    resources: [{ id: azureRedisCache.id }]
    host: azureRedisCache.properties.hostName
    port: azureRedisCache.properties.port
    secrets: {
      password: azureRedisCache.listKeys().primaryKey
    }
  }
}

This is the escape hatch for brownfield adoption — Radius manages the connections and application graph even when it doesn’t provision the infrastructure.


Connections

Connections are the mechanism that wires together application components. When a container declares a connection to a resource, Radius:

  1. Resolves the target resource (including running its Recipe if needed)
  2. Retrieves the connection values and secrets from the recipe’s result output
  3. Injects environment variables into the container

Environment Variable Injection

The naming convention for injected variables is:

CONNECTION_<CONNECTIONNAME>_<PROPERTY>

For a connection named cache to a Redis resource with host and port in its values output:

CONNECTION_CACHE_HOST=redis-abc123.dev-workloads.svc.cluster.local
CONNECTION_CACHE_PORT=6379
CONNECTION_CACHE_USERNAME=
CONNECTION_CACHE_PASSWORD=<from secrets>

The full Bicep connection definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
resource backend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'backend'
  properties: {
    application: application
    container: {
      image: 'myorg/backend:latest'
      ports: {
        http: { containerPort: 8080 }
      }
    }
    connections: {
      cache: {
        source: cache.id          // Redis resource
      }
      queue: {
        source: messageQueue.id   // RabbitMQ resource
      }
      storage: {
        source: blobContainer.id  // Azure Blob — connection kind 'azure'
        iam: {
          kind: 'azure'
          roles: ['Storage Blob Data Contributor']
        }
      }
    }
  }
}

Azure Managed Identity via Connections

When connecting to Azure resources, Radius can automatically configure workload identity and assign RBAC roles. The iam block in the connection tells Radius to:

  1. Create an Azure AD role assignment on the target resource
  2. Annotate the pod for the Azure Workload Identity mutating webhook
  3. Inject the resource URI and identity endpoint variables into the container

No connection string is needed — the container uses its managed identity to authenticate. The developer declares iam.roles: ['Key Vault Secrets User']; Radius handles the role assignment.

AWS IAM via Connections

For AWS resources, Radius uses IAM Roles for Service Accounts (IRSA). Radius registers the cluster’s OIDC provider with AWS STS and annotates service accounts with the appropriate IAM role ARN, enabling pods to assume AWS roles without storing credentials.

The Application Graph

Connections are not just for environment variable injection — they build the application graph, a queryable data structure representing how all resources in an application relate to each other.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# View the application graph in the terminal
rad app graph -a myapp

# Output (simplified):
# myapp
# ├── backend (Applications.Core/containers)
# │   ├── -> cache (Applications.Datastores/redisCaches) [connection: cache]
# │   │   └── Kubernetes Deployment: redis-abc123
# │   │   └── Kubernetes Service: redis-abc123
# │   └── -> queue (Applications.Messaging/rabbitMQQueues) [connection: queue]
# └── gateway (Applications.Core/gateways)
#     └── -> backend [route: /]

The Radius Dashboard (a Backstage-based UI) visualizes this graph graphically, showing the full topology of services and infrastructure with their connections and underlying Kubernetes resources.


Real-World Workflow: Developer + Platform Team

This is how the pieces combine in practice.

Platform Team Responsibilities

The platform team owns environments and recipes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 1. Create environments for each tier
rad install kubernetes
rad group create platform-resources
rad env create dev   --group platform-resources --namespace dev
rad env create staging --group platform-resources --namespace staging
rad env create prod  --group platform-resources --namespace prod

# 2. Configure cloud credentials
rad credential register azure \
  --client-id $CLIENT_ID \
  --client-secret $CLIENT_SECRET \
  --tenant-id $TENANT_ID \
  --subscription-id $SUB_ID

# 3. Publish recipes to the organization's container registry
rad bicep publish --file ./recipes/redis-dev.bicep \
  --target br:myorg.azurecr.io/recipes/redis-dev:1.0.0

rad bicep publish --file ./recipes/redis-azure.bicep \
  --target br:myorg.azurecr.io/recipes/redis-azure:1.0.0

# 4. Register recipes with environments
rad recipe register default \
  --environment dev \
  --resource-type Applications.Datastores/redisCaches \
  --template-kind bicep \
  --template-path myorg.azurecr.io/recipes/redis-dev:1.0.0

rad recipe register default \
  --environment prod \
  --resource-type Applications.Datastores/redisCaches \
  --template-kind bicep \
  --template-path myorg.azurecr.io/recipes/redis-azure:1.0.0

Developer Responsibilities

The developer writes one app.bicep that works everywhere:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// app.bicep — identical for dev, staging, and production
import radius as radius

param application string
param environment string

resource api 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'api'
  properties: {
    application: application
    container: {
      image: 'myorg/api:${tag}'
      ports: {
        http: { containerPort: 8080 }
      }
    }
    connections: {
      cache: { source: sessionCache.id }
    }
  }
}

resource sessionCache 'Applications.Datastores/redisCaches@2023-10-01-preview' = {
  name: 'session-cache'
  properties: {
    application: application
    environment: environment
  }
}

Deploy to dev (local Redis container spun up automatically):

1
2
3
4
5
6
rad run app.bicep --environment dev
# → Deploys api container
# → Runs local-dev Redis recipe (spins up redis:7-alpine in-cluster)
# → Injects CONNECTION_CACHE_HOST, CONNECTION_CACHE_PORT into api pod
# → Sets up port forwarding to localhost:8080
# → Streams logs

Deploy to production (Azure Cache for Redis provisioned via Terraform):

1
2
3
4
5
6
rad deploy app.bicep --environment prod --parameters tag=v1.2.3
# → Deploys api container
# → Runs Azure Redis Terraform recipe
# → Provisions Azure Cache for Redis in prod resource group
# → Injects CONNECTION_CACHE_HOST, CONNECTION_CACHE_PORT, CONNECTION_CACHE_PASSWORD
# → Returns gateway URL

The developer ran two commands. The environment determines the difference.

GitOps Integration

For production deployments through GitOps:

1
2
3
4
5
6
7
8
9
# Convert app.bicep to a Kubernetes-native DeploymentTemplate
rad bicep generate-kubernetes-manifest app.bicep \
  --environment prod \
  --output deployment-template.yaml

# Commit to your GitOps repo — ArgoCD/Flux picks it up
git add deployment-template.yaml
git commit -m "deploy: v1.2.3"
git push

The DeploymentTemplate CRD is reconciled by a Radius controller, which triggers the same Deployment Engine flow as rad deploy but through a Kubernetes control loop.


The rad CLI Reference

Key commands you will use regularly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Installation and setup
rad version                        # Show CLI version
rad install kubernetes             # Install Radius control plane on current cluster
rad install kubernetes --set ...   # Install with custom Helm values
rad uninstall kubernetes --purge   # Full removal including CRDs

# Initialization
rad init                           # Interactive: install + create dev environment
rad init --full                    # Interactive: full config including cloud providers

# Workspace and group management
rad workspace list                 # List workspaces (cluster + namespace combinations)
rad workspace switch <name>        # Switch active workspace
rad group create <name>            # Create a resource group
rad group list                     # List resource groups

# Environment management
rad env create <name>              # Create environment
rad env list                       # List environments
rad env show <name>                # Show environment config
rad env switch <name>              # Set default environment
rad env update <name> ...          # Update environment config

# Application deployment
rad run app.bicep                  # Deploy and port-forward + log stream (dev)
rad run app.bicep -a myapp         # Specify application name
rad deploy app.bicep               # Deploy to active environment (no port-forward)
rad deploy app.bicep --environment prod  # Deploy to specific environment
rad deploy app.bicep --parameters "key=val"  # Pass parameters

# Application management
rad app list                       # List applications
rad app show <name>                # Show application details
rad app graph -a <name>            # Show application dependency graph
rad app connections -a <name>      # Show connections
rad app status -a <name>           # Show running status including URLs
rad app delete <name>              # Delete application and all resources

# Resource inspection
rad resource list Applications.Core/containers          # List containers
rad resource list Applications.Datastores/redisCaches  # List Redis resources
rad resource show Applications.Core/containers backend  # Show resource details
rad resource logs Applications.Core/containers backend  # Stream container logs
rad resource expose Applications.Core/containers backend --port 8080  # Port forward

# Recipe management
rad recipe list --environment prod                        # List environment's recipes
rad recipe register <name> --environment <env> \
  --resource-type <type> \
  --template-kind bicep \
  --template-path <registry/path:tag>                   # Register a recipe
rad recipe show <name> --environment <env> \
  --resource-type <type>                                 # Show recipe + parameters
rad recipe unregister <name> --environment <env> \
  --resource-type <type>                                 # Remove recipe

# Cloud credentials
rad credential register azure --client-id ... \
  --client-secret ... --tenant-id ... --subscription-id ...
rad credential register aws --access-key-id ... --secret-access-key ...
rad credential list                                      # List registered credentials

# Bicep utilities
rad bicep publish --file recipe.bicep --target br:registry/path:tag
rad bicep generate-kubernetes-manifest app.bicep -o manifest.yaml
rad bicep publish-extension -f resourcetype.yaml --target types.tgz

# Rollback
rad rollback --application <name> kubernetes            # Rollback to previous version

# Debugging
rad debug-logs                     # Capture control plane logs for support

Supported Infrastructure

Kubernetes (in-cluster)

All container workloads run in the Kubernetes cluster hosting Radius. Radius currently only deploys containers to the cluster where it is installed — multi-cluster deployment of workloads is not yet supported, though this is on the roadmap.

Azure

Radius has native Azure integration. Resources available through direct Bicep definitions or the Azure plane:

  • Azure Cache for Redis
  • Azure Cosmos DB
  • Azure Service Bus
  • Azure Key Vault
  • Azure Blob Storage
  • Azure SQL Database
  • Any ARM resource (via Microsoft.* resource types in Bicep)

Azure workload identity is natively supported via the iam connection block — no static credentials needed.

AWS

AWS resources are accessible through the Radius AWS plane:

  • Amazon ElastiCache
  • Amazon DynamoDB
  • Amazon S3
  • Amazon SQS / SNS
  • Amazon RDS
  • Any CloudFormation-supported resource

AWS credential management supports both IAM access keys and IRSA (IAM Roles for Service Accounts) for secretless operation.

Google Cloud

GCP support is not yet available. It is on the roadmap but has no committed timeline as of early 2026.


Comparison: Radius vs. the Ecosystem

Platform engineering has several competing approaches. Understanding where Radius sits requires comparing it honestly.

Radius vs. Helm

Helm templates Kubernetes YAML. It solves packaging and environment-specific configuration (values.yaml), but:

  • There is no application model — a “Helm application” is just a collection of Kubernetes resources
  • Developers must understand Kubernetes resource types
  • Connection strings are managed as secrets or config maps, manually
  • No recipe system — platform teams write complex, conditional templates

Radius reduces the Kubernetes knowledge required and adds the recipe/connection abstraction, but it adds new tooling on top of Helm. The two can coexist: Radius can deploy Helm charts through Bicep’s Kubernetes extension.

Radius vs. Crossplane

Both are Kubernetes-native control planes, but they target different problems at different layers:

Radius Crossplane
Primary abstraction Application (containers + dependencies together) Infrastructure resource (single cloud resource)
Developer interface app.bicep with portable resource types Claim objects in Kubernetes
Platform team interface Recipes (Bicep/Terraform modules) Compositions (XRDs + Compositions)
Connection injection Automatic via connections: block Manual (typically ExternalSecrets or custom)
Application graph Built-in Not built-in
Cloud portability Yes — same app.bicep for any environment Provider-specific resources
Composability Crossplane can be used inside a Radius Recipe N/A

The critical difference: Radius keeps developers writing application code, not infrastructure code. Crossplane still requires developers (or their GitOps configs) to know what Crossplane resource types exist and how to use claims. Radius adds an additional abstraction layer that Crossplane does not have.

They can coexist: a Radius Recipe can invoke a Crossplane-managed resource, using Crossplane for infrastructure lifecycle and Radius for application-level connections and graph tracking.

Radius vs. Score

Score is a spec for describing workload configuration that can be rendered to different targets (Docker Compose, Kubernetes, Humanitec). Score is:

  • A workload-level spec (single service), not an application-level model
  • Focused on portability of the workload definition, not the infrastructure recipe system
  • Target-agnostic by design — a Score file renders to Kubernetes via score-k8s or to Humanitec via the Humanitec platform

Radius and Score solve adjacent problems. Score handles “how do I run this workload anywhere without Kubernetes knowledge.” Radius handles “how do I describe an entire application with all its dependencies and have infrastructure provisioned consistently.” Score does not have a recipe system or an application graph. A platform engineering team could realistically use both.

Radius vs. Humanitec

Humanitec is a commercial internal developer platform product. Its Workload Specification format is similar in intent to Radius’s application model, and its Resource Graph plays a similar role to Radius’s application graph. Key differences:

  • Humanitec is commercial; Radius is open source (CNCF)
  • Humanitec’s orchestration layer is proprietary; Radius is fully open
  • Humanitec has more mature enterprise features (approval flows, GitOps-first design, RBAC at deploy time, richer self-service portal)
  • Radius is earlier stage; Humanitec has production deployments at scale
  • Radius does not yet have built-in approval workflows or deploy gates

If you need production-grade enterprise IDP features today, Humanitec is more mature. If you prefer open source, are willing to build around rough edges, and want to avoid vendor lock-in, Radius is the path.

Radius vs. KubeVela

KubeVela (CNCF incubating) is another application-centric platform with a similar philosophy. KubeVela uses Application CRDs with traits and components. The primary differences:

  • KubeVela uses YAML-first (OAM model); Radius uses Bicep-first
  • KubeVela has more mature multi-cluster deployment support
  • Radius has a stronger recipe/marketplace story with the OCI registry integration
  • Radius has richer cloud-provider integration for Azure and AWS

CNCF Status and Maturity

Radius was accepted as a CNCF Sandbox project on April 16, 2024. The CNCF Sandbox is the entry point for early-stage projects — it indicates vendor-neutral governance and open-source commitment, not production readiness.

As of early 2026, Radius is at v0.56.x. The semantic versioning signals early maturity. The project itself explicitly states it is not yet ready for production workloads. This is not spin — it is accurate. Expect:

  • API breaking changes between minor versions
  • Missing features that matter for production (no GCP support, single-cluster container deployment only, no built-in approval flows)
  • Limited ecosystem tooling compared to Helm/Crossplane

Current Limitations

Infrastructure deployment scope: Radius deploys containers only to the Kubernetes cluster it runs on. You cannot use Radius to deploy to a remote cluster. Cloud resources (Azure, AWS) are provisioned in the configured scope, but workloads are local.

Google Cloud: No GCP support. Azure and AWS only.

RBAC and deploy gates: Radius inherits Kubernetes RBAC for control-plane access, but there is no built-in concept of “require approval before deploying to prod.” This needs to be layered on via GitOps workflows.

Terraform state management: Radius manages Terraform state internally via the Kubernetes API, but this is not as flexible as running Terraform with remote state backends (S3, Azure Blob). Large-scale Terraform workflows may feel constrained.

GitOps maturity: The DeploymentTemplate CRD path works but is newer and less battle-tested than the rad deploy CLI path. Documentation and examples are thinner.

Dashboard: The Backstage-based dashboard is functional but not yet a full self-service portal. It is more of an application graph viewer than a developer portal.

Community size: ~1,600 GitHub stars and 122 forks as of early 2026 is modest. The ecosystem is small compared to ArgoCD, Crossplane, or Helm.

What Is Solid

Despite the caveats, several things work reliably:

  • The core application model (app.bicep + environments + recipes) is well-designed and internally consistent
  • The recipe system is genuinely flexible — existing Terraform modules adapt to recipes with minimal changes
  • Local development with rad run is excellent — it is dramatically faster to get a full application stack running than with raw Kubernetes
  • The application graph is useful for understanding complex applications
  • The Bicep extensibility integration (using the official Bicep compiler since v0.37) is a mature dependency

Getting Started

Prerequisites

  • Kubernetes cluster (kind, k3d, AKS, EKS, GKE — any will work)
  • kubectl configured with cluster-admin permissions
  • Kubernetes v1.23.8 or higher

Step 1: Install the rad CLI

1
2
3
4
5
# Linux / macOS / WSL
wget -q "https://raw.githubusercontent.com/radius-project/radius/main/deploy/install.sh" -O - | /bin/bash

# Verify
rad version

Step 2: Initialize Radius

1
2
mkdir my-radius-app && cd my-radius-app
rad init

rad init does four things:

  1. Installs Radius into the radius-system namespace on your cluster
  2. Creates a default environment with local-dev recipes pre-registered
  3. Creates a default resource group
  4. Scaffolds an example app.bicep

The scaffolded app.bicep is a simple container app that you can deploy immediately.

Step 3: Configure bicepconfig.json

rad init generates this automatically, but for manual setup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "experimentalFeaturesEnabled": {
    "extensibility": true
  },
  "extensions": {
    "radius": {
      "node": "br:biceptypes.azurecr.io/radius:latest"
    },
    "aws": {
      "node": "br:biceptypes.azurecr.io/aws:latest"
    }
  }
}

This configures the Bicep compiler to load the Radius and AWS type extensions, giving you IDE autocomplete and type checking for Radius resources.

Step 4: Write Your Application

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// app.bicep
import radius as radius

param application string
param environment string

resource frontend 'Applications.Core/containers@2023-10-01-preview' = {
  name: 'frontend'
  properties: {
    application: application
    container: {
      image: 'ghcr.io/radius-project/samples/demo:latest'
      ports: {
        web: { containerPort: 3000 }
      }
    }
    connections: {
      db: { source: mongodb.id }
    }
  }
}

resource mongodb 'Applications.Datastores/mongoDatabases@2023-10-01-preview' = {
  name: 'mongodb'
  properties: {
    application: application
    environment: environment
  }
}

Step 5: Deploy and Explore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Deploy with port-forwarding and log streaming
rad run app.bicep -a todoapp

# In another terminal: view the application graph
rad app graph -a todoapp

# View all resources in the application
rad resource list Applications.Core/containers
rad resource list Applications.Datastores/mongoDatabases

# Check application status and URLs
rad app status -a todoapp

# Access the Radius dashboard
kubectl port-forward -n radius-system svc/dashboard 7007:80
# Open http://localhost:7007

Step 6: Add a Cloud Environment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Configure Azure credentials
rad credential register azure \
  --client-id $ARM_CLIENT_ID \
  --client-secret $ARM_CLIENT_SECRET \
  --tenant-id $ARM_TENANT_ID \
  --subscription-id $ARM_SUBSCRIPTION_ID

# Create a production environment targeting Azure
cat > prod-env.bicep << 'EOF'
extension radius

resource prodEnv 'Applications.Core/environments@2023-10-01-preview' = {
  name: 'prod'
  properties: {
    compute: {
      kind: 'kubernetes'
      resourceId: 'self'
      namespace: 'prod'
    }
    providers: {
      azure: {
        scope: '/subscriptions/${subscriptionId}/resourceGroups/myapp-prod'
      }
    }
    recipes: {
      'Applications.Datastores/mongoDatabases': {
        default: {
          templateKind: 'bicep'
          templatePath: 'ghcr.io/radius-project/recipes/azure/mongodatabases:latest'
        }
      }
    }
  }
}
EOF

rad deploy prod-env.bicep

# Deploy your app (same app.bicep) to production
rad deploy app.bicep --environment prod

Positioning Radius in Your Platform Stack

Radius fills a specific niche. It is most valuable when:

  1. Your organization has multiple environments (dev/staging/prod) with different infrastructure backends and you are tired of maintaining per-environment Helm values or Terraform workspaces
  2. Developers need to consume infrastructure without understanding it — the recipe model with automatic connection injection removes a real category of toil
  3. You are Azure-heavy — the Azure integration is the most mature, the tooling (Bicep) comes from the Azure ecosystem, and the workload identity story is well-developed
  4. You are building an IDP and want an open-source application graph as a foundation rather than building one from scratch

It is less valuable (or premature) if:

  • You need production stability guarantees now — wait for a 1.0
  • You need GCP support
  • Your developers are comfortable with Kubernetes and existing tooling works fine
  • You need enterprise features like approval flows and audit logging without building them yourself

The project’s lineage (from the team that built KEDA and Dapr) gives reasonable confidence in long-term technical direction. The CNCF sandbox status is a meaningful governance milestone. But the v0.x versioning, the “not production ready” disclaimer, and the relatively small community are real signals — evaluate carefully before adopting for anything critical.

For platform engineers building the next generation of developer tooling, Radius is worth a serious evaluation. The core model is sound, the recipe system solves a real problem, and the separation between developer and platform engineer concerns is exactly right. The question is whether your organization can tolerate early-adopter roughness, or whether you need to wait 12-18 months for the project to mature.


Further Reading

Comments