Building my first Private Azure Kubernetes Cluster WAF aligned.

Building my first private AKS cluster

Hello welcome back to my blog! Today I want to talk about a different topic than I normally write about, namely Azure Kubernetes.

It is a subject that I would like to know more about and that I will delve into in the coming period to increase my knowledge in this area.

Of course building an basic Azure Kubernetes Cluster is fun, but I want to do it based on best practices and therefore WAF aligned. What I also find important in setting up my resources, is to do this as much as possible with Infrastructure as Code, which is why I am going to build this AKS cluster with Bicep.

For this we need to deploy more resources than just the AKS cluster, why? because we need to be able to access the cluster after the deployment, and we want to configure a test application within the cluster. This test application is taken from the Microsoft Learn site and I will use it to show that the AKS cluster works.

These resources are mostly created using Bicep and the Azure Verified Modules, only the part for setting up the p2s to get a secure connection is installed and done trough the portal, but I'll come back to that later.

So use this cluster especially when you, like me, want to test applications, expand your knowledge and test deployments it's easy to deploy because of the IaC and when you are done with testing you can remove it when not needed anymore.

I would like to take you through my process.


What is the set-up and what do we need.

As I have already indicated, we are going to build the resources using Bicep and make use of the Azure Verified Modules. We're going to start by setting up the bicep and bicepparam files.

  • Resource Group for the Azure Kubernetes Cluster and other resources.
  • Resource Group for the system and work nodes and VMSS.
  • Log Analytics Workspace for logging and monitoring.
  • Managed Identity with rights on the AKS cluster.
  • Private DNS zone because the AKS cluster will be private.
  • Azure Kubernetes Cluster ;-)
  • Virtual Network with different subnets.
  • Virtual Network Gateway for creation of the Point 2 Site connection.

We use a deployment script written in powershell, it will deploy the Bicep in the environment. In this script, the Bicep deployment is not only started, but it also create some extra things to be able to use the cluster properly and to have as little administrative work afterwards, I came up with the following:

  • The script first checks in which resource group the resources are placed.
  • In the next step, of course, the bicep is deployed with a what if statement.
  • Two Entra ID groups are created reader and administrator based on the name variable.
  • After this step, a reader and administrator role will be assigned to these new Entra ID groups.
  • In between a timeout is taken to avoid replication errors when it needs to be added to the resource group.
  • The script looks at who is performing the deployment and automatically places them in the Administrator Entra ID group.
  • These rights are of course placed on the Resource Group.


Let's check the specific files

Now let's look further at the Bicep, Bicepparam and Deployment file, in this Bicep file we have multiple resources that are going to be built. You can see an example in the screenshot. In this example I use a Bicepparam file for variables that i don't want to have in the main bicep file.

Bicep file

The AKS resource is WAF aligned and uses best practices, if you don't want to use it in a private connection you need to change values in the bicep but then it will not be waf aligned anymore ;-), so that is not preferable.

  1targetScope = 'subscription'
  2
  3param updatedBy string 
  4
  5@allowed([
  6  'test'
  7  'dev'
  8  'prod'
  9  'acc'
 10])
 11param environmentType string 
 12
 13param subscriptionId string 
 14
 15@description('Unique identifier for the deployment')
 16param deploymentGuid string = newGuid()
 17
 18@description('Product Type: example aks.')
 19param productType string
 20
 21@description('Azure Region to deploy the resources in.')
 22@allowed([
 23  'westeurope'
 24  'northeurope'
 25 
 26])
 27param location string 
 28@description('Location shortcode. Used for end of resource names.')
 29param locationShortCode string 
 30
 31@description('Add tags as required as Name:Value')
 32param tags object = {
 33  Environment: environmentType
 34  LastUpdatedOn: utcNow('d')
 35  LastDeployedBy: updatedBy
 36}
 37
 38// resource group parameters
 39param resourceGroupName string = 'rg-${productType}-${environmentType}-${locationShortCode}'
 40param nodeResourceGroup string = '${resourceGroupName}-aks-nodes'
 41
 42//parameters for AKS Cluster
 43param aksClusterName string
 44
 45//parameters for Log Analytics Workspace
 46param logAnalyticsWorkspaceName string
 47
 48//parameters for Managed Identity
 49param managedIdentityName string
 50
 51
 52
 53
 54//virtual network parameters
 55param virtualNetworkName string	
 56param defaultSubnetName string
 57param defaultPrefix string
 58param addressPrefix string
 59param privateEndpointPrefix string
 60param privateEndpointSubnetName string
 61param aksSubnetName string
 62param aksPrefix string
 63param gatewaySubnetName string
 64param gatewayPrefix string
 65
 66
 67var VNetConfiguration = {
 68    Subnets: [
 69      {
 70        name: gatewaySubnetName
 71        addressPrefix: gatewayPrefix
 72    }
 73    {
 74        name: aksSubnetName
 75        addressPrefix: aksPrefix
 76    }
 77    {
 78        name: defaultSubnetName
 79        addressPrefix: defaultPrefix
 80    }
 81    {
 82      name: privateEndpointSubnetName
 83      addressPrefix: privateEndpointPrefix
 84  }
 85        ]
 86    
 87  }
 88
 89//paramamters for the Azure VPN Gateway
 90param vpnGatewayName string 
 91
 92
 93module createResourceGroup 'br/public:avm/res/resources/resource-group:0.4.0' = { 
 94    scope: subscription(subscriptionId)
 95    name: 'rg-${deploymentGuid}'
 96    params: {
 97      name: resourceGroupName
 98      location: location
 99      tags: tags
100      
101               
102    }
103    
104}
105
106module createLogAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
107  name: 'law-${deploymentGuid}'
108  scope: resourceGroup(resourceGroupName)
109  params: {
110    name: logAnalyticsWorkspaceName
111    location: location
112    tags: tags
113  }
114  dependsOn: [createResourceGroup]
115}
116
117module createManagedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.0' = {
118  name: 'mi-${deploymentGuid}'
119  scope: resourceGroup(resourceGroupName)
120  params: {
121    name: managedIdentityName
122    location: location
123    
124
125    tags: tags
126  }
127  dependsOn: [createResourceGroup]
128}
129
130module createPrivateDNSZone 'br/public:avm/res/network/private-dns-zone:0.2.0' = {
131  scope: resourceGroup(resourceGroupName)
132  name: 'dns-${deploymentGuid}'
133  params: {
134    name: 'privatelink.westeurope.azmk8s.io'
135    virtualNetworkLinks: [
136      {
137        name: 'vnet-${deploymentGuid}'
138        registrationEnabled: true
139        virtualNetworkResourceId: createVnet.outputs.resourceId
140      }
141    ]
142    location: 'global'
143    roleAssignments:[             {
144      roleDefinitionIdOrName: 'Contributor'
145      principalId: createManagedIdentity.outputs.principalId
146      principalType: 'ServicePrincipal'
147    }
148]
149    tags: tags
150  }
151  dependsOn: [createResourceGroup]
152}
153
154module createVnet 'br/public:avm/res/network/virtual-network:0.5.4' = {
155  name: 'vnet-${deploymentGuid}'
156  scope: resourceGroup(resourceGroupName)
157  params: {
158    name: virtualNetworkName
159    addressPrefixes: [
160      addressPrefix
161    ]
162    subnets: VNetConfiguration.Subnets
163    roleAssignments:[             {
164      roleDefinitionIdOrName: 'Contributor'
165      principalId: createManagedIdentity.outputs.principalId
166      principalType: 'ServicePrincipal'
167    }
168]
169    location: location
170    tags: tags
171    
172  }
173  dependsOn: [createResourceGroup]
174}
175
176module createAksCluster 'br/public:avm/res/container-service/managed-cluster:0.8.3' = {
177    name: 'aks-${deploymentGuid}'
178    scope: resourceGroup(resourceGroupName)
179        params: {
180        name: aksClusterName
181        enablePrivateCluster: true
182        primaryAgentPoolProfiles: [
183          {
184            availabilityZones: [
185              3
186            ]
187            count: 1
188            enableAutoScaling: true
189            maxCount: 1
190            maxPods: 50
191            minCount: 1
192            mode: 'System'
193            name: 'systempool'
194            nodeTaints: [
195              'CriticalAddonsOnly=true:NoSchedule'
196            ]
197            osDiskSizeGB: 0
198            osType: 'Linux'
199            type: 'VirtualMachineScaleSets'
200            vmSize: 'Standard_DS2_v2'
201            vnetSubnetResourceId: createVnet.outputs.subnetResourceIds[1]
202          }
203        ]
204        agentPools: [
205          {
206            availabilityZones: [
207              3
208            ]
209            count: 1
210            enableAutoScaling: true
211            maxCount: 1
212            maxPods: 50
213            minCount: 1
214            minPods: 2
215            mode: 'User'
216            name: 'userpool1'
217            nodeLabels: {}
218            osDiskType: 'Ephemeral'
219            osDiskSizeGB: 60
220            osType: 'Linux'
221            scaleSetEvictionPolicy: 'Delete'
222            scaleSetPriority: 'Regular'
223            type: 'VirtualMachineScaleSets'
224            vmSize: 'Standard_DS2_v2'
225            vnetSubnetResourceId: createVnet.outputs.subnetResourceIds[1]
226          }
227          {
228            availabilityZones: [
229              3
230            ]
231            count: 1
232            enableAutoScaling: true
233            maxCount: 1
234            maxPods: 50
235            minCount: 1
236            minPods: 2
237            mode: 'User'
238            name: 'userpool2'
239            nodeLabels: {}
240            osDiskType: 'Ephemeral'
241            osDiskSizeGB: 60
242            osType: 'Linux'
243            scaleSetEvictionPolicy: 'Delete'
244            scaleSetPriority: 'Regular'
245            type: 'VirtualMachineScaleSets'
246            vmSize: 'Standard_DS2_v2'
247          }
248        ]
249        autoUpgradeProfileUpgradeChannel: 'stable'
250        autoNodeOsUpgradeProfileUpgradeChannel: 'Unmanaged'
251        maintenanceConfigurations: [
252          {
253            name: 'aksManagedAutoUpgradeSchedule'
254            maintenanceWindow: {
255              schedule: {
256                weekly: {
257                  intervalWeeks: 1
258                  dayOfWeek: 'Sunday'
259                }
260              }
261              durationHours: 4
262              utcOffset: '+00:00'
263              startDate: '2024-07-15'
264              startTime: '00:00'
265            }
266          }
267          {
268            name: 'aksManagedNodeOSUpgradeSchedule'
269            maintenanceWindow: {
270              schedule: {
271                weekly: {
272                  intervalWeeks: 1
273                  dayOfWeek: 'Sunday'
274                }
275              }
276              durationHours: 4
277              utcOffset: '+00:00'
278              startDate: '2024-07-15'
279              startTime: '00:00'
280            }
281          }
282        ]
283        networkPlugin: 'azure'
284        networkPolicy: 'azure'
285        skuTier: 'Standard'
286        dnsServiceIP: '10.10.200.10'
287        serviceCidr: '10.10.200.0/24'
288        omsAgentEnabled: true
289        monitoringWorkspaceResourceId: createLogAnalyticsWorkspace.outputs.resourceId
290        disableLocalAccounts: true
291        enableAzureDefender: true
292        diagnosticSettings: [
293          {
294            name: 'customSetting'
295            logCategoriesAndGroups: [
296              {
297                category: 'kube-apiserver'
298              }
299              {
300                category: 'kube-controller-manager'
301              }
302              {
303                category: 'kube-scheduler'
304              }
305              {
306                category: 'cluster-autoscaler'
307              }
308            ]
309            metricCategories: [
310              {
311                category: 'AllMetrics'
312              }
313            ]
314            workspaceResourceId: createLogAnalyticsWorkspace.outputs.resourceId
315          }
316        ]
317        privateDNSZone: createPrivateDNSZone.outputs.resourceId
318        nodeResourceGroup: nodeResourceGroup
319        managedIdentities: {
320          userAssignedResourceIds: [
321            createManagedIdentity.outputs.resourceId
322          ]
323        }
324        tags: tags
325        aadProfile: {
326          aadProfileEnableAzureRBAC: true
327          aadProfileManaged: true
328        }
329      }
330      dependsOn: [
331        createResourceGroup
332        
333      ]
334    }
335
336module createAzureVpnGateway 'br/public:avm/res/network/virtual-network-gateway:0.6.0' = {
337  scope: resourceGroup(resourceGroupName)
338  name: 'vpn-${deploymentGuid}'
339  params: {
340    name: vpnGatewayName
341    gatewayType: 'Vpn'
342    vpnType: 'RouteBased'
343    skuName: 'VpnGw1AZ'
344    clusterSettings:{
345      clusterMode:'activePassiveNoBgp'
346    }
347    virtualNetworkResourceId: createVnet.outputs.resourceId
348    location: location
349    tags: tags
350  }
351  dependsOn: [createResourceGroup]
352}

Bicepparam

 1using 'aks.bicep'
 2
 3//parameters for the deployment.
 4param updatedBy = 'updatedby'
 5param subscriptionId = 'yoursubscriptionID'
 6param environmentType = 'prod' 
 7param location = 'westeurope' 
 8param locationShortCode = 'weu' 
 9param productType = 'aks'
10
11param virtualNetworkName = 'vnet-${productType}-${environmentType}-${locationShortCode}'
12param defaultSubnetName = 'snet-default'
13param defaultPrefix = '10.0.2.0/24'
14param addressPrefix = '10.0.0.0/16'
15
16param aksSubnetName = 'snet-aks'
17param aksPrefix = '10.0.3.0/24'
18
19param privateEndpointPrefix = '10.0.4.0/24'
20param privateEndpointSubnetName = 'snet-pe'
21
22param gatewayPrefix = '10.0.1.0/27'
23param gatewaySubnetName = 'GatewaySubnet'
24
25param aksClusterName = '${productType}-pof-${environmentType}-${locationShortCode}'
26
27param logAnalyticsWorkspaceName = 'law-${productType}-${environmentType}-${locationShortCode}'
28
29param managedIdentityName = 'mi-${productType}-${environmentType}-${locationShortCode}'
30
31param vpnGatewayName = 'vgw-${productType}-${environmentType}-${locationShortCode}'

