Running Kubernetes often means reconciling an existing pile of YAML manifests with the desire to manage everything consistently from Terraform HCL. This article covers how the Terraform Kubernetes Provider relates to YAML, the real-world limits, and the decision points exams tend to focus on.
Up front: Terraform performs CRUD against the Kubernetes API; it does not replicate kubectl apply behavior — especially the field ownership and conflict resolution of server-side apply. We focus on stable, documented behavior and call out areas where provider versions matter.
Terraform pushes HCL-defined resources to the Kubernetes API via a provider. Plain kube YAML is applied through kubectl or other clients. Helm renders YAML templates and ultimately ships YAML (or equivalent JSON) into the Kubernetes API.
The destination is the same, but the path and the state-management philosophy differ. Terraform shines at state management and plan-driven workflows; kubectl and Helm shine at flexible, Kubernetes-native application.
Conceptual paths to Kubernetes
Minimal Kubernetes provider configuration (using a kubeconfig)
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0"
}
}
}
provider "kubernetes" {
config_path = var.kubeconfig_path
config_context = var.kube_context
}
variable "kubeconfig_path" { type = string }
variable "kube_context" { type = string }Typed Kubernetes Provider resources (kubernetes_deployment_v1, kubernetes_service_v1, etc.) give you schema-level pre-validation and crisp diff visibility at plan time. They also play well with Terraform references and for_each, and exams treat them as the baseline pattern you should know.
On the other hand, not every Kubernetes API or CRD is covered with typed resources. Server-side defaulting and rewriting by admission/mutating webhooks and controllers can introduce diffs Terraform did not intend — pod-template annotations and automatic sidecar injection are notorious for showing up in every plan. The common workaround is to ignore specific fields via Terraform's lifecycle.ignore_changes.
Managing a Deployment in HCL while suppressing annotation diffs
resource "kubernetes_deployment_v1" "web" {
metadata {
name = "web"
namespace = "default"
labels = { app = "web" }
# Admissionやコントローラで変わりやすい注釈は無視
annotations = { managed-by = "terraform" }
}
spec {
replicas = 2
selector { match_labels = { app = "web" } }
template {
metadata {
labels = { app = "web" }
annotations = { sidecar.istio.io/inject = "false" }
}
spec {
container {
name = "nginx"
image = "nginx:1.25"
port { container_port = 80 }
}
}
}
}
lifecycle {
ignore_changes = [
metadata[0].annotations,
spec[0].template[0].metadata[0].annotations
]
}
}When you want to apply existing YAML as-is, convert it into an HCL object with Terraform's yamldecode function and pass it to a generic manifest-style resource (for example kubernetes_manifest — availability and naming depend on the provider version). This works for CRDs and APIs without typed coverage, which makes it a realistic compromise.
A couple of caveats: when applying multi-document YAML in bulk, split first and manage each chunk with for_each keyed by something stable (kind/name/namespace). Also enforce CRD-before-CR ordering explicitly with depends_on.
Read YAML and pass it to a generic manifest resource (single document)
locals {
deploy = yamldecode(file("${path.module}/manifests/deploy.yaml"))
}
# プロバイダが提供する汎用マニフェスト系リソース名はバージョンで異なる場合があります
resource "kubernetes_manifest" "deploy" {
manifest = local.deploy
}
The Helm Provider's helm_release manages a Chart distribution within Terraform's lifecycle. You can pass values.yaml as a raw string or generate it from HCL via yamlencode. If the Chart contains CRDs, design the ordering so CRDs exist before any CR is applied.
helm_release does not record individual Kubernetes objects as discrete Terraform resources — it manages at the release level. If you need fine-grained per-object diff management, split responsibilities between Helm and manifest-style resources.
Apply a Chart with helm_release and pass values.yaml
terraform {
required_providers {
helm = {
source = "hashicorp/helm"
version = ">= 2.9"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0"
}
}
}
provider "helm" {
kubernetes {
config_path = var.kubeconfig_path
config_context = var.kube_context
}
}
resource "helm_release" "nginx" {
name = "nginx"
repository = "https://charts.bitnami.com/bitnami"
chart = "nginx"
namespace = "web"
create_namespace = true
values = [file("${path.module}/values.yaml")]
}
The Kubernetes API server and various controllers apply defaults to objects and rewrite annotations or fields. kubectl apply (particularly server-side apply) and Terraform differ in field ownership and conflict resolution, so mixing them causes drift. Standardize on one or the other; if you must combine them, draw clear lines of responsibility.
CRD/CR consistency, eventual consistency (reads right after apply may not yet reflect changes), list-order drift, and generated-field diffs are all best suppressed with Terraform tools: depends_on, time_sleep (when truly needed), ignore_changes, and stable keys on for_each.
Guaranteeing CRD → CR order (CRDs via Helm, CRs via YAML)
locals {
cr_files = fileset("${path.module}/crs", "*.yaml")
cr_objs = [for f in local.cr_files : yamldecode(file("${path.module}/crs/${f}"))]
cr_map = { for m in local.cr_objs : "${m["kind"]}/${m["metadata"]["namespace"]}/${m["metadata"]["name"]}" => m }
}
# 例: CRDを含むChart
resource "helm_release" "crds" {
name = "operator-crds"
repository = var.operator_repo
chart = var.operator_crd_chart
namespace = "operators"
}
# CRをYAMLから適用(汎用マニフェスト系リソース想定)
resource "kubernetes_manifest" "cr" {
for_each = local.cr_map
manifest = each.value
depends_on = [helm_release.crds]
}
variable "operator_repo" { type = string }
variable "operator_crd_chart" { type = string }Associate/Pro exams frequently ask about choosing between typed HCL management, YAML ingestion, and Helm — plus the judgement calls around drift suppression. In production the same themes drive success: coexisting with CRDs and existing YAML, handling Secrets, and controlling order.
The table below summarises the trade-offs of each approach.
| Approach | How YAML is handled | Best-fit use case | Main limitations |
|---|---|---|---|
| Typed Kubernetes resources (HCL) | Not needed (declared in HCL) | Strict management of mainstream resources like Deployment/Service | CRDs and uncovered APIs are out of scope; webhook mutation tends to produce diffs |
| Generic manifest apply (yamldecode + manifest-style) | Apply YAML as-is (decoded into HCL) | CRD/CR, APIs without typed coverage, reusing existing YAML | Provider features and version differences matter; field-level strict validation is weaker |
| Helm (helm_release) | Injected into templates via values.yaml | Bulk distribute and update many objects as a single Chart | State is per-release; fine-grained per-object control is weaker |
| Running kubectl via local-exec (for reference) | kubectl apply applied directly to YAML | Minimal migrations or temporary workarounds | Disconnected from Terraform plan/state; weak reproducibility and drift handling |
Snippet: split multi-document YAML and apply with for_each
locals {
# --- 区切りで分割(改行を含む区切りを考慮)。空要素は除外
raw = file("${path.module}/bundle.yaml")
docs = [for d in split("\n---\n", local.raw) : d if trimspace(d) != ""]
objs = [for d in local.docs : yamldecode(d)]
mp = { for m in local.objs : "${m["kind"]}/${coalesce(try(m["metadata"]["namespace"], null), "default")}/${m["metadata"]["name"]}" => m }
}
resource "kubernetes_manifest" "multi" {
for_each = local.mp
manifest = each.value
}
Associate / Pro
問題 1
You want to manage multiple CustomResources (CRs) defined in existing YAML via Terraform. CRDs are provided by a Helm Chart and must be applied before the CRs. Which pattern minimises drift and preserves plan-ability best?
正解: A
Applying CRDs first with helm_release, then managing CRs via yamldecode plus a generic manifest-style resource with depends_on for ordering, is the most plan-friendly and drift-resistant approach in Terraform. kubectl via local-exec has no state and is unsuitable. Force-fitting into typed resources is impractical. Embedding CRs into values gives weak ordering control and poor per-object diff visibility.
Does Terraform behave the same as kubectl apply (server-side apply)?
No. Terraform performs CRUD against the Kubernetes API and does not replicate the field-ownership and conflict-resolution semantics of kubectl server-side apply. Some provider versions ship a similar mode, but do not assume it — check the official docs. Mixing both tools on the same object is a common source of drift.
Can Terraform handle multi-document YAML (--- separators) as-is?
Not as a single object. The usual pattern is to read the file, split on ---, decode each chunk with yamldecode, then apply them individually via for_each. Building a stable key (kind/namespace/name) keeps plans and diffs predictable.
Is it safe to manage Secrets with Terraform?
Secret values can end up in the Terraform state file. We recommend using Terraform Cloud/Enterprise or an encrypted remote backend, or combining Terraform with an external secrets manager such as Vault or External Secrets and letting Terraform handle only references and wiring.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...