Tutorial: Add a custom resource type

Learn how to define and deploy a resource type in your Radius application

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.

  1. 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 convention Radius.Resources is recommended but any name in the form PrimaryName.SecondaryName can be used
    • types: The resource type name
    • capabilities: This specifies features of the resource type. The only available option is SupportsRecipes which indicates that the resource type can be deployed via a Recipe.
    • apiVersions: The version of the schema defined below
    • schema: The OpenAPI v3 schema which defines the properties of the resource type
      • environment: The Radius environment ID which the resource is deployed to, this property is set by the Radius CLI when the resource is deployed
      • application: The application ID which the resource belongs to
      • size: The size of the PostgreSQL database
      • host: The hostname of the database server
      • port: The port of the database server
      • username: The username
      • password: The password

    The host, port, username, and password properties are read-only properties set by Recipe.

  2. 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).

  1. 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"
    
  2. 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 the radiusResources.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.

  1. 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
        }
      }
    }
    
  2. Register the Terraform configuration as a Recipe called default. Since Recipes are registered with Environments, use the default 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 named recipes 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"
    
  3. 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.

  1. 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
      } 
    }
    
  2. Publish the Recipe to the OCI registry. Make sure to replace host and registry 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"
    
  3. Register the Bicep template as a Recipe called default. Since Recipes are registered with Environments, use the default 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"
    
  4. 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

  1. Edit the app.bicep file from the previous tutorial and add the radiusResources extension at the top of the file.

    extension radius
    + extension radiusResources
    
  2. 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'
    +   }
    + }
    
  3. 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'
          }
        }
      }
    }
    

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.



Next step: Create a composite Recipe →