ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(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를 도입했다. 이유만 정리하면 아래와 같다.
     

    1. 동시 Reconcile을 제어하기 위해
      • App-of-Apps 안쪽에서 여러 Application이 동시에 배포될 때
        의존성 있는 리소스가 꼬이지 않도록 순서를 강제로 부여.
    2. GitOps와 Runbook을 일치시키기 위해
      • Wave 번호 자체가 “운영 순서”와 동일하니까
        장애 대응할 때도 “Wave 10이 멈췄으면 Secrets부터 본다”처럼 바로 체크리스트로 전환할 수 있었다.

    Wave를 도입한 이후로 인프라 레이어가 조립 순서대로 차곡차곡 올라가기 시작했고,
    Git에 남는 숫자(예: 15-alb-controller.yaml)가 운영 순서랑 일치시키면서 가시성을 높였다.

    이번 편은 오직 ArgoCD Sync Wave 순서와 의존성만 다룬다.
    RBAC/네임스페이스, API 얘기는 다음 편으로 넘기고,
    “왜 이 Wave 순서가 나왔는지”만 한 번에 정리해 보려고 한다.
    기준 문서는 여기다: ARGOCD_SYNC_WAVE_PLAN.md

    Sync 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.md

    Sync 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.yaml
    2 Namespaces 13개 Namespace Wave 0 02-namespaces.yaml
    3 RBAC · Storage ServiceAccount, ClusterRole, StorageClass Wave 2 03-rbac-storage.yaml
    5 CNI Calico (VXLAN) Wave 3 05-calico.yaml
    6 NetworkPolicy default-deny, tier 격리 Wave 5 06-network-policies.yaml

    한 줄씩 보면서 “이게 왜 이 순서였는지”를 실제 경험으로 연결해 본다.

    4. Wave 배치 예시 (네임스페이스 ~ API까지)

    문서에 있는 표를 그대로 가져오면 이런 구조다.

    Wave 계층 대표 리소스 선행 의존성 파일 예시
    0 CRD Seed ALB, Prometheus, Postgres, ESO CRDs 없음 00-crds.yaml
    2 Namespaces 13개 Namespace (tier, domain) Wave 0 02-namespaces.yaml
    3 RBAC · Storage ServiceAccount, ClusterRole, StorageClass Wave 2 03-rbac-storage.yaml
    5 CNI Calico (VXLAN) Wave 3 05-calico.yaml
    6 NetworkPolicy default-deny, tier 기반 격리 Wave 5 06-network-policies.yaml
    10 Secrets Operator ExternalSecrets Operator Wave 6 10-secrets-operator.yaml
    11 Secrets CR ExternalSecret CR (SSM → K8s Secret) Wave 10 11-secrets-cr.yaml
    15 Ingress Controller AWS Load Balancer Controller Wave 11 15-alb-controller.yaml
    16 DNS Automation ExternalDNS Wave 11, 15 16-external-dns.yaml
    20 Monitoring Operator kube-prometheus-stack Wave 15 20-monitoring-operator.yaml
    21 Observability UI Grafana Wave 20 21-grafana.yaml
    25 Data Operators Postgres/Redis/RabbitMQ Operators Wave 20 25-data-operators.yaml
    35 Data Instances PostgresCluster, RedisFailover CR Wave 25 35-data-cr.yaml
    60 Applications 7개 도메인 API Wave 35 60-apis-appset.yaml
    70 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)를 깔아둔다.

    각 절차 내 세부 의존성은 아래와 같다.

    1. CRD → Operator → Instance
      • 커스텀 리소스가 필요한 건 “언어 정의 → 컨트롤러 → 인스턴스” 순서를 지킨다.
    2. Network와 TLS 선행
      • ExternalSecrets를 올리기 전에 Calico/NetworkPolicy가 준비돼 있어야 하고,
        ALB Controller는 ACM 인증서(이미 Terraform에서 만든)를 필요로 한다.
    3. 모니터링은 Ingress보다 앞
      • Prometheus Operator가 늦게 올라오면 Admission Webhook이 꼬인다.
        그래서 Ingress Wave(15)보다 Monitoring Wave(20)를 항상 높게 잡았다.
    4. 데이터 계층 보호
      • Operator와 Instance 사이에 Wave를 비워 둬서 Drift나 실패 지점을 빠르게 찾는다.
    5. Exporter는 데이터 이후
      • Exporter/ServiceMonitor는 DB/Queue Endpoint가 준비된 뒤에만 배포한다.
    6. 애플리케이션은 진짜 마지막
      • 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로 쪼갰다.

    1. Wave 10: External Secrets Operator (컨트롤러)
    2. Wave 11: ExternalSecret CR (SSM Path 매핑)

    이 순서로 바꾸고 나서야 ALB Controller가 안정적으로 뜨기 시작했다.

    clusters/dev/apps/10-secrets-operator.yaml11-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를 연결해 외부에 엔드포인트를 노출한다.


    변화 사례

    1. Secrets → ALB 순서
      • 초기엔 ALB Controller가 Secret 없다고 튕기고, Route53 레코드는 Pending 상태였다.
      • Wave 10/11로 쪼갠 뒤부터는 secrets operator → secret CR → ALB → 외부 DNS 순으로 정리돼서
        ALB 관련 장애가 눈에 띄게 줄었다.
    2. CNI → NetworkPolicy 순서
      • Calico보다 네트워크 정책이 일찍 올라가면 pod가 전부 CrashLoop로 넘어간다.
      • Wave 5/6로 나눠서 “Calico→Default deny” 순서를 강제하고 나서,
        네트워크 관련 장애를 훨씬 쉽게 추적할 수 있게 됐다.
    3. Data Operator → Instance 분리
      • Postgres Operator를 도입하려다 놓친 부분이 많아서 Operator와 Instance Wave 사이에 간격을 두고 나니 Operator 업그레이드 → Instance 재사용 흐름을 명확히 살릴 수 있었다.
      • 나중에 Helm으로 회귀해도, “데이터 컨트롤러 → 데이터 클러스터” 순서는 그대로 유지하고 있다.

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango