Deploy AKS Cluster in Azure With Terraform

Piyush Sachdeva
6 min readDec 11, 2022

--

In this project, we will provision infrastructure in Azure using Terraform custom modules.🎯

The below resources will be created as part of the demo:

  • Azure Service Principle
  • Azure Resource Group
  • Azure Key Vault
  • Azure Secrets
  • Azure Kubernetes Cluster

If you are a visual learner, you can watch the video below for end-to-end implementation or continue with the blog.

Suppose you have been tasked with providing an Azure Kubernetes Cluster using Terraform. You will first have to create a resource group in which all the related resources would reside, and then a service principle will be used to provision the AKS cluster. Credentials for the service principle will be stored in the key vault we create as part of this, and finally, the AKS cluster will be provisioned. Let’s go!πŸš€

Your directory structure would look something like this:

β”œβ”€β”€ main.tf
β”œβ”€β”€ terraform.tfvars
β”œβ”€β”€ versions.tf
β”œβ”€β”€ variables.tf
β”œβ”€β”€ modules
β”‚ β”œβ”€β”€ ServicePrincipal
β”‚ β”‚ β”œβ”€β”€ variables.tf
β”‚ β”‚ β”œβ”€β”€ outputs.tf
β”‚ β”‚ β”œβ”€β”€ main.tf
β”‚ β”œβ”€β”€ aks
β”‚ β”‚ β”œβ”€β”€ main.tf
β”‚ β”‚ β”œβ”€β”€ output.tf
β”‚ β”‚ └── variables.tf
β”‚ β”œβ”€β”€ KeyVault
β”‚ β”‚ β”œβ”€β”€ main.tf
β”‚ β”‚ └── variables.tf

Let’s start with the first file i.e main.tf

By default, terraform creates a root module using main.tf. We first create our resource group in main.tf using the below code

provider "azurerm" {
features {

}
}

resource "azurerm_resource_group" "rg1" {
name = var.rgname
location = var.location
}

Variables in Terraform:

rgname and location are two variables we have used. To use a variable, we have to initialize it and declare it. For this, we are using two separate file variables.tf and terraform.tfvars.

Content of variables.tf, which resides at the root level:

variable "rgname" {
type = string
description = "resource group name"
}

variable "location" {
type = string
default = "canadacentral"
}

variable "service_principal_name" {
type = string
}

variable "keyvault_name" {
type = string
}

Content of the terraform.tfvars file, which resides at the root level (You can change the values per your needs or keep my name. I won’t mind πŸ€·β€β™‚οΈπŸ˜œ

rgname                 = "test-piyush-rg"
location = "canadacentral"
service_principal_name = "test-piyush-spn"
keyvault_name = "test-piyush-kv"

We also have versions.tf file contains the versions of the providers we used in the demo. It is important to lock a particular version as providers and terraform updates work independently, and you could run into dependency issues if you keep the latest provider version.

terraform {

required_providers {
azuread = "~> 2.9.0"
random = "~> 3.1"
azurerm = "~> 3.0.0"

}
}

Our resource group code is done. Now, let’s create a custom module for the Service Principle. In our root directory, create a directory and name it as modules, and create a sub-directory inside the module and name it as ServicePrincipal.Inside that, create a main.tf and add the below content

data "azuread_client_config" "current" {}

resource "azuread_application" "main" {
display_name = var.service_principal_name
owners = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal" "main" {
application_id = azuread_application.main.application_id
app_role_assignment_required = true
owners = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal_password" "main" {
service_principal_id = azuread_service_principal.main.object_id
}

Also, add a variables.tf with the below content

variable service_principal_name {
type = string
}

and an output.tf so that we can use the secret_id and secret_secret generated from this module as input to the root or AKS child modules.

output "service_principal_name" {
description = "The object id of service principal. Can be used to assign roles to user."
value = azuread_service_principal.main.display_name
}

output "service_principal_object_id" {
description = "The object id of service principal. Can be used to assign roles to user."
value = azuread_service_principal.main.object_id
}

output "service_principal_tenant_id" {
value = azuread_service_principal.main.application_tenant_id
}

output "service_principal_application_id" {
description = "The object id of service principal. Can be used to assign roles to user."
value = azuread_service_principal.main.application_id
}

output "client_id" {
description = "The application id of AzureAD application created."
value = azuread_application.main.application_id
}

output "client_secret" {
description = "Password for service principal."
value = azuread_service_principal_password.main.value

}

The above steps will create your service principal module. Now let’s import this module in our main.tf of root module using the below code:

module "ServicePrincipal" {
source = "./modules/ServicePrincipal"
service_principal_name = var.service_principal_name

depends_on = [
azurerm_resource_group.rg1
]
}

resource "azurerm_role_assignment" "rolespn" {

scope = "/subscriptions/5f5470df-f806-47ee-8f78-6520f817df59"
role_definition_name = "Contributor"
principal_id = module.ServicePrincipal.service_principal_object_id

depends_on = [
module.ServicePrincipal
]
}

We import the module using the source and the absolute path of the module as value.

Similarly, we can create modules for the key vault and AKS cluster.

Below is the final main.tf file:

provider "azurerm" {
features {

}
}

resource "azurerm_resource_group" "rg1" {
name = var.rgname
location = var.location
}

module "ServicePrincipal" {
source = "./modules/ServicePrincipal"
service_principal_name = var.service_principal_name

depends_on = [
azurerm_resource_group.rg1
]
}

resource "azurerm_role_assignment" "rolespn" {

scope = "/subscriptions/5f5470df-f806-47ee-8f78-6520f817df59"
role_definition_name = "Contributor"
principal_id = module.ServicePrincipal.service_principal_object_id

depends_on = [
module.ServicePrincipal
]
}

module "keyvault" {
source = "./modules/keyvault"
keyvault_name = var.keyvault_name
location = var.location
resource_group_name = var.rgname
service_principal_name = var.service_principal_name
service_principal_object_id = module.ServicePrincipal.service_principal_object_id
service_principal_tenant_id = module.ServicePrincipal.service_principal_tenant_id

depends_on = [
module.ServicePrincipal
]
}

resource "azurerm_key_vault_secret" "example" {
name = module.ServicePrincipal.client_id
value = module.ServicePrincipal.client_secret
key_vault_id = module.keyvault.keyvault_id

depends_on = [
module.keyvault
]
}

#create Azure Kubernetes Service
module "aks" {
source = "./modules/aks/"
service_principal_name = var.service_principal_name
client_id = module.ServicePrincipal.client_id
client_secret = module.ServicePrincipal.client_secret
location = var.location
resource_group_name = var.rgname

depends_on = [
module.ServicePrincipal
]

}

main.tf for key vault:

data "azurerm_client_config" "current" {}

resource "azurerm_key_vault" "kv" {
name = var.keyvault_name
location = var.location
resource_group_name = var.resource_group_name
enabled_for_disk_encryption = true
tenant_id = data.azurerm_client_config.current.tenant_id
purge_protection_enabled = false
sku_name = "premium"
soft_delete_retention_days = 7
enable_rbac_authorization = true

}

main.tf for aks

# Datasource to get Latest Azure AKS latest Version
data "azurerm_kubernetes_service_versions" "current" {
location = var.location
include_preview = false
}


resource "azurerm_kubernetes_cluster" "aks-cluster" {
name = "techtutorialwithpiyush-aks-cluster"
location = var.location
resource_group_name = var.resource_group_name
dns_prefix = "${var.resource_group_name}-cluster"
kubernetes_version = data.azurerm_kubernetes_service_versions.current.latest_version
node_resource_group = "${var.resource_group_name}-nrg"

default_node_pool {
name = "defaultpool"
vm_size = "Standard_DS2_v2"
zones = [1, 2, 3]
enable_auto_scaling = true
max_count = 3
min_count = 1
os_disk_size_gb = 30
type = "VirtualMachineScaleSets"
node_labels = {
"nodepool-type" = "system"
"environment" = "prod"
"nodepoolos" = "linux"
}
tags = {
"nodepool-type" = "system"
"environment" = "prod"
"nodepoolos" = "linux"
}
}

service_principal {
client_id = var.client_id
client_secret = var.client_secret
}



linux_profile {
admin_username = "ubuntu"
ssh_key {
key_data = file(var.ssh_public_key)
}
}

network_profile {
network_plugin = "azure"
load_balancer_sku = "standard"
}


}

Your entire flow will look something like the below:

Now, you can run the terraform init to initialize the modules you have created, and it will download all the required dependencies from the providers.

Moment of truth! You can run the Terraform plan now

If everything is done right, you will see eight resources to add:

Now feel free to terraform apply β€” auto-approve to provision the infrastructure, and do not forget to destroy the infra once your work is done.

I hope you have enjoyed the session and learned something from it today. If you did, feel free to clap below as often as possible. It makes the blog reachable to more audiences and helps them as it helped you.

References:

--

--