grep

DevOps

여기어때 CI/CD 개선기 Part 4: 공통 Helm Chart 설계와 추상화

Jupiter여기어때

2025년 9월 10일

원문에서 보기 ↗

안녕하세요, 여기어때컴퍼니 DevOps팀 주피터입니다.

지난 3편에서는 공통 Helm Chart를 위한 저장소(Registry)를 마련했는데요,

이번 글에서는 그 안에 채워질 ‘공통 Helm Chart’를 어떻게 설계했는지 본격적인 이야기를 시작해보려고 합니다.

지난 3편에서는 흩어져 있던 Helm Chart를 한데 모으기 위해 AWS ECR을 중앙 Chart Registry로 도입한 과정을 소개해 드렸습니다.

마침내 공통 Chart가 머무를 안전한 집이 생긴 셈이죠. 이제 그 집을 채울 차례입니다.

이번 4편에서는 이 공통 Helm Chart를 어떻게 설계 했는지, 그리고 개발자가 신경 써야 할 코드와 인프라의 복잡성 사이에서 어떻게 균형을 맞췄는지에 대한 저희 팀의 깊은 고민과 전략을 공유하고자 합니다.

CD 공통화, 어떤 난관이 있었을까?

“공통 Helm Chart를 만들자!”는 목표는 쉬웠지만, 실행은 만만치 않았습니다.

수십 개의 서비스가 각자의 방식으로 운영되던 환경을 하나로 표준화하는 길에는 여러 난관이 도사리고 있었습니다.

같은 듯 다른, 파편화된 Helm Chart

가장 큰 문제는 ‘파편화’였습니다.

같은 Spring Boot API라도 팀마다 배포 Spec의 구성이 미묘하게 달랐습니다.

어떤 팀은 Pod 수량을 안정적으로 제어할 수 있는 HPA와 PDB를 사용하지 않는 팀도 있었고,

어떤 팀은 KEDA와 HPA를 활용하여 커스텀 메트릭 기준으로 설정하는 등 활용도가 천차만별이었습니다.

# 일부 팀에서만 사용하던 KEDA
  {{- if .Values.keda.triggers.prometheus.tomcatThreadsUtilization }}
  - type: prometheus
    metricType: Value
    metadata:
      serverAddress: {{ .Values.keda.triggers.prometheus.serverAddress }}
      metricName: tomcat_threads_current_utilization
      threshold: '{{ .Values.keda.triggers.prometheus.tomcatThreadsUtilization }}'
      query: >
        avg(
          sum_over_time(tomcat_threads_busy_threads{job="{{ .Values.keda.triggers.prometheus.jobName }}"}[30s])
          /
          sum_over_time(tomcat_threads_config_max_threads{job="{{ .Values.keda.triggers.prometheus.jobName }}"}[30s])
        ) * 100
  {{- end }}

중요한 것은 이러한 설정이 한 개발팀 내에서도 균일하지도 않았으며,

복사-붙여넣기로 인해 해당 설정이 왜 필요한지 모른 채 사용하는 경우가 많았습니다

또한, 어떤 설정이 왜 추가, 삭제, 변경되었는지에 대한 원인 추적이 어려웠습니다

고통스러운 공통 설정 변경

이러한 파편화는 공통 설정을 변경할 때 고통으로 다가왔습니다.

예를 들어, 전사 서비스에 특정 환경변수를 추가해야 하는 상황이 발생하면, DevOps팀은 수십 개의 Repository를 일일이 방문하여 코드를 수정하고 MR을 요청해야 했습니다. 단순 반복 작업은 물론, 누락의 위험도 컸습니다.

어디까지 추상화할 것인가?

이 모든 문제를 해결하기 위해, 저희는 근본적인 질문에 답해야 했습니다.

“개발자가 알아야 하는 최소한의 Kubernetes 설정은 어디까지일까?”

개발자가 인프라의 모든 것을 알 필요는 없지만, 자신의 애플리케이션에 영향을 주는 핵심 설정(Resource, Replica 등)은 직접 제어할 수 있어야 합니다.

저희는 이 질문에 대한 답을 찾아가며, 불필요한 복잡성은 숨기고 핵심만 남기는 추상화를 공통 Helm Chart 설계의 핵심 원칙으로 삼았습니다.

또한, 버그 하나가 전체 서비스 장애로 이어질 수 있는 단일 실패 지점의 위험을 인지하고, 이를 최소화하기 위한 안정성 확보 장치를 마련하는 것도 중요한 과제였습니다.

공통 Helm Chart 설계: 관심사는 최소로, 변화에는 유연하게

수많은 고민 끝에 저희가 내린 결론은 명확했습니다. “개발자는 애플리케이션에만 집중하게 하자.”

이를 위해 다음과 같은 설계 원칙을 적용했습니다.

관심사의 분리: 공통 템플릿과 개별 values.yaml

공통 Chart의 핵심 구조는 ‘공통 템플릿’과 서비스별 ‘values.yaml’의 조합입니다.

공통 템플릿 (in Common Helm Chart)

deployment.yaml, service.yaml 등 모든 서비스에 필요한 K8s 리소스의 뼈대를 정의합니다.

여기에는 보안 설정, 표준 Label/Annotation 등 DevOps팀이 관리하는 불변의 영역이 포함됩니다.

# 개발자가 몰라도 되는 설정 예시
# 컨테이너 보안 영역
{{- define "common.workload.containerSecurityContext.options" -}}
seLinuxOptions: null
privileged: false
allowPrivilegeEscalation: false
capabilities:
  drop: [ "ALL" ]
seccompProfile:
  type: "RuntimeDefault"
{{- end }}

{{- if eq $.Values.global.environment "dev" -}}
# ${AWS_ACCOUNT}/${PROFILE}
{{- else if eq $.Values.global.environment "stage" -}}
# ${AWS_ACCOUNT}/${PROFILE}
{{- else if eq $.Values.global.environment "release" -}}
# ${AWS_ACCOUNT}/${PROFILE}
{{- else }}

개별 values.yaml (in Service Repository)

# 개발자가 알고 있어야 되는 설정 예시
workload:
  - kind: Deployment
    name: # 자신의 Application 이름
    image: # ECR 이름
    tag: # 배포할 이미지 태그
    ...
    livenessProbe:
      path: # Health Check EndPoint
    ...
    service:
      targetPort: # Application이 사용하는 Port
    resources: # 사용할 Pod Spec
      cpu: 500m
      memory: 1000Mi

이 구조를 통해 개발자는 복잡한 K8s Manifest 템플릿을 직접 만질 필요 없이, 자신의 관심사인 values.yaml 파일만 관리하면 됩니다.

공통이지만 변경 가능합니다! (Escape Hatch)

표준화는 중요하지만, 모든 예외 케이스를 공통 Chart가 감당할 수는 없습니다.

이를 위해 저희는 개발팀 운영 서비스에 맞는 설정값을 조절할 수 있는 탈출구(Escape Hatch)를 열어두었습니다.

# 공통 Values에 내장되어 있는 다양한 기본값
common:
  pdb:
    ...
  service:
    ...
    probe:
      interval: 15
      timeout: 5
      healthyCount: 2
      unhealthyCount: 2
  keda:
    pollingInterval: 10
    cooldownPeriod: 60
  workload:
    imagePullPolicy: IfNotPresent
    priorityClassName: ""
    terminationGracePeriodSeconds: 60
    minReadySeconds: 60
    lifecycle:
      preStopSleep: 40
    ...

이는 자주 변경되지는 않지만 변경이 필요한 경우가 있을때 사용됩니다!

이러한 공통 설정이 없이 helm의 default 키워드를 사용하는 방법도 활용할 수 있습니다

# 공통 설정을 사용한 경우
rollingUpdate:
  maxUnavailable: {{ $.Values.common.workload.updateStrategy.rollingUpdate.maxUnavailable }}
  
# default 키워드를 사용한 경우
serviceAccountName: {{ .serviceAccountName | default "..." }}

다만, 이 경우 Deployment, Statefulset, Rollout, CronJob 등 Workload의 종류가 많아질수록 해당 값을 일일히 찾아 변경해야 하기는 어려움이 있습니다.

또한, 공통 설정값을 한곳에 모아서 관리하기 어렵다은 운영 포인트가 있기 때문에 공통 Values를 채택했습니다.

이는 공통 Chart의 편리함을 누리면서도, 필요할 땐 유연하게 확장할 수 있는 구조입니다.

안정성을 위한 이중 장치: CI for Chart & Unit Test

한편, 단일 실패 지점의 위험을 해소하기 위해, 저희는 공통 Helm Chart Repository 자체에 강력한 CI 파이프라인과 테스트 코드를 적용했습니다.

Helm Chart CI

Chart에 변경이 생기면 helm lint로 문법 오류를 잡고, helm template 명령으로 실제 K8s Manifest가 의도대로 렌더링되는지 검증합니다.

Helm Unit Test

helm-unittest 플러그인을 도입하여 템플릿 로직을 테스트합니다.

예를 들어 아래와 같은 테스트 케이스를 작성하여 로직의 정확성을 보장합니다.

suite: Deployment Generation Test
templates:
  - templates/deployment.yaml
values:
  - ../samples/base/values.yaml
tests:
  - it: 최소 설정 값인 경우, Deployment가 올바르게 생성 되어야 한다
    values:
      - ../samples/override/deployment.default.values.yaml
    asserts:
      - isKind:
          of: Deployment
      # Metadata
      - equal:
          path: metadata.name
          value: my-deployment
      - equal:
          path: metadata.namespace
          value: hello-world
      - isSubset:
          path: metadata.labels
          any: true
          content:
            app: my-deployment
            app.kubernetes.io/managed-by: Helm
            app.kubernetes.io/name: my-deployment
            app.kubernetes.io/part-of: my-app

또한, 시맨틱 버저닝(SemVer)을 엄격하게 준수합니다.

버그 수정은 패치(1.0.x), 하위 호환되는 기능 추가는 마이너(1.x.0), 설정 변경이 필요한 큰 변화는 메이저(x.0.0) 버전으로 관리하여 버전 변경의 영향을 예측 가능하게 만들었습니다.

개선 결과: 무엇이 달라졌을까요?

이러한 노력 끝에 개발자와 DevOps팀 모두에게 긍정적인 변화가 찾아왔습니다.

Manifest 코드량의 혁신적인 감소

가장 눈에 띄는 변화는 코드량입니다.

기존에는 서비스마다 수백 줄에 달하는 K8s Manifest 파일을 모두 관리해야 했지만,

이제는 20~30줄 내외의 values.yaml 파일 하나면 충분합니다.

# 개발자가 알고 있어야 되는 설정 예시
workload:
  - kind: Deployment
    name: # 자신의 Application 이름
    image: # ECR 이름
    tag: # 배포할 이미지 태그
    ...
    livenessProbe:
      path: # Health Check EndPoint
    ...
    service:
      targetPort: # Application이 사용하는 Port
    resources: # 사용할 Pod Spec
      cpu: 500m
      memory: 1000Mi

쉬워진 설정, 높아진 편의성

복잡했던 설정들이 간단한 On/Off 스위치로 바뀌었습니다.

APM 도구인 Pinpoint 연동이나 OpenTelemetry 설정 등을 적용하고 싶다면,

개발자는 values.yaml에서 해당 옵션을 true로 바꾸기만 하면 됩니다.

# 개발자는 On/Off만 신경 쓰면 됩니다.
pinpoint:
  enabled: true
opentelemetry:
  enabled: true

간편하고 안전한 일괄 변경

전사 서비스에 일괄적인 변경을 위해 Helm Chart 버전업이 필요할때도 피해 반경을 최소화하며 진행할 수 있습니다.

아래와 같이 새로운 패치 버전을 GitOps 내에서 세밀하게 제어하며 단일 서비스 → 팀별 → 클러스터 전체로 점진적으로 확대 적용할 수 있습니다

# 단일 서비스 릴리즈 예시
# ./${devTeam}/values.${profile}.yaml
global:
  teamName: ${devTeam}
  ...
applications:
  - project: cart
    namespace: ...
    services:
      - name: cancel-api
        helmStandardVersion: 1.1.6 # Specific Helm Chart Version UP!
      - name: order-api
# 팀별 릴리즈 릴리즈 예시
# ./${devTeam}/values.${profile}.yaml
global:
  teamName: ${devTeam}
  helmStandardVersion: 1.1.6 # Team Helm Chart Version UP!
applications:
  - project: cart
    namespace: ...
    services:
      - name: cancel-api
      - name: order-api
# 클러스터 전체 버전 설정 예시
# values.${profile}.yaml
global:
  environment: dev
  helmStandardVersion: 1.1.6 # Cluster Helm Chart Version UP!

마무리하며

파편화된 CI/CD의 문제 진단을 시작으로, CI 모듈화, Chart Registry 도입을 거쳐, 마침내 개발자의 경험에 초점을 맞춘 공통 Helm Chart 설계까지 달려왔습니다.

이 여정은 끝이 아니라 새로운 시작입니다. 저희는 앞으로도 공통 Chart가 더 다양한 Workload(StatefulSet, CronJob 등)를 지원하도록 개선하고, 개발자가 더 편하게 배포 설정을 할 수 있는 도구를 제공하는 등 더 나은 개발 문화를 위한 고민을 계속해 나갈 것입니다.

마지막 5편에서는 여기어때 CI/CD 는 개선되었지만, 기존의 모니터링 및 로깅 방식으로는 충분하지 않아 이를 개선한 내용에 대해 공유 하겠습니다.

긴 글 함께해주셔서 감사합니다. 🚀