PAUL'S BLOG

Learn. Build. Share. Repeat.

Deploying AKS Automatic with Pulumi: A step-by-step guide

2025-01-21 20 min read Tutorial

Yo! Been a while since I last wrote here. Been busy with stuff but I’m back with a new article. This time, I’m going to show you how to deploy an AKS Automatic cluster with Pulumi.

If you don’t already know, Pulumi is a modern Infrastructure as Code (IaC) tool that allows you to use your favorite programming language to deploy and manage cloud resources. I like it because I can use Go and the Pulumi Go SDK to write my infrastructure code. There are other languages supported like Python, TypeScript, .NET, and more so be sure to check out their docs for more information.

This is fairly long article, so let’s dive right in.

Prerequisites

Before we start, you need to have an Azure subscription and the following tools installed:

Login to Azure and Pulumi

As mentioned above, I’ll be using the Go SDK and using Pulumi’s Azure Native provider to deploy Azure resources and will need Azure credentials to authenticate. I’ll use the Azure CLI to authenticate because it’s the easiest way to get started, but there are other ways to authenticate with Azure including OpenID Connect, Service Principal, and Managed Identity.

Open a terminal and run the following command to login to Azure.

az login --use-device-code

Pulumi uses its own cloud-based service to store state information by default. You can setup a an account here, but if you don’t want to create a Pulumi account, you can store state information locally in a file. I prefer this for quick demos and getting started as it’s just easier for me and I don’t have to worry about setting up or logging into an account. In a production scenario, you should consider using the cloud-based service or another backend options. Refer to the Pulumi documentation for more information.

Run the following command to login to Pulumi using a local file.

pulumi login file://~/.pulumi

Initialize Pulumi project

Now that we got the prerequisites out of the way, let’s create a new Pulumi project for our AKS deployment.

Run the following command to create a new folder for the project.

mkdir aks-pulumi-demo
cd aks-pulumi-demo

Run the following command to create a new Pulumi project.

pulumi new azure-go \
--name aks-pulumi-demo \
--description "sample aks deployment with pulumi" \
--stack dev \
--secrets-provider default

Here is a breakdown of the command:

  • pulumi new azure-go creates a new Pulumi project using the Azure Go template
  • --name sets the name of the project to aks-pulumi-demo
  • --description sets the description of the project
  • --stack sets the stack name to dev; think of stack as a way to manage different environments like dev, staging, and production
  • --secrets-provider sets the secrets provider to default which uses the local file to store secrets; you can use other providers like default, passphrase, awskms, azurekeyvault, gcpkms, or hashivault

You will be prompted to enter a passphrase to encrypt the secrets. You can leave it blank for now and hit enter to proceed.

You will also be prompted to select a region to deploy the resources. Just be careful here and make sure you set the location to a region that supports all the Azure resources you intend to deploy. You can refer to the Azure documentation for more information on product availability by region. I’ll use swedencentral for this demo.

As noted in the Azure docs, AKS Automatic tries to dynamically select a virtual machine SKU for the system node pool based on the capacity available in the subscription. Make sure your subscription has quota for 16 vCPUs of any of the following SKUs in the region you’re deploying the cluster to: Standard_D4pds_v5, Standard_D4lds_v5, Standard_D4ads_v5, Standard_D4ds_v5, Standard_D4d_v5, Standard_D4d_v4, Standard_DS3_v2, Standard_DS12_v2. You can view quotas for specific VM-families and submit quota increase requests through the Azure portal.

You can also set the location by running the following command.

pulumi config set azure-native:location swedencentral

Setup main.go file

At this point you should have a new Go project with a main.go file and some sample code.

Here is what the directory structure should look like:

.
├── go.mod
├── go.sum
├── main.go
├── Pulumi.dev.yaml
└── Pulumi.yaml

1 directory, 5 files

Open the folder with VSCode and open the main.go file. Here you will see sample code to create an Azure storage account and export its keys. Delete the sample code and replace it with the code below to create an Azure resource group.

package main

import (
	"github.com/pulumi/pulumi-azure-native-sdk/resources/v2"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// Create an Azure Resource Group
		resourceGroup, err := resources.NewResourceGroup(ctx, "resourceGroup", nil)
		if err != nil {
			return err
		}

		return nil
	})
}

This code will import the Pulumi SDK and Pulumi Azure Native SDK to create resource groups and sets up the main function to make a pulumi.Run function call to run the Pulumi program. This function accepts a RunFunc type which is where you define the body of the Pulumi program to run.

You’ve just added code to create an Azure resource group using the resources.NewResourceGroup function. This function is available via the resources module in the Azure Native SDK. The NewResourceGroup function accepts a pulumi.Context and a string to set the name of the resource group object.

It is important to note that the value of resourceGroup in the NewResourceGroup function above is not the actual name of the resource group that will be created in Azure. It’s the name of the object within the Pulumi state file. More on that in a bit…

If you are using VSCode with the Go extension, you might notice a squiggly line under the resourceGroup variable. That’s because Go does not like it when variables are declared but not used. To fix this, we can export the value of the resource group by adding the following code just before the return nil line inside the pulumi.Run function. This will allow us to see the actual resource group name once the deployment is complete.

Add the following code to the code fille.

ctx.Export("resourceGroupName", pulumi.All(resourceGroup.Name).ApplyT(
	func(args []interface{}) string {
		return args[0].(string)
	}))

Running Pulumi programs

We now have a basic Pulumi program that creates an Azure resource group and exports the value of the resource group name. Let’s deploy the program and see the output.

Save the main.go file then open the VSCode terminal and run the following command to provision the infrastructure.

pulumi up

I like to deploy as I go, but you can also run pulumi preview to see what resources will be created without actually creating them. This is useful for checking your work before deploying.

You will be prompted to enter a passphrase to decrypt the secrets. If you left it blank earlier, you can just hit enter to proceed.

Enter your passphrase to unlock config/secrets
    (set PULUMI_CONFIG_PASSPHRASE or PULUMI_CONFIG_PASSPHRASE_FILE to remember):
Enter your passphrase to unlock config/secrets
Previewing update (dev):
     Type                                     Name                 Plan
 +   pulumi:pulumi:Stack                      aks-pulumi-demo-dev  create
 +   └─ azure-native:resources:ResourceGroup  resourceGroup        create

Outputs:
    resourceGroupName: output<string>

Resources:
    + 2 to create

Within a few seconds you will be prompted to perform the update. Use the arrow keys to select yes and hit enter to confirm you want to deploy Azure resources.

Do you want to perform this update?  [Use arrows to move, type to filter]
> yes
  no
  details

Once the deployment is complete, you will see the outputs of the resources that were created. Here is what the output should look like:

Updating (dev):
     Type                                     Name                 Status
 +   pulumi:pulumi:Stack                      aks-pulumi-demo-dev  created (1s)
 +   └─ azure-native:resources:ResourceGroup  resourceGroup        created (0.93s)

Outputs:
    resourceGroupName: "resourceGroup3f007f94"

Resources:
    + 2 created

Duration: 3s

Randomizing resource names

Did you notice that the resource group name was randomized by Pulumi? It did so by taking the name of the resourceGroup object and appending some random text to the end. If you have naming conventions that you like to follow you will want to set the actual resource group name yourself. To do this, you can pass in a resources.ResourceGroupArgs struct to the NewResourceGroup function. This struct has fields that you can set to control the resource group name and other properties of the resource group.

I don’t have strict naming conventions to follow so I often randomize resource names myself to meet global naming requirements for some Azure resources. To implement my own randomization, I’ll use Pulumi’s random provider to generate a random integer then pass in the name that we want the resource group name to use.

Add the following import statement to the top of the file to import the random package.

"github.com/pulumi/pulumi-random/sdk/v4/go/random"

Pulumi docs are your friend. You can find the documentation for any resource and find the latest version of the provider to use by viewing the code sample on the provider’s page. For example, the RandomInteger resource page will show you the proper import path to use in the example code block.

Next, add this code to the top of the pulumi.Run function, just above the resource group creation block.

// Create a 4 digit random integer to be used for resource names
randomInt, _ := random.NewRandomInteger(ctx, "randomInt", &random.RandomIntegerArgs{
	Min: pulumi.Int(1000),
	Max: pulumi.Int(9999),
})

Now we have a random integer that we can use to generate unique resource names.

Update the resource group creation block to include a new resources.ResourceGroupArgs struct that will set the ResourceGroupName field using the random integer to create a unique name. Here is the updated code to create the resource group.

// Create an Azure Resource Group
resourceGroup, err := resources.NewResourceGroup(ctx, "resourceGroup", &resources.ResourceGroupArgs{
	ResourceGroupName: pulumi.Sprintf("rg-akspulumidemo%v", randomInt.Result),
})
if err != nil {
	return err
}

Save the main.go file then run the following command in a terminal to install any missing dependencies.

pulumi install

Now run the following command to update the infrastructure.

pulumi up

You can run export PULUMI_CONFIG_PASSPHRASE="" to avoid entering the passphrase every time you run pulumi up.

After you confirm the changes, you will see the output of the deployment and see that the original resource group was replaced with a new one since renaming resources is “not a thing” in Azure. Here is what the output should look like:

Previewing update (dev):
     Type                                     Name                 Plan        Info
     pulumi:pulumi:Stack                      aks-pulumi-demo-dev
 +   ├─ random:index:RandomInteger            randomInt            create
 +-  └─ azure-native:resources:ResourceGroup  resourceGroup        replace     [diff: ~resourceGroupName]

Resources:
    + 1 to create
    +-1 to replace
    2 changes. 1 unchanged

Do you want to perform this update? yes
Updating (dev):
     Type                                     Name                 Status              Info
     pulumi:pulumi:Stack                      aks-pulumi-demo-dev
 +   ├─ random:index:RandomInteger            randomInt            created (0.00s)
 +-  └─ azure-native:resources:ResourceGroup  resourceGroup        replaced (16s)      [diff: ~resourceGroupName]

Outputs:
  ~ resourceGroupName: "resourceGroup3f007f94" => "rg-akspulumidemo8622"

Resources:
    + 1 created
    +-1 replaced
    2 changes. 1 unchanged

Duration: 19s

Great job! We now have a resource group with a custom and unique name. Let’s move on to creating an a few more resources before we deploy the AKS cluster.

Deploy a container registry

With the resource group in place, we can now add other resources based on the workload requirements. I don’t have any specific requirements for this demo, but I’ll add an Azure Container Registry to show you how you can attach the registry to the AKS cluster.

Add the following import statement to the top of the file to import the azure-native.containerregistry package.

"github.com/pulumi/pulumi-azure-native-sdk/containerregistry/v2"

Next, add the following code in the main function to create an Azure Container Registry and export the name of the registry.

// Create Azure Container Registry
containerRegistry, err := containerregistry.NewRegistry(ctx, "containerRegistry", &containerregistry.RegistryArgs{
	ResourceGroupName: resourceGroup.Name,
	RegistryName:      pulumi.Sprintf("acrakspulumidemo%v", randomInt.Result),
	Sku: &containerregistry.SkuArgs{
		Name: pulumi.String("Standard"),
	},
})
if err != nil {
	return err
}

ctx.Export("containerRegistry", pulumi.All(containerRegistry.Name).ApplyT(
	func(args []interface{}) string {
		return args[0].(string)
	}))

Whether you keep all the resource creation code together and all the export code together is up to you. I like to keep the resource creation code together and the export code together so I can easily see what resources are being created and what values are being exported. Feel free to organize the code in a way that makes sense to you.

Deploy observability tools

You may also want to add observability resources like Azure Log Analytics Workspace for application logging, Azure Monitor Workspace for Prometheus metrics, and Azure Managed Grafana to visualize resources in your AKS cluster.

Add Azure Log Analytics Workspace

Add the following import statement to the top of the file to import the azure-native.operationalinsights package.

"github.com/pulumi/pulumi-azure-native-sdk/operationalinsights/v2"

Next, add the following code to create an Azure Log Analytics Workspace and export the name of the workspace.

// Create Azure Log Analytics Workspace for Container Insights
logAnalyticsWorkspace, err := operationalinsights.NewWorkspace(ctx, "logAnalyticsWorkspace", &operationalinsights.WorkspaceArgs{
	ResourceGroupName: resourceGroup.Name,
	RetentionInDays:   pulumi.Int(30),
	Sku: &operationalinsights.WorkspaceSkuArgs{
		Name: pulumi.String("PerGB2018"),
	},
	WorkspaceName: pulumi.Sprintf("law-akspulumidemo%v", randomInt.Result),
})
if err != nil {
	return err
}

ctx.Export("logAnalyticsWorkspace", pulumi.All(logAnalyticsWorkspace.Name).ApplyT(
	func(args []interface{}) string {
		return args[0].(string)
	}))

This code will create an Azure Log Analytics Workspace with a retention period of 30 days and export the name of the workspace. We’ll also randomize the name of the workspace to meet Azure’s global naming requirements.

Add Azure Managed Prometheus

Add the following import statement to the top of the file to import the azure-native.monitor package.

"github.com/pulumi/pulumi-azure-native-sdk/monitor/v2"

Now add the following code to create an Azure Monitor Workspace and export the name of the workspace.

// Create Azure Monitor Workspace for managed Prometheus
azureMonitorWorkspace, err := monitor.NewAzureMonitorWorkspace(ctx, "azureMonitorWorkspace", &monitor.AzureMonitorWorkspaceArgs{
	ResourceGroupName:         resourceGroup.Name,
	AzureMonitorWorkspaceName: pulumi.Sprintf("prom-akspulumidemo%v", randomInt.Result),
})
if err != nil {
	return err
}

ctx.Export("azureMonitorWorkspace", pulumi.All(azureMonitorWorkspace.Name).ApplyT(
	func(args []interface{}) string {
		return args[0].(string)
	}))

This code will create an Azure Monitor Workspace for managed Prometheus and export the name of the workspace.

Add Azure Managed Grafana

Add the following import statement to the top of the file to import the azure-native.dashboard package.

"github.com/pulumi/pulumi-azure-native-sdk/dashboard/v2"

Then add the following code to create an Azure Managed Grafana resource with Azure Monitor Workspace integration, and export the name of the dashboard.

// Create Azure Managed Grafana with Azure Monitor Workspace integration
grafanaDashboard, err := dashboard.NewGrafana(ctx, "grafanaDashboard", &dashboard.GrafanaArgs{
	Identity: &dashboard.ManagedServiceIdentityArgs{
		Type: pulumi.String("SystemAssigned"),
	},
	Properties: dashboard.ManagedGrafanaPropertiesArgs{
		ApiKey:              pulumi.String("Enabled"),
		PublicNetworkAccess: pulumi.String("Enabled"),
		GrafanaIntegrations: dashboard.GrafanaIntegrationsArgs{
			AzureMonitorWorkspaceIntegrations: dashboard.AzureMonitorWorkspaceIntegrationArray{
				&dashboard.AzureMonitorWorkspaceIntegrationArgs{
					AzureMonitorWorkspaceResourceId: azureMonitorWorkspace.ID(),
				},
			},
		},
	},
	ResourceGroupName: resourceGroup.Name,
	Sku: &dashboard.ResourceSkuArgs{
		Name: pulumi.String("Standard"),
	},
	WorkspaceName: pulumi.Sprintf("graf-akspulumidemo%v", randomInt.Result),
})
if err != nil {
	return err
}

ctx.Export("grafanaDashboard", pulumi.All(grafanaDashboard.Name).ApplyT(
	func(args []interface{}) string {
		return args[0].(string)
	}))

This code will create an Azure Managed Grafana with a system-assigned managed identity and Azure Monitor Workspace integration and export the name of the dashboard.

Provision Azure resources

That should do it for the observability resources. Let’s save the file and run the program to deploy what we have so far.

Run the following commands to update the infrastructure.

pulumi install
pulumi up

Once the deployment is complete, you will see the outputs of the new resources that were added. Here is what the output should look like:

Previewing update (dev):
     Type                                           Name                   Plan
     pulumi:pulumi:Stack                            aks-pulumi-demo-dev
 +   ├─ azure-native:monitor:AzureMonitorWorkspace  azureMonitorWorkspace  create
 +   ├─ azure-native:containerregistry:Registry     containerRegistry      create
 +   ├─ azure-native:dashboard:Grafana              grafanaDashboard       create
 +   └─ azure-native:operationalinsights:Workspace  logAnalyticsWorkspace  create

Outputs:
  + azureMonitorWorkspace: output<string>
  + containerRegistry    : output<string>
  + grafanaDashboard     : output<string>
  + logAnalyticsWorkspace: output<string>

Resources:
    + 4 to create
    3 unchanged

Do you want to perform this update? yes
Updating (dev):
     Type                                           Name                   Status
     pulumi:pulumi:Stack                            aks-pulumi-demo-dev
 +   ├─ azure-native:containerregistry:Registry     containerRegistry      created (11s)
 +   ├─ azure-native:operationalinsights:Workspace  logAnalyticsWorkspace  created (14s)
 +   ├─ azure-native:monitor:AzureMonitorWorkspace  azureMonitorWorkspace  created (12s)
 +   └─ azure-native:dashboard:Grafana              grafanaDashboard       created (139s)

Outputs:
  + azureMonitorWorkspace: "prom-akspulumidemo4266"
  + containerRegistry    : "acrakspulumidemo4266"
  + grafanaDashboard     : "graf-akspulumidemo4266"
  + logAnalyticsWorkspace: "law-akspulumidemo4266"
    resourceGroupName    : "rg-akspulumidemo4266"

Resources:
    + 4 created
    3 unchanged

Duration: 2m35s

Deploy AKS Automatic cluster

Now, the moment you’ve been waiting for! Let’s add the AKS Automatic cluster.

Add the following import statement to the top of the file to import the azure-native.containerservice package.

"github.com/pulumi/pulumi-azure-native-sdk/containerservice/v2"

Next, add the following code to create an Azure Kubernetes Service Automatic cluster and export the name of the cluster.

// Create AKS Automatic cluster
managedCluster, err := containerservice.NewManagedCluster(ctx, "managedCluster", &containerservice.ManagedClusterArgs{
	ResourceGroupName: resourceGroup.Name,
	ResourceName:      pulumi.Sprintf("aks-%v", randomInt.Result),
	Sku: &containerservice.ManagedClusterSKUArgs{
		Name: pulumi.String("Automatic"),
		Tier: pulumi.String("Standard"),
	},
	AgentPoolProfiles: containerservice.ManagedClusterAgentPoolProfileArray{
		&containerservice.ManagedClusterAgentPoolProfileArgs{
			Mode:   pulumi.String("System"),
			Name:   pulumi.String("systempool"),
			VmSize: pulumi.String("Standard_D4pds_v6"),
			Count:  pulumi.Int(3),
		},
	},
	AddonProfiles: containerservice.ManagedClusterAddonProfileMap{
		"omsagent": &containerservice.ManagedClusterAddonProfileArgs{
			Enabled: pulumi.Bool(true),
			Config: pulumi.StringMap{
				"logAnalyticsWorkspaceResourceID": logAnalyticsWorkspace.ID(),
				"useAADAuth":                      pulumi.String("true"),
			},
		},
	},
	AzureMonitorProfile: containerservice.ManagedClusterAzureMonitorProfileArgs{
		Metrics: containerservice.ManagedClusterAzureMonitorProfileMetricsArgs{
			Enabled: pulumi.Bool(true),
			KubeStateMetrics: containerservice.ManagedClusterAzureMonitorProfileKubeStateMetricsArgs{
				MetricAnnotationsAllowList: pulumi.String(""),
				MetricLabelsAllowlist:      pulumi.String(""),
			},
		},
	},
	Identity: &containerservice.ManagedClusterIdentityArgs{
		Type: containerservice.ResourceIdentityTypeSystemAssigned,
	},
})
if err != nil {
	return err
}

ctx.Export("aksName", pulumi.All(managedCluster.Name).ApplyT(
	func(args []interface{}) string {
		return args[0].(string)
	}))

This code will create an AKS Automatic cluster with the following configuration:

  • Sets the SKU and Tier to Automatic and Standard respectively
  • Adds a system pool with 3 nodes of size Standard_D4pds_v6
  • Enables the Azure Monitor Addon with Log Analytics Workspace integration for Container Insights
  • Enables the Azure Monitor Profile with metrics enabled for Prometheus

As noted above, AKS Automatic can automatically select the VM size based on its requirements and SKU availability in the region you selected. If you have specific VM requirements, you can set the VmSize field to the desired SKU but it’s not required for AKS Automatic.

Let’s save the file and run the following command to update the infrastructure.

pulumi install
pulumi up

After about 10-15 minutes, the deployment should be complete and you will see the output of the AKS cluster name. Here is what the output should look like:

Previewing update (dev):
     Type                                             Name                 Plan       
     pulumi:pulumi:Stack                              aks-pulumi-demo-dev             
 +   └─ azure-native:containerservice:ManagedCluster  managedCluster       create     

Outputs:
  + aksName              : output<string>

Resources:
    + 1 to create
    7 unchanged

Do you want to perform this update? yes
Updating (dev):
     Type                                             Name                 Status             
     pulumi:pulumi:Stack                              aks-pulumi-demo-dev                     
 +   └─ azure-native:containerservice:ManagedCluster  managedCluster       created (767s)     

Outputs:
  + aksName              : "aks-4266"
    azureMonitorWorkspace: "prom-akspulumidemo4266"
    containerRegistry    : "acrakspulumidemo4266"
    grafanaDashboard     : "graf-akspulumidemo4266"
    logAnalyticsWorkspace: "law-akspulumidemo4266"
    resourceGroupName    : "rg-akspulumidemo4266"

Resources:
    + 1 created
    7 unchanged

Duration: 12m50s

Deploy role assignments

At this point, all the resources are in place, but now we need to add a bunch of role permissions like AcrPull for the Azure Container Registry, Azure Monitor Reader for the Azure Monitor Workspace, and give your user access to the AKS cluster and Azure Managed Grafana.

Add the following import statement to the top of the file to import the azure-native.authorization package.

"github.com/pulumi/pulumi-azure-native-sdk/authorization/v2"

Grant cluster permissions for pulling images

Granting an AKS cluster access to an Azure Container Registry has always been a little tricky. Since the kubelet is the one pulling images from the registry, we need to assign the AcrPull role to the kubelet’s principal ID which is available once the AKS cluster is created.

Add the following code to retrieve the kubelet’s principal ID.

// Get the kubelet's principal ID for AcrPull role assignment
kubeletPrincipalId := managedCluster.IdentityProfile.MapIndex(pulumi.String("kubeletidentity")).ObjectId()

Next add the following code to create the AcrPull role assignment for the kubelet.

// Create a role assignment so that the kubelet can pull images from ACR
_, err = authorization.NewRoleAssignment(ctx, "kubeletRoleAssignment", &authorization.RoleAssignmentArgs{
	PrincipalId:      kubeletPrincipalId.Elem().ToStringOutput(),
	RoleDefinitionId: pulumi.String("/providers/Microsoft.Authorization/roleDefinitions/7f951dda-4ed3-4680-a7ca-43fe172d538d"),
	Scope:            containerRegistry.ID(),
	PrincipalType:    pulumi.String("ServicePrincipal"),
})
if err != nil {
	return err
}

Grant permissions for monitoring

In order to view cluster metrics from the Azure Managed Grafana, the Monitoring Reader role must be assigned to the system-assigned managed identity of the Azure Managed Grafana resource.

Add the following code to create the Azure Monitor Reader role assignment.

// Create a role assignment so that Azure Managed Grafana can query the Azure Monitor Workspace and Log Analytics Workspace
_, err = authorization.NewRoleAssignment(ctx, "azureMonitorWorkspaceRoleAssignment1", &authorization.RoleAssignmentArgs{
	PrincipalId:      grafanaDashboard.Identity.Elem().PrincipalId(),
	RoleDefinitionId: pulumi.String("/providers/Microsoft.Authorization/roleDefinitions/43d0d8ad-25c7-4714-9337-8ba259a9fe05"),
	Scope:            resourceGroup.ID(),
	PrincipalType:    pulumi.String("ServicePrincipal"),
})
if err != nil {
	return err
}

Grant yourself permissions for access

Finally, you’ll need to grant yourself access to the AKS cluster and Azure Managed Grafana.

Add the following code to retrieve the your user principal ID.

// Get current user principal
client, err := authorization.GetClientConfig(ctx, pulumi.CompositeInvoke())
if err != nil {
	return err
}

The AKS Automatic cluster authorization is managed by Microsoft Entra ID, so we need to assign the Azure Kubernetes Service RBAC Cluster Admin role to your user principal ID. Without this, you would not be able to run kubectl commands against the AKS cluster.

// Create a role assignment so I can access the kubeapiserver
_, err = authorization.NewRoleAssignment(ctx, "managedClusterRoleAssignment", &authorization.RoleAssignmentArgs{
	PrincipalId:      pulumi.String(client.ObjectId),
	RoleDefinitionId: pulumi.String("/providers/Microsoft.Authorization/roleDefinitions/b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b"),
	Scope:            managedCluster.ID(),
	PrincipalType:    pulumi.String("User"),
})
if err != nil {
	return err
}

Finally, give yourself access to the Azure Monitor Workspace and Azure Managed Grafana.

// Create a role assignment so I can query the Azure Monitor Workspace
_, err = authorization.NewRoleAssignment(ctx, "azureMonitorWorkspaceRoleAssignment2", &authorization.RoleAssignmentArgs{
	PrincipalId:      pulumi.String(client.ObjectId),
	RoleDefinitionId: pulumi.String("/providers/Microsoft.Authorization/roleDefinitions/43d0d8ad-25c7-4714-9337-8ba259a9fe05"),
	Scope:            azureMonitorWorkspace.ID(),
	PrincipalType:    pulumi.String("User"),
})
if err != nil {
	return err
}

// Create a role assignment so I can access Azure Managed Grafana dashboards
_, err = authorization.NewRoleAssignment(ctx, "grafanaRoleAssignment", &authorization.RoleAssignmentArgs{
	PrincipalId:      pulumi.String(client.ObjectId),
	RoleDefinitionId: pulumi.String("/providers/Microsoft.Authorization/roleDefinitions/22926164-76b3-42b3-bc55-97df8dab3e41"),
	Scope:            grafanaDashboard.ID(),
	PrincipalType:    pulumi.String("User"),
})
if err != nil {
	return err
}

Provision role assignments

Now that we have all the role assignments in place, let’s save the file and run the following command to update the infrastructure one last time.

pulumi install
pulumi up

After a few seconds, the deployment should be complete and you will see the output of the role assignments. Here is what the output should look like:

Previewing update (dev):
     Type                                          Name                                  Plan       
     pulumi:pulumi:Stack                           aks-pulumi-demo-dev                              
 +   ├─ azure-native:authorization:RoleAssignment  azureMonitorWorkspaceRoleAssignment1  create     
 +   ├─ azure-native:authorization:RoleAssignment  kubeletRoleAssignment                 create     
 +   ├─ azure-native:authorization:RoleAssignment  managedClusterRoleAssignment          create     
 +   ├─ azure-native:authorization:RoleAssignment  azureMonitorWorkspaceRoleAssignment2  create     
 +   └─ azure-native:authorization:RoleAssignment  grafanaRoleAssignment                 create     

Resources:
    + 5 to create
    8 unchanged

Do you want to perform this update? yes
Updating (dev):
     Type                                          Name                                  Status           
     pulumi:pulumi:Stack                           aks-pulumi-demo-dev                                    
 +   ├─ azure-native:authorization:RoleAssignment  azureMonitorWorkspaceRoleAssignment1  created (3s)     
 +   ├─ azure-native:authorization:RoleAssignment  kubeletRoleAssignment                 created (4s)     
 +   ├─ azure-native:authorization:RoleAssignment  grafanaRoleAssignment                 created (3s)     
 +   ├─ azure-native:authorization:RoleAssignment  azureMonitorWorkspaceRoleAssignment2  created (3s)     
 +   └─ azure-native:authorization:RoleAssignment  managedClusterRoleAssignment          created (3s)     

Outputs:
    aksName              : "aks-4266"
    azureMonitorWorkspace: "prom-akspulumidemo4266"
    containerRegistry    : "acrakspulumidemo4266"
    grafanaDashboard     : "graf-akspulumidemo4266"
    logAnalyticsWorkspace: "law-akspulumidemo4266"
    resourceGroupName    : "rg-akspulumidemo4266"

Resources:
    + 5 created
    8 unchanged

Duration: 7s

Test the deployment

Now that you’ve given yourself the proper permissions to the Azure resources, you can test access to the AKS cluster using the kubectl command line tool.

If you don’t have kubectl installed, you can to install it using the az aks install-cli command.

To configure kubectl to use the AKS cluster, run the following command.

export PULUMI_CONFIG_PASSPHRASE="" #<-- set this to whatever you set when you created the stack
az aks get-credentials -n $(pulumi stack output aksName) -g $(pulumi stack output resourceGroupName) 

You can now run kubectl commands against the AKS cluster. For example, you can run the following command to get the status of the nodes in the cluster.

kubectl cluster-info

You should be presented with a URL to authenticate with Microsoft Entra ID. Once you authenticate, you can run the following command to get the status of the nodes in the cluster.

From there, feel free to browse to the Azure Portal and check out the resources that were created. You can also access the Azure Managed Grafana dashboard to view the metrics of the AKS cluster.

What’s next?

This was a long but simple example of deploying an AKS Automatic cluster with Pulumi. You can extend this example by adding more resources like Prometheus data collection endpoints, rule groups, and alerts. I won’t cover that in this article because it would make this article way too long, but you can view the code sample in my GitHub repo here.

Clean up

After you’re done with the resources, delete them by running the following command.

pulumi destroy

After the resources are deleted, remove the stack by running the following command.

pulumi stack rm dev

Finally, remove the Pulumi project by running the following command.

rm -rf ~/.pulumi/.pulumi/stacks/aks-pulumi-demo*

Summary

In this article, I showed you how to deploy an AKS Automatic cluster with Pulumi. I used the Pulumi Go SDK and the Azure Native provider to create Azure resources like resource groups, Azure Container Registry, Azure Log Analytics Workspace, Azure Monitor Workspace, and Azure Managed Grafana.

Working with Pulumi to deploy Azure resources is a good alternative to using the other IaC tools out there like Terraform and Azure Bicep. Pulumi allows you to use your favorite programming language to create resources for your workload. The pattern of creating resources and exporting their values is the same for all resources as shown in the example above.

It is also worth nothing that Terraform also offers a Cloud Development Kit (CDK) that allows you to use your favorite programming language to create resources. Let me know in the comments if you would like to see a similar example using Terraform CDK.

Until then check out some of the resources below and let me know if you have any questions

Happy coding!

Resources