[ kt cloud Foundation플랫폼팀 이지은 님 ]
1. Intro
DevOps는 빠르게 움직이는 세상입니다. 이 안에서 속도와 안정성이라는 두 마리 토끼를 동시에 잡는 건 결코 쉬운 일이 아니죠.
이런 과제를 해결하기 위한 방법으로 주목받는 게 GitOps입니다.
GitOps는 버전 관리 시스템(Git)과 자동화 도구를 활용해 인프라와 애플리케이션의 배포 과정을 안정적이면서도 반복 가능하게 만들어줍니다.
그리고 GitOps를 실현하는 대표적인 도구가 바로 Argo CD입니다.
Argo CD는 Kubernetes 환경에서 애플리케이션을 선언적으로 관리하고 자동으로 배포해주는 GitOps 툴로,
직관적인 UI와 강력한 기능 덕분에 널리 사용되고 있어요.
하지만 현실에서는 단일 앱만 있는 게 아니라
여러 개의 클러스터, 여러 개의 환경(dev/stage/prod), 수많은 마이크로서비스가 함께 움직입니다.
이런 복잡한 환경을 효율적으로 운영하려면 더 체계적인 관리 방식이 필요하죠.
그래서 등장한 것이 App of Apps 전략입니다.
Argo CD의 App of Apps는 여러 개의 애플리케이션을 트리 구조로 연결하고 관리할 수 있게 해주며,
규모가 커질수록 그 진가를 발휘하는 구조적 배포 전략입니다.
이번 발표에서는 GitOps의 기본 개념을 짚고,
Argo CD의 App of Apps 전략이 어떻게 DevOps 환경을 더 스마트하게 바꿔주는지 직접적인 예시와 운영 전략을 통해 알아보겠습니다.
2. App of Apps?
Argo CD에서의 "App of Apps"는 말 그대로
하나의 애플리케이션(Application 리소스) 안에 다른 여러 애플리케이션들을 정의하는 구조입니다.
"루트 앱"이 있고, 그 아래로 여러 "하위 앱들"이 연결되는 트리 구조, 즉 루트 애플리케이션(Root App)이 여러 하위 애플리케이션(Child Apps)을 정의하고 관리합니다. 루트 애플리케이션은 하위 애플리케이션의 정의가 포함된 디렉토리를 가리키며, 각 하위 애플리케이션은 실제 애플리케이션 매니페스트를 참조합니다.
예를 들어, 루트 애플리케이션은 manifests/apps 디렉토리를 가리키고, 이 디렉토리에는 Application 리소스가 정의된 YAML 파일들이 위치합니다. 각 하위 애플리케이션은 해당 애플리케이션의 매니페스트가 위치한 디렉토리를 참조하여 배포됩니다.
<root-app.yaml 예시>
root-app.yaml
Argo CD에 등록되는 루트 Application, apps/ 디렉토리에서 Application 리소스를 자동으로 탐색하고 관리
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/argoproj/argocd-example-apps
targetRevision: HEAD
path: apps # child app 위치
directory:
recurse: true
destination:
server: https://kubernetes.default.svc # 자기자신. 다른 클러스터 배포 시 변경 필요
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
apps/child-app.yaml
실제 배포 대상이 되는 하위 앱 정의 (guestbook/의 리소스를 배포함)
guestbook/
배포 대상 디렉토리. Helm 차트나 K8s 매니페스트를 포함할 수 있음
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: guestbook
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/argoproj/argocd-example-apps
targetRevision: HEAD
path: ../guestbook # 배포 대상
destination:
server: https://kubernetes.default.svc # 자기자신. 다른 클러스터 배포 시 변경 필요
namespace: guestbook
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
다른 클러스터에 배포할 경우
argocd cluster add
디렉토리 구조 예시
my-gitops-repo/
├── root-app.yaml
├── apps/
│ └── child-app.yaml # 하위 Application 리소스
└── guestbook/
├── Chart.yaml # 실제 배포할 Helm 차트 혹은 K8s 리소스
├── values.yaml
└── templates/
└── ...
왜 이런 구조를 쓸까요?
- 프로젝트 수가 많아지면 Application 리소스를 하나하나 등록하는 게 번거로움
- 환경(dev/stage/prod)별로 같은 앱을 각각 관리해야 하는 리소스 중복 발생
- 앱 간 의존관계나 순서를 효율적으로 관리하고 싶음
→ App of Apps로 대규모 클러스터 운영 환경에서 여러 앱을 중앙에서 선언적, 일괄적으로 관리할 수 있다!
Argo CD에서 root-app만 등록해도 그 아래 하위 앱들의 배포와 상태 관리를 할 수 있다!
즉, 한 곳에서 전체 배포의 진입점을 컨트롤 할 수 있다!
3. 디렉토리 구조와 Git 저장소 전략
디렉토리 구조 - Monorepo vs Multirepo 전략
Monorepo: 하나의 Git 저장소에 모든 애플리케이션과 환경 구성을 포함
- apps/: 공통 또는 앱별 구성 저장
- environments/: dev, stage, prod 환경별 오버레이 정의
- projects/: AppProject 리소스 정의 (권한, 접근 제어 등)
- root-app.yaml: 모든 앱의 진입점 역할을 하는 루트 Application 리소스
my-gitops-repo/
├── apps/
│ ├── frontend/
│ └── backend/
├── environments/
│ ├── dev/
│ └── prod/
├── projects/
│ └── default-project.yaml
└── root-app.yaml
Multirepo: 애플리케이션이나 환경별로 별도의 Git 저장소를 운영
git@github.com:org/frontend-config.git → frontend 앱 구성
git@github.com:org/backend-config.git → backend 앱 구성
git@github.com:org/dev-environment.git → dev 환경 오버레이
git@github.com:org/prod-environment.git → prod 환경 오버레이
Kustomize 기반의 base / overlays 구조 및 Patch 방식
App of Apps 패턴을 도입하면서 가장 많이 함께 활용되는 것이 바로 Kustomize 기반의 base / overlays 디렉토리 구조입니다. 이 구조는 GitOps 환경에서 공통 구성은 재사용하고, 환경별 설정은 분리 관리할 수 있도록 도와주는 매우 강력한 전략입니다.
Kubernetes 리소스를 코드로 관리할 때, 모든 환경(dev, stage, prod 등)에 대해 중복된 YAML 파일을 생성하는 건 유지보수에 큰 부담을 줍니다. 예를 들어, 클러스터를 구성하는 경우 dev 환경에서는 control-plane 노드 1개, prod 환경에서는 고가용성을 위해 control-plane 노드 3개가 필요할 수 있고, 각기 다른 인증 설정이나 로깅 옵션을 사용해야 할 수도 있습니다.
이런 상황에서 매번 모든 환경별로 Deployment, Config, Node 설정을 복사해서 따로 관리하면 변경이 발생했을 때 전체 환경을 일일이 수정해야 하므로 실수가 생기기 쉽고 관리 효율도 떨어집니다.
이럴 때 모든 환경에 대해 Deployment를 새로 복사하는 대신, 공통 부분은 base 디렉토리에 정의하고, 환경별 차이는 overlays 디렉토리에서 덮어쓰기하는 방식으로 관리하는 것이죠.
- 중복 제거: 공통 리소스를 base로 통합 → 수정은 한 곳에서만
- 유지보수 용이성: 환경별 차이만 patch로 관리 → 변경 범위 최소화
- 배포 자동화: App of Apps에서 각 overlay를 하위 앱으로 선언 → 전체 환경 자동 배포 가능
예를 들어 아래와 같은 구조를 생각해볼 수 있습니다:
my-gitops-repo/
├── root-app.yaml # 루트 Application, overlays/ 하위의 앱들을 탐색 (directory.recurse=true)
├── base/ # 공통 리소스스
│ ├── control-plane-deployment.yaml # 공통 리소스 정의
│ ├── service.yaml # 예시: 공통 서비스 리소스
│ └── kustomization.yaml # base 디렉토리 내 리소스 나열
└── overlays/ # 환경별로 필요한 설정만 patch 형태로 정의하고, kustomization.yaml로 base를 오버레이
├── dev/
│ ├── patch-replicas.yaml # dev 환경용 패치 (e.g., replica 1)
│ ├── kustomization.yaml # base를 포함하고 패치 적용
│ └── application.yaml # dev 환경용 ArgoCD Application 리소스
└── prod/
├── patch-replicas.yaml # prod 환경용 패치 (e.g., replica 3)
├── patch-logging.yaml # prod 전용 로깅 설정 추가
├── kustomization.yaml
└── application.yaml # prod 환경용 ArgoCD Application 리소스
<yaml 예시>
# base/kustomization.yaml
resources:
- control-plane-deployment.yaml
- service.yaml
각 overlays/ 디렉토리는 kustomization.yaml을 통해 base를 불러오고 환경별 패치를 적용합니다.
# overlays/dev/kustomization.yaml
resources:
- ../../base
patchesStrategicMerge:
- replica-patch.yaml
# overlays/dev/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: control-plane
spec:
replicas: 1
# overlays/dev/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: dev-cluster
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/my-gitops-repo.git
targetRevision: main
path: overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Kustomized Helm 구조 및 Helm values override
Kustomized Helm 구조는 Helm 차트를 먼저 적용한 다음 Kustomize 오버레이를 적용하는 방식을 사용하며, Helm의 템플릿 기능과 Kustomize의 오버레이 기능을 결합한 방식으로, 더욱 유연하고 강력한 배포 구성이 가능합니다. 특히 Helm 차트를 기반으로 하면서도 환경별로 세밀한 설정을 조정하고 싶을 때 Helm values override 방법이 유용합니다.
예를 들어, Helm 차트를 그대로 쓰면서도 dev/prod마다 replica 수, env 값, secret 등을 다르게 적용하고 싶을 때 활용할 수 있습니다.
- base: Helm 차트를 Kustomize로 감싸는 구조
- overlays: 환경별 Helm values 파일만 바꾸는 방식
my-gitops-repo/
├── base/
│ ├── helm/
│ │ ├── Chart.yaml
│ │ ├── values.yaml # 공통 values
│ │ └── templates/
│ │ └── deployment.yaml # Helm 템플릿 정의
│ ├── kustomization.yaml # Helm 렌더링 → all.yaml 사용, all.yaml을 읽어서 Kustomize 실행
│ └── all.yaml # Helm template 결과 저장 위치
├── overlays/
│ ├── dev/
│ │ ├── values-dev.yaml # dev 환경 설정
│ │ ├── kustomization.yaml # base 불러오고 values 덮어쓰기
│ │ └── application.yaml # Argo CD Application 리소스
│ └── prod/
│ ├── values-prod.yaml # prod 환경 설정
│ ├── kustomization.yaml
│ └── application.yaml
└── root-app.yaml # App of Apps 루트
<Helm values override를 Argo CD가 실제로 처리하는 순서>
1. 먼저 Helm을 실행해서 템플릿을 all.yaml로 렌더링
helm template ./helm -f values-dev.yaml > all.yaml
2. 그리고 그 all.yaml 파일을 기반으로 Kustomize가 덮어쓰기를 적용
kustomize build .
3. 이 결과를 Argo CD가 클러스터에 적용
→ (ex) dev 환경에서는 replica 1개, prod 환경에서는 3개로 각각 배포
그런데 문제는… 기본적인 Argo CD는 이걸 자동으로 못해요. (helmCharts:는 Kustomize v4.0 이상에서 공식 지원되는 기능인데, 현재 Argo CD 기본 설치 버전에서는 미지원)
(1) Config Management Plugin 방식 사용 (Argo CD v2.8+)
그래서 우리는 (1)configManagementPlugins라는 기능을 써야 해요. Argo CD 2.8 이후 버전부턴 argocd-cm 직접 수정 대신, ConfigMap을 따로 만들어서 argocd-repo-server에 마운트하는 방식으로 CMP를 등록해야 합니다.
pluginName이라는 ConfigMap 생성:
# plugin.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: pluginName
namespace: argocd
data:
pluginName.yaml: |
apiVersion: argoproj.io/v1alpha1
kind: ConfigManagementPlugin
metadata:
name: pluginName
spec:
init:
command: ["/bin/sh", "-c"]
args: ["helm dependency build ./helm"]
generate:
command: ["/bin/sh", "-c"]
args: ["helm template ./helm --name-template $ARGOCD_APP_NAME -f values.yaml --include-crds > all.yaml && kustomize build ."]
Argo CD argocd-repo-server 배포 시 위 ConfigMap을 마운트:
volumeMounts:
- name: plugins
mountPath: /home/argocd/cmp-server/plugins
volumes:
- name: plugins
configMap:
name: pluginName
Application 리소스에는 plugin 이름을 지정하면 됩니다:
plugin:
name: pluginName
(2) Helm (renderring된) 결과물을 사용하는 방식
또는 helm Helm 결과물을 만들어서 all.yaml에 저장하는 방식을 사용할 수 있어요.
cd base/
helm template ./helm -f ../overlays/dev/values-dev.yaml > all.yaml
Helm은 템플릿 기반이기 때문에 변동사항은 values.yaml 파일을 바꿔서 배포 구성을 제어할 수 있습니다. 예를 들어 base/kustomization.yaml에서 Helm 차트를 이렇게 불러온다고 하면
helmCharts:
- name: my-app
repo: https://charts.example.com
releaseName: my-app
version: 1.2.3
valuesFile: values.yaml
overlays/dev/에서는 values-dev.yaml이라는 파일을 만들어서 replica 개수와 환경변수를 dev 환경에 맞게 바꾸고, overlays/dev/kustomization.yaml에서는 해당 values 파일(values-dev.yaml)을 적용합니다.
# overlays/dev/values-dev.yaml
replicaCount: 1
env:
LOG_LEVEL: DEBUG
# overlays/dev/kustomization.yaml
resources:
- ../../base
helmCharts:
- name: my-app
valuesFile: values-dev.yaml # 변경내용이 작성된 파일
# overlays/dev/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/my-gitops-repo.git
targetRevision: main
path: overlays/dev
plugin:
name: kustomized-helm
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
4. 운영 전략 및 팁
기존에 배포했던 서비스를 새로운 환경에 추가/삭제 시
<추가>
overlay 하위에 새로운 디렉토리를 생성하고 application.yaml 생성하면 root-app 에서 인식되어 배포됨 (recurse: true)
<삭제>
application.yaml을 삭제하면 root-app에 의해 자동 제거. prune: true 활성화 시 클러스터 내 리소스도 정리
App of Apps 패턴에서 App 삭제 시 발생할 수 있는 제약사항
- Orphan App(고아 앱) 발생 가능성: 상위 Application를 삭제할 경우, 하위 Application이 자동으로 삭제되지 않고 클러스터에 남는 경우로 GitOps로 관리되지 않으면서도 클러스터에 남아있는 리소스가 되어 관리상의 혼란과 리스크가 발생할 수 있음
- GitOps 원칙에 따라 선언적으로 관리하지 않으면 기록 누락, 추적 불가, 상태 불일치 등 다양한 문제가 발생할 수 있어, 반드시 Git 선언 관리 및 클러스터 상태 동기화 확인이 필요함
공통 패치(base 업데이트)
base/ 하위에서 공통 내용 수정하면 모든 overlay에 반영됨. 단, 환경 별로 patch 덮어쓴 부분이 우선 적용됨
Sync 전략 & Health Check 관리
automated: true로 설정하면 변경사항 감지 즉시 배포, false면 수동 Sync 필요
CreateNamespace: true 네임스페이스 자동 생성
Prune: true 불필요 리소스 자동 삭제
Health Check: ArgoCD UI에서 실시간 확인 가능
5. 문제 상황과 해결 사례
root-app은 “자동 반영 안 되는 앱”
모든 앱에 sync-wave, CreateNamespace, prune 설정은 필수적으로 검토
장애 대응은 Argo CD + kubectl 병행 확인이 가장 빠름
배포 순서 문제
문제: 하위 앱들 간 의존성이 있는데, 순서 없이 동시에 배포됨
해결 방법: 동기화 순서를 수동 조정. sync-wave annotation 사용, 낮은 숫자 → 먼저 배포됨
metadata:
annotations:
argocd.argoproj.io/sync-wave: "1"
App of Apps는 기본적으로 병렬 배포 → 의존성 있을 땐 반드시 sync-wave 설정
root-app 업데이트 누락 시 처리
문제: root-app.yaml은 Git에 변경했지만 Argo CD에 반영되지 않음, 하위 앱이 최신 상태가 아님
해결 방법: 수동으로 root-app을 Sync or Refresh, 자동화하려면 syncPolicy.automated 옵션과 selfHeal: true 필수
# CLI에서 sync
argocd app sync root-app
# 또는 refresh로 강제 업데이트
argocd app refresh root-app
root-app도 하나의 Application 리소스 → 별도로 Sync 필요함
동기화 실패 시 디버깅 방법
문제: 앱 상태가 OutOfSync or Error, 동기화 중 에러 발생 (권한 문제, values 오류, 리소스 충돌 등)
해결 방법: Argo CD UI → Application 상세 → Events/Logs 확인, kubectl describe로 리소스 이벤트 확인, argocd CLI로 로그 확인, 리소스 상태 확인, kubectl로 리소스 실제 상태 점검
argocd app logs <app-name>
argocd app get <app-name>
오류 메시지가 Helm, YAML, RBAC, 네임스페이스 문제인지 확인하는 게 핵심
6. GitOps 구조의 진화 (마무리)
실제 운영 환경에서는 애플리케이션의 수가 늘어나고, 팀이 커지며, 관리해야 할 환경이 다양해질수록 중앙 집중화된 구성 관리, 환경별 유연한 설정, 그리고 자동화된 배포 파이프라인에 대한 니즈가 급격히 증가하게 됩니다.
이런 상황에서 App of Apps는 계층적이고 선언적인 방식으로 배포 구성을 통합 관리할 수 있게 해주고,
Kustomize와 Helm을 결합한 구조는 하나의 소스에서 다양한 환경 설정을 유연하게 분기하여 적용할 수 있도록 도와줍니다.
특히 팀 간 협업이 많아지고 요구사항이 복잡해질수록, App of Apps 구조는 그 진가를 발휘합니다.
운영자와 개발자 모두가 명확한 구조 안에서 각자의 역할을 분리할 수 있고, Git 중심의 관리로 변경 이력도 투명하게 추적할 수 있기 때문입니다.
이 글이 여러분의 GitOps 운영 체계를 한 단계 업그레이드하는 계기가 되길 바랍니다.
[관련/출처]
Declarative Setup - Argo CD - Declarative GitOps CD for Kubernetes Config Management Plugins - Argo CD - Declarative GitOps CD for Kubernetes |
'Tech Story > DevOps & Container' 카테고리의 다른 글
[기술가이드] 2025년 Kubernetes 관리의 미래: kt cloud Cluster API 아키텍처 완벽 해설 (2) | 2025.04.16 |
---|---|
Harbor, 어떻게 쓸 것인가: Replication Rule (24) | 2024.11.18 |
What is DevOps? - Helm Chart (11) | 2024.11.13 |
What is DevOps? - Slack으로 협업하기 (3) | 2024.11.13 |
What is DevOps? - CI Automation (7) | 2024.11.13 |