Terraform Kubernetes Provider 심화: Server-Side Apply(SSA)와 field manager 충돌을 ‘설계로’ 없애는 실무 패턴

Kubernetes 리소스를 Terraform로 관리할 때 가장 흔한 운영 장애 중 하나는 “의도치 않게 다른 도구(kubectl, 컨트롤러, GitOps)가 관리하던 필드를 덮어써서” 발생하는
Server-Side Apply(SSA) field ownership 충돌입니다.
이 글은 공식 문서 근거로만 SSA의 동작(관리 필드, 충돌, 강제 덮어쓰기)과 Terraform Kubernetes Provider의 kubernetes_manifest 리소스가 SSA를 어떻게 사용하며,
field_manager / computed_fields를 어떻게 설계에 반영해야 “충돌을 해결하는 운영”이 아니라 “충돌이 안 나도록 만드는 설계”로 갈 수 있는지 정리합니다.
TL;DR (실무 결론 6줄)
- SSA는 API 서버가 필드 단위 소유권(관리자)을 추적하고, 다른 관리자가 소유한 필드를 바꾸려 하면 충돌로 거절할 수 있습니다(SSA의 핵심 안전장치).
kubernetes_manifest는 SSA로 apply합니다(즉, 충돌이 “정상 동작”으로 발생할 수 있음).- 팀/파이프라인 단위로
field_manager.name를 고정해 관리자 정체성을 일관되게 유지합니다. - 필드 소유권 경계를 정하고(예: spec는 Terraform, status/일부 annotation은 컨트롤러) Terraform이 ‘의견 없는’ 필드는 선언에서 빼 충돌을 근본적으로 줄입니다.
- Admission/Mutaing Webhook가 값을 바꾸는 필드는
computed_fields로 “적용 후 값이 달라도 오류로 보지 않게” 합니다. force_conflicts는 ‘편의 기능’이 아니라 소유권을 강제로 뺏는 행위입니다. 원칙/예외를 문서화하고 제한적으로만 사용합니다.
1) Server-Side Apply(SSA)에서 “충돌”이 생기는 이유 (공식 동작 요약)
Kubernetes는 SSA를 통해 객체의 각 필드를 누가 관리하는지(=field manager)를 추적합니다.
API 서버는 metadata.managedFields에 관리 정보를 저장하고,
apply 시점에 다른 관리자가 소유한 필드를 다른 값으로 바꾸면 conflict로 요청을 거절합니다.
이는 “협업자(다른 도구/컨트롤러)의 변경을 무심코 덮어쓰지 않게” 해주는 메커니즘입니다.
SSA의 핵심 메커니즘 (필드 소유권/충돌/강제)
- API 서버는 새로 생성된 객체의 managed fields를 추적합니다.
- apply가 다른 관리자가 소유한 필드를 다른 값으로 바꾸려 하면 conflict가 발생할 수 있습니다.
- 충돌이 나면 선택지는 3가지(공식 문서):
- 강제 덮어쓰기(override)하고 소유권을 가져오기(예: kubectl의
--force-conflicts). - 해당 필드에 더 이상 의견이 없으면 manifest에서 제거해 소유권을 내려놓기.
- 서버 값에 맞추어 동일 값으로 선언해 공유 소유권을 만들기(이후 변경 시 다시 충돌 가능).
- 강제 덮어쓰기(override)하고 소유권을 가져오기(예: kubectl의
- apply 요청은
fieldManager식별자가 필요합니다(SSA의 필수 파라미터).
이 구조 때문에 “Kubernetes에 이미 존재하는 리소스를 여러 도구가 만지는 환경”에서는 충돌이 흔합니다.
중요한 포인트는 충돌 자체는 버그가 아니라 SSA의 안전장치라는 점입니다.
(따라서 해결책도 “충돌 무시”가 아니라 “필드 소유권 경계 설계”로 가는 게 정석입니다.)
2) Terraform의 상태(State) 모델이 충돌을 더 눈에 띄게 만드는 지점
Terraform은 관리 대상 인프라/리소스와 설정 사이의 바인딩과 메타데이터를 저장하기 위해 state를 유지합니다.
그리고 apply 전에는 refresh를 수행해 state를 실제 리소스에 맞춥니다.
Terraform은 “설정된 리소스 인스턴스 ↔ 원격 객체”가 1:1로 매핑된다고 기대합니다.
Kubernetes 리소스를 Terraform로 관리할 때,
다른 도구가 객체를 수정하면(특히 SSA 관리 필드가 이동하면)
Terraform 입장에서는 “내가 관리한다고 생각한 필드가 다르게 보이거나, apply 시점에 conflict로 실패”하면서 운영 이슈로 표면화됩니다.
3) kubernetes_manifest 리소스가 SSA를 쓰는 방식 (Provider 공식 문서 기반)
HashiCorp Terraform Kubernetes Provider의 kubernetes_manifest 리소스는
manifest 속성으로 Kubernetes 객체(한 개)를 표현하고,
적용 후에는 object 속성에 API 서버가 반환한 결과(기본값 포함)를 담습니다.
또한 이 리소스는 Server-side Apply로 apply 연산을 수행합니다.
따라서 field manager 충돌이 “특정 환경에서만 가끔”이 아니라,
SSA 협업 환경에서는 설계하지 않으면 반드시 마주치는 정상 동작입니다.
Provider 기능 포인트(실무에서 바로 쓰는 것만)
field_manager블록: field manager의 이름(name)과 충돌 강제(force_conflicts)를 설정할 수 있습니다.computed_fields: apply 후 API 서버가 값을 변경할 수 있는 필드(예: admission이 주입하는 annotation/label)를 “불일치 오류”로 처리하지 않도록 허용합니다.
기본값으로metadata.annotations,metadata.labels가 포함될 수 있습니다(Provider 문서 설명).- 계획(planning) 시점에 API 접근이 필요할 수 있어, 클러스터가 plan 시점에 접근 가능해야 한다는 제한이 있습니다(Provider 문서의 ‘Before you use this resource’).
- 기존 객체는 import로 state에 바인딩할 수 있으며, import ID 문법이 문서화되어 있습니다.
4) 충돌을 “없애는” 설계 패턴 4가지
패턴 A. field manager 이름을 팀/파이프라인 단위로 고정
SSA는 “누가 마지막으로 해당 필드에 대해 주장(assertion)을 했는지”를 manager로 기록합니다.
따라서 Terraform apply가 실행되는 주체가 바뀔 때마다 manager 정체성이 흔들리면(환경/도구가 다르면)
충돌/소유권 이동이 더 자주 발생할 수 있습니다.
Provider가 제공하는 field_manager.name은 이 정체성을 고정하는 장치입니다.
예시 (Terraform)
resource "kubernetes_manifest" "example" {
manifest = {
apiVersion = "v1"
kind = "ConfigMap"
metadata = {
name = "example"
namespace = "default"
}
data = {
foo = "bar"
}
}
field_manager {
name = "platform-terraform" # 팀/파이프라인 기준으로 고정
}
}
패턴 B. “내가 의견 없는 필드”는 선언에서 빼서 소유권 경계를 만든다
SSA 문서에서 충돌 해결 옵션 중 하나가
“그 필드에 더 이상 의견이 없으면 manifest에서 제거해 관리 claim을 내려놓기”입니다.
실무에서는 이를 ‘사후 해결’이 아니라 ‘사전 설계’로 씁니다.
- 컨트롤러가 관리하는 필드(예:
status)는 Terraform이 굳이 선언하지 않습니다. - Admission이 자동으로 주입하는 annotation/label을 Terraform이 “정확히 이 값이어야 한다”로 고정하지 않습니다.
- 가능하면 GitOps/컨트롤러 vs Terraform의 책임 경계를 문서화합니다(예: spec 중 어디까지 Terraform이 소유하는지).
패턴 C. Mutating Admission 환경에서는 computed_fields로 ‘불일치 오류’를 예방
Provider 문서에 따르면, Kubernetes API가 apply 도중 사용자가 지정한 값을 변경할 수 있고(특히 mutating admission controllers),
그 경우 Terraform은 “apply 후 provider가 예상과 다른 값을 반환했다”라는 형태의 오류를 낼 수 있습니다.
computed_fields는 이런 필드를 computed로 지정해 apply 후 값이 달라도 허용합니다.
예시
resource "kubernetes_manifest" "example" {
manifest = {
apiVersion = "v1"
kind = "ConfigMap"
metadata = {
name = "example"
namespace = "default"
labels = {
"app" = "example"
}
}
data = {
foo = "bar"
}
}
# 필요 시 API가 변경할 수 있는 필드를 computed로 선언
# (기본값으로 metadata.labels/annotations가 포함될 수 있다는 설명이 Provider 문서에 있음)
computed_fields = ["metadata.annotations", "metadata.labels"]
}
패턴 D. force_conflicts는 ‘긴급 수단’으로만: 운영 규칙을 만든다
SSA 문서는 충돌 시 “강제로 덮어쓰면 충돌 필드의 값이 override되고 소유권이 이전된다”고 설명합니다.
Provider의 field_manager.force_conflicts는 이 행동을 Terraform 측에서 수행하게 만드는 옵션입니다.
즉, 이 옵션은 편의가 아니라 소유권 강탈입니다.
권장 규칙(원칙):
- 기본은 force_conflicts=false.
- 예외는 “해당 필드가 반드시 Terraform이 소유해야 하는 설계”가 문서로 합의된 경우에만.
- 예외 사용 시에는 대상 리소스/필드/사유/롤백 경로를 함께 기록.
5) 운영 체크리스트 (배포/장애 대응에 바로 쓰는 버전)
- 리소스 소유권 경계 문서화: 어떤 리소스를 누가 관리(Terraform / kubectl / 컨트롤러 / GitOps)하는지.
- field manager 통일: Terraform 파이프라인의
field_manager.name를 고정했는지. - 불필요한 필드 선언 제거: Terraform이 의견 없는 필드를 manifest에서 제거했는지.
- Mutating Admission 파악: admission이 바꾸는 필드를
computed_fields로 처리했는지. - import 절차: 이미 존재하는 리소스를 Terraform로 가져오는 경우 import ID 문법과 1:1 매핑 규칙(중복 import 금지)을 지켰는지.
- force_conflicts 통제: 예외 사용이 원칙/승인/감사 로그로 남는지.
6) 자주 겪는 장애 3종과 “공식 문서 기반” 대응
| 증상 | 원인(공식 개념) | 1차 대응 | 근본 해결(설계) |
|---|---|---|---|
| apply가 conflict로 실패 | SSA에서 다른 manager가 소유한 필드를 변경하려 함 | 어떤 필드가 conflict인지 확인 후, (1) 값 덮어쓰기 의도가 있나? (2) 그 필드에 Terraform이 의견이 있나? 판단 | 필드 소유권 경계를 정하고, 의견 없는 필드는 선언에서 제거. 필요한 경우 field manager 통일 |
| apply 후 “inconsistent result / unexpected new value”류 오류 | API(특히 mutating admission)가 필드 값을 변경. Provider 문서에서 대표 원인으로 언급 | 변경되는 필드 경로를 특정 | 해당 필드를 computed_fields로 선언해 apply 후 값 변화를 허용 |
| 리소스를 import했는데 이후 변경이 꼬임 | Terraform state는 리소스 인스턴스와 원격 객체의 1:1 매핑을 기대 | 동일 원격 객체를 여러 주소로 import하지 않았는지 점검 | import 정책(주소 규칙, 모듈 구조, 중복 금지) 확립 |
7) 참고 자료(원문)
다음 글(연재 후보): (1) Terraform에서 CRD/CR 관리 시 plan-time API 접근 제약을 구조적으로 피하는 모듈 분리 패턴,
(2) Kubernetes managedFields를 운영 모니터링 지표로 쓰는 방법(누가 무엇을 소유하는지 추적).