Terraform kubectl apply for Kubernetes Manifest Management and Deployment Orchestration

The intersection of Terraform and Kubernetes produces a set of operational patterns that center on applying declarative intent to a live cluster. The reference material covers three distinct but overlapping approaches: the native Kubernetes provider that creates kubernetes_deployment_v1 and kubernetes_service_v1 resources, the terraform-provider-kubectl with its kubectl_manifest resource for raw YAML application, and the use of local-exec provisioners and external modules to invoke kubectl apply commands from Terraform. Each approach carries its own execution flow, state handling, and security considerations that affect how a team confirms an apply, verifies running workloads, scales replicas, and migrates provider addresses without drift.

The practical workflow begins with an explicit confirmation step. Terraform presents the execution plan, lists resource actions with symbols such as + create, and requires a literal yes value to approve. The prompt Enter a value: yes is not optional and Terraform rejects any other input. Once approved, the provider reports creation progress with timestamps and an identifier such as [id=default/scalable-nginx-example]. The elapsed time is reported as Creation complete after 4s. After the apply completes, verification is performed outside Terraform with kubectl get deployments and kubectl get services. The output columns NAME READY UP-TO-DATE AVAILABLE AGE for deployments and NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE for services provide the observable proof that the declarative change materialized in the cluster. The real-world impact is that operators gain a reproducible audit trail from plan to apply to verification, and the cluster state becomes inspectable by both Terraform and native Kubernetes tooling.

Native Kubernetes Provider Deployment Creation

The tutorial workflow creates a Deployment named scalable-nginx-example via kubernetes_deployment_v1.nginx. The resource block is declared in kubernetes.tf with a spec block that includes a replicas field. The plan phase reports Plan: 1 to add, 0 to change, 0 to destroy. and the apply phase logs kubernetes_deployment_v1.nginx: Creating... followed by Creation complete after 4s [id=default/scalable-nginx-example]. The impact for the user is a predictable creation window and a stable identifier that can be referenced in later state refreshes.

Verification uses the command kubectl get deployments. The sample output shows scalable-nginx-example 2/2 2 2 15s, indicating two replicas ready, two up-to-date, two available. The contextual connection is that the Terraform resource definition drives the replica count and pod template, and the kubectl verification closes the feedback loop between infrastructure code and runtime health.

Service exposure follows the Deployment. A kubernetes_service_v1.nginx resource is created in a subsequent apply. The plan again shows Plan: 1 to add, 0 to change, 0 to destroy. and the apply logs kubernetes_service_v1.nginx: Creating... with Creation complete after 0s [id=default/nginx-example]. The final message is Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Verification with kubectl get services returns a row nginx-example NodePort 10.96.55.64 <none> 80:30201/TCP 76s. Access is achieved by navigating to http://localhost:30201/ and the health check is performed with curl http://localhost:30201/. The impact is that the service type determines how traffic reaches the pods, and the NodePort value is the concrete network endpoint for local clusters.

The tutorial distinguishes two exposure patterns. If the Kubernetes cluster is hosted locally on kind, the NGINX instance is exposed via NodePort. This exposes the service on each node's IP at a static port, allowing access from outside the cluster at <NodeIP>:<NodePort>. If the cluster is hosted on a cloud provider, the NGINX instance is exposed via LoadBalancer to access the instance. The contextual layer links the choice of service type to the underlying infrastructure, and the user impact is accessibility versus cloud cost and networking configuration.

Scaling Replicas with Terraform Configuration

Scaling is performed by editing the kubernetes.tf resource to change replicas from 2 to 4 inside the spec block of resource "kubernetes_deployment_v1" "nginx". The apply is re-confirmed with a yes. The apply output shows state refresh messages kubernetes_deployment_v1.nginx: Refreshing state... [id=default/scalable-nginx-example] and kubernetes_service_v1.nginx: Refreshing state... The impact is zero-downtime scaling driven by a single field change, and the connection to the previous verification step is that the READY column will later reflect 4/4 after rollout completes.

terraform-provider-kubectl Overview and Manifest Application

The terraform-provider-kubectl is described as offering the most effective method for handling Kubernetes resources in Terraform by leveraging what Kubernetes values most — YAML. The core resource is kubectl_manifest, enabling processing and application of free-form YAML directly to Kubernetes. The YAML object is monitored across its full lifecycle — creation, updates, drift detection and deletion. The impact for operators is drift detection without needing to map every Kubernetes API object to a Terraform resource type.

For reads, the provider exposes a data source kubectl_manifest for ordinary lookups and an ephemeral kubectl_manifest resource for Terraform 1.10+ that fetches Secret payloads, freshly-minted tokens, and any other sensitive data without ever writing the value to terraform.tfstate. The impact is reduced secret leakage risk and the ability to consume dynamic cluster data in plans without persisting it.

The provider has gained widespread adoption in numerous large Kubernetes installations, serving as the primary tool for orchestrating the complete lifecycle of Kubernetes resources. The contextual connection is that kubectl_manifest complements native providers by handling arbitrary manifests, while native providers provide strongly typed resource schemas.

The provider capabilities are summarized as follows:

Type Name Purpose
Resource kubectl_manifest Apply a raw YAML manifest to the cluster (full create / update / delete + drift detection).
Resource kubectlserverversion Read API-server version info, with triggers for use in depends_on chains.
Data source kubectl_manifest Read any object from the cluster by GVK + name (+ namespace) and extract fields by dot-path.
Data source kubectlserverversion Read API-server version info.
Data source kubectlfiledocuments Split a multi-document YAML string into individual documents

The table provides a quick reference for which construct to use for write versus read operations and for version-aware dependency chains.

Provider Migration and moved Blocks

Migration between provider addresses is handled with a moved block. The example picks a transitional address with an unused name such as _v3. The configuration sets the required provider source to alekc/kubectl:

terraform { required_providers { kubectl = { source = "alekc/kubectl" } } }

The move is declared as:

