| title | How to Provision a Kubernetes Cluster with Infrastructure as Code | ||||||
|---|---|---|---|---|---|---|---|
| date | 2026-08-24 | ||||||
| draft | false | ||||||
| meta_desc | How to provision a Kubernetes cluster with infrastructure as code on EKS, AKS, and GKE using real Pulumi examples. | ||||||
| authors |
|
||||||
| tags |
|
||||||
| category | general | ||||||
| faq_schema | true | ||||||
| howto_schema | true | ||||||
| estimated_time | 20 | ||||||
| related_posts |
|
||||||
| social |
|
The best way to provision a Kubernetes cluster is with infrastructure as code: define the cluster in a real programming language, run one command to create it, and keep that definition in version control so every change is reviewable and repeatable. Pulumi does this for EKS, AKS, and GKE in Python, TypeScript, Go, C#, or Java, no YAML templates required.
The best way to provision a Kubernetes cluster is with infrastructure as code, rather than clicking through a cloud console or hand-writing YAML. Infrastructure as code lets you define the cluster, its node pools, and its networking in a single program, review changes before they apply, and reproduce the same cluster in another region or account. Pulumi supports this pattern natively for EKS, AKS, and GKE.
Provisioning a managed Kubernetes cluster with Pulumi follows the same shape regardless of cloud. Before starting, you need a Pulumi CLI installation, an account with the cloud you're targeting (an AWS account, an Azure account, or a Google Cloud account), and the runtime for your chosen language installed — Node.js and npm for the TypeScript examples below, or Python for the Python ones.
- Install the Pulumi CLI and run
pulumi newto scaffold a project, choosing the template for your target cloud and language. - Authenticate the Pulumi provider against your cloud account using its normal credential mechanism, such as environment variables or a CLI login.
- Declare the cluster resource in your program: an
eks.Clusterfor AWS, acontainerservice.ManagedClusterfor Azure, or agcp.container.Clusterfor Google Cloud. - Configure the cluster's size and node type through the resource's arguments rather than a separate config file.
- Run
pulumi previewto see exactly which cloud resources will be created before anything changes. - Run
pulumi upto provision the cluster; Pulumi waits until the control plane and nodes are ready. - Export the cluster's
kubeconfig(or assemble it from the cluster's outputs, on GKE) sokubectland Pulumi's own Kubernetes provider can reach it. - Commit the program to version control so the cluster's definition, not just its running state, is reviewable by your team.
The @pulumi/eks package wraps the VPC, IAM roles, and node group an EKS cluster needs into a single Cluster component, so a working cluster takes only a few lines:
{{< chooser language "typescript,python" / >}}
{{% choosable language "typescript" %}}
import * as eks from "@pulumi/eks";
const cluster = new eks.Cluster("eks-cluster", {
desiredCapacity: 2,
minSize: 1,
maxSize: 3,
instanceType: "t3.medium",
});
export const kubeconfig = cluster.kubeconfig;{{% /choosable %}}
{{% choosable language "python" %}}
import pulumi
import pulumi_eks as eks
cluster = eks.Cluster("eks-cluster",
desired_capacity=2,
min_size=1,
max_size=3,
instance_type="t3.medium")
pulumi.export("kubeconfig", cluster.kubeconfig){{% /choosable %}}
instanceType describes the default managed node group; a fleet with mixed instance types uses a separate node group resource instead. EKS Auto Mode, covered in our EKS Auto Mode post, is a newer alternative that lets AWS manage node provisioning entirely, but the default node group above remains the registry's canonical starting point.
AKS clusters go through @pulumi/azure-native's ManagedCluster resource. Credentials for kubectl come back from a separate call that must be decoded from base64:
{{< chooser language "typescript,python" / >}}
{{% choosable language "typescript" %}}
import * as containerservice from "@pulumi/azure-native/containerservice";
import * as resources from "@pulumi/azure-native/resources";
const resourceGroup = new resources.ResourceGroup("rg");
const cluster = new containerservice.ManagedCluster("aks-cluster", {
resourceGroupName: resourceGroup.name,
agentPoolProfiles: [{
count: 3,
mode: "System",
name: "agentpool",
osType: "Linux",
type: "VirtualMachineScaleSets",
vmSize: "Standard_DS2_v2",
}],
dnsPrefix: resourceGroup.name,
enableRBAC: true,
identity: { type: "SystemAssigned" },
});
const creds = containerservice.listManagedClusterUserCredentialsOutput({
resourceGroupName: resourceGroup.name,
resourceName: cluster.name,
});
export const kubeconfig = creds.kubeconfigs[0].value
.apply(enc => Buffer.from(enc, "base64").toString());{{% /choosable %}}
{{% choosable language "python" %}}
import base64
import pulumi
from pulumi_azure_native import containerservice, resources
resource_group = resources.ResourceGroup("rg")
cluster = containerservice.ManagedCluster("aks-cluster",
resource_group_name=resource_group.name,
agent_pool_profiles=[{
"count": 3,
"mode": "System",
"name": "agentpool",
"os_type": "Linux",
"type": "VirtualMachineScaleSets",
"vm_size": "Standard_DS2_v2",
}],
dns_prefix=resource_group.name,
enable_rbac=True,
identity={"type": "SystemAssigned"})
creds = containerservice.list_managed_cluster_user_credentials_output(
resource_group_name=resource_group.name,
resource_name=cluster.name)
def decode(kubeconfigs):
return base64.b64decode(kubeconfigs[0]["value"]).decode()
pulumi.export("kubeconfig", creds.kubeconfigs.apply(decode)){{% /choosable %}}
Azure returns the kubeconfig base64-encoded from listManagedClusterUserCredentialsOutput, so decoding it is a required step, not an optional cleanup pass.
GKE splits cluster and node pool into two resources in @pulumi/gcp, and it defaults deletionProtection to true, which must be set explicitly for a cluster you intend to tear down later:
{{< chooser language "typescript,python" / >}}
{{% choosable language "typescript" %}}
import * as gcp from "@pulumi/gcp";
const cluster = new gcp.container.Cluster("gke-cluster", {
initialNodeCount: 1,
removeDefaultNodePool: true,
deletionProtection: false,
});
const nodePool = new gcp.container.NodePool("primary", {
cluster: cluster.name,
location: cluster.location,
initialNodeCount: 2,
nodeConfig: { machineType: "e2-medium" },
}, { dependsOn: [cluster] });
export const clusterName = cluster.name;{{% /choosable %}}
{{% choosable language "python" %}}
import pulumi
from pulumi_gcp import container
cluster = container.Cluster("gke-cluster",
initial_node_count=1,
remove_default_node_pool=True,
deletion_protection=False)
node_pool = container.NodePool("primary",
cluster=cluster.name,
location=cluster.location,
initial_node_count=2,
node_config={"machine_type": "e2-medium"},
opts=pulumi.ResourceOptions(depends_on=[cluster]))
pulumi.export("cluster_name", cluster.name){{% /choosable %}}
There is no @pulumi/gke package and no built-in .kubeconfig output the way @pulumi/eks provides one; a GKE kubeconfig is assembled from the cluster's name, endpoint, and masterAuth.clusterCaCertificate, plus the gke-gcloud-auth-plugin exec entry Google's provider now requires. That asymmetry across the three clouds is real, not an oversight in this guide.
Provisioning a cluster and deploying an application onto it are separate steps. Once any of the three clusters above is up, point Pulumi's Kubernetes provider at its kubeconfig and declare a Deployment the same way you declared the cluster:
import * as k8s from "@pulumi/kubernetes";
const provider = new k8s.Provider("cluster", { kubeconfig });
const appLabels = { app: "hello" };
new k8s.apps.v1.Deployment("hello", {
spec: {
replicas: 2,
selector: { matchLabels: appLabels },
template: {
metadata: { labels: appLabels },
spec: { containers: [{ name: "hello", image: "nginx:latest" }] },
},
},
}, { provider });For a full walkthrough of deploying a workload onto an existing cluster, including Services and scaling, see Pulumi's Kubernetes get-started guide. That guide assumes a cluster already exists; this post is what creates one.
| EKS (AWS) | AKS (Azure) | GKE (Google Cloud) | |
|---|---|---|---|
| Pulumi package | @pulumi/eks |
@pulumi/azure-native |
@pulumi/gcp |
| Control plane management | Fully managed by AWS | Fully managed by Azure | Fully managed by Google Cloud |
| Node management | Managed node group by default | Agent pool profile | Separate node pool resource |
| Kubeconfig | Direct cluster.kubeconfig output |
Base64-encoded credential call | Assembled from cluster fields plus an auth plugin |
| Teardown gotcha | None by default | None by default | deletionProtection must be set to false first |
- Best tools for managing Kubernetes infrastructure as code in 2026 compares Pulumi against Terraform, CDK, and Crossplane for Kubernetes workloads.
- Terraform and Kubernetes: a practical guide for 2026 looks at what changes if you're migrating an existing Terraform-managed cluster.
- Pulumi's cloud provider guides cover AWS and Azure setup in more depth than fits in one post.
- Deploying to Kubernetes with Pulumi is the next step once your cluster is running.
Use the @pulumi/eks package (or pulumi_eks in Python), which wraps the underlying AWS resources an EKS cluster needs into a single Cluster component. A minimal program creates the cluster, its managed node group, and its kubeconfig output in under twenty lines, with no separate IAM role or VPC wiring required for a default setup. Running pulumi up provisions the whole cluster in one step.
You provision the cluster first, then deploy applications onto it separately. Once a cluster exists, point Pulumi's Kubernetes provider at its kubeconfig and declare Deployments, Services, and other objects the same way you declared the cluster itself. Pulumi's Kubernetes get-started guide walks through deploying a workload to an existing cluster step by step.
Yes. Pulumi projects are not tied to a single cloud, so one program can create an EKS cluster with @pulumi/eks, an AKS cluster with @pulumi/azure-native, and a GKE cluster with @pulumi/gcp side by side, using ordinary conditionals or separate stacks per cloud. Each provider authenticates against its own cloud account, and Pulumi tracks all three clusters' state independently.
Provisioning time is set by the cloud provider, not Pulumi: EKS control planes typically take around ten minutes, AKS around ten minutes, and GKE five to ten minutes, plus a few minutes more for worker nodes to join. pulumi up blocks until the cluster and its kubeconfig are ready, so the program's own runtime adds negligible overhead beyond the cloud's provisioning time.
No. Pulumi's Kubernetes provider represents Deployments, Services, and every other Kubernetes object as typed resources in Python, TypeScript, Go, C#, or Java, so you get autocomplete, type checking, and real functions and loops instead of YAML templating. You can still apply existing YAML manifests directly if you have them, but writing new YAML is never required.
Run pulumi destroy from the same project. For EKS and AKS this tears down the cluster and its supporting resources directly. For GKE, set deletionProtection: false on the Cluster resource before destroying; Google's provider defaults deletionProtection to true, and pulumi destroy fails against a protected cluster until that flag is turned off.
For most teams, yes. EKS, AKS, and GKE each run and patch the control plane for you, handle etcd and API server availability, and integrate with their cloud's identity and networking systems out of the box. Self-managed Kubernetes gives more control over the control plane itself, but that control comes with real operational cost most teams would rather spend on their applications.
Provisioning creates the cluster: the control plane, node pools, and networking a cloud provider manages on your behalf. Deploying means declaring the applications and services that run inside an already-running cluster, such as Deployments and Services managed through Kubernetes' own API. Pulumi handles both, but they are separate steps using separate resources.