Terraform’s Kubernetes provider is a powerful tool for managing Kubernetes resources, but its true magic lies in its ability to treat Kubernetes objects as immutable infrastructure, even though Kubernetes itself is a mutable system.
Let’s see this in action. Imagine we want to deploy a simple Nginx deployment and service.
provider "kubernetes" {
config_path = "~/.kube/config"
}
resource "kubernetes_deployment" "nginx" {
metadata {
name = "nginx-deployment"
labels = {
app = "nginx"
}
}
spec {
replicas = 2
selector {
match_labels = {
app = "nginx"
}
}
template {
metadata {
labels = {
app = "nginx"
}
}
spec {
container {
image = "nginx:1.17.10"
name = "nginx"
ports {
container_port = 80
}
}
}
}
}
}
resource "kubernetes_service" "nginx" {
metadata {
name = "nginx-service"
}
spec {
selector = {
app = "nginx"
}
port {
port = 80
target_port = 80
}
type = "LoadBalancer"
}
}
When you run terraform apply, Terraform doesn’t just "apply" these definitions to Kubernetes. Instead, it compares the desired state defined in your Terraform code with the actual state of resources in your cluster. If a resource doesn’t exist, Terraform creates it. If it exists but has drifted from the desired state, Terraform updates it. If the resource is no longer defined in your Terraform code, Terraform will destroy it.
This declarative approach means you’re not writing imperative commands like kubectl create deployment ... or kubectl apply -f deployment.yaml. Instead, you’re defining what you want the cluster to look like, and Terraform figures out how to get there.
The core problem the Kubernetes provider solves is bridging the gap between Terraform’s infrastructure-as-code paradigm and Kubernetes’ dynamic, mutable nature. Kubernetes is designed to ensure a desired state, but it does so by constantly reconciling the current state with the desired state. The Terraform Kubernetes provider acts as an intelligent orchestrator, translating your declarative Terraform configurations into the specific API calls Kubernetes understands, while also maintaining a record of what it thinks is deployed.
When you use terraform import, you’re telling Terraform, "This resource already exists in Kubernetes; here’s its ID, please manage it." Terraform then queries the Kubernetes API to get the current state of that resource and stores it in its state file. From that point on, terraform plan and terraform apply will track and manage that resource.
The key levers you control are the resource definitions themselves. Each Kubernetes object (Deployment, Service, Pod, ConfigMap, etc.) becomes a resource block in Terraform. You define metadata like name and labels, and spec fields that correspond directly to the Kubernetes API schema. The provider "kubernetes" block configures how Terraform connects to your cluster, typically via config_path pointing to your ~/.kube/config file.
A common point of confusion is how Terraform handles updates to mutable fields within Kubernetes objects. For example, if you change the image tag in a kubernetes_deployment resource and run terraform apply, Terraform will generate an update operation. However, Kubernetes itself might not immediately update the pods if the spec.template.spec.containers[0].image field is what’s being changed directly within an existing Deployment object. Instead, Kubernetes will create a new ReplicaSet with the updated image, and then gradually roll out new pods based on that ReplicaSet, terminating old ones. Terraform, by reading the state back from the Kubernetes API after an apply, accurately reflects this eventual state.
When you modify a resource’s spec.replicas field in Terraform and apply it, Terraform doesn’t just tell Kubernetes "make it 5." It reads the current number of replicas from the Kubernetes API, compares it to your desired 5, and instructs the Kubernetes API to adjust the spec.replicas field of the Deployment object. Kubernetes then takes over, ensuring that the actual number of running pods matches the spec.replicas value.
The next concept you’ll likely encounter is managing more complex Kubernetes objects like StatefulSets, DaemonSets, and custom resource definitions (CRDs).