-
이코에코(Eco²) GitOps #05 Sync Wave이코에코(Eco²)/Kubernetes Cluster+GitOps+Service Mesh 2025. 11. 25. 03:57
🛎️ 본 포스팅은 이미 구현이 완료된 사안만 다룹니다. 현재 이코에코 14-nodes cluster는 ap-northeast-2 리전에 배포돼 있습니다.

드디어 기다리던 Sync-wave 포스팅이다.
가장 시간을 많이 썼으면서도 그만큼 유용했기에 이번 구축에서 가장 애정하는 기능이다.
왜 굳이 Sync Wave였나?
Ansible bootstrap부터 App-of-Apps로 가는 동안 가장 큰 병목지점은 Reconcile Loop가 동시에 돌면서 서로 엉키는 일이었다.- CRD가 아직 없는데 Operator가 먼저 올라와서 CrashLoop
- Secret이 없는데 Ingress가 먼저 뜨면서 BadRequest
- 데이터 Operator/Instance가 한꺼번에 올라오다가 FailedCreate
ArgoCD 자체는 Sync Wave 없이도 이런 걸 알아서 처리하진 않는다.
App-of-Apps 구조에서도 기본은 “모든 App을 동시에 Sync”라서, 의존성을 명시해 주지 않으면 결국 운에 맡기는 셈이었다. 그래서 Wave를 도입했다. 이유만 정리하면 아래와 같다.
- 동시 Reconcile을 제어하기 위해
- App-of-Apps 안쪽에서 여러 Application이 동시에 배포될 때
의존성 있는 리소스가 꼬이지 않도록 순서를 강제로 부여.
- App-of-Apps 안쪽에서 여러 Application이 동시에 배포될 때
- GitOps와 Runbook을 일치시키기 위해
- Wave 번호 자체가 “운영 순서”와 동일하니까
장애 대응할 때도 “Wave 10이 멈췄으면 Secrets부터 본다”처럼 바로 체크리스트로 전환할 수 있었다.
- Wave 번호 자체가 “운영 순서”와 동일하니까
Wave를 도입한 이후로 인프라 레이어가 조립 순서대로 차곡차곡 올라가기 시작했고,
Git에 남는 숫자(예:15-alb-controller.yaml)가 운영 순서랑 일치시키면서 가시성을 높였다.이번 편은 오직 ArgoCD Sync Wave 순서와 의존성만 다룬다.
RBAC/네임스페이스, API 얘기는 다음 편으로 넘기고,
“왜 이 Wave 순서가 나왔는지”만 한 번에 정리해 보려고 한다.
기준 문서는 여기다:ARGOCD_SYNC_WAVE_PLAN.mdSync Wave는 정확히 뭐고, 왜 쓰게 됐나?
1. 개념부터 짚고 가자
ArgoCD는 기본적으로 “root-app을 기반으로 클러스터 내 모든 Application을 동시에 Sync”하는 구조다.
App-of-Apps 패턴을 쓰면root-app아래에 있는 수십 개의 Application이
동시에 Reconcile을 시도한다. 의존성이 얽혀 있으면 이때 문제가 터진다.그래서 ArgoCD는
argocd.argoproj.io/sync-wave라는 Annotation을 제공한다.- 숫자가 낮을수록 먼저 Sync 된다.
- 같은 Wave 안에서는 리소스 이름 순으로 진행된다.
- Wave 번호는 음수/정수 모두 가능하고, 보통
-1,0,5,10… 식으로 띄워서 쓴다.
먼저 이코에코에선 이 개념을 정책으로 정리했다:
→docs/gitops/ARGOCD_SYNC_WAVE_PLAN.mdSync Wave를 이용해 인프라 → 데이터 → 애플리케이션 순서로 배포한다. 그 뒤에 Wave별 리소스, 의존성, 실제 ArgoCD Application 파일명을 명시했다.
2. Sync hook
Sync Wave만으로 순서를 제어하기 어렵거나, 특정 작업을 따로 끼워 넣고 싶을 때는 Sync Hook을 같이 쓴다.
ArgoCD 는 Presync, Sync, PostSync, Skip, SyncFail Hook 타입을 제공한다.
PreSync- Wave가 진행되기 전에 가장 먼저 실행된다.
- DB 마이그레이션, ConfigMap 생성, 사전 점검 같은 걸 넣을 때 유용하다.
Sync
- 일반 리소스가 적용되는 기본 단계. 별도 Hook을 지정하지 않으면 모두 Sync 단계로 취급된다.
PostSync
- Sync가 성공적으로 끝난 뒤 실행된다.
- Smoke 테스트, Slack 알림, Health 체크 스크립트 등으로 자주 활용된다.
SyncFail
- Sync 과정에서 실패가 발생했을 때 동작한다.
- 롤백 스크립트, 에러 리포트, 리소스 정리 작업을 넣을 수 있다.
이코에코에서는 일반 Sync를 제외하면 PostSync의 중요도가 높다.
주로 도메인 앱이 배포될 때 DB(PostgreSQL)에 스키마와 데이터를 주입할 때 활용된다.
상세한 동작 방식은 차후 location API 개발 과정에 기록해 두겠다.3. 러프한 Wave → 문서화된 Wave
처음 루트 앱을 짰을 때는 Ansible Bootstrap의 절차를 그대로 옮기려 했다.
그러다 특정 Helm 혹은 Operator에서 Sync 시 충돌이 날 때,
그때그때argocd.argoproj.io/sync-wave숫자를 아무거나 붙여 넘기는 식이었다.- Calico보다 NetworkPolicy가 먼저 올라가서 파드가 다 죽는다든가
- External Secrets보다 ALB Controller가 먼저 떠서 Secret 못 찾고 CrashLoop 돌고…
그렇지만 이런 러프한 wave로는 전체 앱을 안정적으로 배포하긴 힘들었다.
OutOfSync와 Missing, Sync Error를 헤매며 시간을 꽤 많이 소모한 듯 싶다.
v0.7.4 즈음에 Wave 표를 만들기 시작했다. 그게 지금ARGOCD_SYNC_WAVE_PLAN.md에 있는 표다.
표의 맨 윗줄은 이렇게 생겼다.Wave 계층 대표 리소스 선행 의존성 예시 파일 0 CRD Seed ALB, Prometheus, Postgres, ESO CRDs - 00-crds.yaml2 Namespaces 13개 Namespace Wave 0 02-namespaces.yaml3 RBAC · Storage ServiceAccount, ClusterRole, StorageClass Wave 2 03-rbac-storage.yaml5 CNI Calico (VXLAN) Wave 3 05-calico.yaml6 NetworkPolicy default-deny, tier 격리 Wave 5 06-network-policies.yaml… … … … … 한 줄씩 보면서 “이게 왜 이 순서였는지”를 실제 경험으로 연결해 본다.
4. Wave 배치 예시 (네임스페이스 ~ API까지)
문서에 있는 표를 그대로 가져오면 이런 구조다.
Wave 계층 대표 리소스 선행 의존성 파일 예시 0 CRD Seed ALB, Prometheus, Postgres, ESO CRDs 없음 00-crds.yaml2 Namespaces 13개 Namespace (tier, domain) Wave 0 02-namespaces.yaml3 RBAC · Storage ServiceAccount, ClusterRole, StorageClass Wave 2 03-rbac-storage.yaml5 CNI Calico (VXLAN) Wave 3 05-calico.yaml6 NetworkPolicy default-deny, tier 기반 격리 Wave 5 06-network-policies.yaml10 Secrets Operator ExternalSecrets Operator Wave 6 10-secrets-operator.yaml11 Secrets CR ExternalSecret CR (SSM → K8s Secret) Wave 10 11-secrets-cr.yaml15 Ingress Controller AWS Load Balancer Controller Wave 11 15-alb-controller.yaml16 DNS Automation ExternalDNS Wave 11, 15 16-external-dns.yaml20 Monitoring Operator kube-prometheus-stack Wave 15 20-monitoring-operator.yaml21 Observability UI Grafana Wave 20 21-grafana.yaml25 Data Operators Postgres/Redis/RabbitMQ Operators Wave 20 25-data-operators.yaml35 Data Instances PostgresCluster, RedisFailover CR Wave 25 35-data-cr.yaml60 Applications 7개 도메인 API Wave 35 60-apis-appset.yaml70 Application Ingress ALB Path routing Wave 60 70-ingress.yaml
각 Wave의 이유는 앞선 글에서 사례 중심으로 설명했지만, 간략히 요약하면:- Wave 0~6: 네트워크(네임스페이스, RBAC, CNI, NetworkPolicy)
- Wave 10~16: Secrets와 인프라 출입구(ExternalSecrets Operator → Secret → ALB → ExternalDNS)
- Wave 20~35: 모니터링과 데이터 계층
- Wave 60~70: 애플리케이션과 Ingress
5. 규칙을 몇 가지로 정리해뒀다
문서에는 Wave 설계 규칙도 6개로 정리되어 있다.
큰 흐름은 리소스 -> 네트워크 + ALB 컨트롤러 -> DB + 모니터링 -> App + Instance 순이다.
먼저 클러스터에서 사용할 K8s 리소스들을 깔고, 네트워크 Policy와 ALB 설정을 끝낸다.
이 절차에서 부트스트래핑이 연상된다면 정확하다. 여기까진 그 구조다.
그다음 App을 실행하기 전 연결할 인프라(DB, Monitoring)를 깔아둔다.
각 절차 내 세부 의존성은 아래와 같다.- CRD → Operator → Instance
- 커스텀 리소스가 필요한 건 “언어 정의 → 컨트롤러 → 인스턴스” 순서를 지킨다.
- Network와 TLS 선행
- ExternalSecrets를 올리기 전에 Calico/NetworkPolicy가 준비돼 있어야 하고,
ALB Controller는 ACM 인증서(이미 Terraform에서 만든)를 필요로 한다.
- ExternalSecrets를 올리기 전에 Calico/NetworkPolicy가 준비돼 있어야 하고,
- 모니터링은 Ingress보다 앞
- Prometheus Operator가 늦게 올라오면 Admission Webhook이 꼬인다.
그래서 Ingress Wave(15)보다 Monitoring Wave(20)를 항상 높게 잡았다.
- Prometheus Operator가 늦게 올라오면 Admission Webhook이 꼬인다.
- 데이터 계층 보호
- Operator와 Instance 사이에 Wave를 비워 둬서 Drift나 실패 지점을 빠르게 찾는다.
- Exporter는 데이터 이후
- Exporter/ServiceMonitor는 DB/Queue Endpoint가 준비된 뒤에만 배포한다.
- 애플리케이션은 진짜 마지막
- API/Worker는 Wave 60 이후, 인프라 전체가 Healthy 상태일 때만 올라오도록 강제한다.
6. ArgoCD 설정은 이렇게 쓴다
Wave를 붙이는 방식은 yaml에 Annotation 하나 추가하는 걸로 끝이다.apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: data-operators namespace: argocd annotations: argocd.argoproj.io/sync-wave: "25" spec: project: infrastructure source: repoURL: https://github.com/SeSACTHON/backend.git path: k8s/operators targetRevision: main destination: server: https://kubernetes.default.svc namespace: data-operators syncPolicy: automated: prune: true selfHeal: trueWave
번호만 맞춰줘도, Root App에서 하위 앱을 Sync할 때 이 순서를 그대로 따라간다. 이제 파트별로 그 순서와 절차를 알아보자.CRD → 네임스페이스 → RBAC → CNI → NetworkPolicy
1. CRD Seed (Wave 0)- ALB Controller, Prometheus Operator, Postgres Operator, External Secrets Operator 등
컨트롤러가 쓰는 CRD를 묶어서 Wave 0에 배치했다.
clusters/dev/apps/00-crds.yaml같은 파일이 이 단계에 해당한다.2. Namespaces (Wave 2)
CRD가 깔리면 바로 네임스페이스를 만든다.- auth, my, scan, character, location 같은 도메인 네임스페이스
- data, databases, monitoring 같은 인프라 네임스페이스도 같이.
- 레이블을
tier,domain으로 미리 붙여서,
나중에 NetworkPolicy / NodeAffinity / RBAC이 다 이 레이블을 기준으로 움직이게 했다.
clusters/dev/apps/02-namespaces.yaml파일이 올라오는 단계가 바로 여기다.3. RBAC · Storage (Wave 3)
ServiceAccount,ClusterRole,RoleBinding,StorageClass,EBS CSI Driver같은 것들이 여기.- namesapce가 먼저 있어야 SA/Role이 붙을 수 있으니 Wave 3으로 뒀다.
03-rbac-storage.yaml파일에서 정의.4. CNI → NetworkPolicy (Wave 5 ~ 6)
초기엔 네트워크 정책을 너무 빨리 올렸다가,
Calico DaemonSet이 올라오기도 전에default-deny가 적용돼서 트래픽이 막히는 일이 있었다.그래서 CNI → NetworkPolicy 순서를 딱 붙였다.
- Wave 5: Calico (VXLAN, BGP Off)
- Wave 6: NetworkPolicy (단계별 격리, default-deny)
Calico 설치 애노테이션만 맞춰두면 네트워크 레벨이 안정되고,
그 다음에 Pod 간 통신 제한을 실제로 적용한다.
Secrets → Ingress → DNS
1. External Secrets Operator → ExternalSecret (Wave 10, 11)
여기서 실제로 겪었던 문제는 이거다.- ALB Controller가 먼저 뜨는데,
AWS 자격증명/리전/클러스터 이름 같은 값을 담은 Secret이 아직 준비 안 돼 있으면 컨트롤러가 CrashLoopBackOff에 빠진다.
그래서 두 Wave로 쪼갰다.- Wave 10: External Secrets Operator (컨트롤러)
- Wave 11: ExternalSecret CR (SSM Path 매핑)
이 순서로 바꾸고 나서야 ALB Controller가 안정적으로 뜨기 시작했다.
clusters/dev/apps/10-secrets-operator.yaml과11-secrets-cr.yaml에 해당.2. ALB Controller → ExternalDNS (Wave 15, 16)
그 다음에 트래픽 출입구를 세팅한다.- Wave 15: ALB Controller (Helm)
- Wave 16: ExternalDNS
ExternalDNS를 ALB보다 먼저 올리면,
Route53에서는 레코드를 만들려고 하는데 Ingress가 아직 없어서 대기하거나 실패한다.
반대로 ALB Controller가 먼저 뜨면, Ingress 생성 → ALB/TG 생성 → ExternalDNS가 Route53 반영까지 이어진다.
이 순서 하나 바꿨을 뿐인데,
Route53 레코드가 늦게 생성돼서 프런트엔드에서 404가 난다거나 하는 일이 확 줄었다.
모니터링과 데이터 계층
1. Prometheus Operator → Grafana (Wave 20, 21)
Prometheus Operator랑 Grafana는 한 Wave에 같이 넣어도 될 줄 알았다가,
한 번은 이 순서가 꼬여서 Prometheus 웹훅이 제대로 등록되지 못한 적이 있었다. 그후론 완전히 떼어냈다.- Wave 20:
kube-prometheus-stack(Operator) - Wave 21: Grafana
이제 Wave 20이 끝나면
/metrics를 긁어오는 상태가 이미 확보되고,
Wave 21에서 Grafana가 올라와서 대시보드를 붙인다.2. Data Operators → Data Instances (Wave 25, 35)
데이터 계층은 Operator와 Instance를 완전히 나눴다.- Wave 25: Postgres/Redis/RabbitMQ Operators (혹은 Helm 기반 컨트롤러)
- Wave 35: PostgresCluster, RedisFailover 같은 Instance CR
이 사이에 Wave를 하나라도 띄워두면,
Operator 실수를 Instance Wave가 잡아내기도 훨씬 쉽다.
나중에 Operator를 부분적으로만 쓰고 Helm으로 되돌아오긴 했지만 Wave 구조는 그대로 유지했다.
애플리케이션 → Ingress
1 Applications (Wave 60)
몇 없는 ApplicationSet이다.
이 Wave에서 auth, location, character, my, scan, info, chat 같은 API들이 한꺼번에 올라온다.
루트앱 쪽에서는60-apis-appset.yaml이 이 Wave에 해당한다.중요한 건 “이 Wave 이전에 모든 인프라 Wave가 Healthy가 돼 있어야 한다”는 거다.
- 네트워크 (Calico + NetworkPolicy)
- Secret (ExternalSecret → K8s Secret)
- ALB / ExternalDNS
- 모니터링
- 데이터베이스
이게 하나라도 빠진 상태에서 API를 올리면
“Pod는 떴는데 DB 접속을 못 해서 CrashLoop” 같은 일이 그대로 터진다.
2. Application Ingress (Wave 70)
그리고 마지막 Wave가 Ingress다.- API 라우팅 (
/api/,/auth/,/scan/등) - ArgoCD / Grafana / Prometheus UI용 Ingress
- Dev/Prod 도메인 연결, ACM 인증서 적용
Ingress는 마지막이다. 앞선 Wave에서 API Pods가 Healthy 되었다는 것까지 확인한 뒤 ingress를 연결해 외부에 엔드포인트를 노출한다.
변화 사례
- Secrets → ALB 순서
- 초기엔 ALB Controller가 Secret 없다고 튕기고, Route53 레코드는 Pending 상태였다.
- Wave 10/11로 쪼갠 뒤부터는 secrets operator → secret CR → ALB → 외부 DNS 순으로 정리돼서
ALB 관련 장애가 눈에 띄게 줄었다.
- CNI → NetworkPolicy 순서
- Calico보다 네트워크 정책이 일찍 올라가면 pod가 전부
CrashLoop로 넘어간다. - Wave 5/6로 나눠서 “Calico→Default deny” 순서를 강제하고 나서,
네트워크 관련 장애를 훨씬 쉽게 추적할 수 있게 됐다.
- Calico보다 네트워크 정책이 일찍 올라가면 pod가 전부
- Data Operator → Instance 분리
- Postgres Operator를 도입하려다 놓친 부분이 많아서 Operator와 Instance Wave 사이에 간격을 두고 나니 Operator 업그레이드 → Instance 재사용 흐름을 명확히 살릴 수 있었다.
- 나중에 Helm으로 회귀해도, “데이터 컨트롤러 → 데이터 클러스터” 순서는 그대로 유지하고 있다.
'이코에코(Eco²) > Kubernetes Cluster+GitOps+Service Mesh' 카테고리의 다른 글
이코에코(Eco²) Service Mesh #1: Istio Sidecar 마이그레이션 (0) 2025.12.08 이코에코(Eco²) GitOps #06 - Namespace · RBAC · NetworkPolicy를 한 뿌리에서 (3) 2025.11.25 이코에코(Eco²) GitOps #04 Operator와 Helm-charts를 오가며 겪은 시행착오들 (0) 2025.11.24 이코에코(Eco²) GitOps #03 네트워크 안정화 (0) 2025.11.24 이코에코(Eco²) GitOps #02 Ansible 의존성 감소, Kustomize Overlays 패턴 적용 (0) 2025.11.24