Terraform ignore_changes can target a single annotation key, not just the whole map
kubectl rollout restart is the quick way to bounce a deployment. Under the hood, it patches the pod template with a kubectl.kubernetes.io/restartedAt annotation set to the current timestamp. Kubernetes sees a template change, triggers a rolling update. Clean, simple.
Until Terraform runs next.
The drift
Terraform manages the deployment resource. It doesn’t know about the annotation kubectl added. Next terraform apply, it sees drift: “this annotation exists in the cluster but not in my config.” It plans to remove it, which changes the pod template, which triggers another rolling restart. Every terraform apply after a manual restart causes an unnecessary redeploy.
The obvious fix is ignore_changes on annotations. But that’s too broad:
lifecycle {
ignore_changes = [
spec[0].template[0].metadata[0].annotations,
]
}
This ignores ALL annotation changes. We have a checksum/secrets annotation that Terraform manages deliberately – it triggers a redeploy when the secret content changes. Ignoring the whole map means secret rotations stop propagating. That’s worse than the original problem.
The fix
Target the single key with indexed path syntax:
lifecycle {
ignore_changes = [
spec[0].template[0].metadata[0].annotations["kubectl.kubernetes.io/restartedAt"],
]
}
Terraform ignores drift on that one annotation key. Everything else in the annotations map stays tracked. Secret checksum changes still trigger deploys. Manual restarts stop causing phantom drift.
The syntax
The [0] indexing is required because spec, template, and metadata are all single-element lists in the Terraform Kubernetes provider schema, not direct objects. Terraform’s lifecycle block doesn’t support wildcards or splats – you must walk the exact path through each list element.
The ["key.with/slashes"] bracket syntax at the end is how you index into a map attribute by key. It works the same way you’d reference it in an expression: kubernetes_deployment.foo.spec[0].template[0].metadata[0].annotations["kubectl.kubernetes.io/restartedAt"].
Why this is hard to find
Most ignore_changes examples show ignoring an entire block (tags, annotations, labels). The Terraform docs mention that you can target map keys, but the examples don’t cover Kubernetes-style deeply nested paths. You end up guessing the syntax or giving up and ignoring the whole map.
The indexed path approach works for any map attribute at any depth. If you can reference it in a Terraform expression, you can ignore changes to it.