Deployment Engine Architecture
March 13, 2026 · View on GitHub
The Deployment Engine (DE) is the component in Radius responsible for processing ARM/Bicep template deployments. It is an external service (written in C#) that evaluates ARM JSON templates — the compiled output of Bicep — and orchestrates the creation, update, and deletion of the resources defined within them. The deployment engine acts as a bridge between declarative template definitions and the imperative resource operations handled by UCP and the resource providers.
High-Level Overview
graph TD
subgraph "User Entry Points"
CLI["rad CLI<br/>cmd/rad"]
K8sCRD["DeploymentTemplate CRD<br/>Kubernetes"]
RecipeEntry["Bicep Recipe Driver<br/>pkg/recipes/driver/bicep"]
end
subgraph "Radius Control Plane"
UCP["UCP<br/>cmd/ucpd"]
Controller["Kubernetes Controller<br/>cmd/controller"]
subgraph "Deployment Engine"
DE["Deployment Engine<br/>(bicep-de container)"]
end
subgraph "Resource Providers"
DynRP["Dynamic RP<br/>cmd/dynamic-rp"]
end
end
subgraph "External Systems"
Azure["Azure"]
AWS["AWS"]
K8s["Kubernetes"]
end
K8sCRD -->|"watch/reconcile"| Controller
CLI -->|"1. PUT deployment"| UCP
Controller -->|"1. PUT deployment"| UCP
RecipeEntry -->|"1. PUT deployment"| UCP
UCP -->|"2. Proxy to DE"| DE
DE -->|"3. PUT each resource"| UCP
UCP -->|"4. Route to RP"| DynRP
DynRP --> K8s
The deployment engine sits at the center of Radius's deployment pipeline. It receives compiled ARM JSON templates, evaluates expressions, resolves dependencies between resources in the template, and issues individual PUT/DELETE requests back to UCP for each resource. UCP then routes those requests to the appropriate resource provider.
Key Components
Deployment Engine Container (bicep-de)
- Image:
ghcr.io/radius-project/deployment-engine - Language: .NET/C# (external to this Go repository)
- Port: 6443 in Helm/self-hosted deployments; debug setups commonly expose
localhost:5017 (for example, via
5017:5445port-forwarding in Kubernetes debug manifests or5017:8080in the Docker debug script) - Helm chart: deploy/Chart/templates/de/
The deployment engine is a .NET service deployed as a Kubernetes pod. It
implements the ARM deployment API (Microsoft.Resources/deployments) and
is responsible for:
- Parsing and evaluating ARM JSON templates (compiled Bicep output)
- Resolving template expressions, functions, and dependencies
- Orchestrating the order of resource deployments based on
dependsOndeclarations - Issuing individual resource PUT/DELETE operations back to UCP
- Tracking deployment operations and their status
- Reporting deployment progress and output resources
Configuration
The deployment engine is configured via environment variables:
| Variable | Purpose | Example |
|---|---|---|
RADIUSBACKENDURL | UCP endpoint for callbacks | https://ucp.radius-system:443/apis/api.ucp.dev/v1alpha3 |
ASPNETCORE_URLS | Listening URL | http://+:6443 |
ARM_AUTH_METHOD | Authentication method | UCPCredential |
SKIP_ARM | Skip Azure ARM integration | true / false |
kubernetes | Enable Kubernetes mode | true |
In production the RADIUSBACKENDURL points to the UCP service inside the
cluster (https://ucp.radius-system:443/apis/api.ucp.dev/v1alpha3). In
development, debug scripts/manifests set it to host-reachable UCP endpoints
such as http://host.docker.internal:9000/apis/api.ucp.dev/v1alpha3 (Docker
debug) or http://host.k3d.internal:9000/apis/api.ucp.dev/v1alpha3
(Kubernetes debug manifest).
Debug manifests may also include provider toggles such as AZURE_ENABLED,
AWS_ENABLED, and KUBERNETES_ENABLED.
How Deployments Work
There are three main entry points that trigger deployments through the deployment engine:
rad deployCLI command — interactive user-initiated deployments- Kubernetes
DeploymentTemplateCRD — declarative Kubernetes-native deployments - Bicep recipe execution — infrastructure provisioning as part of portable resource creation
All three follow the same core pattern: compile/prepare an ARM JSON template, submit it to the deployment engine via UCP, and poll for completion.
Entry Point 1: rad deploy CLI Command
sequenceDiagram
participant User
participant CLI as rad CLI
participant Bicep as Bicep Compiler
participant UCP as UCP
participant DE as Deployment Engine
participant RP as Resource Provider
User->>CLI: rad deploy myapp.bicep
CLI->>Bicep: PrepareTemplate(myapp.bicep)
Bicep-->>CLI: ARM JSON template
CLI->>CLI: Inject environment/application params
CLI->>CLI: Build ProviderConfig (Radius, Azure, AWS scopes)
CLI->>UCP: PUT /planes/radius/local/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/{name}
UCP->>DE: Proxy PUT request
DE->>DE: Evaluate template expressions
DE->>DE: Resolve resource dependencies
loop For each resource in template
DE->>UCP: PUT /planes/radius/local/resourceGroups/{rg}/providers/{type}/{name}
UCP->>RP: Route to appropriate RP
RP-->>UCP: Resource created/updated
UCP-->>DE: Response
end
DE-->>UCP: Deployment complete (202 → 200)
loop Poll for progress
CLI->>UCP: GET .../deployments/{name}/operations
UCP->>DE: Proxy GET
DE-->>UCP: Operation status
UCP-->>CLI: Progress update
CLI->>User: Display resource status
end
UCP-->>CLI: Final deployment result
CLI->>User: Deployment Complete
Step-by-step walkthrough:
-
The user runs
rad deploy myapp.bicep. The CLI compiles the Bicep file into ARM JSON using the Bicep compiler (pkg/cli/cmd/deploy/deploy.go). -
The CLI injects automatic parameters (
environment,application) viainjectAutomaticParametersin (pkg/cli/cmd/deploy/deploy.go) and builds aProviderConfigthat specifies the Radius resource group scope, and optionally Azure and AWS scopes viaGetProviderConfigsin (pkg/cli/deployment/deploy.go). -
A
ResourceDeploymentsClientsends a PUT request to UCP at a path like:/planes/radius/local/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/rad-deploy-{uuid}The request body includes the compiled template, parameters, and provider config (pkg/sdk/clients/resourcedeploymentsclient.go).
-
UCP's proxy controller receives the request and routes it to the deployment engine based on the
Microsoft.Resourcesresource provider registration (pkg/ucp/frontend/controller/radius/proxy.go). -
The deployment engine evaluates the template, resolves
dependsOndependencies, evaluates Bicep/ARM functions, and issues individual PUT requests back to UCP for each resource in the template. -
UCP routes each resource request to the appropriate resource provider, with current authoring work primarily flowing through Dynamic RP, using
ValidateDownstream(pkg/ucp/frontend/controller/resourceGroups/util.go). -
The CLI polls the deployment operations endpoint to display progress to the user. Nested Bicep modules are tracked recursively.
Entry Point 2: Kubernetes DeploymentTemplate CRD
sequenceDiagram
participant K8s as Kubernetes API
participant Ctrl as DeploymentTemplate Reconciler
participant UCP as UCP
participant DE as Deployment Engine
participant RP as Resource Provider
K8s->>Ctrl: DeploymentTemplate created/updated
Ctrl->>Ctrl: Check if spec hash changed
Ctrl->>Ctrl: Unmarshal template + providerConfig
Ctrl->>Ctrl: Create resource group if needed
Ctrl->>UCP: PUT .../Microsoft.Resources/deployments/{name}
UCP->>DE: Proxy to Deployment Engine
DE->>DE: Evaluate template
loop For each resource
DE->>UCP: PUT resource
UCP->>RP: Route to RP
RP-->>UCP: Response
UCP-->>DE: Response
end
DE-->>UCP: Deployment complete
UCP-->>Ctrl: Response with outputResources
Ctrl->>K8s: Create DeploymentResource CRDs for outputs
Ctrl->>K8s: Update DeploymentTemplate status → Ready
The DeploymentTemplateReconciler
(pkg/controller/reconciler/deploymenttemplate_reconciler.go)
enables Kubernetes-native deployment of Bicep/ARM templates through a CRD. It:
- Watches
DeploymentTemplatecustom resources - Computes a hash of the spec to detect changes
- Submits templates to the deployment engine via
ResourceDeploymentsClient.CreateOrUpdate - Polls for completion using resume tokens
- Creates
DeploymentResourceCRDs for each output resource - Manages the full lifecycle including deletion via finalizers
Entry Point 3: Bicep Recipe Execution
sequenceDiagram
participant User as User/Application
participant RP as Resource Provider
participant Engine as Recipe Engine
participant BicepDriver as Bicep Driver
participant Registry as OCI Registry
participant UCP as UCP
participant DE as Deployment Engine
User->>RP: Create portable resource (e.g., Redis)
RP->>Engine: Execute recipe
Engine->>BicepDriver: Execute(opts)
BicepDriver->>Registry: Download Bicep template (OCI)
Registry-->>BicepDriver: ARM JSON template
BicepDriver->>BicepDriver: Build parameters + recipeContext
BicepDriver->>BicepDriver: Build ProviderConfig (Azure/AWS scopes)
BicepDriver->>UCP: PUT .../Microsoft.Resources/deployments/recipe{timestamp}
UCP->>DE: Proxy to Deployment Engine
loop For each resource in recipe template
DE->>UCP: PUT resource
UCP->>RP: Route to appropriate RP
RP-->>UCP: Resource created
UCP-->>DE: Response
end
DE-->>UCP: Deployment complete
UCP-->>BicepDriver: Response with outputResources
BicepDriver->>BicepDriver: Garbage collect stale resources
BicepDriver-->>Engine: RecipeOutput
Engine-->>RP: Recipe result
When a portable resource (like a Redis cache or Dapr state store) is created with a Bicep recipe, the recipe engine's Bicep driver (pkg/recipes/driver/bicep/bicep.go) executes the recipe by:
- Downloading the Bicep template from an OCI container registry
- Building a
recipeContextparameter with metadata about the requesting resource - Constructing a
ProviderConfigwith the appropriate cloud provider scopes - Submitting the template to the deployment engine via the same
ResourceDeploymentsClient.CreateOrUpdateAPI - Polling until the deployment completes
- Garbage-collecting output resources that are no longer needed from previous deployments
Component Interaction Diagram
graph LR
subgraph "SDK Clients (pkg/sdk/clients)"
RDC["ResourceDeploymentsClient<br/>Interface"]
RDCI["ResourceDeploymentsClientImpl"]
RDOC["ResourceDeploymentOperationsClient"]
PC["ProviderConfig"]
end
subgraph "CLI Layer (pkg/cli)"
Deploy["deploy.DeployWithProgress"]
DCClient["deployment.ResourceDeploymentClient"]
RadDeploy["cmd/deploy Runner"]
end
subgraph "Controller Layer (pkg/controller)"
DTR["DeploymentTemplateReconciler"]
end
subgraph "Recipe Layer (pkg/recipes)"
BicepDriver["bicep.bicepDriver"]
RecipeEngine["engine.Engine"]
end
subgraph "UCP (pkg/ucp)"
ProxyCtrl["ProxyController"]
ValidateDS["ValidateDownstream"]
Routes["Radius Routes"]
end
subgraph "Deployment Engine"
DE["bicep-de<br/>(external .NET service)"]
end
RadDeploy --> Deploy
Deploy --> DCClient
DCClient --> RDC
DTR --> RDC
RecipeEngine --> BicepDriver
BicepDriver --> RDC
RDC -.->|implements| RDCI
RDCI -->|"HTTP PUT/DELETE"| Routes
RDOC -->|"HTTP GET"| Routes
Routes --> ProxyCtrl
ProxyCtrl --> ValidateDS
ProxyCtrl -->|"proxy"| DE
DE -->|"PUT each resource"| Routes
Key Interfaces and Types
| Type | Location | Purpose |
|---|---|---|
ResourceDeploymentsClient | pkg/sdk/clients/resourcedeploymentsclient.go | Interface for creating/deleting deployments |
ResourceDeploymentsClientImpl | Same file | HTTP implementation that talks to UCP |
ResourceDeploymentOperationsClient | pkg/sdk/clients/resourcedeploymentoperationsclient.go | Client for listing deployment operations (progress) |
Deployment / DeploymentProperties | pkg/sdk/clients/resourcedeploymentsclient.go | Request payload containing template, parameters, and provider config |
ProviderConfig | pkg/sdk/clients/resourcedeploymentsclient.go | Scopes for Radius, Azure, AWS, and deployment targets (helper functions in providerconfig.go) |
ResourceDeploymentClient | pkg/cli/deployment/deploy.go | CLI-layer wrapper that adds progress monitoring |
ProxyController | pkg/ucp/frontend/controller/radius/proxy.go | UCP controller that proxies requests to downstream RPs and the DE |
DeploymentTemplateReconciler | pkg/controller/reconciler/deploymenttemplate_reconciler.go | Kubernetes reconciler for DeploymentTemplate CRDs |
bicepDriver | pkg/recipes/driver/bicep/bicep.go | Recipe driver that deploys Bicep templates via the DE |
Deployment Lifecycle
Request Flow
stateDiagram-v2
[*] --> TemplateSubmitted: PUT deployment
TemplateSubmitted --> Accepted: 202 Accepted
Accepted --> Evaluating: DE processes template
Evaluating --> DeployingResources: Resolve dependencies
DeployingResources --> DeployingResources: PUT each resource via UCP
DeployingResources --> Succeeded: All resources deployed
DeployingResources --> Failed: Resource deployment error
Succeeded --> [*]: 200 OK
Failed --> [*]: Error response
-
Template Submitted: A client sends a PUT request to create a
Microsoft.Resources/deploymentsresource. The resource ID follows the pattern:/planes/radius/local/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/{name} -
Accepted (202): UCP proxies the request to the deployment engine, which returns a 202 Accepted with a location header for polling.
-
Evaluating: The deployment engine parses the ARM JSON template, evaluates all expressions and functions, and builds a dependency graph of resources.
-
Deploying Resources: For each resource (in dependency order), the deployment engine sends a PUT request back to UCP at the resource's full resource ID. UCP routes each request to the correct resource provider.
-
Complete: When all resources are deployed, the deployment engine returns the final status with the list of output resources and any template outputs.
Provider Config and Scoping
A scope is the base resource ID prefix that the deployment engine prepends
to each resource it deploys. Because a single Bicep template can declare
resources across multiple platforms (Radius, Azure, AWS), the deployment engine
needs to know where each category of resource lives. For example, a Radius
resource's full ID is formed by combining the Radius scope
(/planes/radius/local/resourceGroups/my-rg) with the provider namespace,
resource type, and name (for example,
/providers/<radius-provider>/<resource-type>/my-resource). An Azure resource uses its
Azure subscription and resource group as the scope, and an AWS resource uses
its account and region. Without scopes, the deployment engine would not know
which plane, subscription, account, or resource group to target when issuing
PUT requests back to UCP.
The ProviderConfig tells the deployment engine which scopes to use for
different resource types:
{
"radius": {
"type": "Radius",
"value": {
"scope": "/planes/radius/local/resourceGroups/my-rg"
}
},
"deployments": {
"type": "Microsoft.Resources",
"value": {
"scope": "/planes/radius/local/resourceGroups/my-rg"
}
},
"az": {
"type": "AzureResourceManager",
"value": {
"scope": "/subscriptions/{sub-id}/resourceGroups/{azure-rg}"
}
},
"aws": {
"type": "AWS",
"value": {
"scope": "/planes/aws/aws/accounts/{account}/regions/{region}"
}
}
}
radius: Scope for Radius resource types handled by the control planedeployments: Scope for nestedMicrosoft.Resources/deploymentsaz: Optional Azure subscription/resource group scopeaws: Optional AWS account/region scope
Why ProviderConfig Exists
A compiled Bicep/ARM template contains resource types (e.g.
<radius-provider>/<resource-type>, Microsoft.Storage/storageAccounts,
AWS.S3/Bucket) but does not contain any information about which Radius
resource group, Azure subscription, or AWS account those resources should be
created in. That targeting information is external to the template — it depends
on the caller's environment, workspace, and cloud credentials.
The ProviderConfig bridges this gap. It is a per-deployment sidecar payload
that maps each cloud provider to a scope (a base resource ID prefix). When the
deployment engine processes a resource, it looks up the provider that owns that
resource type and prepends the corresponding scope to form the full resource
ID. This design keeps templates portable: the same Bicep file can be deployed
into different resource groups, subscriptions, or AWS accounts simply by
changing the ProviderConfig — without modifying the template itself.
Where the Scopes Come From
The cloud provider scopes originate from user input during environment setup and flow through the system as follows:
-
User configures an environment — When a user runs
rad init, interactive prompts collect Azure credentials (subscription ID, resource group) and/or AWS credentials (account ID, region). These are converted into scope strings byCreateEnvProviders()in pkg/cli/cmd/utils.go:- Azure:
/subscriptions/{sub-id}/resourceGroups/{rg} - AWS:
/planes/aws/aws/accounts/{account-id}/regions/{region}
The scopes are stored as part of the
Providersfield on the Radius Environment resource. - Azure:
-
CLI reads providers from the environment — At deploy time, the
rad deployrunner fetches the environment resource and extracts the provider scopes viasetupCloudProviders()in pkg/cli/cmd/deploy/deploy.go. The Radius scope (resource group) comes from the workspace configuration. -
Scopes are assembled into ProviderConfig —
GetProviderConfigs()in pkg/cli/deployment/deploy.go takes those scopes and constructs theProviderConfigthat is sent with the deployment request.
For recipes, the same environment provider scopes are available in the
ExecuteOptions.Configuration.Providers field passed to the Bicep driver,
which reads them in newProviderConfig().
For DeploymentTemplate CRDs, the provider config is set in the CRD's
spec.providerConfig field by whatever controller or tool creates the custom
resource — typically the Radius controller, which pulls scopes from the
same environment resource.
How ProviderConfig Is Built
ProviderConfig is not a config file on disk — it is a Go struct
(pkg/sdk/clients/resourcedeploymentsclient.go)
that is constructed at runtime and serialized to JSON inside the deployment
request body (DeploymentProperties.ProviderConfig). Helper functions for
building it live in
pkg/sdk/clients/providerconfig.go.
There are three places where a ProviderConfig is constructed:
-
CLI deploys —
GetProviderConfigs()in pkg/cli/deployment/deploy.go callsNewDefaultProviderConfig()to set the Radius and deployments scopes, then optionally adds Azure and AWS scopes based on the user's workspace configuration. -
Bicep recipe driver —
newProviderConfig()in pkg/recipes/driver/bicep/bicep.go builds a config using the recipe's environment provider settings (Azure subscription/resource group, AWS account/region). -
DeploymentTemplate reconciler — pkg/controller/reconciler/deploymenttemplate_reconciler.go reads the provider config from the
DeploymentTemplateCRD'sspec.providerConfigfield, which is populated by whoever creates the Kubernetes custom resource.
In all cases the ProviderConfig travels as part of the PUT request body to
the deployment engine, which reads it to determine the scope prefix for each
resource's ID.
UCP Routing
When the deployment engine sends resource requests back to UCP, the
ProxyController determines the downstream destination:
graph TD
DE["Deployment Engine"] -->|"PUT resource"| UCP["UCP ProxyController"]
UCP --> Validate["ValidateDownstream"]
Validate --> PlaneCheck["Validate Plane Exists"]
PlaneCheck --> RGCheck["Validate Resource Group"]
RGCheck --> RTCheck["ValidateResourceType"]
RTCheck -->|"Location has address"| RPAddress["Use RP Address"]
RTCheck -->|"No address configured"| DefaultRP["Use Default Downstream<br/>(Dynamic RP)"]
RPAddress --> RP["Resource Provider"]
DefaultRP --> DynRP["Dynamic RP"]
Resource provider URLs are registered in UCP's database as part of the resource provider manifest system. UCP looks up the registered location for a resource type and routes the request to its configured address. If no specific address is found, requests fall through to the default downstream (Dynamic RP).
Deployment in Kubernetes
graph TD
subgraph "radius-system namespace"
UCP_Pod["UCP Pod<br/>port 443"]
DE_Pod["bicep-de Pod<br/>port 6443"]
CoreRP_Pod["Applications RP Pod"]
DynRP_Pod["Dynamic RP Pod"]
Controller_Pod["Controller Pod"]
end
UCP_Svc["ucp Service<br/>:443"] --> UCP_Pod
DE_Svc["bicep-de Service<br/>:6443"] --> DE_Pod
UCP_Pod -->|"proxy"| DE_Pod
DE_Pod -->|"RADIUSBACKENDURL"| UCP_Svc
Controller_Pod -->|"deployments API"| UCP_Svc
In a production Kubernetes cluster, the deployment engine runs as:
- Deployment:
bicep-dewith 1 replica - Service:
bicep-deon port 6443 - ServiceAccount:
bicep-dewith cluster-admin privileges — required because Bicep templates can use the Kubernetes extensibility provider (extension kubernetes) to deploy arbitrary Kubernetes resource types (Deployments, Services, Secrets, CRDs, etc.) directly to the Kubernetes API, bypassing UCP. Since the set of resource types and target namespaces is determined by user templates and is unbounded, a fixed set of RBAC rules is not feasible. - ConfigMap:
bicep-de-configwith ASP.NET Core app settings
The deployment engine communicates with UCP via the RADIUSBACKENDURL
environment variable, which points to the UCP service's internal cluster URL.
Notable Details
Asynchronous Operation Pattern
Deployments are long-running operations. The deployment engine returns HTTP 202 (Accepted) with a polling URL. Callers use Azure SDK pollers to track progress:
ResourceDeploymentsClient.CreateOrUpdate()returns aPollerthat can be polled withPollUntilDone()- The Kubernetes reconcilers use
ContinueCreateOperation()with resume tokens to survive pod restarts across reconciliation loops - The CLI polls the
/operationssub-resource to display per-resource progress
Bicep Compilation Happens Client-Side
The Bicep compiler runs on the client (CLI machine), not inside the cluster. The deployment engine only understands ARM JSON — the compiled output format. This means:
rad deploy myapp.bicepfirst compiles to ARM JSON viaPrepareTemplate()- Recipe templates are stored as compiled ARM JSON in OCI registries
- The
DeploymentTemplateCRD stores pre-compiled ARM JSON in itsspec.templatefield
Callback Pattern
The deployment engine and UCP form a bidirectional communication loop:
- Client → UCP → DE: The initial deployment request flows through UCP to the deployment engine
- DE → UCP → RP: For each resource, the deployment engine calls back to UCP, which routes to the appropriate resource provider
This means the deployment engine must have network access to UCP (via
RADIUSBACKENDURL) and UCP must have network access to the deployment engine
(via the registered service URL).
Progress Monitoring
The CLI provides real-time deployment progress by polling the deployment
operations endpoint. The monitorProgress function in
pkg/cli/deployment/deploy.go
recursively monitors nested Bicep modules by spawning additional polling
goroutines when it detects Microsoft.Resources/deployments sub-operations.
DeploymentProcessor vs. Deployment Engine
These are two different concepts that can be confused:
- Deployment Engine (
bicep-de): External .NET service that evaluates ARM templates and orchestrates multi-resource deployments - DeploymentProcessor (pkg/corerp/backend/deployment/deploymentprocessor.go):
Go interface inside the Applications RP that handles rendering and deploying
individual Radius resources (containers, gateways, volumes) to their target
infrastructure (e.g., Kubernetes). The deployment engine creates the
individual resources; the
DeploymentProcessorhandles what happens inside each resource provider when it receives a PUT request.