Skip to content

Create Azure Kubernetes Service using Terraform

Introduction

In today’s article we will create managed an Azure Kubernetes Service using Terraform.

For my example’s today, I am running in Windows 10 OS and I use PowerShell for scripting. You can follow the same overall steps in Linux OS as well.

Before starting dipping into details, it’s is important to have the following items :

  • An Azure subscription! If you do not yet have an Azure account you can create an account with a few clicks (https://azure.microsoft.com). Azure is giving away a month’s trial with a $ 200 credit.
  • Azure CLI, as we are going to make some scripts to create some resources.
  • Terraform CLI to execute our automation scripts.
  • VS Code or any other text editor.

NOTE : For Windows users, I recommend using the Package manager “Chocolatey” to setup the CLI. As the process of installation is simple. Just copy paste the commands available for each package on your PowerShell terminal.

Terraform CLI : https://community.chocolatey.org/packages/terraform
Azure CLI : https://community.chocolatey.org/packages/azure-cli

Let’s start !😊

Create Terraform Backend in Azure to handle state deployments

Before creating our Azure Kubernetes Service (AKS) through Terraform, It is mandatory to understand the state management in Terraform.

Terraform used what we call the state in order to map what is really created in your infrastructure vs the configuration files you have. That’s to avoid creating or destructing some existing resources or to improve performance when it comes to large infrastructure.

So once you apply multiple deployments based on the same config files, Terraform will use the state to check the changes between your code and the infrastructure and apply only the new changes. This is what we call an idempotent deployment.

By default, Terraform will store this state in your local machine, but it is not a good practice as it will not suits most scenarios where multiple persons should work on the same project, or you have a CI / CD pipeline that handles your automation.

So, Terraform has a feature called backend that will be used to store our state remotely. For our example we will use Azure Blob Storage to store our state.

Let’s first create our blob storage that will store the state.

Open a terminal shell and execute the command below to connect to Azure. A login page will prompt you to enter your credentials:

az login

NOTE : If you have multiple subscriptions, please make sure that the subscription that you would like to work on has the flag “isDefault”: true. If it is not the case execute the following command :
az account set –subscription <id>, where <id> is the subscription id.

Create the Azure resource group

 $rg_name = "terraform-backend-rg"
 $location = "westeurope"
 az group create --location $location --name $rg_name

You should have something like this if the creation is successful :

Create the Azure storage account :

 $st_name ="terraformbackendst001"
 az storage account create --name $st_name `
 --resource-group $rg_name `
 --location $location `
 --sku Standard_LRS `
 --kind StorageV2

NOTE: The storage name must only contains letters and numbers.For more information about Azure storage, SKU and the different options, visit the link below : https://docs.microsoft.com/en-us/azure/storage/

Create the blob container in the file storage using the commands below :

 $container_name = "tf-state"
 $account_key = az storage account keys list -g $rg_name -n $st_name | ConvertFrom-Json
 az storage container create --name $container_name --public-access container --account-name $st_name --account-key $account_key[0].value

If everything is done correctly we should have something like this in the CLI response :

And in the Azure portal, something like this :

Init Terraform Backend

Now we have our backend ready for our state, we should initialize it.

To do so, we will need to create some files inside a new folder named : terraform-aks

Using VS code, create these new files inside the folder terraform-aks

  • terraform.tf : Contains the configuration related to the backend and the required providers
  • provider.tf : Contains the configuration related to the providers. For our example, we will need only the subscription id used for our deployments.

NOTE : It is not mandatory to name these files as described above, but you still need to have the file extension .tf in the end of the file names.

File terraform.tf :

 terraform {
  
   required_version = ">= 0.13"
   
   required_providers {
     azurerm = {
       version = "2.49.0"
     }
   }
  
      backend "azurerm" {
         resource_group_name  = "terraform-backend-rg"
         storage_account_name = "terraformbackendst001"
         container_name       = "tf-state"
         key                  = "tfstate.infrastructure"
         subscription_id      = "xxxxxxx-xxxxx-xxx-xxxx-xxxxxxxxxxxx"
     }
 }

File provider.tf :

 # Provider name
 provider "azurerm" {
     features {}
     subscription_id      = "xxxxxxx-xxxxx-xxx-xxxx-xxxxxxxxxxxx"
 }

NOTE : Please note that you have to change the subscription id accordingly. You can have the subscription id either from the Azure portal or from the Azure CLI.

In a PowerShell command line, open the folder terraform-aks and execute the following commands :

 terraform init 

After the execution of the command, you will see something like this :

NOTE : If you have an error like below stating something like :Error building ARM Config: obtain subscription(xxx-xxxxxx-xxxxxxxxx-xxxxxx-xxxxxx) from Azure CLI: parsing json result from the Azure CLI: waiting for the Azure CLITry to login again to Azure using the command : az login

You can now check the state generated in the container tf-state using Azure porta

Create resource group using Terraform

In this section we will create the resource group that will host our Azure Kubernetes Service using Terraform.

For this matter, we will create a terraform module that handles resource group creation.

Inside the root folder terraform-aks, create a new folder named : modules.

Inside the folder modules, create a new folder named az-rg that will contain the module that will manage the creation of our resource groups.

According to terraform documentation (https://www.terraform.io/language/modules/develop ), each module should consists of three files :

  • Input variables to accept values from the calling module.
  • Output values to return results to the calling module, which it can then use to populate arguments elsewhere.
  • Resources to define one or more infrastructure objects that the module will manage.

In our case, we will be creating these files inside our module az-rg  :

  • az-rg-main.tf
  • az-rg-outputs.tf
  • az-rg-variables.tf

File az-rg-main.tf

 #######################################################
 #           Ressource group
 #######################################################
  
 resource "random_string" "random" {
   length  = 5
   special = false
   upper   = false
 }
 resource "azurerm_resource_group" "rg" { 
   name     = "${var.name}-${random_string.random.result}"
   location = var.location
   tags     = var.tags
 }

File az-rg-variables.tf

 variable "name" {
   type        = string
   description = "This variable defines the name of the resource"
 }
  
 variable "location" {
   type        = string
   description = "This variable defines the location of the resource"
 }
  
 variable "tags" {
   type = object({
     project     = string
     location    = string
     CreatedBy  = string
   })
   description = "This variable defines the resources tags"
 }

File az-rg-outputs.tf

 output "az_rg_id" {
   value = azurerm_resource_group.rg.id
 }
 output "az_rg_name" {
   value = azurerm_resource_group.rg.name
 }

Now, we will create a file named locals.tf in the root of our project.

This file contains some variables that can be reused across all our project. It is not mandatary, but I highly recommend to use such file if you want to keep your code clean and maintainable over time.

NOTE : For more information about local files, you can check the link below :https://www.terraform.io/language/values/locals

Our local file should look something like :

 locals {
  
 #################################################
 #           Project information 
 #################################################
     company                     = "toto"
     code_project                = "test001"
     application_name            = "terraformaks"
  
     resource_prefix             = "${lower(local.company)}-${lower(local.code_project)}-${lower(local.application_name)}"
     resource_prefix_attached    = "${lower(local.company)}${lower(local.code_project)}${lower(local.application_name)}"
  
      
 #################################################
 #           Global variables
 #################################################
     
     location                    = "westeurope"
     azure_subscription_id       = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
     
 #################################################
 #           Global tags
 #################################################
   tags = {
     project    = local.application_name
     location   = local.location
     CreatedBy = "Terraform"
   }
 }

The file contains variables that can be reused across different modules such as location and name prefixes.

For now, we have our module and local file ready, we will create the file that will trigger our module deployment.

For this purpose, we will create a new file called “main.tf” in the root of the project.

 ###################################################################################
 #           Instantiate the global components
 ###################################################################################
 module "mod_rg" {
     source                      = "./modules/az-rg"
     name                        = "${local.resource_prefix}-rg" 
     tags                        = local.tags
     location                    = local.location
 }

In this file, we call the module responsible of creating resource groups and we pass different variables declared in our local file.

We will need to execute again the init command in order to install the module that handles resource group. Please make sure that you are in the root folder of the project !

terraform init  

Next, we will execute the following command that checks our scripts and the execution outcome. This command will not execute the script yet in our infrastructure, it will only checks if terraform can execute the scripts.

terraform plan 

If everything is OK, we should have something like the screenshot below :

Wonderful ! Our plan was executed correctly and now we can apply our changes using the following command :

terraform apply --auto-approve

NOTE : The auto-approve argument is used to skip the cli prompt to confirm the deployment.

After the deployment is completed we can see the output below :

And in the Azure portal :

Create Virtual network using Terraform

The network topology that will be used for our cluster is Azure CNI.

NOTE : For more information about network concepts in AKS, please refer to this link :https://docs.microsoft.com/en-us/azure/aks/concepts-network

We need therefore to create a Virtual network with a CIDR range of 10.0.0.0/16 and a dedicated subnet for our cluster with a CIDR range of 10.0.8.0/21.

Like for the resource group, we will create a dedicated module to handle vnet and subnet creation.

Under the folder modules, create a new folder named : az-vnet and the following files :

  • az-vnet-main.tf : Handle the Vnet resource
  • az-vnet-outputs.tf : output variables from the module
  • az-vnet-variables.tf : input variables used to create the module.

File az-vnet-main.tf :

 #######################################################
 #           VNET
 #######################################################
  
 resource "azurerm_virtual_network" "vnet" {
   name                = var.vnet_name
   address_space       = [var.address_space]
   resource_group_name = var.resource_group_name
   location            = var.location
  
   tags = var.tags
 }
  
 #######################################################
 #           Subnet
 #######################################################
  
 resource "azurerm_subnet" "subnet" {
   depends_on                                     = [azurerm_virtual_network.vnet]
   count                                          = var.count_instances
   name                                           = var.subnet_data[count.index].subnet_name
   resource_group_name                            = var.resource_group_name
   virtual_network_name                           = var.vnet_name
   address_prefixes                               = [var.subnet_data[count.index].subnet_cidr]
   service_endpoints                              = var.subnet_data[count.index].service_endpoints
   enforce_private_link_endpoint_network_policies = var.subnet_data[count.index].enforce_private_link_endpoint_network_policies
 }

File az-vnet-outputs.tf

 output "az_vnet" {
   value = azurerm_virtual_network.vnet
 }
  
 output "az_subnet" {
   value = azurerm_subnet.subnet
 }

File az-vnet-variables.tf

 variable "vnet_name" {
   type        = string
   description = "This variable defines the vnet name"
 }
  
 variable "address_space" {
   type        = string
   description = "This variable defines the adress space used by the Vnet"
 }
  
 variable "resource_group_name" {
   type        = string
   description = "This variable defines the resource group name"
 }
  
 variable "location" {
   type        = string
   description = "This variable defines the location of the resource"
 }
  
 variable "tags" {
   type = object({           
     project     = string
     location    = string
     CreatedBy  = string
   })
   description = "This variable defines the resources tags"
 }
  
 variable "count_instances" {
   type        = number
   description = "This variable defines the number of instances"
   default = 1
 }
  
 variable "subnet_data" {
   type = list(object({
     subnet_name = string
     subnet_cidr = string
     service_endpoints = list(string)
     enforce_private_link_endpoint_network_policies = bool
   }))
 }
  
 variable "service_endpoints" {
   description = "Subnet Service Endpoints."
   type        = list(string)
   default     = []
 }

Add the following section to the local file :

 #################################################
 #           Vnet data
 #################################################
  
 vnet_default_cidr = "10.0.0.0/16" #Total of 65,536 adresses
 subnet_cidr = "10.0.8.0/21" #Total of 2,048 adresses
 subnet_data_count = length(local.subnet_data)
  
 subnet_data = [
         {
             subnet_name = "aks_subnet"
             subnet_cidr = local.subnet_cidr
             service_endpoints = []
             enforce_private_link_endpoint_network_policies = false
         }
     ]
 }

And update the main.tf file with the following section :

 module "mod_vnet_subnet" {
     source                  = "./modules/az-vnet"
     vnet_name               = "${local.resource_prefix}-vnet"
     tags                    = local.tags
     location                = local.location
     resource_group_name     = module.mod_rg.az_rg_name
     address_space           = local.vnet_default_cidr
     count_instances         = local.subnet_data_count
     subnet_data             = local.subnet_data
 }

Apply again the following commands and make sure the resources are created correctly in Azure :

terraform plan
terraform apply --auto-approve

Create Azure Kubernetes Service using Terraform

For our test today, We will create an AKS cluster with the following options :

  • Kube version : 1.22.11
  • subnet aks_subnet (Azure CNI Network).
  • 1 node ( no autoscaling enabled).
  • The vm size : Standard_B2s ( 2 vcpus, 4 GiB memory )
  • OS disk size : 60 GB.

Under the folder modules, create a new folder named : az-aks and the following files :

  • az-aks-main.tf : Handle the AKS resource
  • az-aks-outputs.tf : output variables from the module
  • az-aks-variables.tf : input variables used to create the module.

File az-aks-main.tf

 #######################################################
 #           AKS
 #######################################################
  
 resource "azurerm_kubernetes_cluster" "aks" {
   name                = var.name
   location            = var.location
   resource_group_name = var.resource_group_name
   dns_prefix          = var.dns_prefix
   kubernetes_version  = var.kubernetes_version
  
   # Default node pool
  
   default_node_pool {
     name                = var.default_node_pool.name
     node_count          = var.default_node_pool.node_count
     vm_size             = var.default_node_pool.vm_size
     availability_zones  = var.default_node_pool.availability_zones
     enable_auto_scaling = var.default_node_pool.enable_auto_scaling
     min_count           = var.default_node_pool.min_count
     max_count           = var.default_node_pool.max_count
     max_pods            = var.default_node_pool.max_pods
     os_disk_size_gb     = var.default_node_pool.os_disk_size_gb
     type                = var.default_node_pool.type
     vnet_subnet_id      = var.vnet_subnet_id
   }
  
    role_based_access_control {
     enabled = true
   }
  
   # Network profile
   network_profile {
     network_plugin     = var.network_plugin
     network_policy     = var.network_policy
     service_cidr       = var.service_cidr
     dns_service_ip     = var.dns_service_ip
     load_balancer_sku  = var.load_balancer_sku
     docker_bridge_cidr = var.docker_bridge_cidr
   }
  
  # Identity
     identity {
     type = "SystemAssigned"
   }
  
   lifecycle {
     prevent_destroy = false
     ignore_changes = [
       default_node_pool[0].node_count
     ]
   }
  
   tags = var.tags
 }

File az-aks-outputs.tf

 output "az_aks_principal_id" {
   value = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id
   description = "AKS principal ID. Refers to MSI XXX_agentpool in the MC resource group"
 }
 output "az_aks_id" {
   value = azurerm_kubernetes_cluster.aks.id
   description = "AKS resource id"
 }
  
 output "az_aks_name" {
   value = azurerm_kubernetes_cluster.aks.name
   description = "AKS cluster name"
 }

File az-aks-variables.tf

 variable "name" {
   type        = string
   description = "The name of the AKS cluster"
 }
  
 variable "location" {
   type        = string
   description = "The location of the AKS cluster"
 }
  
 variable "resource_group_name" {
   type        = string
   description = "The rg name"
 }
  
 variable "dns_prefix" {
   type        = string
   description = "The DNS prefix name"
 }
  
 variable "kubernetes_version" {
   type        = string
   description = "The version of kubernetes"
 }
  
 variable "default_node_pool" {
   type = object({
     name                = string
     node_count          = number
     vm_size             = string
     availability_zones  = list(number)
     enable_auto_scaling = bool
     min_count           = number
     max_count           = number
     max_pods            = number
     os_disk_size_gb     = number
     type                = string
   })
   description = "This variable defines the subnets data to be created"
 }
  
 variable "network_plugin" {
   type        = string
   description = "Network plugin for kubenretes network overlay (azure or kubnet)."
 }
  
 variable "vnet_subnet_id" {
   type        = string
   description = "Network plugin for kubenretes network overlay (azure or kubnet)."
 }
  
 variable "network_policy" {
   type        = string
   description = "network policy (azure or calico)."
 }
  
 variable "service_cidr" {
   type        = string
   description = "kubernetes internal service cidr range."
 }
  
 variable "dns_service_ip" {
   type        = string
   description = "kubernetes internal that will be used by kube_dns."
 }
  
 variable "load_balancer_sku" {
   description = "Load balancer sku."
   type        = string
   default     = "Standard"
 }
  
 variable "docker_bridge_cidr" {
   type        = string
   description = "kubernetes internal docker service cidr range."
   default     = "172.17.0.1/16"
 }
  
 variable "tags" {
   type = object({        
     project     = string
     location    = string
     CreatedBy  = string
   })
   description = "This variable defines the resources tags"
 }

Apply again the following commands and make sure the resources are created correctly in Azure :

 terraform plan
 terraform apply --auto-approve

You should have the following output from Terraform CLI, once the command finishes ( it may takes some minutes before completion).

Voila 😊,

Congratulations, you go through all the steps to create your AKS cluster using terraform as IaC platform.

Final words …

In this tutorial, we saw how we create an AKS cluster using terraform as an infrastructure automation tool.

First, we set up Terraform backend to store state in Azure using Azure blob storage, and then we created scripts to create resource group, Virtual network and finally The Azure Kubernetes Service.

The complete code source is available in the following repository : https://github.com/salahlemtiri/terraform-aks

I hope this topic was helpful to start using Terraform to automate Azure resource creation.

Leave a comment if you find this article useful, and I will be pleased to answer your questions.

Salah.

Published inAKSAzureDevOpsTerraform

One Comment

  1. LOUBNA LOUBNA

    Thank you for sharing

Leave a Reply

Your email address will not be published. Required fields are marked *