Skip to content

Direct module support for Recipes#11876

Draft
Reshrahim wants to merge 3 commits into
radius-project:mainfrom
Reshrahim:001-direct-module-support
Draft

Direct module support for Recipes#11876
Reshrahim wants to merge 3 commits into
radius-project:mainfrom
Reshrahim:001-direct-module-support

Conversation

@Reshrahim
Copy link
Copy Markdown
Contributor

@Reshrahim Reshrahim commented May 13, 2026

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

  • Introduces support for referencing standard Bicep or Terraform modules directly in the location field of a recipe, eliminating the need for Radius-specific wrappers and simplifying adoption and maintenance.
  • Details the user personas (platform engineers, application developers), their challenges with the current system, and the positive outcomes enabled by this feature.

Core features and scenarios

  • Describes three main features: direct module execution, template expression resolution for parameters, and output mapping from module outputs to resource properties.
  • Provides real-world scenarios and usage examples for Terraform registry modules, Azure Verified Modules, Git-hosted modules, and environment-level parameter overrides.

Risks, dependencies, and limitations

  • Outlines key dependencies (such as module compatibility and API stability) and explicitly lists non-goals for the initial release.

User experience and implementation details

  • Walks through the desired user experience, including how parameters and outputs are mapped
    Please explain the changes you've made.

Type of change

  • This pull request fixes a bug in Radius and has an approved issue (issue link required).
  • This pull request adds or changes features of Radius and has an approved issue (issue link required).
  • This pull request is a minor refactor, code cleanup, test improvement, or other maintenance task and doesn't change the functionality of Radius (issue link optional).
  • This pull request is a design document and only includes files in the eng/design-notes directory.

Fixes: #issue_number

Contributor checklist

Please verify that the PR meets the following requirements, where applicable:

  • An overview of proposed schema changes is included in a linked GitHub issue.
    • Yes
    • Not applicable
  • A design document is added or updated under eng/design-notes/ in this repository, if new APIs are being introduced.
    • Yes
    • Not applicable
  • The design document has been reviewed and approved by Radius maintainers/approvers.
    • Yes
    • Not applicable
  • A PR for resource-types-contrib is created, if resource types or recipes are affected by the changes in this PR.
    • Yes
    • Not applicable
  • A PR for dashboard is created, if the Radius Dashboard is affected by the changes in this PR.
    • Yes
    • Not applicable
  • A PR for the documentation repository is created, if the changes in this PR affect the documentation or any user facing updates are made.
    • Yes
    • Not applicable

Reshrahim and others added 2 commits May 12, 2026 14:05
Signed-off-by: Reshma Abdul Rahim <61033581+Reshrahim@users.noreply.github.com>
@radius-functional-tests
Copy link
Copy Markdown

radius-functional-tests Bot commented May 13, 2026

Radius functional test overview

🔍 Go to test action run

Click here to see the test run details
Name Value
Repository Reshrahim/radius
Commit ref 534bd68
Unique ID func5e7c2189fb
Image tag pr-func5e7c2189fb
  • gotestsum 1.13.0
  • KinD: v0.29.0
  • Dapr: 1.14.4
  • Azure KeyVault CSI driver: 1.4.2
  • Azure Workload identity webhook: 1.3.0
  • Bicep recipe location ghcr.io/radius-project/dev/test/testrecipes/test-bicep-recipes/<name>:pr-func5e7c2189fb
  • Terraform recipe location http://tf-module-server.radius-test-tf-module-server.svc.cluster.local/<name>.zip (in cluster)
  • applications-rp test image location: ghcr.io/radius-project/dev/applications-rp:pr-func5e7c2189fb
  • dynamic-rp test image location: ghcr.io/radius-project/dev/dynamic-rp:pr-func5e7c2189fb
  • controller test image location: ghcr.io/radius-project/dev/controller:pr-func5e7c2189fb
  • ucp test image location: ghcr.io/radius-project/dev/ucpd:pr-func5e7c2189fb
  • deployment-engine test image location: ghcr.io/radius-project/deployment-engine:latest

Test Status

⌛ Building Radius and pushing container images for functional tests...
✅ Container images build succeeded
⌛ Publishing Bicep Recipes for functional tests...
✅ Recipe publishing succeeded
⌛ Starting ucp-cloud functional tests...
⌛ Starting corerp-cloud functional tests...
✅ ucp-cloud functional tests succeeded
✅ corerp-cloud functional tests succeeded

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.69%. Comparing base (251d7f3) to head (534bd68).
⚠️ Report is 27 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread specs/001-direct-module-support/spec.md Outdated
@@ -0,0 +1,207 @@
# Feature Specification: Direct Module Support
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread specs/001-direct-module-support/spec.md Outdated
Comment on lines +32 to +36
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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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*).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mention secrets output mapping like you did under Terraform?

Comment thread specs/001-direct-module-support/spec.md Outdated
Comment on lines +52 to +58
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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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*).

Comment thread specs/001-direct-module-support/spec.md Outdated
Comment on lines +72 to +74
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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is any of this new functionality?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context resolution at deploy time doesn't exists today

Comment thread specs/001-direct-module-support/spec.md Outdated
Comment on lines +77 to +84
### 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this covered in the extensibility world already ?

Comment thread specs/001-direct-module-support/spec.md Outdated
Comment on lines +92 to +98
### 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Radius does this today, no? If so, we can remove.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it does this correctly but can check whats missing.

Comment on lines +19 to +37
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'
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Copy Markdown
Contributor Author

@Reshrahim Reshrahim May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt this be a uri like terraform.io/aws/rds/aws?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a version here?

recipes: {
'Radius.Data/mySqlDatabases': {
kind: 'terraform'
location: 'terraform-aws-modules/rds/aws'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add an example for the secrets?

}
}
}
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice example

}
}
outputs: {
host: 'fqdn'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are these outputs? why are they set?

kind: 'terraform'
location: 'terraform-aws-modules/rds/aws'
parameters: {
identifier: '{{context.resource.name}}'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants