Deploying AKS Automatic with Pulumi: A step-by-step guide
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:
- Azure CLI version 2.67.0 or later
- Pulumi CLI version 3.143.0 or later
- Go version 1.23.4 or later
- VSCode version 1.96.2 or later
- Go extension for VSCode version 0.45.0 or later
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 toaks-pulumi-demo
--description
sets the description of the project--stack
sets the stack name todev
; 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 theNewResourceGroup
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 runpulumi 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 theaz 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!