Terraform provider - ignore_changes not working in kubernetes_manifest resource

Cluster information:

Kubernetes version: 1.28
Cloud being used: AWS
Installation method: Terraform

Hi folks. I’m encountering an issue with Terraform provider for Kubernetes where lifecycle.ignore_changes does not appear to be working in the kubernetes_manifest resource. I’ve filed a GitHub issue for same.

Terraform configuration

locals {
  applications = {
    application-1 = {
      chart = "application-1"
    }
    application-2 = {
      chart = "application-2"
    }
    application-3 = {
      chart   = "application-3"
      version = 3.46.0
    }
  }
}

resource "kubernetes_manifest" "argocd_application" {
  for_each = local.applications

  manifest = {
    apiVersion = "argoproj.io/v1alpha1"
    kind       = "Application"

    metadata = {
      name      = each.key
      namespace = "kube-system"

      finalizers = [
        # Foreground cascading deletion.
        "resources-finalizer.argocd.argoproj.io"
      ]
    }

    spec = {
      project = "default"

      source = {
        repoURL        = "https://artifactory.example.com/artifactory/helm-all"
        chart          = each.value["chart"]
        targetRevision = try(var.charts[each.value["chart"]]["version"], null)

        helm = {
          valuesObject = {
            replicas = 1
          }

          parameters = concat([
            {
              name = "image.name"
              value = each.key
            }
          ],
            try(each.value["version"], null) != null ? [
              {
                name = "image.tag"
                value = each.value["version"]
              }
            ] : [],
          )
        }
      }

      destination = {
        server    = "https://kubernetes.default.svc"
        namespace = "mynamespace"
      }

      syncPolicy = {
        automated = {
          prune    = true
          selfHeal = true
        }
      }
    }
  }
}

Question

I am using the kubernetes_manifest resource in order to install Argo CD Application objects (spec is defined here).

Initial object creation works without issue, and subsequent runs of terraform apply show no changes (as I would expect).

However, if the input value of manifest.spec.source.helm.valuesObject changes (say, the value of foo changes from bar to bar123), the subsequent run of terraform apply says that the entire kubernetes_manifest resource must be replaced, alongside a massive number of added and changed fields:

Terraform will perform the following actions:

  # kubernetes_manifest.argocd_application["application-3"] must be replaced
