- Published on
Terraform EKS 상태 꼬임으로 apply 무한 반복 끊기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 팀이 같은 EKS를 건드리거나(콘솔/CLI 핫픽스), 모듈 업그레이드로 리소스 스키마가 바뀌거나, EKS 애드온/노드그룹이 AWS 쪽에서 자동으로 값을 보정하면서 Terraform의 기대값과 실제값이 계속 어긋나는 순간이 있습니다. 이때 흔히 겪는 증상이 terraform apply가 성공했는데 다음 실행에서 또 같은 diff가 뜨는 무한 반복입니다.
이 글은 “왜 루프가 생기는지”를 패턴으로 나누고, 가장 안전한 순서로 원인을 좁혀가며 state를 정상화하는 방법을 다룹니다. 특히 EKS는 관리형 서비스 특성상 AWS가 값을 자동 조정하는 영역이 있어, 단순히 apply를 더 돌린다고 해결되지 않는 경우가 많습니다.
> 참고: 네트워크/클러스터 통신 계열 이슈가 함께 섞여 “apply는 돌아가는데 kubernetes provider가 타임아웃” 같은 형태로 보일 수도 있습니다. 이 경우 아래 글들도 같이 확인하면 원인 분리가 빨라집니다. > - Kubernetes apiserver i/o timeout 원인과 해결 > - EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS
apply 무한 반복(루프)의 전형적인 징후
다음 중 하나라도 해당하면 “state 꼬임/드리프트 루프”를 의심합니다.
terraform apply가 성공했는데 다음plan에서 동일한 변경이 재등장- 매번
~ update in-place가 뜨며 특정 필드가 A↔B로 토글 known after apply가 반복되며, 실제로는 값이 변하지 않는데도 diff가 지속kubernetes_*리소스가 계속 재적용되거나,helm_release가 매번 변경으로 인식- EKS Addon(예: coredns, vpc-cni)이나 Node Group의 특정 옵션이 계속 바뀌었다가 돌아옴
핵심은 “Terraform이 보는 값(상태/state, 데이터 소스 결과)”과 “AWS/EKS가 유지하는 값”이 같지 않은데 그 차이가 자동으로 계속 재발한다는 점입니다.
원인 패턴 6가지(실무에서 가장 자주 터지는 것)
1) AWS가 자동 보정하는 필드(Managed drift)
EKS 애드온, 노드그룹, 보안그룹 규칙 등에는 AWS가 내부 정책/버전 정책으로 값을 자동 정규화하는 경우가 있습니다.
- 예: 애드온
configuration_values의 JSON 정렬/공백/키 순서 차이 - 예: 특정 필드가 API 응답에서 기본값으로 채워져 Terraform과 비교 시 매번 diff
이 경우는 정확히 같은 의미인데 표현이 달라서 루프가 생기기도 하고, 실제로는 AWS가 강제로 되돌려서 토글이 생기기도 합니다.
2) kubernetes / helm provider의 “서버 측 디폴트”와 충돌
Kubernetes 리소스는 서버가 기본값을 채우는 필드가 많습니다. Terraform이 그 필드를 “내가 설정한 적 없음”으로 간주했다가, 다음 refresh에서 “값이 생겼네?”로 diff를 만들 수 있습니다.
metadata.annotations에 컨트롤러가 주입하는 값spec의 기본값(예:revisionHistoryLimit,strategy세부 필드)
3) provider 버전/모듈 업그레이드로 스키마가 바뀜
예전에는 computed였던 필드가 configurable로 바뀌거나, 반대로 ForceNew가 붙는 등 스키마가 바뀌면 기존 state와의 비교가 틀어질 수 있습니다.
- AWS provider 업그레이드 후 EKS 관련 리소스 diff 폭증
- terraform-aws-eks 모듈 메이저 업그레이드 후 node group / addon 리소스 주소 변경
4) 리소스 주소(address) 변경으로 “새로 만들고 지우기”가 반복
모듈 내부 리소스가 count에서 for_each로 바뀌거나 키가 바뀌면 주소가 달라져, Terraform은 기존 리소스를 “다른 것”으로 인식합니다.
module.eks.aws_eks_addon.this[0]→module.eks.aws_eks_addon.this["coredns"]
이걸 제대로 moved 처리(또는 state mv)하지 않으면, 매번 recreate가 발생하거나 꼬여서 apply가 끝없이 반복될 수 있습니다.
5) 콘솔/CLI 핫픽스(Out-of-band change)
운영 중 급하게 콘솔에서 보안그룹 규칙을 열었다가, Terraform이 다음 apply에서 다시 닫고, 또 사람이 다시 열고… 이런 식으로 “인간-테라폼 줄다리기”가 루프를 만듭니다.
6) EKS 클러스터/노드 상태 문제로 인해 provider가 불완전한 refresh
EKS API는 되는데 Kubernetes API가 불안정하면, kubernetes_* 리소스 refresh가 실패하거나 부분 성공하면서 state가 애매하게 남을 수 있습니다.
- apiserver i/o timeout
- TLS handshake timeout
- CNI 문제로 노드 NotReady → 애드온/데몬셋이 정상화되지 않아 의도한 상태로 수렴하지 않음
이 경우는 우선 클러스터 상태부터 안정화해야 합니다. (예: CNI/노드 상태) 필요하면 EKS kubelet NotReady - CNI plugin not initialized 해결도 함께 점검하세요.
해결 전략: “루프를 끊는” 안전한 순서
아래 순서대로 진행하면, 불필요한 파괴를 피하면서 원인을 빠르게 좁힐 수 있습니다.
- 반드시 백업: state 백업 + 계획 출력 저장
plan을 “어떤 필드가 반복되는지” 기준으로 최소 단위로 쪼개 관찰- out-of-band 변경 여부 확인(CloudTrail/콘솔 이력/팀 커뮤니케이션)
- 스키마/주소 변경이면
moved또는state mv로 정리 - AWS가 보정하는 필드면
ignore_changes또는 입력값 정규화(JSON 등) - Kubernetes/Helm이면 서버 주입 필드 제거/ignore, 또는
manifest/chart 값 정규화 - 마지막 수단으로
state rm+import로 재정렬
이제 각 단계를 실전 명령과 코드로 설명합니다.
1) state 백업과 재현 가능한 plan 확보
state 백업
원격 backend(S3 등)라도 로컬로 백업 파일을 떠두는 게 안전합니다.
terraform state pull > state-backup-$(date +%Y%m%d-%H%M%S).json
반복 diff를 파일로 고정
매번 같은 diff인지 확인하려면 plan을 파일로 저장하고, show로 비교합니다.
terraform plan -out tfplan
terraform show -no-color tfplan > plan.txt
여기서 항상 바뀌는 리소스 주소와 항상 바뀌는 필드를 체크리스트로 뽑습니다.
2) 리소스 주소 변경(모듈 업그레이드)이라면 moved/state mv로 먼저 정리
모듈/코드 리팩토링 후 루프가 시작됐다면, 1순위로 의심할 건 “주소 변경”입니다.
Terraform 1.1+ moved 블록 사용(권장)
moved {
from = module.eks.aws_eks_addon.this[0]
to = module.eks.aws_eks_addon.this["coredns"]
}
이렇게 하면 state가 새 주소로 안전하게 이동되어, Terraform이 더 이상 “새 리소스”로 착각하지 않습니다.
이미 꼬였다면 terraform state mv
terraform state mv \
'module.eks.aws_eks_addon.this[0]' \
'module.eks.aws_eks_addon.this["coredns"]'
주의점:
state mv는 즉시 state를 바꾸므로, 반드시 백업 후 진행- 주소 문자열(따옴표/이스케이프)을 정확히 써야 함
3) AWS가 보정하는 필드/표현 차이: 입력값 정규화 + ignore_changes
(A) JSON 문자열은 정규화해서 넣기
EKS Addon의 configuration_values는 JSON 문자열로 넣는 경우가 많고, 이게 문자열 비교로 diff를 유발합니다. jsonencode()로 정규화하면 불필요한 차이를 줄일 수 있습니다.
resource "aws_eks_addon" "vpc_cni" {
cluster_name = aws_eks_cluster.this.name
addon_name = "vpc-cni"
addon_version = "v1.16.0-eksbuild.1"
configuration_values = jsonencode({
env = {
ENABLE_PREFIX_DELEGATION = "true"
WARM_PREFIX_TARGET = "1"
}
})
}
(B) 정말로 AWS가 강제 보정하는 필드는 ignore_changes
의미적으로 “AWS가 관리하는 영역”이라면 Terraform이 집착하지 않게 만드는 게 낫습니다.
resource "aws_eks_addon" "coredns" {
cluster_name = aws_eks_cluster.this.name
addon_name = "coredns"
addon_version = "v1.11.1-eksbuild.4"
lifecycle {
ignore_changes = [
configuration_values,
]
}
}
권장 기준:
- 운영 정책상 반드시 고정해야 하는 값이면 ignore하지 말고, 정규화/원인 제거
- AWS가 자동으로 바꾸는 게 정상 동작이고, Terraform이 그걸 계속 되돌리려 한다면 ignore 고려
4) Kubernetes/Helm 리소스가 매번 바뀔 때: “서버 주입 필드”를 다루는 법
kubernetes provider: 불필요한 필드 선언을 줄이기
서버가 주입하는 annotation/label을 코드에 억지로 고정하면 루프가 생깁니다. 가능하면 필수 필드만 선언합니다.
resource "kubernetes_config_map" "example" {
metadata {
name = "example"
namespace = "default"
}
data = {
"app.conf" = "key=value\n"
}
}
Helm: values 정규화 + set 블록 일관성
values에 YAML 문자열을 직접 쓰면 공백/정렬 차이로 drift가 생길 수 있습니다. 가능하면 yamlencode()를 사용합니다.
resource "helm_release" "metrics_server" {
name = "metrics-server"
repository = "https://kubernetes-sigs.github.io/metrics-server/"
chart = "metrics-server"
namespace = "kube-system"
values = [
yamlencode({
args = ["--kubelet-insecure-tls"]
})
]
}
또한 set {}과 values를 혼용하면 최종 렌더링 결과가 애매해져 diff가 반복되는 경우가 있으니, 한 방식으로 통일하는 편이 좋습니다.
5) Out-of-band 변경을 끊기: 팀 프로세스와 락
기술적으로는 state를 맞춰도, 사람이 콘솔에서 다시 바꾸면 루프는 재발합니다.
- 변경은 Terraform으로만 한다는 규칙
- 급한 핫픽스는 사후에 반드시 코드 반영(PR)
- 원격 backend에 state lock(DynamoDB)을 켜서 동시 apply 방지
S3+DynamoDB 락 예시:
terraform {
backend "s3" {
bucket = "my-tfstate"
key = "eks/prod/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-lock"
encrypt = true
}
}
6) 최후의 정리: state rm + import로 “현실”에 state를 다시 붙이기
위 조치로도 해결이 안 되면, 특정 리소스만 state에서 제거 후 실제 리소스를 import하여 재동기화합니다. 이 방법은 강력하지만, import 대상/주소를 정확히 해야 합니다.
예: EKS Addon import
- state에서 제거(리소스는 삭제되지 않음)
terraform state rm 'aws_eks_addon.coredns'
- import (ID 포맷은 보통
cluster_name:addon_name)
terraform import aws_eks_addon.coredns my-cluster:coredns
- plan으로 수렴 확인
terraform plan
주의:
state rm직후apply를 하면 Terraform이 “없으니 새로 만들자”고 할 수 있으니, 반드시 import까지 한 세트로 진행- 모듈 내부 리소스면 주소가
module.eks.aws_eks_addon.coredns같은 형태일 수 있음
루프를 빠르게 진단하는 체크리스트(10분 컷)
-
terraform plan에서 매번 바뀌는 필드가 정확히 무엇인지 텍스트로 추출했는가? - 최근에 provider/모듈 버전이 바뀌었는가? (특히 terraform-aws-eks)
- 리소스 주소가 바뀐 흔적이 있는가? (
count→for_each, key 변경) - JSON/YAML 문자열 비교로 인한 diff인가? (
jsonencode,yamlencode로 정규화) - AWS/EKS가 관리하는 필드인가? (필요 시
ignore_changes) - 콘솔/CLI에서 누군가 바꾸고 있지 않은가? (CloudTrail/운영 기록)
- Kubernetes API가 불안정해서 refresh가 깨지지 않는가? (timeout, TLS)
- 필요 최소 리소스만
-target으로 좁혀서 원인 리소스를 특정했는가?
-target은 남용하면 의존성 그래프가 깨질 수 있지만, 원인 리소스 식별용으로는 유용합니다.
terraform plan -target=aws_eks_addon.coredns
마무리: “정상 수렴”의 기준을 먼저 정하자
Terraform apply 무한 반복은 단순한 버그가 아니라, 대개 관리 주체가 둘 이상이거나(사람/자동화/AWS), 표현(문자열)과 의미(구성)가 섞여 비교되면서 생깁니다. 해결의 핵심은 다음 두 가지입니다.
- Terraform이 책임져야 하는 값과 AWS/EKS가 책임지는 값을 분리한다(필요 시
ignore_changes). - state를 “코드의 주소 체계”에 맞게 정리한다(
moved,state mv,import).
이 두 축을 잡고 위 순서대로 처리하면, 대부분의 EKS 상태 꼬임 루프는 리소스 파괴 없이 끊을 수 있습니다.