How-To: Author a Radius Recipe

Learn how to author and register a custom Recipe template to automate infrastructure provisioning

Prerequisites

Before you get started, you’ll need to make sure you have the following tools and resources:

Step 1: Author a Recipe template

Begin by creating your Bicep or Terraform template. Ensure that the resource name(s) are unique and repeatable, so they don’t overlap when used by multiple applications.

To make naming easy, a context parameter is automatically injected into your template. It contains metadata about the backing app/environment which you can use to generate unique and repeatable names.


Create app.bicep and use the context parameter for naming and namespace configuration:

@description('Radius-provided object containing information about the resource calling the Recipe')
param context object

// Import Kubernetes resources into Bicep
import kubernetes as kubernetes {
  kubeConfig: ''
  namespace: context.runtime.kubernetes.namespace
}

resource redis 'apps/Deployment@v1' = {
  metadata: {
    // Ensure the resource name is unique and repeatable
    name: 'redis-${uniqueString(context.resource.id)}'
  }
  spec: {
    selector: {
      matchLabels: {
        app: 'redis'
        resource: context.resource.name
      }
    }
    template: {
      metadata: {
        labels: {
          app: 'redis'
          resource: context.resource.name
        }
      }
      spec: {
        containers: [
          {
            name: 'redis'
            image: 'redis:6'
            ports: [
              {
                containerPort: 6379
              }
            ]

          }
        ]
      }
    }
  }
}

resource svc 'core/Service@v1' = {
  metadata: {
    name: 'redis-${uniqueString(context.resource.id)}'
  }
  spec: {
    type: 'ClusterIP'
    selector: {
      app: 'redis'
      resource: context.resource.name
    }
    ports: [
      {
        port: port
        targetPort: '6379'
      }
    ]
  }
}

Add the context parameter to your variable.tf file:

variable "context" {
  description = "Radius-provided object containing information about the resource calling the Recipe."
  type = any
}

Ensure that your main.tf has:

  • Defined required_providers for any providers you leverage in your module. This allows Radius to inject configuration and credentials.
  • Utilizes the context parameter for naming and namespace configuration. This ensures your resources don’t unintentionally collide with other uses of the Recipe.
terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = ">= 2.0"
    }
  }
}

resource "kubernetes_deployment" "redis" {
  metadata {
    name = "redis-${sha512(var.context.resource.id)}"
    namespace = var.context.runtime.kubernetes.namespace
    labels = {
      app = "redis"
    }
  }
  spec {
    selector {
      match_labels = {
        app = "redis"
        resource = var.context.resource.name
      }
    }
    template {
      metadata {
        labels = {
          app = "redis"
          resource = var.context.resource.name
        }
      }
      spec {
        container {
          name  = "redis"
          image = "redis:6"
          port {
            container_port = 6379
          }
        }
      }
    }
  }
}

resource "kubernetes_service" "redis" {
  metadata {
    name = "redis-${sha512(var.context.resource.id)}"
    namespace = var.context.runtime.kubernetes.namespace
  }
  spec {
    type = "ClusterIP"
    selector = {
      app = "redis"
      resource = var.context.resource.name
    }
    port {
      port        = var.port
      target_port = "6379"
    }
  }
}

Step 2: Add parameters/variables for Recipe customization (optional)

You can optionally add parameters/variables to your Recipe to allow developers and/or operators to customize the Recipe for an application/environment. Parameters can be set both when a Recipe is added to an environment by an operator, or by a developer when the Recipe is called.


You can create any parameter type supported by Bicep:

@description('The port Redis is offered on. Defaults to 6379.')
param port int = 6379

You can create any variable type supported by Terraform:

variable "port" {
  description = "The port Redis is offered on. Defaults to 6379."
  type = number
  default = 6379
}

Step 3: Output the result

Once you have defined your IaC template you will need to output a result object with the values, secrets, and resources that the target resource requires. This is how Radius “wires up” the Recipe.

The result object must include:

  • values: The fields that the target resource requires. (username, host, port, etc.)
  • secrets: The fields that the target resource requires, but should be treated as secrets. (password, connectionString, etc.)
  • resources: The UCP ID(s) of the resources providing the backing service. Used by UCP to track dependencies and manage deletion.

Resources are populated automatically for Bicep Recipes for any new Azure or AWS resource (Kubernetes coming soon). For now, make sure to include the UCP ID of any Kubernetes resources your Recipe creates.

@description('The result of the Recipe. Must match the target resource\'s schema.')
output result object = {
  values: {
    host: '${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local'
    port: svc.spec.ports[0].port
    username: ''
  }
  secrets: {
    // Temporarily disable linter until secret outputs are added
    #disable-next-line outputs-should-not-contain-secrets
    password: ''
  }
  // UCP IDs for the above Kubernetes resources
  resources: [
    '/planes/kubernetes/local/namespaces/${svc.metadata.namespace}/providers/core/Service/${svc.metadata.name}'
    '/planes/kubernetes/local/namespaces/${redis.metadata.namespace}/providers/apps/Deployment/${redis.metadata.name}'
  ]
}

output "result" {
  value = {
    values = {
      host = "${kubernetes_service.metadata.name}.${kubernetes_service.metadata.namespace}.svc.cluster.local"
      port = kubernetes_service.spec.port[0].port
      username = ""
    }
    secrets = {
      password = ""
    }
    // UCP resource IDs
    resources = [
        "/planes/kubernetes/local/namespaces/${kubernetes_service.metadata.namespace}/providers/core/Service/${kubernetes_service.metadata.name}",
        "/planes/kubernetes/local/namespaces/${kubernetes_deployment.metadata.namespace}/providers/apps/Deployment/${kubernetes_deployment.metadata.name}"
    ]
  }
  description = "The result of the Recipe. Must match the target resource's schema."
  sensitive = true
}

Step 4: Store your template


Recipes leverage Bicep registries for template storage. Once you’ve authored a Recipe, you can publish it to your preferred OCI-compliant registry with rad bicep publish:

rad bicep publish --file myrecipe.bicep --target br:ghcr.io/USERNAME/recipes/myrecipe:1.1.0

Follow the Terraform module publishing docs to setup and publish a Terraform module to a Terraform registry.

Step 5: Register your Recipe with your environment

Now that your Recipe template has been stored, you can add it your Radius Environment to be used by developers. This allows you to mix-and-match templates for each of your environments such as dev, canary, and prod.


rad recipe register myrecipe --environment myenv --resource-type Applications.Datastores/redisCaches --template-kind bicep --template-path ghcr.io/USERNAME/recipes/myrecipe:1.1.0

The template path value should represent the source path found in your Terraform module registry.

rad recipe register myrecipe --environment myenv --resource-type Applications.Datastores/redisCaches --template-kind terraform --template-path user/recipes/myrecipe --template-version "1.1.0"

import radius as radius

resource env 'Applications.Core/environments@2023-10-01-preview' = {
  name: 'prod'
  properties: {
    compute: {
      kind: 'kubernetes'
      resourceId: 'self'
      namespace: 'default'
    }
    recipes: {
      'Applications.Datastores/redisCaches':{
        'redis-bicep': {
          templateKind: 'bicep'
          templatePath: 'https://ghcr.io/USERNAME/recipes/myrecipe:1.1.0'
          // Optionally set parameters for all resources calling this Recipe
          parameters: {
            port: 3000
          }
        }
        'redis-terraform': {
          templateKind: 'terraform'
          templatePath: 'user/recipes/myrecipe'
          templateVersion: '1.1.0'
          // Optionally set parameters for all resources calling this Recipe
          parameters: {
            port: 3000
          }
        }
      }
    }
  }
}

Step 6 : Use the custom recipe in your application

You can now use your custom Recipe with its accompanying resource in your application.

Note that if your Recipe is registered with the name “default”, you do not need to provide a Recipe name in your application, as it will automatically pick up the default Recipe.

resource redis 'Applications.Datastores/redisCaches@2023-10-01-preview'= {
  name: 'myresource'
  properties: {
    environment: environment
    application: application
    recipe: {
      name: 'myrecipe'
    }
  }
}

Further reading