Kubernetes provider for Bicep together with user-defined types
Tags: azure, bicep, kubernetes
Published: 2023-05-23 18:34:42

In the [latest release](https://github.com/Azure/bicep/releases/tag/v0.17.1) 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](https://mattias.engineer/posts/advent-of-code-2022/), 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:

```json
{
    "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:

```bicep
@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:

```bicep
@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:

```bicep
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:

```bicep
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:

```bicep
@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:

```bicep
@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!