Pulumi lets you define Kubernetes resources using familiar programming languages, which is way more powerful than YAML.
Here’s what a basic deployment looks like in Python:
import pulumi
import pulumi_kubernetes as kubernetes
app_labels = { "app": "nginx" }
nginx = kubernetes.apps.v1.Deployment("nginx-deployment",
spec={
"selector": { "match_labels": app_labels },
"replicas": 2,
"template": {
"metadata": { "labels": app_labels },
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:1.14.2",
"ports": [{ "container_port": 80 }],
}]
}
}
})
pulumi.export("name", nginx.metadata["0"].name)
This Python code defines a Kubernetes Deployment named nginx-deployment. It specifies two replicas of an Nginx container using the nginx:1.14.2 image. Pulumi then takes this code, translates it into the equivalent Kubernetes API objects, and applies them to your cluster. The pulumi.export line makes the deployed Nginx deployment’s name available as an output after the Pulumi deployment is complete.
The core problem Pulumi solves is managing the complexity of Kubernetes configurations. Instead of writing and maintaining dozens or hundreds of YAML files, you write code. This code can leverage all the benefits of a full programming language: loops, conditionals, functions, classes, and package management. You can import libraries, reuse components, and implement sophisticated deployment strategies. It’s like moving from static HTML to dynamic web applications.
Internally, Pulumi works by creating a directed acyclic graph (DAG) of your infrastructure resources. When you run pulumi up, Pulumi analyzes this graph, determines the desired state of your infrastructure, compares it to the current state in your Kubernetes cluster, and then computes the minimal set of changes (create, update, delete) needed to reach the desired state. It then uses the Kubernetes API to enact those changes. The pulumi_kubernetes provider is essentially a translator, taking your program’s resource definitions and turning them into the JSON/YAML payloads the Kubernetes API expects.
A key lever you control is the provider argument. By default, pulumi_kubernetes will use your currently configured Kubernetes context (e.g., from ~/.kube/config). However, you can explicitly target different clusters or namespaces by creating a provider instance:
# Target a specific cluster and namespace
k8s_provider = kubernetes.Provider("k8s-provider",
kubeconfig="""apiVersion: v1
clusters:
- cluster:
server: https://my.other.cluster.com
name: my-cluster
contexts:
- context:
cluster: my-cluster
user: my-user
name: my-context
current-context: my-context
kind: Config
users:
- name: my-user
user: {}""",
namespace="staging"
)
app = kubernetes.core.v1.Namespace("app-ns",
metadata={"name": "my-app-staging"},
opts=pulumi.ResourceOptions(provider=k8s_provider)
)
This allows for true infrastructure-as-code, where your entire cloud environment can be managed from a single codebase, with version control, testing, and CI/CD pipelines. You can easily create multiple environments (dev, staging, prod) by parameterizing your code or using different Pulumi stacks.
The one thing that often trips people up is how Pulumi handles dependencies and the order of operations. Unlike imperative scripts where you explicitly call kubectl apply for each file in a specific order, Pulumi automatically infers dependencies. If resource B references resource A (e.g., a Service referencing a Deployment by label selector), Pulumi knows A must be created before B and will orchestrate the API calls accordingly. You don’t manually specify depends_on in most cases; the language constructs handle it.
The next hurdle is managing secrets and sensitive configuration across different environments.