Tutorial: Add a custom resource type
Categories:
Overview
Radius includes several built-in resource types which developers can use to build applications. These include core resource types such as Containers, Gateways, and Secrets. You can also create your own custom resource types. This tutorial guides you through creating a PostgreSQL resource and deploying the sample Todo List application with PostgreSQL.

Prerequisites
This tutorial assumes you have completed the Create a new application tutorial and have Radius installed and the demo application deployed.
Additionally, you will need a location to store your Recipe:
-
Terraform configurations must be stored in a Git repository. Ideally for this tutorial the Git repository has anonymous access. If not, you will need to configure Git authentication.
-
Bicep templates must be stored in an OCI registry. As with Git, you must have anonymous access to the registry or configure authentication.
Finally, Node.js must be installed on the workstation to generate the Bicep extension to deploy the new resource type.
Step 1: Create a PostgreSQL resource type in Radius
To create a PostgreSQL resource type in Radius, first create the resource type definition then add the resource type to Radius.
-
Create a new file called
types.yaml
and add the following:name: Radius.Resources types: postgreSQL: capabilities: ["SupportsRecipes"] apiVersions: '2023-10-01-preview': schema: type: object properties: environment: type: string application: type: string size: type: string description: The size of the PostgreSQL database database: type: string description: The name of the database. readOnly: true host: type: string description: The host name of the database. readOnly: true port: type: string description: The port number of the database. readOnly: true username: type: string description: The username for the database. readOnly: true password: type: string description: The password for the database. readOnly: true
The PostgreSQL resource type definition includes:
name
: The namespace of the resource type, as a conventionRadius.Resources
is recommended but any name in the formPrimaryName.SecondaryName
can be usedtypes
: The resource type namecapabilities
: This specifies features of the resource type. The only available option isSupportsRecipes
which indicates that the resource type can be deployed via a Recipe.apiVersions
: The version of the schema defined belowschema
: The OpenAPI v3 schema which defines the properties of the resource typeenvironment
: The Radius environment ID which the resource is deployed to, this property is set by the Radius CLI when the resource is deployedapplication
: The application ID which the resource belongs tosize
: The size of the PostgreSQL databasehost
: The hostname of the database serverport
: The port of the database serverusername
: The usernamepassword
: The password
The
host
,port
,username
, andpassword
properties are read-only properties set by Recipe. -
Create the resource type using the rad resource-type command:
rad resource-type create postgreSQL -f types.yaml
$ rad resource-type create postgreSQL -f types.yaml Resource provider "Radius.Resources" not found. Creating resource provider Radius.Resources at location global Creating resource type Radius.Resources/postgreSQL Creating API Version Radius.Resources/postgreSQL@2023-10-01-preview Creating location Radius.Resources/global/
Step 2: Create a Bicep extension
The rad resource-type create
command created the resource type in the Radius control plane. The next step is to create a Bicep extension which will be used by the Radius CLI and VS Code (if you have the Bicep VS Code extension installed).
Warning
This step is required even if you use Terraform-based Recipes to deploy the PostgreSQL resource type as part of the application.-
Generate the Bicep extension using the rad bicep publish-extension command.
rad bicep publish-extension -f types.yaml --target radiusResources.tgz
$ rad bicep publish-extension -f types.yaml --target radiusResources.tgz Writing types to /var/folders/w8/89pqzjp52pbg4g256z9cpkww0000gn/T/bicep-extension-2214011863/types.json Writing index to /var/folders/w8/89pqzjp52pbg4g256z9cpkww0000gn/T/bicep-extension-2214011863/index.json Writing documentation to /var/folders/w8/89pqzjp52pbg4g256z9cpkww0000gn/T/bicep-extension-2214011863/index.md WARNING: The 'publish-extension' CLI command group is an experimental feature. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking. Successfully published Bicep extension "types.yaml" to "radiusResources.tgz"
-
Open the
bicepconfig.json
file and modify the contents.{ "experimentalFeaturesEnabled": { "extensibility": true }, "extensions": { "radius": "br:biceptypes.azurecr.io/radius:latest", - "aws": "br:biceptypes.azurecr.io/aws:latest" + "aws": "br:biceptypes.azurecr.io/aws:latest", + "radiusResources": "radiusResources.tgz" } }
The final file should be:
{ "experimentalFeaturesEnabled": { "extensibility": true }, "extensions": { "radius": "br:biceptypes.azurecr.io/radius:latest", "aws": "br:biceptypes.azurecr.io/aws:latest", "radiusResources": "radiusResources.tgz" } }
Now, any Bicep template with
extension radiusResources
will reference theradiusResources.tgz
file for details about the PostgreSQL resource type.
Step 3: Create a Recipe for the PostgreSQL resource type
Recipes define how resource are deployed. Recipes can be either Terraform configurations or Bicep templates. Once the Terraform configuration or Bicep template has been published in a Git repo or OCI registry, it can be registered as a recipe in a Radius environment.
Terraform configurations must be stored in a Git repository accessible by Radius. As discussed in the prerequisites, using a Git repository with anonymous access is easiest for this tutorial, otherwise you will need to configure Git authentication. Learn more about Recipes in this How-to guide.
-
Create a new directory in your Git repository for the PostgreSQL Terraform module then create the
main.tf
file and add the following:terraform { required_providers { kubernetes = { source = "hashicorp/kubernetes" version = ">= 2.0" } } } variable "context" { description = "This variable contains Radius recipe context." type = any } variable "memory" { description = "Memory limits for the PostgreSQL container" type = map(object({ memoryRequest = string memoryLimit = string })) default = { S = { memoryRequest = "512Mi" memoryLimit = "1024Mi" }, M = { memoryRequest = "1Gi" memoryLimit = "2Gi" } } } locals { uniqueName = var.context.resource.name port = 5432 namespace = var.context.runtime.kubernetes.namespace } resource "random_password" "password" { length = 16 } resource "kubernetes_deployment" "postgresql" { metadata { name = local.uniqueName namespace = local.namespace } spec { selector { match_labels = { app = "postgres" } } template { metadata { labels = { app = "postgres" } } spec { container { image = "postgres:16-alpine" name = "postgres" resources { requests = { memory = var.memory[var.context.resource.properties.size].memoryRequest } limits = { memory= var.memory[var.context.resource.properties.size].memoryLimit } } env { name = "POSTGRES_PASSWORD" value = random_password.password.result } env { name = "POSTGRES_USER" value = "postgres" } env { name = "POSTGRES_DB" value = "postgres_db" } port { container_port = local.port } } } } } } resource "kubernetes_service" "postgres" { metadata { name = local.uniqueName namespace = local.namespace } spec { selector = { app = "postgres" } port { port = local.port target_port = local.port } } } output "result" { value = { values = { host = "${kubernetes_service.postgres.metadata[0].name}.${kubernetes_service.postgres.metadata[0].namespace}.svc.cluster.local" port = local.port database = "postgres_db" username = "postgres" password = random_password.password.result } } }
-
Register the Terraform configuration as a Recipe called
default
. Since Recipes are registered with Environments, use thedefault
environment created in the previous tutorial.rad recipe register default \ --environment default \ --resource-type Radius.Resources/postgreSQL \ --template-kind terraform \ --template-path git::<git-server-name>/<repository-name>.git//<directory>/<subdirectory>
For example, if the
main.tf
file is in a GitHub repository namedrecipes
in a directory called/kubernetes/postgresql
, the command would look like this:--template-path git::https://github.com/<github-user-name>/recipes.git//kubernetes/postgresql
The output will be:
Successfully linked recipe "default" to environment "default"
-
Verify the Recipe is registered using the
rad recipe list
command.rad recipe list
$ rad recipe list RECIPE TYPE TEMPLATE KIND TEMPLATE VERSION TEMPLATE ... default Radius.Resources/postgreSQL terraform git::https://github.com/<github-user-name>/recipes.git//kubernetes/postgres
Bicep templates must be stored in an OCI registry accessible by Radius. As discussed in the prerequisites, using an OCI registry with anonymous access is easiest for this tutorial, otherwise you will need to configure authentication. Learn more about Recipes in this How-to guide.
-
Create a new file called
postgresql.bicep
and add the following:@description('Information about what resource is calling this Recipe. Generated by Radius.') param context object @description('Name of the PostgreSQL database. Defaults to the name of the Radius resource.') param database string = context.resource.name @description('PostgreSQL username') param user string = 'postgres' @description('PostgreSQL password') @secure() #disable-next-line secure-parameter-default param password string = uniqueString(context.resource.id) @description('Tag to pull for the postgres container image.') param tag string = '16-alpine' @description('Memory limits for the PostgreSQL container') var memory ={ S: { memoryRequest: '512Mi' memoryLimit: '1024Mi' } M: { memoryRequest: '1Gi' memoryLimit: '2Gi' } } extension kubernetes with { kubeConfig: '' namespace: context.runtime.kubernetes.namespace } as kubernetes var uniqueName = 'postgres-${uniqueString(context.resource.id)}' var port = 5432 // Based on https://hub.docker.com/_/postgres/ resource postgresql 'apps/Deployment@v1' = { metadata: { name: uniqueName } spec: { selector: { matchLabels: { app: 'postgresql' resource: context.resource.name } } template: { metadata: { labels: { app: 'postgresql' resource: context.resource.name // Label pods with the application name so `rad run` can find the logs. 'radapp.io/application': context.application == null ? '' : context.application.name } } spec: { containers: [ { // This container is the running postgresql instance. name: 'postgres' image: 'postgres:${tag}' ports: [ { containerPort: port } ] resources: { requests: { memory: memory[context.resource.properties.size].memoryRequest } limits: { memory: memory[context.resource.properties.size].memoryLimit } } env: [ { name: 'POSTGRES_USER' value: user } { name: 'POSTGRES_PASSWORD' value: password } { name: 'POSTGRES_DB' value: database } ] } ] } } } } resource svc 'core/Service@v1' = { metadata: { name: uniqueName labels: { name: uniqueName } } spec: { type: 'ClusterIP' selector: { app: 'postgresql' resource: context.resource.name } ports: [ { port: port } ] } } output result object = { resources: [ '/planes/kubernetes/local/namespaces/${svc.metadata.namespace}/providers/core/Service/${svc.metadata.name}' '/planes/kubernetes/local/namespaces/${postgresql.metadata.namespace}/providers/apps/Deployment/${postgresql.metadata.name}' ] values: { host: '${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local' port: port database: database username: user } secrets: { #disable-next-line outputs-should-not-contain-secrets password: password } }
-
Publish the Recipe to the OCI registry. Make sure to replace
host
andregistry
with your container registry.rad bicep publish --file postgresql.bicep --target br:<host>/<registry>/postgresql:latest
Successfully published Bicep file "postgresql.bicep" to "<host>/<registry>/postgresql:latest"
-
Register the Bicep template as a Recipe called
default
. Since Recipes are registered with Environments, use thedefault
environment created in the previous tutorial.rad recipe register default --environment default \ --resource-type Radius.Resources/postgreSQL \ --template-kind bicep \ --template-path <host>/<registry>/postgresql:latest
Successfully linked recipe "default" to environment "default"
-
Verify the Recipe is registered using the
rad recipe list
command. You should see output similar to:rad recipe list
$ rad recipe list RECIPE TYPE TEMPLATE KIND TEMPLATE VERSION TEMPLATE ... default Radius.Resources/postgreSQL bicep <host>/<repository>/postgresql:latest
Step 4: Replace MongoDB with PostgreSQL
-
Edit the
app.bicep
file from the previous tutorial and add theradiusResources
extension at the top of the file.extension radius + extension radiusResources
-
Remove the MongoDB resource and replace it with a PostgreSQL resource.
- resource mongodb 'Applications.Datastores/mongoDatabases@2023-10-01-preview' = { - name: 'mongodb' - properties: { - environment: environment - application: application - } - } + resource postgresql 'Radius.Resources/postgreSQL@2023-10-01-preview' = { + name: 'postgresql' + properties: { + environment: environment + application: application + size: 'S' + } + }
-
Modify the
demo
container to use the PostgreSQL. Because PostgreSQL is a custom resource type, the environment variables must be manually specified.resource demo 'Applications.Core/containers@2023-10-01-preview' = { name: 'demo' properties: { application: application container: { image: 'ghcr.io/radius-project/samples/demo:latest' ports: { web: { containerPort: 3000 } } + env: { + CONNECTION_POSTGRES_HOST: { + value: postgresql.properties.host + } + CONNECTION_POSTGRES_PORT: { + value: string(postgresql.properties.port) + } + CONNECTION_POSTGRES_USERNAME: { + value: postgresql.properties.username + } + CONNECTION_POSTGRES_DATABASE: { + value: postgresql.properties.database + } + //This is stored and passed as cleartext for demo purposes. In production, use a secret store. + CONNECTION_POSTGRES_PASSWORD: { + value: postgresql.properties.password + } + } } connections: { - mongodb: { - source: mongodb.id - } + postgresql: { + source: postgresql.id + } backend: { source: 'http://backend:80' } } } }
Caution
In this example the POSTGRESQL_PASSWORD is stored as a cleartext property for demo purposes. In production environments, always use secrets to store and reference sensitive information like passwords.
Step 5: Run the application
Run the application using rad run
. The rad run
command sets up port forwarding to the application. .
rad deploy app.bicep
$ rad deploy app.bicep
Building app.bicep...
WARNING: The following experimental Bicep features have been enabled: Extensibility. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.
Deploying template 'app.bicep' for application 'todolist' and environment '/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/default' from workspace 'default'...
Deployment In Progress...
Completed postgresql Radius.Resources/postgreSQL
Completed backend Applications.Core/containers
Completed gateway Applications.Core/gateways
Completed demo Applications.Core/containers
Deployment Complete
Resources:
backend Applications.Core/containers
demo Applications.Core/containers
gateway Applications.Core/gateways
postgresql Radius.Resources/postgreSQL
Public Endpoints:
gateway Applications.Core/gateways http://gateway.todolist.172.18.0.6.nip.io
Open the gateway URL in your browser. The Radius Connections section now has PostgreSQL details and MongoDB is no longer there.

Feedback
Was this page helpful?
Glad to hear it! Please feel free to star our repo and join our Discord server to stay up to date with the project.
Sorry to hear that. If you would like to also contribute a suggestion visit and tell us how we can improve.