Deployment file

Keep in mind that you change the values where you have stored your bicepparam and bicep file, so it can use that location for the deployment.

--template-file

--parameters

  1param(
  2    [Parameter(Mandatory = $true)][string] $subscriptionID = "",
  3    [Parameter(Mandatory = $true)][ValidateSet("northeurope", "westeurope")][string] $location = "", 
  4    [Parameter(Mandatory = $true, ParameterSetName = 'Default')] $productType = "",
  5    [Parameter(Mandatory = $true, Position = 3)] [ValidateSet("prod", "acc", "dev", "test")] [string] $environmentType = "",
  6    [switch] $deploy
  7)
  8
  9# Ensure parameters are captured
 10Write-Host "Subscription ID: $subscriptionID"
 11Write-Host "Location: $location"
 12Write-Host "Product Type: $productType"
 13Write-Host "Environment Type: $environmentType"
 14
 15$deploymentID = (New-Guid).Guid
 16
 17<# Set Variables #>
 18az account set --subscription $subscriptionID --output none
 19if (!$?) {
 20    Write-Host "Something went wrong while setting the correct subscription. Please check and try again." -ForegroundColor Red
 21    exit 1
 22}
 23
 24$updatedBy = (az account show | ConvertFrom-Json).user.name 
 25$userId = (az ad signed-in-user show --query id --output tsv)
 26$location = $location.ToLower() -replace " ", ""
 27
 28$LocationShortCodeMap = @{
 29    "westeurope"  = "weu";
 30    "northeurope" = "neu";
 31}
 32
 33$locationShortCode = $LocationShortCodeMap.$location
 34$resourceGroup = "rg-$productType-$environmentType-$locationShortCode"
 35
 36Write-Host "Using Resource Group Name: '$resourceGroup'" -ForegroundColor Cyan
 37
 38if ($deploy) {
 39    Write-Host "Running a Bicep deployment with ID: '$deploymentID' for Environment: '$environmentType'..." -ForegroundColor Green
 40    az deployment sub create `
 41    --name $deploymentID `
 42    --location $location `
 43    --template-file ./aks.bicep `
 44    --parameters ./aks.bicepparam `
 45    --parameters updatedBy=$updatedBy location=$location locationShortCode=$LocationShortCode productType=$productType environmentType=$environmentType `
 46    --confirm-with-what-if `
 47    
 48    if (!$?) {
 49        Write-Host "Bicep deployment failed. Stopping execution." -ForegroundColor Red
 50        exit 1
 51    }
 52    Write-Host "Bicep deployment completed successfully." -ForegroundColor Green
 53}
 54
 55Write-Host "Proceeding with Entra ID group creation and role assignment for Resource Group '$resourceGroup'." -ForegroundColor Cyan
 56
 57# List of required Entra ID groups and their corresponding Azure roles
 58$groupRoleMap = @{
 59    "aks-gg-$environmentType-administrators" = "Azure Kubernetes Service RBAC Cluster Admin";
 60    "aks-gg-$environmentType-reader"         = "Azure Kubernetes Service RBAC Reader";
 61}
 62
 63# Ensure Entra ID groups exist and assign roles
 64foreach ($groupName in $groupRoleMap.Keys) {
 65    $existingGroup = az ad group list --query "[?displayName=='$groupName']" --output json | ConvertFrom-Json
 66
 67    if (-not $existingGroup) {
 68        Write-Host "Creating Entra ID group: $groupName" -ForegroundColor Yellow
 69        $groupId = az ad group create --display-name $groupName --mail-nickname $groupName.Replace("-", "") --query id --output tsv
 70
 71        if (!$groupId) {
 72            Write-Host "Failed to create Entra ID group: $groupName. Please check your permissions." -ForegroundColor Red
 73            exit 1
 74        }
 75        Write-Host "Entra ID group '$groupName' created successfully." -ForegroundColor Green
 76    } else {
 77        Write-Host "Entra ID group '$groupName' already exists. Skipping creation." -ForegroundColor Cyan
 78        $groupId = $existingGroup[0].id
 79    }
 80
 81    # Wait for group replication before assigning roles
 82    Write-Host "Waiting for Entra ID replication..." -ForegroundColor Cyan
 83    Start-Sleep -Seconds 30
 84
 85    # Assign role at the Resource Group level (retry logic)
 86    $roleName = $groupRoleMap[$groupName]
 87    $maxRetries = 3
 88    $retryCount = 0
 89    $roleAssignmentSuccess = $false
 90
 91    while ($retryCount -lt $maxRetries -and -not $roleAssignmentSuccess) {
 92        Write-Host "Assigning '$roleName' role to group '$groupName' on Resource Group '$resourceGroup'" -ForegroundColor Yellow
 93        az role assignment create --assignee $groupId --role "$roleName" --scope /subscriptions/$subscriptionID/resourceGroups/$resourceGroup --output none
 94        
 95        if ($?) {
 96            Write-Host "Successfully assigned '$roleName' role to '$groupName'." -ForegroundColor Green
 97            $roleAssignmentSuccess = $true
 98        } else {
 99            Write-Host "Retrying role assignment in 20 seconds..." -ForegroundColor Yellow
100            Start-Sleep -Seconds 20
101            $retryCount++
102        }
103    }
104
105    if (-not $roleAssignmentSuccess) {
106        Write-Host "Failed to assign '$roleName' role to '$groupName' after multiple attempts." -ForegroundColor Red
107    }
108
109    # Add the signed-in user to the administrator group
110    if ($groupName -match "administrators") {
111        Write-Host "Adding current user ($updatedBy) to group '$groupName'." -ForegroundColor Yellow
112        az ad group member add --group $groupName --member-id $userId
113        
114        if (!$?) {
115            Write-Host "Failed to add user '$updatedBy' to '$groupName'. Please check permissions." -ForegroundColor Red
116        } else {
117            Write-Host "Successfully added user '$updatedBy' to '$groupName'." -ForegroundColor Green
118        }
119    }
120}

What do we need to do after the deployment?

When we are done deploying there are still a few things that need to be created and configured to access the cluster.

We start by setting up a point 2 site connection, more information on how to set this up can be found in the Microsoft documentation and can be found at this link.

Let's make a summery:

  • Grant consent and install the Enterprise Application using this link, Keep in mind that the account you do this with needs to a Cloud Administrator.

  • Fill in the below information in the screenshot and use your own TenantID, use also a address range that doesn't conflict with IP addresses.

  • Download the Azure VPN client

  • Download the VPN configuration from the Point 2 Site configuration and extract it to a new folder, use this folder when importing the settings to the Azure VPN and save the connection.

  • Use the Account that has started the deployment and test if the tunnel will connect.


To connect to the AKS cluster we can use two ways to access this, let me explain.

The first option is, when you use the WAF aligned Azure Verified Module to add the private DNS in the lmhost file.

We now need to add the private DNS zone record (ip-address) from the Azure Kubernetes in the lmhosts file so you can use the private DNS record to connect to your cluster, this is needed because we don't have an DNS solution installed and the cluster is not public acccesible.

How can you do this?

  • Go to your created private DNS zone and check and copy the entress.

  • Add the address in the lmhost files.

The second option is to modify the Bicep Module to use the enablePrivateClusterPublicFQDN: true this will let you connect to the Cluster when using the --public-fqdn switch. This is still WAF aligned because the API is still only accessible private.


Let's connect to the AKS cluster?

Ok great! We have done the preparations now let's connect to the AKS cluster.

You can do this by going to the cluster in the Azure Portal and clicking on the connect button. This will show you the steps you need to take to connect to the Azure Kubernetes Cluster.

When you click connect, the commands you need to enter into your terminal, will appear on the right side, and as i explained in the above section you can also use the --public-fqdn after the az aks get-credentials --resource-group rg-aks-prod-weu --name aks-pof-prod-weu --overwrite-existing when you have used the enablePrivateClusterPublicFQDN: true in the Bicep file.

Please note that you log in with the account that you used when deploying Bicep, as this account is immediately placed in the administrator group and therefore has rights to the cluster. If you want to use a different account, place it in the administrator group that was created at the beginning.

Once you have executed the commands correctly, you will be connected to the cluster and you can run the following command to test this:

kubectl get namespaces

If successful you will get the following namespaces back.


Let's test and execute a deployment.

It is of course nice that we have set this up, but let's also test the AKS cluster and create a (web)application. I will use the examples given on the Microsoft site., this will basically create a dummy website.

  • Copy paste and save this file as a yaml file in your directory.
  1apiVersion: apps/v1
  2kind: StatefulSet
  3metadata:
  4  name: rabbitmq
  5spec:
  6  serviceName: rabbitmq
  7  replicas: 1
  8  selector:
  9    matchLabels:
 10      app: rabbitmq
 11  template:
 12    metadata:
 13      labels:
 14        app: rabbitmq
 15    spec:
 16      nodeSelector:
 17        "kubernetes.io/os": linux
 18      containers:
 19      - name: rabbitmq
 20        image: mcr.microsoft.com/mirror/docker/library/rabbitmq:3.10-management-alpine
 21        ports:
 22        - containerPort: 5672
 23          name: rabbitmq-amqp
 24        - containerPort: 15672
 25          name: rabbitmq-http
 26        env:
 27        - name: RABBITMQ_DEFAULT_USER
 28          value: "username"
 29        - name: RABBITMQ_DEFAULT_PASS
 30          value: "password"
 31        resources:
 32          requests:
 33            cpu: 10m
 34            memory: 128Mi
 35          limits:
 36            cpu: 250m
 37            memory: 256Mi
 38        volumeMounts:
 39        - name: rabbitmq-enabled-plugins
 40          mountPath: /etc/rabbitmq/enabled_plugins
 41          subPath: enabled_plugins
 42      volumes:
 43      - name: rabbitmq-enabled-plugins
 44        configMap:
 45          name: rabbitmq-enabled-plugins
 46          items:
 47          - key: rabbitmq_enabled_plugins
 48            path: enabled_plugins
 49---
 50apiVersion: v1
 51data:
 52  rabbitmq_enabled_plugins: |
 53    [rabbitmq_management,rabbitmq_prometheus,rabbitmq_amqp1_0].
 54kind: ConfigMap
 55metadata:
 56  name: rabbitmq-enabled-plugins            
 57---
 58apiVersion: v1
 59kind: Service
 60metadata:
 61  name: rabbitmq
 62spec:
 63  selector:
 64    app: rabbitmq
 65  ports:
 66    - name: rabbitmq-amqp
 67      port: 5672
 68      targetPort: 5672
 69    - name: rabbitmq-http
 70      port: 15672
 71      targetPort: 15672
 72  type: ClusterIP
 73---
 74apiVersion: apps/v1
 75kind: Deployment
 76metadata:
 77  name: order-service
 78spec:
 79  replicas: 1
 80  selector:
 81    matchLabels:
 82      app: order-service
 83  template:
 84    metadata:
 85      labels:
 86        app: order-service
 87    spec:
 88      nodeSelector:
 89        "kubernetes.io/os": linux
 90      containers:
 91      - name: order-service
 92        image: ghcr.io/azure-samples/aks-store-demo/order-service:latest
 93        ports:
 94        - containerPort: 3000
 95        env:
 96        - name: ORDER_QUEUE_HOSTNAME
 97          value: "rabbitmq"
 98        - name: ORDER_QUEUE_PORT
 99          value: "5672"
100        - name: ORDER_QUEUE_USERNAME
101          value: "username"
102        - name: ORDER_QUEUE_PASSWORD
103          value: "password"
104        - name: ORDER_QUEUE_NAME
105          value: "orders"
106        - name: FASTIFY_ADDRESS
107          value: "0.0.0.0"
108        resources:
109          requests:
110            cpu: 1m
111            memory: 50Mi
112          limits:
113            cpu: 75m
114            memory: 128Mi
115        startupProbe:
116          httpGet:
117            path: /health
118            port: 3000
119          failureThreshold: 5
120          initialDelaySeconds: 20
121          periodSeconds: 10
122        readinessProbe:
123          httpGet:
124            path: /health
125            port: 3000
126          failureThreshold: 3
127          initialDelaySeconds: 3
128          periodSeconds: 5
129        livenessProbe:
130          httpGet:
131            path: /health
132            port: 3000
133          failureThreshold: 5
134          initialDelaySeconds: 3
135          periodSeconds: 3
136      initContainers:
137      - name: wait-for-rabbitmq
138        image: busybox
139        command: ['sh', '-c', 'until nc -zv rabbitmq 5672; do echo waiting for rabbitmq; sleep 2; done;']
140        resources:
141          requests:
142            cpu: 1m
143            memory: 50Mi
144          limits:
145            cpu: 75m
146            memory: 128Mi    
147---
148apiVersion: v1
149kind: Service
150metadata:
151  name: order-service
152spec:
153  type: ClusterIP
154  ports:
155  - name: http
156    port: 3000
157    targetPort: 3000
158  selector:
159    app: order-service
160---
161apiVersion: apps/v1
162kind: Deployment
163metadata:
164  name: product-service
165spec:
166  replicas: 1
167  selector:
168    matchLabels:
169      app: product-service
170  template:
171    metadata:
172      labels:
173        app: product-service
174    spec:
175      nodeSelector:
176        "kubernetes.io/os": linux
177      containers:
178      - name: product-service
179        image: ghcr.io/azure-samples/aks-store-demo/product-service:latest
180        ports:
181        - containerPort: 3002
182        env: 
183        - name: AI_SERVICE_URL
184          value: "http://ai-service:5001/"
185        resources:
186          requests:
187            cpu: 1m
188            memory: 1Mi
189          limits:
190            cpu: 2m
191            memory: 20Mi
192        readinessProbe:
193          httpGet:
194            path: /health
195            port: 3002
196          failureThreshold: 3
197          initialDelaySeconds: 3
198          periodSeconds: 5
199        livenessProbe:
200          httpGet:
201            path: /health
202            port: 3002
203          failureThreshold: 5
204          initialDelaySeconds: 3
205          periodSeconds: 3
206---
207apiVersion: v1
208kind: Service
209metadata:
210  name: product-service
211spec:
212  type: ClusterIP
213  ports:
214  - name: http
215    port: 3002
216    targetPort: 3002
217  selector:
218    app: product-service
219---
220apiVersion: apps/v1
221kind: Deployment
222metadata:
223  name: store-front
224spec:
225  replicas: 1
226  selector:
227    matchLabels:
228      app: store-front
229  template:
230    metadata:
231      labels:
232        app: store-front
233    spec:
234      nodeSelector:
235        "kubernetes.io/os": linux
236      containers:
237      - name: store-front
238        image: ghcr.io/azure-samples/aks-store-demo/store-front:latest
239        ports:
240        - containerPort: 8080
241          name: store-front
242        env: 
243        - name: VUE_APP_ORDER_SERVICE_URL
244          value: "http://order-service:3000/"
245        - name: VUE_APP_PRODUCT_SERVICE_URL
246          value: "http://product-service:3002/"
247        resources:
248          requests:
249            cpu: 1m
250            memory: 200Mi
251          limits:
252            cpu: 1000m
253            memory: 512Mi
254        startupProbe:
255          httpGet:
256            path: /health
257            port: 8080
258          failureThreshold: 3
259          initialDelaySeconds: 5
260          periodSeconds: 5
261        readinessProbe:
262          httpGet:
263            path: /health
264            port: 8080
265          failureThreshold: 3
266          initialDelaySeconds: 3
267          periodSeconds: 3
268        livenessProbe:
269          httpGet:
270            path: /health
271            port: 8080
272          failureThreshold: 5
273          initialDelaySeconds: 3
274          periodSeconds: 3
275---
276apiVersion: v1
277kind: Service
278metadata:
279  name: store-front
280spec:
281  ports:
282  - port: 80
283    targetPort: 8080
284  selector:
285    app: store-front
286  type: LoadBalancer
  • In your terminal connect to the cluster as explained in the above steps.
  • Use the command kubectl create namespace test-application
  • In your terminal browse where you have stored your yaml file that you have saved.
  • Use the command kubectl apply -f .\aks-quickstart.yaml -n test-application this will execute the deployment in namespace test-application

If everything is running as planned you will see deployments being created.

  • If you use the command kubectl get pods -n test-application you will see the pods that have a running state.

  • If you use the command kubectl get service store-front --watch -n test-application you will see the IP addresses needed to access the webapplication.

If you now go to your browser and use the external ip-address, you will get the website, and yes it's not connected with a certificate so you will see the not secure message, but it's only for testing purposes.

So we have an operating WAF aligned AKS cluster!


In this blog we have explained how a private aks cluster can be easily set up using Bicep and Azure Verified Modules with the necessary resources to connect to the Azure Kubernetes Cluster.

With this AKS cluster you can easily set up and test applications in a secure way and of course it is also ideal to use when you want to test your own AKS skills and of course expand this.

I hope I have inspired you in this article to roll this out and test it yourself. See you next time!