Skip to main content

Kubernetes provider for Bicep together with user-defined types

·1004 words·5 mins
Azure Bicep Kubernetes

In the latest release of Azure Bicep user-defined functions was introduced. That is an excellent news, but I will not be covering that in this post. I want to introduce the idea of modules being very much like user-defined functions. In the past I have done crazy things like solving Advent of Code problems using Azure Bicep, and the most important thing I learned from that adventure was that a module is the closest thing to a function that Bicep has. This will of course change with the introduction of user-defined functions.

In this short post I want to use a Bicep module to deploy Deployment-objects in a Kubernetes cluster. So I will be using two experimental features of Bicep: the Kubernetes provider and user-defined types. I will create a Bicep module for Kubernetes deployments, and illustrate how that module can work like a function that generates deployments for you.

Bicep configuration
#

To be able to work with Kubernetes resources in Azure, you need to set the experimentalFeaturesEnabled.extensibility to true in bicepconfig.json. You will also need to set experimentalFeaturesEnabled.userDefinedTypes to true to be able to use user-defined types. Your bicepconfig.json should look like this:

{
    "experimentalFeaturesEnabled": {
        "extensibility": true,
        "userDefinedTypes": true
    }
}

Now we are ready to write some Bicep!

Writing the deployment module
#

When I write a module in Bicep I usually create a modules directory and put my modules there. This is true this time as well. In my modules directory I create a file called deployment.bicep where I will define my Kubernetes deployment module.

To start off I will create a user-defined type for my Kubernetes deployments. A deployment in Kubernetes can be complex, so to keep the complexity down a bit I will only require the deployment to have a name and an image. My custom type looks like this:

@description('Custom type for a Kubernetes deployment')
@sealed()
type deploymentConfigType = {
  name: string
  image: string
}

I use the @sealed() decorator to specify that I can’t provide additional fields to parameters using this type, I will specifically require the name and the image fields. If you use this example as a starting-point for your own work then this user-defined type needs to be extended to include additional fields.

Next up I define three parameters for my module:

@description('kubeconfig to authenticate to the cluster')
param kubeConfig string

@description('Kubernetes namespace for the deployments')
param namespace string

@description('Configurations for the deployments')
param deployments deploymentConfigType[]

The kubeConfig parameter is used to authenticate to the Kubernetes cluster I want to create the deployments in. The namespace parameter is the Kubernetes namespace where the deployments should live, this is required for the configuration of the Kubernetes provider (see below). The last parameter is deployments which is a list of my user-defined type deploymentConfigType. This list will contain the definitions of all the Kubernetes deployments I want to create.

Next I need to import the Kubernetes provider:

import 'kubernetes@1.0.0' with {
  kubeConfig: kubeConfig
  namespace: namespace
}

Here I used the kubeConfig and namespace parameters. When I have imported the Kubernetes provider I can start creating Kubernetes resources just like I would create Azure resources! So, the last step in this module is to do just that. I create my deployment objects in Kubernetes like so:

resource deploy 'apps/Deployment@v1' = [for deployment in deployments: {
  metadata: {
    name: deployment.name
  }
  spec: {
    selector: {
      matchLabels: {
        app: deployment.name
      }
    }
    template: {
      metadata: {
        name: deployment.name
        labels: {
          app: deployment.name
        }
      }
      spec: {
        containers: [
          {
            name: 'main'
            image: deployment.image
          }
        ]
      }
    }
  }
}]

If you are familiar with Kubernetes deployments then this will look very familiar. It is simply a Kubernetes deployment in Bicep code. As I mentioned above, it is a simplified deployment object where I only specify a few things. To summarize, the deployment.bicep module should look like the following:

@description('Custom type for a Kubernetes deployment')
@sealed()
type deploymentConfigType = {
  name: string
  image: string
}

@description('kubeconfig to authenticate to the cluster')
param kubeConfig string

@description('Kubernetes namespace for the deployments')
param namespace string

@description('Configurations for the deployments')
param deployments deploymentConfigType[]

import 'kubernetes@1.0.0' with {
  kubeConfig: kubeConfig
  namespace: namespace
}

resource deploy 'apps/Deployment@v1' = [for deployment in deployments: {
  metadata: {
    name: deployment.name
  }
  spec: {
    selector: {
      matchLabels: {
        app: deployment.name
      }
    }
    template: {
      metadata: {
        name: deployment.name
        labels: {
          app: deployment.name
        }
      }
      spec: {
        containers: [
          {
            name: 'main'
            image: deployment.image
          }
        ]
      }
    }
  }
}]

Using the module
#

Now it is time to use the module! I create a main.bicep file with the following content:

@description('kubeconfig to authenticate to the cluster')
param kubeConfig string

@description('Kubernetes namespace for the deployments')
param namespace string

var deployments = [
  {
    name: 'nginx1'
    image: 'nginx:1.23.4'
  }
  {
    name: 'nginx2'
    image: 'nginx:1.24.0'
  }
]

module deploymentModule 'modules/deployment.bicep' = {
  name: 'deployment-module'
  params: {
    deployments: deployments
    kubeConfig: kubeConfig
    namespace: namespace
  }
}

I have the kubeConfig and namespace parameters repeated here, so I expect them to be sent into the deployment command. I define a variable deployments which is an array of deployment objects. Each object follows the user-defined type I have in my deployment.bicep file. Note that as of now (May 2023) you can’t put your user-defined types in a separate file and import them to where you need them. This is why I keep the user-defined type in deployment.bicep only, for now. If you use the Bicep extension in VS Code you will get a warning if you forget a certain property, or if you add a property the type does not contain.

In this case I created two Kubernetes deployments, with two different versions of nginx. This was so simplify my own life a bit, because the nginx container will start up without any need for a command. This allowed me to keep the user-defined type to a minimum, in order to not get bogged down in details about containers!

Now you are ready to run a Bicep deployment to create your Kubernetes deployments, good luck!

Mattias Fjellström
Author
Mattias Fjellström
Cloud architect consultant and an HashiCorp Ambassador