fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • postgresql 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • gaps and islands: SQL 창 함수로 이어진 구간 찾기
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Alpine 이미지의 Java 에서 링크 바꿔치기(TOCTOU)를 막지 못하는 이유
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • personal-projects 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/devops/Helm과 ArgoCD로 GitOps 하기 …
devops

Helm과 ArgoCD로 GitOps 하기 — chart, Application, 그리고 새 컴포넌트 추가 흐름

쿠버네티스에 새 컴포넌트(ingress controller 하나)를 추가하는 작업을 맡고 나서야, 그동안 "어딘가에서 알아서 배포되던" 그 과정의 구조를 처음 들여다봤다. Helm 차트가 뭐고, ArgoCD가 뭘 하고, Application이라는 게 왜 또 따로 있는지. 막상 정리해보니 큰 그림은 단순했다. 그 구조와, 실제로 새 컴포넌트를 추가하려면 어디...

2026.06.09·7 min read·99 views·SERIES · 백엔드 개발자를 위한 쿠버네티스 기본기 · 8/8

쿠버네티스에 새 컴포넌트(ingress controller 하나)를 추가하는 작업을 맡고 나서야, 그동안 "어딘가에서 알아서 배포되던" 그 과정의 구조를 처음 들여다봤다. Helm 차트가 뭐고, ArgoCD가 뭘 하고, Application이라는 게 왜 또 따로 있는지. 막상 정리해보니 큰 그림은 단순했다. 그 구조와, 실제로 새 컴포넌트를 추가하려면 어디서부터 손대야 하는지를 정리한다.

왜 이런 도구들이 필요한가

순수하게 하면, 쿠버네티스에 뭘 배포하려면 YAML(Deployment, Service, Ingress...)을 손으로 쓰고 kubectl apply 하면 된다. 그런데 실무에서 이 방식은 금방 무너진다.

  • 환경(테스트/스테이징/운영)마다 값만 다르고 구조는 똑같은 YAML을 중복해서 관리해야 한다.
  • 누가 언제 뭘 바꿨는지 추적이 안 된다. 누군가 kubectl edit으로 클러스터를 직접 고치면 기록이 안 남는다.
  • 클러스터의 실제 상태와 "원래 의도했던 상태"가 어긋나도 알아챌 방법이 없다.

이 두 종류의 문제를 각각 푸는 게 Helm(중복·템플릿 문제)과 ArgoCD(추적·동기화 문제)다. 둘은 역할이 다르고, 같이 쓴다.

Helm — 쿠버네티스 YAML의 템플릿 엔진

Helm은 템플릿에 값을 채워 최종 YAML을 만들어주는 도구다. 매번 YAML을 손으로 쓰는 대신, 빈칸({{ }})이 뚫린 템플릿을 만들어두고 값만 갈아끼운다.

이 패키지 단위를 Chart라고 부른다. 차트 디렉터리 구조는 대략 이렇게 생겼다.

plaintext
my-component/
├── Chart.yaml          # 차트 메타 + 의존성 선언
├── values.yaml         # 기본값 (모든 환경 공통)
├── alpha-values.yaml   # 환경별 오버라이드
└── charts/
    └── some-dependency-1.0.0.tgz   # 의존하는 외부 차트

각 파일의 역할:

  • Chart.yaml — 차트 이름, 버전, 그리고 의존성. 남이 만든 차트(예: 공식 ingress-nginx)에 의존한다면 여기에 선언한다.
  • values.yaml — 템플릿에 끼워넣을 기본값. {{ .Values.foo }}로 템플릿에서 참조한다.
  • {stage}-values.yaml — 환경별로 덮어쓸 값. Helm은 -f values.yaml -f alpha-values.yaml처럼 여러 값 파일을 주면 뒤가 앞을 덮어쓴다. 그래서 공통값은 values.yaml에 두고, 환경마다 다른 부분만 {stage}-values.yaml에 둔다.
  • charts/ — 의존하는 외부 차트를 압축 파일로 담아둔다(vendoring). 인터넷에서 매번 받아오는 대신 레포에 박아두는 방식이다.

두 가지 차트 패턴 — 이 차이가 핵심

차트를 만드는 방식에는 두 갈래가 있다. 둘 다 실무에서 쓴다.

패턴방식templates/
의존 차트 재사용남이 만든 차트(공식 ingress-nginx 등)를 가져와 설정값만 준다없음
직접 templates 작성내 Deployment/Service/Ingress를 직접 템플릿으로 쓴다있음
  • ingress controller처럼 이미 잘 만들어진 공식 차트가 있으면, 그걸 의존성으로 두고 values로 설정만 조정한다. 이때는 내가 쓸 YAML이 없으니 templates/ 폴더가 없다.
  • 반대로 내 애플리케이션은 공식 차트가 없으니, templates/ 안에 deployment.yaml, service.yaml, ingress.yaml을 직접 템플릿으로 쓴다.

templates/ 안의 파일이 바로 {{ .Values.foo }} 빈칸이 뚫린 YAML이고, 거기에 values를 끼워 최종 매니페스트가 만들어진다. 이 렌더링 결과를 미리 확인하는 명령이 helm template인데, 이게 배포 전 검증의 핵심이다(뒤에서 다시 나온다).

ArgoCD — git을 정답으로 삼는 GitOps

Helm이 YAML을 만들어준다면, ArgoCD는 그 YAML을 언제 어떻게 클러스터에 반영할지를 맡는다. 핵심 사상은 GitOps다.

git에 적힌 상태 = 클러스터가 있어야 할 상태. ArgoCD는 git을 계속 감시하다가, 클러스터가 git과 다르면 sync로 맞춘다. 그래서:

  • git이 single source of truth가 된다. 클러스터를 kubectl로 직접 고치는 게 아니라, git을 고치고 sync한다. 모든 변경이 git 커밋으로 남으니 추적이 된다.
  • sync 정책은 두 가지다. auto-sync는 git이 바뀌면 자동 반영하고, manual sync는 사람이 sync 버튼을 눌러야 반영한다. 중요한 인프라는 manual로 두는 경우가 많다. git에 머지됐다고 바로 배포되는 게 아니라, 사람이 한 번 더 확인하고 누르는 안전장치인 셈이다.

ArgoCD Application — "이 차트를 배포해라"는 선언

ArgoCD에게 어느 git의 / 어느 경로 차트를 / 어느 값으로 / 어느 클러스터·namespace에 배포할지를 알려주는 리소스가 Application이다. 대략 이렇게 생겼다.

yaml
kind: Application
spec:
  source:
    repoURL: https://.../my-repo.git        # 어느 git
    path: applications/my-component          # 어느 차트
    helm:
      valueFiles:
        - values.yaml
        - alpha-values.yaml                  # 어느 값
  destination:
    namespace: my-component                  # 어느 namespace

여기서 한 가지 헷갈렸던 게 있다. 배포할 내용(차트)과 배포하라는 선언(Application)은 별개라는 점이다. 차트만 있고 Application이 없으면 ArgoCD는 그 차트의 존재를 모른다. 둘 다 있어야 동작한다.

app-of-apps 패턴

레포 구조를 보다가 영리한 패턴을 하나 발견했다. argocd/ 디렉터리 자체가 하나의 Helm 차트이고, 그 templates/ 안에서 여러 개의 Application을 찍어낸다.

plaintext
argocd/templates/
├── ingress-nginx-application.yaml        # 사설 controller 배포 선언
├── app-application.yaml                  # 앱들 배포 선언
└── my-component-application.yaml         # 내가 추가한 선언

이걸 app of apps라고 부른다. Application들을 만들어내는 상위 Application이다. 그래서 새 컴포넌트를 추가할 때는 여기에 Application 하나를 더 얹으면 된다.

처음엔 누가 영리하게 짜둔 우리 레포만의 구조인 줄 알았는데, 알고 보니 ArgoCD 공식 문서에 Cluster Bootstrapping이라는 이름으로 실려 있는 표준 패턴이었다.

존재 이유는 닭과 달걀 문제다. ArgoCD로 뭘 배포하려면 Application이 있어야 하는데, 그 Application 자체는 누가 만드나. 손으로 kubectl apply하면 클러스터를 직접 건드리는 것이라 GitOps 원칙이 깨지고 기록도 안 남는다. 컴포넌트가 열 개면 열 번 반복해야 하고, 새 클러스터를 만들 때마다 또 반복이다.

그래서 Application 만드는 일까지 ArgoCD에게 시킨다. 손으로 만드는 건 최상위 하나뿐이고, 나머지는 git에 파일을 추가하면 생긴다.

대가 — 2단계 sync

공짜는 아니다. 이 패턴을 쓰면 sync가 두 단계가 된다.

Application의 정의를 바꿨다면 상위 app-of-apps를 먼저 sync해야 그 정의가 갱신되고, 그다음 해당 앱을 sync해야 실제 리소스에 반영된다. 순서를 건너뛰면 옛 정의로 렌더되는데 화면에는 Synced라고 뜬다.

실제로 겪었다. Application의 helm 파라미터 하나를 제거하는 변경을 머지하고 해당 앱만 sync했는데 아무것도 안 바뀌었다. Application 리소스를 직접 열어보니 파라미터가 그대로 살아 있었다. 상위를 먼저 돌리지 않았으니 당연한 결과였는데, ArgoCD 화면이 초록색이라 한참을 엉뚱한 데서 원인을 찾았다.

구분 기준은 단순하다.

  • argocd/templates/를 고쳤다 → 정의 변경 → app-of-apps 먼저
  • applications/를 고쳤다 → 내용 변경 → 해당 앱만

한 가지 더. ArgoCD는 git을 주기적으로 폴링하므로 머지 직후에는 아직 옛 커밋 기준일 수 있다. 이때도 화면은 Synced다. 리비전이 새 커밋인지 확인하고, 아니면 Refresh를 먼저 눌러야 한다.

ApplicationSet — 요즘의 대안

Application을 Helm 템플릿으로 하나씩 쓰는 대신, 생성기(generator)로 만들어내는 ApplicationSet이 있다. "git의 이 디렉터리 하위 폴더마다 Application 하나씩" 같은 규칙을 주면 알아서 생성한다.

app of appsApplicationSet
방식Application 파일을 직접 템플릿으로 작성규칙을 주면 자동 생성
적합컴포넌트 십여 개, 구성이 제각각클러스터·환경이 많고 패턴이 반복
장단파일이 눈에 보여 직관적, 개수만큼 파일이 늘어남반복이 사라지지만 생성 규칙을 이해해야 함

환경당 컴포넌트가 열 개 남짓이고 각각 설정이 다르면 지금 방식이 낫다고 본다. 파일이 그대로 보이는 게 디버깅에 유리하다. ApplicationSet은 클러스터가 수십 개로 늘어날 때 값을 한다.

전체 관계도

지금까지를 한 그림으로 묶으면 이렇다.

두 갈래만 기억하면 된다. 배포할 내용(차트)은 applications/에, 배포하라는 선언(Application)은 argocd/templates/에. 둘 다 있어야 한 컴포넌트가 동작한다.

새 컴포넌트를 추가한다면 — 어디서 시작하나

내가 실제로 ingress controller를 하나 추가하며 밟은 단계를 정리하면, 비슷한 컴포넌트를 추가할 때 이대로 따라가면 된다.

차트 만들기(applications/{name}/)

  • 디렉터리를 만든다.
  • Chart.yaml을 쓴다. 공식 차트에 의존한다면 dependencies에 명시한다.
  • 의존 차트를 charts/에 압축 파일로 둔다. (기존에 쓰던 걸 복사하면 된다.)
  • values.yaml에 공통 설정을 쓴다.
  • {stage}-values.yaml에 환경별로 다른 부분만 쓴다.

ArgoCD Application 추가(argocd/templates/{name}-application.yaml)

  • 기존 Application 파일 하나를 복사해서 고친다.
  • path(차트 위치), namespace(배포 대상), valueFiles를 지정한다.
  • 리소스 생성 순서가 중요하면 sync-wave로 순서를 제어한다.

로컬 렌더 검증 — 배포 전 필수

  • helm template {name} . -f values.yaml -f alpha-values.yaml로 렌더해서, 실제로 어떤 YAML이 나오는지 눈으로 확인한다.
  • 나는 이 단계에서 의도대로 리소스가 분리되는지를 확인했다. 이 단계를 건너뛰면, 잘못된 YAML이 클러스터에 가서야 문제가 드러난다. 미리 보는 비용이 훨씬 싸다.

git에 반영

  • 브랜치 → commit → PR → 머지. GitOps라 git에 들어가야 ArgoCD가 인식한다.

ArgoCD에서 sync

  • manual sync라면 사람이 sync를 눌러야 실제로 배포된다.
  • sync 후 kubectl get으로 실제로 떴는지 확인한다.

여기까지 밟고 나서 잡힌 감각은 이거다. 내가 만들 건 결국 차트(applications/)와, 그걸 배포하라는 Application(argocd/templates/) 두 개다. 그리고 클러스터에 손대기 전에 helm template로 먼저 눈으로 확인하고, git을 통해서만 배포한다. 이 흐름만 잡으면 새 컴포넌트 추가는 거의 같은 패턴의 반복이었다.

관련 글

  • 쿠버네티스 핵심 객체 4종 — 차트가 만들어내는 Pod/Service/Ingress가 뭔지
  • ingress-nginx 운영에서 부딪힌 디테일들 — 이 흐름으로 실제 추가한 컴포넌트의 운영 디테일

참고 링크

  • Helm 공식 문서
  • Argo CD 공식 문서
  • Argo CD — App of Apps 패턴
on this page
  • 01왜 이런 도구들이 필요한가
  • 02Helm — 쿠버네티스 YAML의 템플릿 엔진
  • 두 가지 차트 패턴 — 이 차이가 핵심
  • 03ArgoCD — git을 정답으로 삼는 GitOps
  • 04ArgoCD Application — "이 차트를 배포해라"는 선언
  • app-of-apps 패턴
  • 05전체 관계도
  • 06새 컴포넌트를 추가한다면 — 어디서 시작하나
  • 07관련 글
  • 08참고 링크
tags
#study📚 백엔드 개발자를 위한 쿠버네티스 기본기
← PREVIOUSArgo CD — git 을 정답으로 삼는 배포 컨트롤러

이런 글도

  • devops

    종료 신호는 어디서 멈추는가

    2026.09.02
  • devops

    파드가 1개면 minAvailable 이 노드 교체를 막는다

    2026.08.20
  • devops

    ArgoCD는 초록불인데 서비스는 죽어 있었다 — 불변 필드와 재조정의 경계

    2026.08.11
  • devops

    파드가 서비스 이름을 못 푸는 이유 — dnsPolicy 4종과 ndots

    2026.08.03

댓글 (0)