ArgoCD App-of-Apps
역할: Root Application이 하위 앱 전체를 선언적으로 관리하는 CD 구조 설명
ArgoCD가 동기화하는 앱은 크게 세 가지 방식으로 구성됩니다.
| 방식 | 설명 | 주요 사용처 |
|---|---|---|
| OCI Chart | Helm 공식 레지스트리·ECR에서 직접 참조하는 외부 차트 | Istio, Karpenter, Prometheus Stack 등 오픈소스 인프라 |
| Git Chart | 303 레포에 직접 작성한 커스텀 Helm 차트 | 백엔드 앱, 네임스페이스, RBAC, NetworkPolicy 등 |
| ApplicationSet | 동일한 차트 구조를 여러 서비스에 반복 적용하는 템플릿 | staging-webs/ai 서비스, loadtest 서비스 |
OCI Chart를 쓰는 이유
Istio, Karpenter 같은 오픈소스 인프라는 직접 차트를 관리하지 않고 공식 Helm 레지스트리를 그대로 참조합니다.
- 버전 고정:
version: "1.29.1"처럼 차트 버전을 명시해 환경 간 재현성 보장 - 업스트림 업데이트: values만 유지하면 되고 차트 내부 로직은 직접 관리 불필요
- OCI 프로토콜: Karpenter는
public.ecr.aws/karpenterECR 레지스트리에서 직접 pull — Helm repo add 없이oci://URL로 참조 가능
# values.yaml (303 레포) — OCI Chart 선언 예시
karpenter:
repoURL: public.ecr.aws/karpenter
chart: karpenter
version: "1.11.1"
syncWave: "-8"
valueFiles:
- $values/ca-staging/values/infra/values-karpenter.yaml
Git Chart — 커스텀 Helm 차트
303 레포에 직접 작성한 차트입니다. 모든 Java 서비스가 common-charts/apps/java-service 하나를 공유하고, 서비스별 values-<name>.yaml로 이미지·리소스·환경변수만 오버라이드합니다.
앱 차트: common-charts/apps/java-service/templates/deployment.yaml
deployment.yaml 전문 보기
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "java-service.fullname" . }}
labels:
{{- include "java-service.labels" . | nindent 4 }}
spec:
revisionHistoryLimit: 3
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "java-service.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
reloader.stakater.com/auto: "true"
sidecar.istio.io/inject: "{{ .Values.istio.sidecar }}"
{{- if .Values.istio.sidecar }}
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
{{- end }}
labels:
{{- include "java-service.podLabels" . | nindent 8 }}
{{- include "java-service.versionLabel" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
{{- if .Values.otel.enabled }}
initContainers:
- name: otel-agent-init
image: busybox:1.36
command:
- sh
- -c
- |
wget -O /otel/opentelemetry-javaagent.jar \
https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v{{ .Values.otel.agentVersion }}/opentelemetry-javaagent.jar
volumeMounts:
- name: otel-agent
mountPath: /otel
{{- end }}
containers:
- name: {{ include "java-service.name" . }}
{{- if and .Values.ecr.accountId .Values.image.name }}
image: "{{ .Values.ecr.accountId }}.dkr.ecr.{{ .Values.ecr.region }}.amazonaws.com/{{ .Values.image.name }}:{{ .Values.image.tag }}"
{{- else }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- if .Values.metrics.enabled }}
- name: management
containerPort: {{ .Values.metrics.port }}
protocol: TCP
{{- end }}
env:
- name: JAVA_OPTS
value: {{ .Values.javaOpts | quote }}
{{- if .Values.otel.enabled }}
- name: JAVA_TOOL_OPTIONS
value: "-javaagent:/otel/opentelemetry-javaagent.jar"
- name: OTEL_SERVICE_NAME
value: {{ .Release.Name }}
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: {{ .Values.otel.collectorEndpoint | quote }}
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: "grpc"
- name: OTEL_METRIC_EXPORT_INTERVAL
value: "15000"
{{- end }}
{{- with .Values.env }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: log-dir
mountPath: /var/log/backend
{{- if .Values.otel.enabled }}
- name: otel-agent
mountPath: /otel
readOnly: true
{{- end }}
livenessProbe:
httpGet:
path: {{ .Values.contextPath }}{{ .Values.livenessProbe.httpGet.path }}
port: {{ .Values.livenessProbe.httpGet.port }}
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds | default 90 }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds | default 10 }}
readinessProbe:
httpGet:
path: {{ .Values.contextPath }}{{ .Values.readinessProbe.httpGet.path }}
port: {{ .Values.readinessProbe.httpGet.port }}
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds | default 45 }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds | default 5 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: log-dir
emptyDir: {}
{{- if .Values.otel.enabled }}
- name: otel-agent
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/part-of: {{ .Values.partOf | default "staging-webs" }}
topologyKey: kubernetes.io/hostname
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
인프라 차트: common-charts/infra/network-policies/templates/
network-policies 전문 보기
{{- range $ns, $config := .Values.namespaces }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress
namespace: {{ $ns }}
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
{{- range $config.allowIngressFrom }}
- from:
{{- if .namespaceSelector }}
- namespaceSelector:
{{- toYaml .namespaceSelector | nindent 12 }}
{{- if .podSelector }}
podSelector:
{{- toYaml .podSelector | nindent 12 }}
{{- end }}
{{- else if .podSelector }}
- podSelector:
{{- toYaml .podSelector | nindent 12 }}
{{- end }}
{{- end }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress
namespace: {{ $ns }}
spec:
podSelector: {}
policyTypes:
- Egress
egress:
{{- range $config.allowEgressTo }}
- to:
{{- if .namespaceSelector }}
- namespaceSelector:
{{- toYaml .namespaceSelector | nindent 12 }}
{{- if .podSelector }}
podSelector:
{{- toYaml .podSelector | nindent 12 }}
{{- end }}
{{- else if .podSelector }}
- podSelector:
{{- toYaml .podSelector | nindent 12 }}
{{- end }}
{{- end }}
# Always allow DNS resolution
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- end }}
{{- if .Values.defaultDeny.enabled }}
{{- range $ns, $config := .Values.namespaces }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: {{ $ns }}
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
{{- end }}
{{- end }}
App-of-Apps 패턴
단일 ArgoCD Application(Root App)이 다른 Application 리소스들을 Helm 템플릿으로 렌더링해 배포합니다. 302 Bootstrap에서 Root App을 최초 등록하면, 이후 303 레포의 values.yaml 변경만으로 앱 추가/수정/삭제가 가능합니다.
root-staging (Root Application)
├── [gitCharts] namespaces (wave -10)
├── [gitCharts] network-policies (wave -9)
├── [ociCharts] external-secrets (wave -9)
├── [ociCharts] aws-lb-controller (wave -9)
├── [ociCharts] karpenter (wave -8)
├── [ociCharts] external-dns (wave -8)
├── [ociCharts] kyverno (wave -7)
├── [gitCharts] karpenter-config (wave -6)
├── [ociCharts] policy-reporter (wave -6)
├── [gitCharts] ai-service-secrets (wave -6)
├── [gitCharts] kafka (wave -5)
├── [ociCharts] clickhouse (wave -4)
├── [ociCharts] istio-base (wave -4)
├── [ociCharts] istiod (wave -3)
├── [gitCharts] istio-gateway (wave -2)
├── [gitCharts] istiod-monitoring (wave -2)
├── [ociCharts] istio-ingressgateway (wave -2)
├── [gitCharts] istio-security (wave -1)
├── [ociCharts] kube-prometheus-stack (wave -1)
├── [ociCharts] loki (wave 0)
├── [ociCharts] tempo (wave 0)
├── [ociCharts] grafana (wave 0)
├── [ociCharts] alloy (wave 0)
├── [gitCharts] loadtest-route (wave 0)
├── [ociCharts] opentelemetry-collector (wave 1)
├── [ociCharts] kafka-exporter (wave 1)
├── [gitCharts] alb-ingress (wave 5)
├── [AppSet] staging-webs-services (wave 10~12)
├── [AppSet] staging-ai-services (wave 10)
├── [AppSet] loadtest-webs-services (wave 15~17)
└── [AppSet] loadtest-ai-services (wave 15~16)
Sync Wave 순서
ArgoCD Sync Wave는 숫자가 낮을수록 먼저 배포됩니다. 인프라 의존성 순서를 강제하여 CRD 미설치로 인한 배포 실패를 방지합니다.
각 wave는 이전 wave의 모든 리소스가 healthy 상태가 된 후에 시작됩니다.
| 타입 | healthy 조건 |
|---|---|
| Deployment | replicas 전부 Ready |
| Job | 완료 (succeeded) |
| StatefulSet | replicas 전부 Ready |
| PVC | Bound |
| 그 외 (ConfigMap, Secret 등) | apply 직후 즉시 healthy |
| Wave | 컴포넌트 | 이유 |
|---|---|---|
| -10 | Namespace | 모든 리소스의 네임스페이스 선행 |
| -9 | NetworkPolicy, ESO, AWS LB Controller, Metrics Server | CRD·IRSA 기반 오퍼레이터 먼저 |
| -8 | Karpenter, External DNS | 노드 프로비저닝·DNS 등록 |
| -7 | Kyverno | 정책 엔진 (이후 리소스에 웹훅 적용) |
| -6 | Karpenter NodePool, Policy Reporter, AI Secrets | Karpenter CRD 의존 |
| -5 | Kafka | 앱 의존 인프라 |
| -4 | ClickHouse, Istio Base CRD | 서비스 메시 CRD 선행 |
| -3 | istiod | Control Plane |
| -2 | Istio Gateway, IngressGateway | 트래픽 수신 |
| -1 | Istio Security, Prometheus Stack | mTLS·메트릭 수집 |
| 0 | Loki, Tempo, Grafana, Alloy, OTel Collector | 관측성 스택 |
| 5 | ALB Ingress, CloudBeaver | 외부 노출 |
| 10~12 | staging-webs-services | 앱 서비스 (auth-guard → 나머지 → api-gateway) |
| 10 | staging-ai-services | AI 서비스 |
| 15~17 | loadtest-* services | 부하테스트 전용 네임스페이스 |
ApplicationSet
반복적인 서비스 배포를 ApplicationSet으로 처리합니다. values.yaml의 applicationSets 섹션에 서비스 목록을 선언하면 ArgoCD가 자동으로 개별 Application을 생성합니다.
staging-webs-services
staging-webs 네임스페이스의 Java 백엔드 서비스 6개. 의존성 순서를 Sync Wave로 강제합니다.
| 서비스 | Wave | 역할 |
|---|---|---|
| auth-guard | 10 | JWT 발급/검증, 로그인/회원가입 API |
| queue | 11 | 대기열 관리, 입장 순서 처리 |
| seat | 11 | 좌석 조회/선택 API |
| seat-v2 | 11 | seat 카나리 배포용 신규 버전 |
| order-core | 11 | 주문 생성/결제, 티켓 발급 |
| api-gateway | 12 | 라우팅, 인증 필터, Rate Limit |
staging-ai-services
staging-ai 네임스페이스의 AI 서비스.
| 서비스 | 역할 |
|---|---|
| ai-defense | 봇 탐지, 트래픽 분석, 정책 평가 |
| authz-adapter | Istio ext_authz gRPC 어댑터 |
loadtest-webs-services / loadtest-ai-services
loadtest-webs, loadtest-ai 네임스페이스. 스테이징과 동일한 서비스 구성을 별도 네임스페이스에 격리해 k6 부하테스트에 사용합니다. Wave 15~17 배치로 스테이징 앱이 완전히 배포된 후 기동됩니다.
CD 트리거 흐름
개발자 → CI (TeamCity)
→ Docker Image Build → ECR Push
→ 303 Helm values.yaml 이미지 태그 업데이트 (PR → merge to argocd-sync/staging)
→ GitHub Webhook → ArgoCD 동기화 트리거
→ ApplicationSet이 해당 서비스 Application Sync
→ Karpenter가 필요 시 노드 자동 프로비저닝
→ Rolling Update 완료