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!