moved { from = kubectl_manifest.my_app # was gavinbunney/kubectl to = kubectl_manifest.my_app_v3 # now alekc/kubectl }

The resource block is renamed to kubectl_manifest.my_app_v3 with the same yaml_body or attributes as before. The workflow is terraform init -upgrade, then terraform plan. The plan reports the resource as moved with no in-place changes:

```

kubectlmanifest.myapp has moved to kubectlmanifest.myapp_v3

resource "kubectlmanifest" "myapp_v3" {

(N unchanged attributes hidden)

}
Plan: 0 to add, 0 to change, 0 to destroy.
```

Running terraform apply commits the move with no-op for the resource itself; only the state address changes. After the move applies, the user can keep the new name or rename back by dropping the _v3 from the resource block, adding a second moved block pointing my_app_v3 to my_app, and applying again. Once addresses match, the moved blocks are removed entirely.

A nuance is that terraform plan -detailed-exitcode returns 2 on the first plan because moved annotations count as changes present even when the resource summary is 0 to add, 0 to change, 0 to destroy. The second plan after apply returns 0 cleanly. The summary line is treated as authoritative. The 20 attributes shared with gavinbunney carry over unchanged. The impact is that teams can upgrade provider sources without recreating resources, preserving history and avoiding downtime.

Using kubectl_manifest with YAML Bodies

A concrete example uses kubectl_manifest with a yaml_body heredoc:

resource "kubectl_manifest" "nginx_deployment" { depends_on = [ kubernetes_namespace.example ] yaml_body = <<YAML apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment namespace: my-namespace spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 YAML }

The provider source in providers.tf is:

terraform { required_providers { kubectl = { source = "gavinbunney/kubectl" version = "~> 1.14" } } }

The depends_on ensures the namespace exists before manifest application. The impact is dependency ordering without implicit graph inference, and the YAML remains the source of truth for the Kubernetes object.

Methods for Invoking kubectl apply from Terraform

Four methods are outlined for managing Kubernetes resources using kubectl commands within Terraform.

Method descriptions are:

Method Description Pros Cons
terraform-provider-kubectl Uses a dedicated Terraform provider to deploy Kubernetes manifests. Simple integration, leverages existing kubectl context. Requires an additional provider

The article emphasizes adapting examples to specific use cases, prioritizing security, and ensuring idempotency and error handling in kubectl commands.

When to use each method:

  • terraform-provider-kubectl: Best for managing Kubernetes manifests directly within Terraform, especially when you need to manage dependencies between Kubernetes resources and other Terraform resources.
  • local-exec provisioner: Useful for simple kubectl commands or when you need to interact with the cluster outside the scope of resource management (e.g., running a script after deployment).

The contextual layer is that provider-based methods integrate with Terraform state and dependency graph, while provisioner-based methods are imperative and should be used sparingly.

local-exec Provisioner Pattern

A simple local-exec pattern is:

resource "null_resource" "kubectl_apply" { provisioner "local-exec" { command = "kubectl apply -f deployment.yaml" } }

A more sensitive variant sets KUBECONFIG before running Terraform:

export KUBECONCONFIG=path/to/your/kubeconfig

Then in main.tf:

resource "null_resource" "kubectl_apply" { provisioner "local-exec" { command = "kubectl apply -f deployment.yaml --kubeconfig=${var.kubeconfig}" } } variable "kubeconfig" { type = string default = env("KUBECONFIG") sensitive = true }

The impact is that the kubeconfig path is not hard-coded and the variable is marked sensitive. The reminder is to adapt examples to specific needs and context, prioritize security by avoiding hardcoded sensitive information and using appropriate secret management techniques, and ensure kubectl commands are idempotent and implement error handling mechanisms for robustness.

External Modules for kubectl Commands

External modules provide higher-level abstractions. An example module call is:

module "kubectl_command" { source = "magnolia-sre/kubectl-cmd/kubernetes" version = "~> 1.0" commands = [ "kubectl get pods -n kube-system", ] }

External modules are ideal for common Kubernetes tasks or complex deployments, as they provide higher-level abstractions and reduce boilerplate code. The usage is to be used with caution due to potential security risks. The impact is reduced repetition for repetitive commands, with the trade-off of trusting external source code.

Security and Best Practices Notes

The document stresses security. Use with caution due to potential security risks. Prioritize security by avoiding hardcoded sensitive information and using appropriate secret management techniques. Ensure kubectl commands are idempotent and implement error handling mechanisms for robustness.

Beyond the basics, explore the full potential of terraform-provider-kubectl by leveraging its data sources for fetching information from your cluster. Consider using tools like kustomize or helm in conjunction with Terraform for templating and managing complex Kubernetes applications.

Terraform best practices still apply:

  • Use modules to organize your code and promote reusability.
  • Implement input validation to ensure the reliability of Terraform code.
  • Leverage Terraform's state management capabilities to track changes and ensure consistency.

Keep learning: The Kubernetes and Terraform ecosystems are constantly evolving. Stay up-to-date with the latest features, best practices, and security considerations.

The overall impact is that combining Terraform state with Kubernetes manifests yields reproducible deployments, but the choice between native provider resources, kubectl_manifest, local-exec, and external modules determines coupling, drift detection, and secret handling characteristics.

Conclusion

The reference material demonstrates that terraform kubectl apply is not a single command but a family of patterns. The native Kubernetes provider creates strongly typed resources such as kubernetes_deployment_v1 and kubernetes_service_v1, confirms plans with an explicit yes, reports creation times and identifiers, and requires post-apply verification with kubectl get deployments and kubectl get services. Service exposure decisions hinge on cluster location, with NodePort for local kind clusters and LoadBalancer for cloud providers, and scaling is achieved by editing the replicas field and re-applying.

The terraform-provider-kubectl with kubectl_manifest shifts focus to raw YAML, providing lifecycle monitoring, drift detection, and read-only data sources including ephemeral resources that avoid writing secrets to state. Provider migrations are handled with moved blocks and transitional addresses, with the nuance that terraform plan -detailed-exitcode returns 2 initially despite a zero change summary.

Alternative invocation methods include local-exec provisioners for imperative kubectl apply -f calls with sensitive KUBECONFIG handling, and external modules such as magnolia-sre/kubectl-cmd/kubernetes for reusable command execution. Security guidance emphasizes avoiding hardcoded secrets, ensuring idempotency, and using appropriate secret management.

Together these patterns form a comprehensive approach to applying Kubernetes manifests from Terraform, balancing declarative state management, YAML fidelity, and operational safety. The choice among them depends on dependency needs, drift sensitivity, secret handling requirements, and the degree of abstraction desired for ongoing cluster operations.

Sources

  1. HashiCorp Developer Terraform Kubernetes Provider Tutorial
  2. terraform-provider-kubectl GitHub Repository
  3. Managing Kubernetes Deployments with Terraform and kubectl apply

Related Posts