Provisioning AWS EKS Clusters with Pulumi
Learn how to create an EKS cluster with Pulumi using the @pulumi/eks component, and deploy a Kubernetes workload to it in the same program
Where the Terraform EKS guide on this site assembles a cluster from the raw aws_eks_cluster and aws_eks_node_group resources, this guide uses @pulumi/eks - a dedicated, higher-level Pulumi package (“Crosswalk for AWS”) purpose-built for EKS. It’s the most notable example of a pattern raw Terraform’s resource model doesn’t really have an equivalent for: a component that wraps many underlying resources behind a small, opinionated API.
Prerequisites
npm install @pulumi/pulumi @pulumi/aws @pulumi/eks @pulumi/kubernetes
The Simplest Possible Cluster
import * as eks from "@pulumi/eks";
const cluster = new eks.Cluster("my-cluster");
export const kubeconfig = cluster.kubeconfig;
That’s a complete, working program. With no arguments, eks.Cluster provisions its own VPC, subnets, security groups, IAM roles, the EKS control plane, and a worker node group - dozens of underlying resources - and cluster.kubeconfig gives you a ready-to-use kubeconfig as a stack output:
pulumi stack output kubeconfig --show-secrets > kubeconfig.json
KUBECONFIG=kubeconfig.json kubectl get nodes
A Cluster in an Existing VPC
In practice you’ll usually deploy into a VPC you already manage (like the one from the Pulumi VPC guide), rather than letting the component create its own:
import * as eks from "@pulumi/eks";
const cluster = new eks.Cluster("app-cluster", {
vpcId: vpc.id,
subnetIds: [...publicSubnets.map(s => s.id), ...privateSubnets.map(s => s.id)],
instanceType: "t3.medium",
desiredCapacity: 3,
minSize: 1,
maxSize: 5,
nodeAssociatePublicIpAddress: false,
});
export const kubeconfig = cluster.kubeconfig;
export const clusterName = cluster.eksCluster.name;
nodeAssociatePublicIpAddress: false keeps worker nodes off public IPs when the subnets you pass include private ones - the same “workers in private subnets, control plane endpoint public” topology the Terraform EKS guide on this site recommends.
Deploying a Workload in the Same Program
Because cluster is just a regular Pulumi resource, you can feed its outputs directly into a Kubernetes provider and deploy workloads in the same pulumi up - no separate kubectl apply or Helm step required to get from “cluster exists” to “app is running”:
import * as k8s from "@pulumi/kubernetes";
const k8sProvider = new k8s.Provider("app-cluster-provider", {
kubeconfig: cluster.kubeconfigJson,
});
const appLabels = { app: "nginx" };
const deployment = new k8s.apps.v1.Deployment("nginx", {
spec: {
selector: { matchLabels: appLabels },
replicas: 2,
template: {
metadata: { labels: appLabels },
spec: {
containers: [{
name: "nginx",
image: "nginx:1.27",
ports: [{ containerPort: 80 }],
}],
},
},
},
}, { provider: k8sProvider });
const service = new k8s.core.v1.Service("nginx", {
spec: {
type: "LoadBalancer",
selector: appLabels,
ports: [{ port: 80, targetPort: 80 }],
},
}, { provider: k8sProvider });
export const serviceHostname = service.status.loadBalancer.ingress[0].hostname;
{ provider: k8sProvider } is a resource option, not an argument - it tells Pulumi which provider instance to use for this resource, the same role a Terraform provider block reference plays. Every resource created against k8sProvider here gets an explicit dependency edge on cluster, so Pulumi always creates the cluster before trying to deploy anything into it, and destroys the workloads before the cluster on pulumi destroy.
Best Practices
- Pass an explicit VPC in anything beyond a demo - letting
eks.Clustercreate its own VPC is convenient for a quickstart but couples your network topology to the cluster’s lifecycle. - Pin
instanceType/desiredCapacityto real capacity planning, not defaults - the same guidance as the raw Terraform EKS guide applies regardless of which tool provisions the cluster. - Keep application manifests as Pulumi
k8sresources (as above) or a separate ArgoCD-managed Git repo - mixing both approaches for the same workloads leads to two systems fighting over reconciliation, the same failure mode covered in this site’s ArgoCD guides.
Conclusion
@pulumi/eks is a genuinely different way of thinking about the cluster compared to Terraform’s raw aws_eks_cluster/aws_eks_node_group resources - fewer lines, an opinionated default topology, and the option to deploy workloads in the same program using the same language as the infrastructure. The tradeoff is less granular control than assembling every resource yourself; reach for the raw aws.eks.* resources instead of the @pulumi/eks component when you need a configuration it doesn’t expose.
For more Pulumi topics, check out: