Provisioning Azure Kubernetes Service (AKS) with Pulumi
Learn how to provision an AKS cluster with Pulumi and TypeScript, including a system-assigned identity and retrieving a working kubeconfig
Provisioning Azure Kubernetes Service (AKS) with Pulumi
This guide provisions an AKS cluster with Pulumi, building on the VNet guide - a system-assigned managed identity, a node pool inside the private subnet, and retrieving a working kubeconfig as a stack output.
The Cluster
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as containerservice from "@pulumi/azure-native/containerservice";
const cluster = new containerservice.ManagedCluster("aks", {
resourceGroupName: resourceGroup.name,
location: location,
resourceName: "app-aks-cluster",
dnsPrefix: "appaks",
kubernetesVersion: "1.30",
identity: {
type: containerservice.ResourceIdentityType.SystemAssigned,
},
agentPoolProfiles: [{
name: "default",
count: 3,
vmSize: "Standard_DS2_v2",
mode: containerservice.AgentPoolMode.System,
osType: containerservice.OSType.Linux,
vnetSubnetID: privateSubnet.id,
}],
networkProfile: {
networkPlugin: containerservice.NetworkPlugin.Azure,
serviceCidr: "10.1.0.0/16",
dnsServiceIP: "10.1.0.10",
},
});
identity.type: SystemAssigned has AKS create and manage its own Azure AD identity for calling other Azure APIs (attaching disks, provisioning load balancers) - the modern default, replacing the older pattern of manually creating a service principal and passing its credentials in. agentPoolProfiles[0].mode: System marks this as the required system node pool every cluster needs for core cluster services; additional mode: User pools can be added later for application workloads that need different sizing or isolation.
networkProfile.serviceCidr/dnsServiceIP must not overlap with the VNet’s own address space (10.0.0.0/16 from the VNet guide) - this is the Kubernetes-internal service network, entirely separate from the Azure VNet address space the nodes themselves live in.
Retrieving a Working Kubeconfig
const credentials = containerservice.listManagedClusterUserCredentialsOutput({
resourceGroupName: resourceGroup.name,
resourceName: cluster.name,
});
export const clusterName = cluster.name;
export const kubeconfig = credentials.kubeconfigs[0].value.apply(
encoded => Buffer.from(encoded, "base64").toString("utf-8")
);
The credentials API returns the kubeconfig base64-encoded inside kubeconfigs[0].value - .apply() decodes it into the plain YAML text you’d actually write to a file. This is the same “call an Output-returning API, then .apply() to transform the result” pattern used for the AWS Lambda IAM policy ARN and the Azure Functions storage connection string elsewhere in this series.
pulumi stack output kubeconfig --show-secrets > kubeconfig.yaml
KUBECONFIG=kubeconfig.yaml kubectl get nodes
Best Practices
- Pin
kubernetesVersionexplicitly and bump it deliberately - AKS auto-upgrades to a default version if you omit it, which can land a minor version bump you didn’t plan for on a routinepulumi up. - Use a
User-mode node pool for application workloads, keeping theSystem-mode pool reserved for cluster-critical pods - this is AKS’s own recommended separation, not just a Pulumi convention. - Mark
kubeconfigas sensitive in how you handle it downstream - it’s a full-access credential to the cluster;pulumi stack output --show-secretsis required to read it precisely because Pulumi already treats credential-shaped outputs from this API as secret by default.
Conclusion
Compare this to the EKS guide: AKS’s ManagedCluster resource is closer to raw aws.eks.Cluster than to the higher-level @pulumi/eks component - there’s no equivalent “one call creates everything, including its own VPC” AKS package, so building on an existing VNet the way this guide does is the normal pattern, not an advanced option.
For more Pulumi topics, check out: