Playball Logo

Command Palette

Search for a command to run...

목차 열기

ArgoCD App-of-Apps

역할: Root Application이 하위 앱 전체를 선언적으로 관리하는 CD 구조 설명

ArgoCD가 동기화하는 앱은 크게 세 가지 방식으로 구성됩니다.

방식설명주요 사용처
OCI ChartHelm 공식 레지스트리·ECR에서 직접 참조하는 외부 차트Istio, Karpenter, Prometheus Stack 등 오픈소스 인프라
Git Chart303 레포에 직접 작성한 커스텀 Helm 차트백엔드 앱, 네임스페이스, RBAC, NetworkPolicy 등
ApplicationSet동일한 차트 구조를 여러 서비스에 반복 적용하는 템플릿staging-webs/ai 서비스, loadtest 서비스

OCI Chart를 쓰는 이유

Istio, Karpenter 같은 오픈소스 인프라는 직접 차트를 관리하지 않고 공식 Helm 레지스트리를 그대로 참조합니다.

  • 버전 고정: version: "1.29.1" 처럼 차트 버전을 명시해 환경 간 재현성 보장
  • 업스트림 업데이트: values만 유지하면 되고 차트 내부 로직은 직접 관리 불필요
  • OCI 프로토콜: Karpenter는 public.ecr.aws/karpenter ECR 레지스트리에서 직접 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 조건
Deploymentreplicas 전부 Ready
Job완료 (succeeded)
StatefulSetreplicas 전부 Ready
PVCBound
그 외 (ConfigMap, Secret 등)apply 직후 즉시 healthy
Wave컴포넌트이유
-10Namespace모든 리소스의 네임스페이스 선행
-9NetworkPolicy, ESO, AWS LB Controller, Metrics ServerCRD·IRSA 기반 오퍼레이터 먼저
-8Karpenter, External DNS노드 프로비저닝·DNS 등록
-7Kyverno정책 엔진 (이후 리소스에 웹훅 적용)
-6Karpenter NodePool, Policy Reporter, AI SecretsKarpenter CRD 의존
-5Kafka앱 의존 인프라
-4ClickHouse, Istio Base CRD서비스 메시 CRD 선행
-3istiodControl Plane
-2Istio Gateway, IngressGateway트래픽 수신
-1Istio Security, Prometheus StackmTLS·메트릭 수집
0Loki, Tempo, Grafana, Alloy, OTel Collector관측성 스택
5ALB Ingress, CloudBeaver외부 노출
10~12staging-webs-services앱 서비스 (auth-guard → 나머지 → api-gateway)
10staging-ai-servicesAI 서비스
15~17loadtest-* services부하테스트 전용 네임스페이스

ApplicationSet

반복적인 서비스 배포를 ApplicationSet으로 처리합니다. values.yamlapplicationSets 섹션에 서비스 목록을 선언하면 ArgoCD가 자동으로 개별 Application을 생성합니다.

staging-webs-services

staging-webs 네임스페이스의 Java 백엔드 서비스 6개. 의존성 순서를 Sync Wave로 강제합니다.

서비스Wave역할
auth-guard10JWT 발급/검증, 로그인/회원가입 API
queue11대기열 관리, 입장 순서 처리
seat11좌석 조회/선택 API
seat-v211seat 카나리 배포용 신규 버전
order-core11주문 생성/결제, 티켓 발급
api-gateway12라우팅, 인증 필터, Rate Limit

staging-ai-services

staging-ai 네임스페이스의 AI 서비스.

서비스역할
ai-defense봇 탐지, 트래픽 분석, 정책 평가
authz-adapterIstio 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 완료