-/+ resource "kubernetes_manifest" "argocd_application" {
      ~ manifest = {
          ~ spec       = {
              ~ source      = {
                  ~ helm           = {
                      ~ valuesObject = {
                          ~ foo                   = "bar" -> "bar123"
                            # (6 unchanged attributes hidden)
                        }
                        # (1 unchanged attribute hidden)
                    }
                    # (3 unchanged attributes hidden)
                }
                # (3 unchanged attributes hidden)
            }
            # (3 unchanged attributes hidden)
        }
      ~ object   = {
          ~ metadata   = {
              + annotations                = (known after apply)
              + creationTimestamp          = (known after apply)
              + deletionGracePeriodSeconds = (known after apply)
              + deletionTimestamp          = (known after apply)
              + generateName               = (known after apply)
              + generation                 = (known after apply)
              + labels                     = (known after apply)
              + managedFields              = (known after apply)
                name                       = "application-3"
              + ownerReferences            = (known after apply)
              + resourceVersion            = (known after apply)
              + selfLink                   = (known after apply)
              + uid                        = (known after apply)
                # (2 unchanged attributes hidden)
            }
          ~ operation  = {
              + info        = (known after apply)
              ~ initiatedBy = {
                  + automated = (known after apply)
                  + username  = (known after apply)
                }
              ~ retry       = {
                  ~ backoff = {
                      + duration    = (known after apply)
                      + factor      = (known after apply)
                      + maxDuration = (known after apply)
                    }
                  + limit   = (known after apply)
                }
              ~ sync        = {
                  + dryRun       = (known after apply)
                  + manifests    = (known after apply)
                  + prune        = (known after apply)
                  + resources    = (known after apply)
                  + revision     = (known after apply)
                  + revisions    = (known after apply)
                  ~ source       = {
                      + chart          = (known after apply)
                      ~ directory      = {
                          + exclude = (known after apply)
                          + include = (known after apply)
                          ~ jsonnet = {
                              + extVars = (known after apply)
                              + libs    = (known after apply)
                              + tlas    = (known after apply)
                            }
                          + recurse = (known after apply)
                        }
                      ~ helm           = {
                          + fileParameters          = (known after apply)
                          + ignoreMissingValueFiles = (known after apply)
                          + parameters              = (known after apply)
                          + passCredentials         = (known after apply)
                          + releaseName             = (known after apply)
                          + skipCrds                = (known after apply)
                          + valueFiles              = (known after apply)
                          + values                  = (known after apply)
                          + valuesObject            = (known after apply)
                          + version                 = (known after apply)
                        }
                      ~ kustomize      = {
                          + commonAnnotations         = (known after apply)
                          + commonAnnotationsEnvsubst = (known after apply)
                          + commonLabels              = (known after apply)
                          + forceCommonAnnotations    = (known after apply)
                          + forceCommonLabels         = (known after apply)
                          + images                    = (known after apply)
                          + namePrefix                = (known after apply)
                          + nameSuffix                = (known after apply)
                          + namespace                 = (known after apply)
                          + patches                   = (known after apply)
                          + replicas                  = (known after apply)
                          + version                   = (known after apply)
                        }
                      + path           = (known after apply)
                      ~ plugin         = {
                          + env        = (known after apply)
                          + name       = (known after apply)
                          + parameters = (known after apply)
                        }
                      + ref            = (known after apply)
                      + repoURL        = (known after apply)
                      + targetRevision = (known after apply)
                    }
                  + sources      = (known after apply)
                  + syncOptions  = (known after apply)
                  ~ syncStrategy = {
                      ~ apply = {
                          + force = (known after apply)
                        }
                      ~ hook  = {
                          + force = (known after apply)
                        }
                    }
                }
            }
          ~ spec       = {
              ~ destination          = {
                  + name      = (known after apply)
                    # (2 unchanged attributes hidden)
                }
              + ignoreDifferences    = (known after apply)
              + info                 = (known after apply)
              + revisionHistoryLimit = (known after apply)
              ~ source               = {
                  ~ directory      = {
                      + exclude = (known after apply)
                      + include = (known after apply)
                      ~ jsonnet = {
                          + extVars = (known after apply)
                          + libs    = (known after apply)
                          + tlas    = (known after apply)
                        }
                      + recurse = (known after apply)
                    }
                  ~ helm           = {
                      + fileParameters          = (known after apply)
                      + ignoreMissingValueFiles = (known after apply)
                      ~ parameters              = [
                          ~ {
                              + forceString = (known after apply)
                                name        = "image.name"
                                # (1 unchanged attribute hidden)
                            },
                          ~ {
                              + forceString = (known after apply)
                                name        = "image.tag"
                                # (1 unchanged attribute hidden)
                            },
                        ]
                      + passCredentials         = (known after apply)
                      + releaseName             = (known after apply)
                      + skipCrds                = (known after apply)
                      + valueFiles              = (known after apply)
                      + values                  = (known after apply)
                      ~ valuesObject            = {
                          + foo                            = "bar"
                            # (7 unchanged attributes hidden)
                        }
                      + version                 = (known after apply)
                    }
                  ~ kustomize      = {
                      + commonAnnotations         = (known after apply)
                      + commonAnnotationsEnvsubst = (known after apply)
                      + commonLabels              = (known after apply)
                      + forceCommonAnnotations    = (known after apply)
                      + forceCommonLabels         = (known after apply)
                      + images                    = (known after apply)
                      + namePrefix                = (known after apply)
                      + nameSuffix                = (known after apply)
                      + namespace                 = (known after apply)
                      + patches                   = (known after apply)
                      + replicas                  = (known after apply)
                      + version                   = (known after apply)
                    }
                  + path           = (known after apply)
                  ~ plugin         = {
                      + env        = (known after apply)
                      + name       = (known after apply)
                      + parameters = (known after apply)
                    }
                  + ref            = (known after apply)
                    # (3 unchanged attributes hidden)
                }
              + sources              = (known after apply)
              ~ syncPolicy           = {
                  ~ automated                = {
                      + allowEmpty = (known after apply)
                        # (2 unchanged attributes hidden)
                    }
                  ~ managedNamespaceMetadata = {
                      + annotations = (known after apply)
                      + labels      = (known after apply)
                    }
                  ~ retry                    = {
                      ~ backoff = {
                          + duration    = (known after apply)
                          + factor      = (known after apply)
                          + maxDuration = (known after apply)
                        }
                      + limit   = (known after apply)
                    }
                  + syncOptions              = (known after apply)
                }
                # (1 unchanged attribute hidden)
            }
            # (2 unchanged attributes hidden)
        }
    }

I understand that objects created by Terraform can change outside of its control, and it appears that Argo CD’s controllers are doing that in this case, by creating default values for optional fields. As such, I added a few lifecycle.ignore_changes entries to instruct Terraform to disregard these:

resource "kubernetes_manifest" "argocd_application" {
  lifecycle {
    ignore_changes = [
      object.metadata.annotations,
      object.metadata.finalizers,
      object.metadata.labels,
      object.operation.initiatedBy.automated
    ]
  }

  [... rest of resource...]
}

However, this appears to have made no difference, as a terraform apply still gives me the same “must be replaced” output with all the same fields.

I feel like I may be missing something here, so I’m asking for assistance at this stage. Thanks! :slightly_smiling_face: