본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 21. 22:48

Ship Happens: Kubernetes를 위해 3B 로컬 모델을 신뢰할 수 있게 만든 방법 (모델을 전혀 믿지 않음으로써)

요약

3B 규모의 작은 로컬 모델을 사용하여 Kubernetes 매니페스트를 생성할 때 발생하는 오류를 방지하는 전략을 소개합니다. 모델의 출력을 무조건 신뢰하는 대신, 실제 클러스터의 dry-run 결과를 피드백으로 활용하여 오류를 교정하는 워크플로우를 제안합니다.

핵심 포인트

  • 작은 모델의 환각(Hallucination)과 문법 오류를 방지하기 위한 검증 프로세스 구축
  • Kubernetes dry-run 결과를 모델에게 직접 전달하여 오류를 자가 수정하도록 유도
  • YAML 구분자(---) 누락이나 플레이스홀더 이미지 사용 등 실질적인 배포 위험 요소 식별

작은 로컬 모델들은 놀라울 정도로 유능합니다. 하지만 아주 작고 놓치기 쉬운 방식으로 틀리기도 합니다. 여기서는 지원이 중단된 API 버전이 나타나거나, 실제로 배포하기 전까지는 진짜처럼 보이는 플레이스홀더 (placeholder) 값이 나타나기도 합니다. 일반적인 해결책은 더 나은 프롬프팅 (prompting)입니다. 저는 다른 방법을 시도했습니다. 모델을 올바르게 만들려고 노력하는 대신, 그저 잘못된 것이 절대 배포되지 않도록 만드는 것입니다.

아이디어

Ship Happens를 사용하면 Kubernetes 배포를 일반적인 영어로 설명할 수 있습니다. 로컬 모델 (qwen2.5:3b, Ollama를 통해 사용)이 매니페스트 (manifest) 초안을 작성합니다. 어떤 것이 "완료"로 간주되기 전에, 해당 매니페스트는 서버 측 드라이 런 (server-side dry-run)을 사용하여 실제 로컬 클러스터 (kind)를 대상으로 테스트됩니다:

kubectl apply --dry-run=server -f manifest.yaml

만약 클러스터가 이를 거부하면, 모호한 "다시 시도하세요"가 아니라 _실제 에러 메시지_가 모델로 직접 전달됩니다:

func RegenerateFromError(instruction, imageName, clusterError string) (string, error) {
    correction := fmt.Sprintf(
        "A previous attempt was tested against a real Kubernetes "+
...

그 후 다시 확인 과정을 거칩니다. 모델은 단순히 맹목적으로 "다시 시도"할 수 없습니다. 모든 재시도는 클러스터가 실제로 무엇이 잘못되었는지 말한 내용에 근거합니다.

예상치 못하게 잡아낸 것들

1. --- 누락으로 인한 리소스의 조용한 병합

Deployment와 Service를 요청하면 모델이 가끔 그 사이의 YAML 문서 구분자를 잊어버립니다. 그 결과는 파싱 (parse) 에러가 아닙니다. 더 심각합니다. ---가 없으면 중복된 키를 가진 하나의 YAML 문서가 생성되며, 대부분의 파서 (kubectl이 사용하는 것을 포함하여)는 각 중복 키에 대해 마지막 값을 유지합니다. Deployment의 spec이 Service의 spec에 의해 조용히 덮어씌워집니다. 설정의 절반이 아무런 경고 없이 사라지게 됩니다.

// 모델은 종종 올바른 예시를 보여주기 전에 잘못된 예시를 보여주거나,
// 단순히 구분자 (separator)를 잊어버리곤 합니다. 어느 쪽이든,
// 이 과정은 검증 (validation)이 실행되기 전에 이를 정규화합니다:
...

2. 모든 검사를 통과하는 플레이스홀더 이미지 (Placeholder images)

모델은 문법적으로 완벽한 YAML인 image: your-app-image:v1을 작성할 수 있습니다. Dry-run은 스키마 (schema)를 검증할 뿐, 해당 문자열이 실제로 가져올 수 있는 (pullable) 실제 이미지인지 여부는 검증하지 않습니다. 이는 검증을 깔끔하게 통과한 뒤, 실제 포드 (pod) 스케줄링 단계인 20분 후에야 ImagePullBackOff 오류로 실패하게 됩니다.

해결책은 더 똑똑한 프롬프트 (prompt)를 사용하는 것이 아니었습니다 (시도해 보았지만, 모델은 그저 다른 플레이스홀더를 생성할 뿐이었습니다). 해결책은 결정론적 치환 (deterministic substitution)이었습니다:

var knownImages = map[string]string{
    "nginx": "nginx:latest", "redis": "redis:7", "postgres": "postgres:16",
    // ...
...

3. 버전을 삭제함으로써 버전을 "수정"해버린 복구 작업

이 사례가 제가 가장 좋아했던 사례입니다. 하나의 매니페스트 (manifest)가 PodDisruptionBudget에 대해 폐기된 (deprecated) policy/v1beta1을 사용하고 있었습니다. Dry-run은 이를 올바르게 거부했습니다. 복구 루프 (repair loop)가 작동하여 에러를 읽고는, 이를... v1으로 "수정"했습니다. policy/v1이 아니라, API 그룹 (API group)을 완전히 생략하고 그냥 v1으로 만든 것입니다.

기술적으로는 다른 문자열입니다. 하지만 여전히 완전히 틀린 결과였습니다. PodDisruptionBudget은 순수 v1 그룹 아래에는 전혀 존재하지 않기 때문입니다. 모델은 무언가 변경되어야 한다는 점은 인식했지만, 자신이 알고 있는 가장 일반적인 범용 apiVersion을 기본값으로 선택했고, 새로운 방식으로 틀려버렸습니다.

해결책은 영리한 것이 아니라 명시적인 것이었습니다:

prompt := `중요: 만약 에러가 폐기되었거나 유효하지 않은 apiVersion에 관한 것이라면,
API GROUP 접두사 (슬래시 앞부분)는 보통 그대로 유지됩니다.
오직 버전 번호만 변경됩니다. ...

그 후 다시 검증했을 때, 정상적으로 통과되었습니다.

"하지만 jsonschema로 이 작업을 할 수 없었나요?"

이 글을 게시했을 때 나온 질문이며, 타당한 질문입니다. 대부분의 경우: 아니요, 위에서 언급한 버그들에 대해서는 불가능합니다.

정적 스키마 (Static schema)는 그것이 포함된 Kubernetes 버전의 스키마에 고정되어 있습니다. policy/v1beta1 버그는 실제로 구조적으로 유효한 형태입니다. 다만 최신 클러스터에서는 제거되었을 뿐입니다. 스키마 패키지는 그것이 얼마나 최신인지에 따라 해당 형태를 여전히 "유효"하다고 판단할 수 있습니다. 실제 클러스터를 대상으로 하는 드라이 런 (Dry-run)은 그러한 지연이 없습니다. 지금 이 순간, 클러스터가 실제로 수용하는 내용과 정확히 대조하여 확인하기 때문입니다.

또한 이는 CRD (Custom Resource Definitions)까지 무료로 지원합니다. 클러스터에 cert-manager, Prometheus 오퍼레이터(operators), 혹은 그 어떤 커스텀 리소스가 있더라도: 이를 위한 범용 스키마는 없지만, 드라이 런은 추가 설정 없이 실제로 설치된 내용에 대해 검증을 수행합니다. 그리고 스키마 검증은 고립된 상태에서 형태(shape)만을 확인하기 때문에 결코 볼 수 없는 실제 어드미션 체인 (admission chain, RBAC 및 모든 웹훅 포함)을 통과합니다.

jsonschema는 더 빠르고 오프라인에서도 작동하므로 합리적인 1차 필터 역할을 합니다. 다만 제가 잡아내려 했던 것들을 처리하기에는 그것만으로는 충분하지 않습니다.

잡아낼 수 없는 것들

여기서 한계점에 대해 솔직해질 필요가 있습니다. 드라이 런은 _구문 (syntax)_을 증명하는 것이지, _아키텍처 (architecture)_를 증명하는 것이 아닙니다. 다음과 같은 사항은 잡아낼 수 없습니다:

  • 3개의 레플리카 (replicas) 간에 공유되는 ReadWriteOnce PersistentVolumeClaim (YAML 자체는 완벽하게 유효하지만, 포드 (pod) 스케줄링 단계에서 실패함)
  • 모호한 요청에 대해 실제 존재하지만 잘못된 이미지를 선택하는 모델 (예: "prometheus"를 요청했을 때, 실제 존재하지만 잘못된 Alertmanager의 이미지를 전달받음)
  • 컨테이너 내부의 시작 요구 사항 (예: ALLOW_EMPTY_PASSWORD=yes 설정 없이는 시작을 거부하는 Bitnami 이미지). 이는 이미지에 내장된 애플리케이션 로직이며, Kubernetes API에는 완전히 보이지 않습니다.

마지막 항목은 이제 잡아낼 수 있습니다. 다만 드라이 런이 아니라, 라이브 포드 상태를 폴링(polling)하는 대시보드(Dashboard)가 잡아냅니다. 그리고 크래시(crash)가 발생하면 실제 로그를 가져와 로컬 모델에게 가공되지 않은 로그 덤프 대신 평이한 영어로 근본 원인을 요약하도록 요청합니다:

🔴 redis-high-availability
0/3 replicas ready
Missing REDIS_PASSWORD leads to startup failure.

그 한 줄은 훨씬 더 길고 지저분한 로그 출력 결과에 대해 모델이 진정으로 직접 요약한 것입니다. 인프라를 생성하는 것이 아니라, 동일한 로컬 모델을 사용하여 발생한 실패를 사후에 설명하는 용도로 사용한 사례입니다.

아직 검증되지 않은 부분

누군가가 직접 고생하며 깨닫게 하기보다는 차라리 제가 직접 말씀드리는 편이 낫겠습니다. 세 번째 모드인 "My Code" 모드가 있는데, 이는 사용자의 소스 코드를 이미지로 빌드하여 클러스터에 직접 배포하도록 설계되었습니다. 이 기능은 구현되어 있습니다. 하지만 저는 아직 이를 엔드 투 엔드(end-to-end)로 실행해 보지 않았습니다. README 파일에는 다른 척하는 대신, 정확히 그 사실을 그대로 명시하고 있습니다.

"Public Image" 모드(설명하면 AI가 초안을 작성)와 "My YAML" 모드(직접 붙여넣어 생성을 완전히 건너뜀)는 모두 반복적으로 테스트되었으며 정상 작동이 확인되었습니다. 소스 빌드(build-from-source) 경로는 아직 그 단계에 도달하지 못했습니다.

핵심 요점

이것은 사실 Kubernetes에 관한 것도 아니고, 이 특정 모델에 관한 것도 아닙니다. 핵심 패턴은 다음과 같습니다: 신뢰할 수 없는 생성기를 단순히 더 나은 지시문(instructions)만으로 더 신뢰할 수 있게 만들려고 시도하지 마세요. 대신, 검증할 수 있는 실제 외부 대상을 제공하고, 모델이 틀렸을 때 실제 실패 사례를 다시 입력값으로 넣어주며, 진실된 결과가 나올 때까지 그 루프(loop)를 실행하게 하세요.

이 방식이 작동하기 위해 3B 모델이 똑똑할 필요는 없습니다. 그저 모델을 검증할 수 있는 정직한 무언가만 있으면 됩니다.

Code: github.com/shouvik12/ship-happens

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0