# Helm CLI를 감싸는 대신 Go SDK로 배포 제어하기

서비스에서 Helm 차트를 배포해야 할 때 가장 빠른 방법은 exec.Command("helm", "upgrade", ...)로 CLI를 호출하는 것입니다. 동작은 하지만, 릴리스 상태를 알려면 출력 문자열을 파싱해야 하고, 실행 환경마다 helm 바이너리 버전이 달라지며, 에러는 종료 코드 하나로만 돌아옵니다.

Helm 3은 CLI와 라이브러리를 분리해 Go SDK를 제공합니다. 이 글에서는 SDK의 구성, action.Configuration 초기화 방법, 릴리스 저장 드라이버의 의미, 그리고 CLI 래핑에서 넘어올 때 달라지는 지점을 정리합니다.

# 1. CLI 래핑의 한계

// 흔한 출발점
out, err := exec.Command("helm", "upgrade", "--install", name, chart,
    "-n", ns, "-f", valuesPath).CombinedOutput()
if err != nil {
    return fmt.Errorf("helm 실패: %s", out)   // 무엇이 왜 실패했는지 구조화되지 않음
}
문제 내용
결과 파싱 릴리스 리비전·상태·렌더 결과를 문자열에서 뽑아야 함
환경 의존 컨테이너 이미지에 helm 바이너리를 넣고 버전을 관리해야 함
자격 증명 kubeconfig 파일을 디스크에 써야 함
에러 처리 종료 코드만으로는 "이미 존재함"과 "권한 없음"을 구분하기 어려움
values 조립 파일로 직렬화 → CLI가 다시 파싱하는 왕복

SDK를 쓰면 이 다섯 가지가 전부 Go 타입으로 바뀝니다.

# 2. SDK 구성

공식 문서가 정리한 주요 패키지는 다음과 같습니다.

패키지 역할
action Helm 동작의 "클라이언트". CLI 자신도 이 패키지를 사용
chart, chartutil 차트 로딩과 조작
cli Helm 환경변수·출력·values 파일 처리
release Release 객체와 상태 정의

actionCLI 자신이 쓰는 것과 같은 경로입니다. CLI로 되는 일은 SDK로도 됩니다.

호환성도 명시돼 있습니다.

breaking changes will only be made with a major version release or to remediate a security issue.

다만 문서는 SDK가 CLI에서 분리되는 과정의 흔적이 남아 있다는 점("some rough edges remaining")도 함께 밝히므로, 마이너 업그레이드 시 릴리스 노트는 확인하는 편이 좋습니다.

# 3. action.Configuration 초기화

모든 동작의 출발점입니다. Init은 네 가지를 받습니다.

인자 의미
RESTClientGetter 어느 클러스터에 접속할지
namespace 대상 네임스페이스(빈 문자열이면 전체 조회)
helmDriver 릴리스 상태 저장 방식
로그 함수 내부 로그 출력
import (
    "helm.sh/helm/v3/pkg/action"
    "helm.sh/helm/v3/pkg/cli"
)

func newConfig(namespace string) (*action.Configuration, error) {
    settings := cli.New()
    settings.SetNamespace(namespace)

    cfg := new(action.Configuration)
    if err := cfg.Init(
        settings.RESTClientGetter(),
        namespace,
        os.Getenv("HELM_DRIVER"),          // 비어 있으면 secret
        func(format string, v ...interface{}) {
            slog.Debug(fmt.Sprintf(format, v...))
        },
    ); err != nil {
        return nil, err
    }
    return cfg, nil
}

여기서 Configuration은 네임스페이스에 묶입니다. 여러 네임스페이스를 다루는 서비스라면 요청마다 만들거나 네임스페이스별로 캐시해야 합니다. 하나를 만들어 전역에서 재사용하면 엉뚱한 네임스페이스에 배포됩니다.

# 4. 릴리스 저장 드라이버

Helm은 릴리스 상태를 클러스터에 저장합니다. HELM_DRIVER로 방식을 고릅니다.

저장 위치 특징
secret (기본) 네임스페이스의 Secret 릴리스 매니페스트가 압축돼 들어감
configmap ConfigMap 과거 방식, Secret 대신 쓸 때
sql (베타) 외부 DB HELM_DRIVER_SQL_CONNECTION_STRING 필요

이 선택이 실무에서 걸리는 지점은 크기 제한입니다. Secret과 ConfigMap 모두 오브젝트 크기 상한(약 1MiB)이 있어 CRD가 많은 대형 차트에서 문제가 될 수 있습니다. 또한 서비스 계정에 해당 네임스페이스의 Secret 읽기·쓰기 권한이 필요하므로, RBAC를 짤 때 차트가 만드는 리소스뿐 아니라 릴리스 저장용 Secret 권한도 포함해야 합니다.

# 5. install / upgrade / list

func Upgrade(ctx context.Context, cfg *action.Configuration,
    name, ns string, ch *chart.Chart, vals map[string]interface{}) (*release.Release, error) {

    up := action.NewUpgrade(cfg)
    up.Namespace = ns
    up.Install = true              // 없으면 설치 (helm upgrade --install)
    up.Atomic = true               // 실패 시 롤백
    up.Wait = true                 // 리소스가 Ready 될 때까지 대기
    up.Timeout = 10 * time.Minute
    up.MaxHistory = 10             // 릴리스 히스토리 상한

    return up.RunWithContext(ctx, name, ch, vals)
}

valuesmap[string]interface{}라는 점이 CLI 대비 가장 큰 차이입니다. 파일 직렬화 왕복 없이 구조체에서 바로 만들 수 있고, 타입 오류를 컴파일 시점이나 검증 단계에서 잡을 수 있습니다.

차트는 로컬 디렉터리, 패키지 파일, 저장소 어디서든 로드할 수 있습니다.

ch, err := loader.Load("./charts/workspace")     // 디렉터리 또는 .tgz

목록 조회는 상태 필터를 지원합니다.

list := action.NewList(cfg)
list.All = true
list.SetStateMask()               // Filter 설정을 반영. 호출하지 않으면 기본 상태만 조회
releases, err := list.Run()

SetStateMask() 호출을 빠뜨려 "분명히 있는데 목록에 안 나오는" 상황이 자주 생깁니다.

# 6. 운영에서 챙길 것

# 6-1. 동시 실행

같은 릴리스에 대한 동시 작업은 충돌합니다. Helm은 릴리스가 진행 중이면 another operation (install/upgrade/rollback) is in progress로 거부하는데, 서비스가 여러 레플리카로 뜬다면 애플리케이션 레벨에서 릴리스 단위 잠금을 두는 편이 안전합니다.

# 6-2. Wait와 타임아웃

Wait = true는 편리하지만 요청 스레드를 길게 잡습니다. 웹 요청 안에서 동기로 기다리기보다, 작업을 큐에 넣고 상태를 폴링하게 만드는 구조가 낫습니다. RunWithContext를 쓰면 상위 컨텍스트 취소가 전달됩니다.

# 6-3. dry-run으로 렌더 검증

배포 전에 렌더 결과만 확인할 수 있습니다. values 스키마 오류를 클러스터에 손대기 전에 잡는 용도로 유용합니다.

inst := action.NewInstall(cfg)
inst.DryRun = true
inst.ClientOnly = true          // 클러스터 접속 없이 렌더만
rel, err := inst.Run(ch, vals)
fmt.Println(rel.Manifest)

# 7. 트러블슈팅

증상 원인 해결
엉뚱한 네임스페이스에 배포됨 Configuration을 전역 재사용 네임스페이스별로 생성
릴리스가 목록에 안 보임 SetStateMask() 미호출 필터 설정 후 호출
another operation in progress 동시 요청 또는 중단된 이전 작업 릴리스 단위 잠금, 필요 시 상태 확인 후 롤백
Secret 권한 오류 릴리스 저장용 Secret 권한 누락 RBAC에 Secret CRUD 추가
대형 차트 저장 실패 오브젝트 크기 상한 차트 분리 또는 sql 드라이버 검토
업그레이드 후 히스토리 폭증 MaxHistory 미설정 상한 지정

# 8. 마무리

  • Helm CLI 래핑은 시작은 빠르지만 결과 파싱·환경 의존·에러 구분에서 비용이 누적됩니다.
  • SDK의 action 패키지는 CLI가 사용하는 것과 같은 경로이므로 기능 차이를 걱정할 필요가 없습니다.
  • action.Configuration네임스페이스에 묶인 객체입니다. 전역 재사용은 사고로 이어집니다.
  • 릴리스 저장 드라이버 선택은 RBAC 설계와 오브젝트 크기 상한에 직접 영향을 줍니다.

# 참고