Direct module support for Recipes#11876
Conversation
Signed-off-by: Reshma Abdul Rahim <61033581+Reshrahim@users.noreply.github.com>
Radius functional test overviewClick here to see the test run details
Test Status⌛ Building Radius and pushing container images for functional tests... |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #11876 +/- ##
==========================================
+ Coverage 51.47% 51.69% +0.21%
==========================================
Files 699 725 +26
Lines 44130 45595 +1465
==========================================
+ Hits 22718 23572 +854
- Misses 19251 19798 +547
- Partials 2161 2225 +64 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| @@ -0,0 +1,207 @@ | |||
| # Feature Specification: Direct Module Support | |||
There was a problem hiding this comment.
I experimented with the Spec Kit approach and decided it was more effective to follow the template in eng/design-notes/templates/feature-spec-template.md. I would give Copilot the following prompt:
Combine spec.md, research.md (if appropriate), quickstart.md, and data-model.md into eng/design-notes/recipdes/2026-05-direct-recipe-modules.md. Only put feature related details in this document and omit implementation details. Create the doc according to eng/design-notes/templates/feature-spec-template.md.
| 1. **Given** a RecipePack with `recipeKind: 'bicep'` and `recipeLocation` set to a standard Bicep module OCI reference, **When** a resource using this recipe is deployed with `recipeParameters` providing values for the module's parameters, **Then** the module receives those values and provisions infrastructure successfully. | ||
| 2. **Given** a Bicep module with output values (e.g., `output endpoint string`), **When** the resource is deployed, **Then** the module's outputs are mapped to the resource type's read-only properties via the `outputs` mapping on the RecipePack. | ||
| 3. **Given** `recipeParameters` containing `{{context.*}}` template expressions (e.g., `name: 'sa-{{context.resource.name}}'`), **When** the resource is deployed, **Then** expressions are resolved to actual Radius context values before being passed to the module as ARM parameters. | ||
| 4. **Given** a `recipeLocation` pointing to a non-existent Bicep module, **When** deployment is attempted, **Then** the system returns a clear error indicating the module cannot be fetched. | ||
| 5. **Given** a resource deployed via a direct Bicep module recipe, **When** the resource is deleted, **Then** the underlying ARM deployment and provisioned infrastructure are cleaned up. |
There was a problem hiding this comment.
| 1. **Given** a RecipePack with `recipeKind: 'bicep'` and `recipeLocation` set to a standard Bicep module OCI reference, **When** a resource using this recipe is deployed with `recipeParameters` providing values for the module's parameters, **Then** the module receives those values and provisions infrastructure successfully. | |
| 2. **Given** a Bicep module with output values (e.g., `output endpoint string`), **When** the resource is deployed, **Then** the module's outputs are mapped to the resource type's read-only properties via the `outputs` mapping on the RecipePack. | |
| 3. **Given** `recipeParameters` containing `{{context.*}}` template expressions (e.g., `name: 'sa-{{context.resource.name}}'`), **When** the resource is deployed, **Then** expressions are resolved to actual Radius context values before being passed to the module as ARM parameters. | |
| 4. **Given** a `recipeLocation` pointing to a non-existent Bicep module, **When** deployment is attempted, **Then** the system returns a clear error indicating the module cannot be fetched. | |
| 5. **Given** a resource deployed via a direct Bicep module recipe, **When** the resource is deleted, **Then** the underlying ARM deployment and provisioned infrastructure are cleaned up. | |
| 1. **Given** a RecipePack with `recipeKind: 'bicep'` and `recipeLocation` set to a standard Bicep module OCI reference, **When** a resource using this recipe is deployed with `recipeParameters` providing values for the module's parameters, **Then** the module receives those values and provisions infrastructure successfully (*existing functionality*). | |
| 2. **Given** a Bicep module with output values (e.g., `output endpoint string`), **When** the resource is deployed, **Then** the module's outputs are mapped to the resource type's read-only properties via the `outputs` mapping on the RecipePack (*new functionality*). | |
| 3. **Given** `recipeParameters` containing `{{context.*}}` template expressions (e.g., `name: 'sa-{{context.resource.name}}'`), **When** the resource is deployed, **Then** expressions are resolved to actual Radius context values before being passed to the module as ARM parameters (*new functionality*). | |
| 4. **Given** a `recipeLocation` pointing to a non-existent Bicep module, **When** deployment is attempted, **Then** the system returns a clear error indicating the module cannot be fetched. | |
| 5. **Given** a resource deployed via a direct Bicep module recipe, **When** the resource is deleted, **Then** the underlying ARM deployment and provisioned infrastructure are cleaned up (*existing functionality*). |
There was a problem hiding this comment.
Should we mention secrets output mapping like you did under Terraform?
| 1. **Given** a RecipePack with `recipeKind: 'terraform'` and `recipeLocation` set to a Terraform registry path (e.g., `ballj/postgresql/kubernetes`), **When** a resource is deployed with `recipeParameters` providing values for the module's input variables, **Then** the module receives those values and executes successfully. | ||
| 2. **Given** a recipe with `recipeLocation` set to a Git URL (e.g., `git::https://github.com/org/module.git`), **When** a resource is deployed, **Then** the system clones the module from Git and executes it. | ||
| 3. **Given** a recipe with `recipeLocation` pointing to a Git URL with a ref specifier (e.g., `?ref=v2.0.0`) or a subdirectory path (e.g., `//modules/vpc`), **When** deployed, **Then** the system uses the specified ref and/or navigates to the subdirectory. | ||
| 4. **Given** a Terraform module with output values, **When** the resource is deployed, **Then** the module's outputs are mapped to the resource type's read-only properties via the `outputs` mapping — non-sensitive outputs in the `Values` map, sensitive outputs (marked `sensitive = true`) in the `Secrets` map. | ||
| 5. **Given** `recipeParameters` containing `{{context.*}}` expressions, **When** the resource is deployed, **Then** expressions are resolved to actual Radius context values before being passed as Terraform input variables. | ||
| 6. **Given** a module that requires a variable not supplied by any parameter source, **When** deployment is attempted, **Then** Terraform surfaces a clear error indicating which required variable is missing. | ||
| 7. **Given** a resource deployed via a direct Terraform module recipe, **When** the resource is deleted, **Then** the system runs `terraform destroy` and cleans up all provisioned infrastructure. |
There was a problem hiding this comment.
| 1. **Given** a RecipePack with `recipeKind: 'terraform'` and `recipeLocation` set to a Terraform registry path (e.g., `ballj/postgresql/kubernetes`), **When** a resource is deployed with `recipeParameters` providing values for the module's input variables, **Then** the module receives those values and executes successfully. | |
| 2. **Given** a recipe with `recipeLocation` set to a Git URL (e.g., `git::https://github.com/org/module.git`), **When** a resource is deployed, **Then** the system clones the module from Git and executes it. | |
| 3. **Given** a recipe with `recipeLocation` pointing to a Git URL with a ref specifier (e.g., `?ref=v2.0.0`) or a subdirectory path (e.g., `//modules/vpc`), **When** deployed, **Then** the system uses the specified ref and/or navigates to the subdirectory. | |
| 4. **Given** a Terraform module with output values, **When** the resource is deployed, **Then** the module's outputs are mapped to the resource type's read-only properties via the `outputs` mapping — non-sensitive outputs in the `Values` map, sensitive outputs (marked `sensitive = true`) in the `Secrets` map. | |
| 5. **Given** `recipeParameters` containing `{{context.*}}` expressions, **When** the resource is deployed, **Then** expressions are resolved to actual Radius context values before being passed as Terraform input variables. | |
| 6. **Given** a module that requires a variable not supplied by any parameter source, **When** deployment is attempted, **Then** Terraform surfaces a clear error indicating which required variable is missing. | |
| 7. **Given** a resource deployed via a direct Terraform module recipe, **When** the resource is deleted, **Then** the system runs `terraform destroy` and cleans up all provisioned infrastructure. | |
| 1. **Given** a RecipePack with `recipeKind: 'terraform'` and `recipeLocation` set to a Terraform registry path (e.g., `ballj/postgresql/kubernetes`), **When** a resource is deployed with `recipeParameters` providing values for the module's input variables, **Then** the module receives those values and executes successfully (*existing functionality*). | |
| 2. **Given** a recipe with `recipeLocation` set to a Git URL (e.g., `git::https://github.com/org/module.git`), **When** a resource is deployed, **Then** the system clones the module from Git and executes it (*existing functionality*). | |
| 3. **Given** a recipe with `recipeLocation` pointing to a Git URL with a ref specifier (e.g., `?ref=v2.0.0`) or a subdirectory path (e.g., `//modules/vpc`), **When** deployed, **Then** the system uses the specified ref and/or navigates to the subdirectory (*existing functionality*). | |
| 4. **Given** a Terraform module with output values, **When** the resource is deployed, **Then** the module's outputs are mapped to the resource type's read-only properties via the `outputs` mapping — non-sensitive outputs in the `Values` map, sensitive outputs (marked `sensitive = true`) in the `Secrets` map (*new functionality*). | |
| 5. **Given** `recipeParameters` containing `{{context.*}}` expressions, **When** the resource is deployed, **Then** expressions are resolved to actual Radius context values before being passed as Terraform input variables. | |
| 6. **Given** a module that requires a variable not supplied by any parameter source, **When** deployment is attempted, **Then** Terraform surfaces a clear error indicating which required variable is missing. | |
| 7. **Given** a resource deployed via a direct Terraform module recipe, **When** the resource is deleted, **Then** the system runs `terraform destroy` and cleans up all provisioned infrastructure (*existing functionality*). |
| 1. **Given** a recipe with `recipeParameters` containing `{{context.resource.properties.*}}` expressions, **When** a resource is deployed with specific property values set by the application developer, **Then** those property values are resolved and passed to the module as input parameters. | ||
| 2. **Given** a recipe with `recipeParameters` containing a ternary expression (e.g., `{{context.resource.properties.size == "s" ? "B_Standard_B1ms" : "GP_Standard_D2s_v3"}}`), **When** resources are deployed with different property values, **Then** each deployment passes the correct resolved value to the module. | ||
| 3. **Given** a recipe with `recipeParameters` that combine context property expressions with literal text (e.g., `name: 'pg-{{context.resource.name}}'`), **When** a resource is deployed, **Then** the module receives the fully resolved parameter values. |
There was a problem hiding this comment.
Is any of this new functionality?
There was a problem hiding this comment.
context resolution at deploy time doesn't exists today
| ### User Story 4 — Private Module Authentication (Priority: P2) | ||
|
|
||
| As a platform engineer, I want to use modules hosted in private registries, private Git repositories, or private OCI registries as recipes, authenticating with credentials configured through the existing secret store. | ||
|
|
||
| **Why this priority**: Enterprise teams host modules in private repositories. Without authentication support, direct module support is limited to public modules. | ||
|
|
||
| **Independent Test**: Link a recipe pointing to a private Terraform registry module or Git repository, configure credentials via the existing secret mechanism, deploy, and verify the module is fetched successfully. | ||
|
|
There was a problem hiding this comment.
I think this can be removed.
There was a problem hiding this comment.
Is this covered in the extensibility world already ?
| ### User Story 5 — Source Reachability Validation at Link Time (Priority: P2) | ||
|
|
||
| As a platform engineer, I want the system to validate that a `recipeLocation` pointing to a direct module source is reachable when I link the recipe to an environment, so I catch typos and inaccessible sources early rather than at deploy time. | ||
|
|
||
| **Why this priority**: Early validation prevents wasted time debugging deploy failures caused by simple typos or unreachable sources. | ||
|
|
||
| **Independent Test**: Link a recipe with a `recipeLocation` pointing to a non-existent module source and verify a validation error is returned. |
There was a problem hiding this comment.
I think Radius does this today, no? If so, we can remove.
There was a problem hiding this comment.
I don't know if it does this correctly but can check whats missing.
| recipeKind: 'terraform' | ||
| recipeLocation: 'Azure/avm-res-dbforpostgresql-flexibleserver/azurerm' | ||
| recipeParameters: { | ||
| name: 'pg-{{context.resource.name}}' | ||
| location: 'eastus2' | ||
| // Single-level ternary: maps "s" to burstable SKU, else general purpose | ||
| sku_name: '{{context.resource.properties.size == "s" ? "B_Standard_B1ms" : "GP_Standard_D2s_v3"}}' | ||
| storage_mb: '{{context.resource.properties.size == "s" ? "32768" : "65536"}}' | ||
| tags: { | ||
| environment: '{{context.environment.name}}' | ||
| application: '{{context.application.name}}' | ||
| } | ||
| } | ||
| outputs: { | ||
| host: 'fqdn' | ||
| port: 'port' | ||
| database: 'database_name' | ||
| username: 'administrator_login' | ||
| } |
There was a problem hiding this comment.
@willtsai @nithyatsu This is why I'm recommending we rename recipeKind to recipe, recipeLocation to location, and recipeParamters to parameters. Then you will have
- recipesPack.recipes.<resource_type>
- .kind
- .location
- .parameters
- .outputs
There was a problem hiding this comment.
We agreed to do that when reviewing an issue -#11759 (comment)
I have asked copilot to fix this. Lets review the PR #11760 and get it merged.
Signed-off-by: Reshma Abdul Rahim <reshmarahim.abdul@microsoft.com>
|
|
||
| - Enable platform engineers to leverage community owned Terraform registry modules and Azure Verified Modules directly as Radius Recipes | ||
| - Eliminate the need to write a Recipe wrapper, publish, and maintain these Recipes separately from the underlying module | ||
| - Eliminate the need for Radius to maintain a catalog of wrapped Recipes in the `resource-types-contrib` repository thus reducing maintenance overhead and surface area for supply chain attacks |
There was a problem hiding this comment.
I'm not sure this approach reduces overhead for supply chain attacks
|
|
||
| ### Scenario 1: Terraform Registry Module | ||
|
|
||
| A platform engineer sets `location` to a Terraform registry path (e.g., `terraform-aws-modules/rds/aws`) and the system deploys it by automatically resolving developer set properties via `context` as Terraform input variables, and mapping module outputs to resource properties via the `outputs` field. |
There was a problem hiding this comment.
shouldnt this be a uri like terraform.io/aws/rds/aws?
There was a problem hiding this comment.
should there be a version here?
| recipes: { | ||
| 'Radius.Data/mySqlDatabases': { | ||
| kind: 'terraform' | ||
| location: 'terraform-aws-modules/rds/aws' |
There was a problem hiding this comment.
should we have a type field? reference
|
|
||
| ### Feature 1: Direct Module Execution | ||
|
|
||
| Use any standard Bicep or Terraform module directly as a recipe by pointing `location` at the module source. The system automatically detects that the module is not a Radius wrapper (no `context` variable), downloads it, and executes it through the existing driver — no wrapper needed. |
There was a problem hiding this comment.
is location overloaded? we use it for azure location too
|
|
||
| ### Feature 2: Template Expression Resolution | ||
|
|
||
| A `{{context.*}}` expression system that resolves Radius application runtime context values into recipe parameters at deploy time. Supports resource metadata, application/environment info, Kubernetes runtime, Azure, and AWS provider context. Includes single-level ternary expressions for conditional value mapping. |
There was a problem hiding this comment.
is there a scenario where the backing module will need information from context that can't be passed via a param?
|
|
||
| ### Feature 3: Output Mapping | ||
|
|
||
| An `outputs` field on `RecipeDefinition` that maps module output names to resource property names. Provides a stable property interface for resource consumers regardless of the underlying module's output naming. Sensitive outputs are automatically routed to the Secrets map. |
There was a problem hiding this comment.
could we add an example for the secrets?
| } | ||
| } | ||
| } | ||
| ``` |
| } | ||
| } | ||
| outputs: { | ||
| host: 'fqdn' |
There was a problem hiding this comment.
what are these outputs? why are they set?
| kind: 'terraform' | ||
| location: 'terraform-aws-modules/rds/aws' | ||
| parameters: { | ||
| identifier: '{{context.resource.name}}' |
There was a problem hiding this comment.
this might be a technical limitation here, I don't know how this will work
| recipes: { | ||
| 'Radius.Data/mySqlDatabases': { | ||
| kind: 'terraform' | ||
| location: 'terraform-aws-modules/rds/aws' |
There was a problem hiding this comment.
one thing to think about - I think currently aws terraform modules that specify an aws provider will fail because we try to set the provider ourselves. we need to fix this first
This pull request adds a comprehensive design note describing the "Direct Recipe Modules" feature for Radius. The document outlines how platform engineers can use standard Bicep or Terraform modules directly as Radius Recipes, removing the need for custom wrappers and reducing maintenance overhead. It covers user personas, challenges, desired outcomes, implementation details, and usage examples.
Key documentation additions:
Feature overview and motivation
locationfield of a recipe, eliminating the need for Radius-specific wrappers and simplifying adoption and maintenance.Core features and scenarios
Risks, dependencies, and limitations
User experience and implementation details
Please explain the changes you've made.
Type of change
Fixes: #issue_number
Contributor checklist
Please verify that the PR meets the following requirements, where applicable:
eng/design-notes/in this repository, if new APIs are being introduced.