sayu.day

GitOps 심화 (상): 철학과 원칙, ArgoCD와 Flux 아키텍처

Git 저장소에 선언된 상태가 곧 클러스터의 실제 상태가 되도록 만드는 것, 이것이 GitOps의 핵심입니다. 이 글에서는 먼저 GitOps의 4가지 원칙과 Push/Pull 배포 모델의 차이를 압축해 정리합니다. 이어서 대표 구현체인 ArgoCD와 Flux의 내부 아키텍처를 나란히 깊게 살펴보고, 마지막에 두 도구의 선택 기준을 비교표로 제시합니다.

전통적 CI/CD의 한계

Jenkins나 GitHub Actions가 코드를 빌드하고, 테스트를 통과하면 kubectl applyhelm upgrade 명령으로 클러스터에 직접 배포하는 방식을 떠올려 봅시다.

sequenceDiagram
    participant Dev as 개발자
    participant Git as Git Repository
    participant CI as CI Server
    participant K8s as Kubernetes

    Dev->>Git: git push
    Git->>CI: Webhook 트리거
    CI->>CI: Build & Test
    CI->>K8s: kubectl apply / helm upgrade
    Note over K8s: 배포 완료

이 방식은 동작하지만 몇 가지 근본적인 문제가 있습니다.

문제 설명
Credential 분산 CI 서버가 프로덕션 클러스터에 접근하는 강력한 권한 보유
Drift 감지 불가 누군가 kubectl edit으로 직접 수정하면 Git과 실제 상태 불일치
Audit Trail 부재 누가, 언제, 왜 변경했는지 추적 어려움
롤백의 복잡성 이전 상태로 돌아가려면 "어떤 버전이 배포되어 있었는지"부터 찾아야 함

주의: CI 서버가 해킹당하면 공격자가 프로덕션 클러스터에 임의 코드를 배포할 수 있습니다. 2021년 Codecov 사태에서는 CI 파이프라인이 공격 벡터가 되어 수천 개 기업의 credential이 유출되었습니다.

GitOps의 정의와 4가지 원칙

GitOps는 Git을 Single Source of Truth(SSOT)로 삼아 원하는 시스템 상태를 선언적으로 정의하고, 자동화된 프로세스가 실제 상태를 Git에 정의된 상태와 지속적으로 일치시키는 운영 방식입니다. 2021년 CNCF 산하 OpenGitOps 프로젝트가 공식화한 4가지 원칙을 차례로 살펴봅니다.

1. Declarative (선언적)

시스템의 원하는 상태(Desired State)를 선언적으로 기술합니다.

# "3개의 nginx Pod가 실행되어야 한다"는 선언
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
  • 명령형(Imperative) 접근: "nginx Pod를 3개 만들어라" (kubectl scale)
  • 선언형(Declarative) 접근: "nginx Pod가 3개인 상태가 되어야 한다" (YAML manifest)

Kubernetes 자체가 선언적 시스템이며, GitOps는 이 철학을 Git과 결합한 것입니다.

2. Versioned and Immutable (버전 관리와 불변성)

Git의 각 커밋은 불변이며 전체 히스토리가 보존됩니다.

# 모든 변경 이력 추적 가능
git log --oneline
# a1b2c3d (HEAD) feat: scale nginx to 5 replicas
# d4e5f6g fix: update nginx image to 1.25.3
# g7h8i9j initial: deploy nginx with 3 replicas

# 특정 시점으로 롤백
git revert a1b2c3d

이 성질이 세 가지 이점을 만듭니다.

  • Audit Trail: 누가, 언제, 왜 변경했는지 모든 기록이 남습니다.
  • Rollback: git revert로 이전 상태로 즉시 복구할 수 있습니다.
  • Compliance: 금융, 헬스케어 등 규제 산업의 감사 요구사항을 충족합니다.

인프라 변경도 Pull Request로 흐르게 되므로, 코드 리뷰와 승인 절차가 배포 파이프라인의 일부가 됩니다.

3. Pulled Automatically (자동 Pull)

원하는 상태의 변경이 Git에 푸시되면 클러스터 내부의 에이전트가 자동으로 감지하고 적용합니다. CI 서버가 클러스터에 접근하는 것이 아니라 에이전트가 Git을 주기적으로 확인하는 Pull 모델이 핵심입니다.

flowchart LR
    subgraph GitRepo [Git Repository]
        Manifest[Kubernetes Manifests]
    end

    subgraph Cluster [Kubernetes Cluster]
        Agent[GitOps Agent\nArgoCD / Flux]
        Workload[Running Workloads]
    end

    Agent -->|1. Pull & Watch| GitRepo
    Agent -->|2. Compare| Workload
    Agent -->|3. Apply changes| Workload

4. Continuously Reconciled (지속적 조정)

에이전트는 변경을 한 번 적용하고 끝내지 않습니다. Reconciliation Loop를 통해 실제 상태와 원하는 상태를 지속적으로 비교하고, 차이가 발생하면 자동으로 수정합니다.

flowchart TB
    subgraph ReconciliationLoop [Reconciliation Loop]
        direction TB
        Observe[1. 현재 상태 관찰\nCurrent State]
        Compare[2. 원하는 상태와 비교\nDesired State in Git]
        Drift{Drift\n발견?}
        Apply[3. 차이 조정\nReconcile]
        Wait[4. 대기\nInterval]
    end

    Observe --> Compare --> Drift
    Drift -->|Yes| Apply --> Wait
    Drift -->|No| Wait
    Wait --> Observe

중요: 누군가 kubectl edit으로 replicas를 5개로 바꾸면 에이전트가 이를 감지하고 Git에 정의된 3개로 자동 복구합니다. 이것이 Drift Detection과 Self-Healing입니다.

Push vs Pull 배포 모델

GitOps를 이해하는 핵심은 Push 모델과 Pull 모델의 차이입니다.

Push 모델 (전통적 CI/CD)

flowchart LR
    subgraph External [클러스터 외부]
        CI[CI Server]
    end

    subgraph Cluster [Kubernetes Cluster]
        API[API Server]
        Pod[Pods]
    end

    CI -->|1. kubectl apply\n강력한 권한 필요| API
    API --> Pod

CI 서버가 클러스터에 직접 접근하므로 admin 수준의 kubeconfig가 필요하고, CI 서버가 침해되면 클러스터도 위험해집니다.

Pull 모델 (GitOps)

flowchart LR
    subgraph External [클러스터 외부]
        Git[Git Repository]
    end

    subgraph Cluster [Kubernetes Cluster]
        Agent[GitOps Agent]
        API[API Server]
        Pod[Pods]
    end

    Agent -->|1. Git clone/pull\n읽기 권한만| Git
    Agent -->|2. kubectl apply\n내부 통신| API
    API --> Pod

클러스터 내부의 에이전트가 Git을 Pull하므로 Git에 대한 읽기 전용 권한만 필요하며, 외부에서 클러스터로 향하는 인바운드 연결이 사라집니다.

보안 관점 비교

관점 Push 모델 Pull 모델
네트워크 방향 외부 → 클러스터 클러스터 → 외부 (Git)
필요 권한 CI에 cluster-admin Agent에 내부 권한
공격 표면 CI 서버 침해 시 클러스터 위험 Git 저장소 보호에 집중
방화벽 인바운드 허용 필요 아웃바운드만 허용

참고: ArgoCD나 Flux도 클러스터 내부에서는 강력한 권한을 가집니다. 다만 공격 벡터가 Git 저장소로 단일화되어 보안 관리가 단순해집니다.

Kubernetes Controller 패턴의 확장

GitOps가 Kubernetes에서 특히 잘 작동하는 이유는 Controller 패턴 때문입니다. 모든 Kubernetes 컨트롤러는 Reconciliation Loop로 동작합니다.

// Kubernetes Controller의 핵심 로직
func (c *Controller) Reconcile(ctx context.Context, req Request) (Result, error) {
    // 1. 현재 상태 조회
    current, err := c.Get(ctx, req.NamespacedName)
    if err != nil {
        return Result{}, err
    }

    // 2. 원하는 상태와 비교
    desired := c.calculateDesiredState(current)

    // 3. 차이가 있으면 조정
    if !reflect.DeepEqual(current.Spec, desired) {
        current.Spec = desired
        return Result{}, c.Update(ctx, current)
    }

    // 4. Requeue for next reconciliation
    return Result{RequeueAfter: 30 * time.Second}, nil
}

GitOps 에이전트는 이 패턴의 상위 레벨 구현입니다. Desired State의 위치가 API Server에서 Git 저장소로 확장되었을 뿐입니다.

개념 Kubernetes Controller GitOps Agent
Desired State Spec in YAML Git Repository
Current State Status in API Server Live Kubernetes Objects
Reconciliation Controller Manager ArgoCD/Flux Controller
Watch mechanism Informer (etcd watch) Git polling / Webhook

GitOps는 Kubernetes의 선언적 모델을 Git까지 확장한 것이며, Infrastructure as Code의 자연스러운 진화라고 할 수 있습니다.

ArgoCD 심화: 중앙 집중형 GitOps

ArgoCD는 Kubernetes를 위한 선언적 GitOps 지속 배포(Continuous Delivery) 도구입니다. 2022년 CNCF Graduated 프로젝트가 되었고, 다음과 같은 특징을 가집니다.

  • Plain YAML, Helm, Kustomize, Jsonnet 등 다양한 매니페스트 지원
  • React 기반의 강력한 Web UI로 실시간 애플리케이션 상태 시각화
  • 단일 인스턴스로 여러 클러스터 관리 가능
  • OIDC, SAML, LDAP, GitHub, GitLab 등 SSO 통합

3개의 핵심 컴포넌트

flowchart TB
    subgraph External [외부]
        User[사용자]
        Git[Git Repository]
        Webhook[Webhook]
    end

    subgraph ArgoCD [ArgoCD Namespace]
        subgraph API [API Server]
            REST[REST API]
            GRPC[gRPC API]
            WebUI[Web UI]
        end

        subgraph Repo [Repo Server]
            Clone[Git Clone]
            Render[Manifest Rendering]
            Cache[Cache]
        end

        subgraph Controller [Application Controller]
            Reconciler[Reconciliation Loop]
            Diff[Diff Engine]
            Sync[Sync Engine]
        end

        Redis[(Redis)]
    end

    subgraph Cluster [Kubernetes Cluster]
        APIServer[K8s API Server]
        Resources[Workloads]
    end

    User --> WebUI & REST
    Git --> Clone
    Webhook --> API

    API <--> Repo
    API <--> Controller
    API <--> Redis

    Controller <--> APIServer
    APIServer --> Resources

    Repo --> Clone
    Clone --> Render
    Render --> Cache

API Server는 ArgoCD의 프론트엔드입니다. REST/gRPC API로 CLI와 Web UI, 외부 시스템과 통신하고, RBAC과 SSO 인증을 처리하며, Git 변경 시 즉시 Sync를 트리거하는 Webhook을 받습니다.

# ArgoCD CLI는 gRPC API 사용
argocd app list
argocd app sync my-app

Repo Server는 Git 저장소 클론과 매니페스트 렌더링을 담당합니다.

sequenceDiagram
    participant AC as Application Controller
    participant RS as Repo Server
    participant Git as Git Repository
    participant Cache as Cache

    AC->>RS: GetManifests(app, revision)
    RS->>Cache: 캐시 확인
    alt 캐시 Hit
        Cache-->>RS: 캐시된 매니페스트
    else 캐시 Miss
        RS->>Git: git clone/fetch
        Git-->>RS: Repository
        RS->>RS: Helm/Kustomize 렌더링
        RS->>Cache: 캐시 저장
    end
    RS-->>AC: Rendered Manifests

팁: Repo Server는 stateless입니다. 렌더링 결과를 Redis에 캐싱하므로 스케일 아웃이 용이합니다.

Application Controller는 ArgoCD의 핵심 엔진으로, 앞서 본 Kubernetes Controller 패턴 그대로 동작합니다.

// 간략화된 Application Controller 로직
func (c *ApplicationController) Reconcile(app *Application) error {
    // 1. Git에서 Desired State 조회 (Repo Server 통해)
    desired, err := c.repoServer.GetManifests(app)
    if err != nil {
        return err
    }

    // 2. 클러스터에서 Current State 조회
    current, err := c.kubectl.GetResources(app.Destination)
    if err != nil {
        return err
    }

    // 3. Diff 계산
    diff := c.diffEngine.Compare(desired, current)

    // 4. 상태 업데이트
    app.Status.Sync.Status = calculateSyncStatus(diff)
    app.Status.Health = calculateHealth(current)

    // 5. Auto Sync 활성화 시 동기화
    if app.Spec.SyncPolicy.Automated != nil && diff.HasChanges() {
        return c.syncEngine.Sync(app, desired)
    }

    return nil
}

Application CRD

ArgoCD가 관리하는 모든 애플리케이션은 Application Custom Resource로 정의됩니다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd  # ArgoCD가 설치된 네임스페이스
  finalizers:
    - resources-finalizer.argocd.argoproj.io  # 삭제 시 리소스 정리
spec:
  # 프로젝트 (RBAC 단위)
  project: default

  # 소스: 어디서 가져올 것인가
  source:
    repoURL: https://github.com/myorg/myrepo.git
    targetRevision: HEAD  # 브랜치, 태그, 커밋 SHA
    path: k8s/overlays/prod  # 매니페스트 경로

  # 목적지: 어디에 배포할 것인가
  destination:
    server: https://kubernetes.default.svc  # 클러스터 API Server
    namespace: production  # 타겟 네임스페이스

  # 동기화 정책
  syncPolicy:
    automated:
      prune: true      # Git에서 삭제된 리소스 클러스터에서도 삭제
      selfHeal: true   # 수동 변경 시 Git 상태로 복구
    syncOptions:
      - CreateNamespace=true  # 네임스페이스 자동 생성

status:
  sync:
    status: Synced  # Synced, OutOfSync, Unknown
    revision: abc123
  health:
    status: Healthy  # Healthy, Progressing, Degraded, Missing
  resources:
    - kind: Deployment
      name: my-app
      status: Synced
      health: Healthy

Helm 차트를 소스로 쓸 때는 values와 parameters를 함께 선언합니다.

spec:
  source:
    repoURL: https://charts.bitnami.com/bitnami
    chart: nginx
    targetRevision: 15.0.0
    helm:
      releaseName: my-nginx
      values: |
        replicaCount: 3
        service:
          type: ClusterIP
      parameters:
        - name: image.tag
          value: "1.25"
      valueFiles:
        - values-prod.yaml

Kustomize 소스도 마찬가지로 세부 설정을 오버라이드할 수 있습니다.

spec:
  source:
    repoURL: https://github.com/myorg/myrepo.git
    path: k8s/overlays/prod
    kustomize:
      namePrefix: prod-
      nameSuffix: -v1
      images:
        - name: my-app
          newTag: v1.2.3
      commonLabels:
        environment: production

Sync 상태와 Health 상태

ArgoCD는 두 가지 축으로 상태를 추적합니다. Sync Status는 Git과 클러스터의 일치 여부이고, Health Status는 배포된 리소스의 실제 동작 상태입니다.

Sync Status 의미
Synced Git과 클러스터 상태 일치
OutOfSync 차이 존재 (Git 변경 또는 수동 수정)
Unknown 상태 확인 불가
Health Status 의미 예시
Healthy 정상 동작 중 Deployment replicas 충족
Progressing 진행 중 롤아웃 중
Degraded 문제 발생 Pod CrashLoopBackOff
Suspended 일시 중지 HPA 일시 중지
Missing 리소스 없음 아직 생성 안 됨

Sync 전략

Manual Sync는 명시적 트리거가 필요하고, Automated Sync는 Git 변경 시 자동으로 동기화합니다.

# Manual Sync: 명시적 트리거 필요
spec:
  syncPolicy: {}  # 또는 생략

# Automated Sync: Git 변경 시 자동 동기화
spec:
  syncPolicy:
    automated:
      prune: true       # Git에서 삭제된 리소스 정리
      selfHeal: true    # Drift 발생 시 자동 복구
      allowEmpty: false # 빈 매니페스트 허용 여부

주의: prune: true는 Git에서 리소스 정의를 삭제하면 클러스터에서도 즉시 삭제합니다. 실수로 파일을 지우면 프로덕션 리소스가 사라질 수 있습니다.

Self-Heal의 동작은 다음과 같습니다.

sequenceDiagram
    participant Ops as 운영자
    participant K8s as Kubernetes
    participant ArgoCD as ArgoCD Controller
    participant Git as Git Repository

    Ops->>K8s: kubectl scale deployment --replicas=5
    Note over K8s: 수동 변경 (Drift)

    ArgoCD->>K8s: 현재 상태 확인 (replicas: 5)
    ArgoCD->>Git: Git 상태 확인 (replicas: 3)
    ArgoCD->>ArgoCD: Drift 감지!

    alt selfHeal: true
        ArgoCD->>K8s: kubectl apply (replicas: 3)
        Note over K8s: Git 상태로 복구
    else selfHeal: false
        Note over ArgoCD: OutOfSync 상태 유지
        Note over K8s: 변경 유지
    end

세밀한 동기화 제어에는 Sync Options를 사용합니다.

spec:
  syncPolicy:
    syncOptions:
      - CreateNamespace=true       # 네임스페이스 자동 생성
      - PrunePropagationPolicy=foreground  # 삭제 전파 정책
      - PruneLast=true             # 다른 리소스 먼저 적용 후 Prune
      - Replace=true               # apply 대신 replace 사용
      - ServerSideApply=true       # Server-Side Apply 사용
      - FailOnSharedResource=true  # 다른 앱과 리소스 공유 금지
      - RespectIgnoreDifferences=true  # ignoreDifferences 존중

Sync Waves와 Hooks

복잡한 배포 시나리오에서 리소스 적용 순서를 제어할 때 사용합니다. Sync Waves는 argocd.argoproj.io/sync-wave 어노테이션으로 순서를 지정합니다.

# Wave 0: 먼저 실행 (기본값)
apiVersion: v1
kind: Namespace
metadata:
  name: my-app
  annotations:
    argocd.argoproj.io/sync-wave: "0"

---
# Wave 1: Namespace 생성 후 실행
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  annotations:
    argocd.argoproj.io/sync-wave: "1"

---
# Wave 2: ConfigMap 이후 실행
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  annotations:
    argocd.argoproj.io/sync-wave: "2"

Sync Hooks는 특정 시점에 Job을 실행합니다.

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
  annotations:
    argocd.argoproj.io/hook: PreSync           # 언제 실행?
    argocd.argoproj.io/hook-delete-policy: HookSucceeded  # 언제 삭제?
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: my-app:latest
        command: ["./migrate.sh"]
      restartPolicy: Never
Hook Phase 시점 사용 사례
PreSync Sync 이전 DB 마이그레이션, 백업
Sync 일반 리소스와 함께 특수 처리 필요 리소스
PostSync Sync 완료 후 알림 전송, 테스트 실행
SyncFail Sync 실패 시 롤백, 알림
Delete Policy 설명
HookSucceeded 성공 시 삭제
HookFailed 실패 시 삭제
BeforeHookCreation 다음 Hook 생성 전 삭제

두 기능을 조합하면 Blue-Green 배포 같은 시나리오를 선언적으로 구성할 수 있습니다.

# PreSync: 새 버전 배포 전 준비
---
apiVersion: batch/v1
kind: Job
metadata:
  name: pre-deploy-check
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/sync-wave: "-1"
spec:
  template:
    spec:
      containers:
      - name: check
        image: curlimages/curl
        command: ["curl", "-f", "http://health-check-endpoint"]
      restartPolicy: Never

---
# Sync: 새 버전 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-green
  annotations:
    argocd.argoproj.io/sync-wave: "0"
spec:
  replicas: 3
  # ...

---
# PostSync: 트래픽 전환
apiVersion: batch/v1
kind: Job
metadata:
  name: switch-traffic
  annotations:
    argocd.argoproj.io/hook: PostSync
    argocd.argoproj.io/sync-wave: "1"
spec:
  template:
    spec:
      containers:
      - name: switch
        image: my-tool:latest
        command: ["./switch-service.sh", "my-app-green"]
      restartPolicy: Never

ApplicationSet: 멀티 클러스터의 답

ApplicationSet은 단일 템플릿에서 여러 Application을 자동 생성합니다. 멀티 클러스터, 멀티 테넌트 환경에서 필수입니다.

flowchart TB
    subgraph Generators [ApplicationSet Generators]
        List[List Generator]
        Cluster[Cluster Generator]
        Git[Git Generator]
        Matrix[Matrix Generator]
        Merge[Merge Generator]
    end

    List --> |정적 목록| Apps1[Applications]
    Cluster --> |등록된 클러스터| Apps2[Applications]
    Git --> |디렉토리/파일 구조| Apps3[Applications]
    Matrix --> |Generator 조합| Apps4[Applications]

List Generator는 정적 목록에서 Application을 만듭니다.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-apps
  namespace: argocd
spec:
  goTemplate: true
  generators:
  - list:
      elements:
      - cluster: dev
        url: https://dev.k8s.example.com
        namespace: dev
      - cluster: staging
        url: https://staging.k8s.example.com
        namespace: staging
      - cluster: prod
        url: https://prod.k8s.example.com
        namespace: prod
  template:
    metadata:
      name: 'my-app-{{.cluster}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/myrepo.git
        targetRevision: HEAD
        path: 'k8s/overlays/{{.cluster}}'
      destination:
        server: '{{.url}}'
        namespace: '{{.namespace}}'

Cluster Generator는 ArgoCD에 등록된 클러스터를 기준으로 자동 생성합니다.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: cluster-addons
  namespace: argocd
spec:
  goTemplate: true
  generators:
  - clusters:
      selector:
        matchLabels:
          environment: production
  template:
    metadata:
      name: '{{.name}}-monitoring'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/cluster-addons.git
        path: monitoring
      destination:
        server: '{{.server}}'
        namespace: monitoring
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

Git Directory Generator는 Git 디렉토리 구조 자체를 소스로 삼습니다.

apps/
├── frontend/
│   └── kustomization.yaml
├── backend/
│   └── kustomization.yaml
└── database/
    └── kustomization.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: apps
  namespace: argocd
spec:
  goTemplate: true
  generators:
  - git:
      repoURL: https://github.com/myorg/gitops.git
      revision: HEAD
      directories:
      - path: apps/*
  template:
    metadata:
      name: '{{.path.basename}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/gitops.git
        path: '{{.path.path}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{.path.basename}}'

팁: Git Directory Generator를 사용하면 디렉토리만 추가해도 자동으로 Application이 생성됩니다. 코드 변경 없이 새 앱을 추가할 수 있습니다.

AppProject와 RBAC

AppProject는 Application을 그룹화하고 접근 제어를 적용합니다.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production
  namespace: argocd
spec:
  description: Production applications

  # 허용된 소스 저장소
  sourceRepos:
  - https://github.com/myorg/*

  # 허용된 목적지
  destinations:
  - namespace: 'prod-*'
    server: https://prod.k8s.example.com

  # 허용된 클러스터 리소스
  clusterResourceWhitelist:
  - group: ''
    kind: Namespace

  # 거부된 네임스페이스 리소스
  namespaceResourceBlacklist:
  - group: ''
    kind: ResourceQuota

  # RBAC 역할
  roles:
  - name: developer
    description: Developer access
    policies:
    - p, proj:production:developer, applications, get, production/*, allow
    - p, proj:production:developer, applications, sync, production/*, allow
    groups:
    - developers

RBAC 정책은 CSV 형식으로 세밀하게 정의합니다.

# p, <role>, <resource>, <action>, <object>, <effect>

# 개발자: 읽기 + sync만 가능
p, role:developer, applications, get, */*, allow
p, role:developer, applications, sync, */*, allow

# 운영자: 모든 권한
p, role:operator, applications, *, */*, allow
p, role:operator, clusters, *, *, allow

# 특정 프로젝트만 접근
p, role:team-a, applications, *, team-a/*, allow

자주 만나는 문제

OutOfSync가 해소되지 않을 때는 HPA처럼 다른 컨트롤러가 관리하는 필드를 의심해야 합니다. ignoreDifferences로 해당 필드를 비교 대상에서 제외합니다.

spec:
  ignoreDifferences:
  - group: apps
    kind: Deployment
    jsonPointers:
    - /spec/replicas  # HPA가 관리하는 필드 무시
  - group: ""
    kind: Service
    jqPathExpressions:
    - .spec.clusterIP  # 자동 할당 필드 무시

Sync 실패 시에는 상태와 이벤트를 확인하고 필요하면 강제 재동기화합니다.

# 동기화 상태 확인
argocd app get my-app

# 이벤트 확인
kubectl describe application my-app -n argocd

# 강제 재동기화
argocd app sync my-app --force

Application 삭제 시 리소스가 남는다면 resources-finalizer.argocd.argoproj.io finalizer가 설정되어 있는지 확인합니다. 리소스를 남긴 채 Application만 지우려면 argocd app delete my-app --cascade=false를 사용합니다.

Flux 심화: GitOps Toolkit

Flux는 Kubernetes를 위한 GitOps 도구 집합입니다. 2019년 Weaveworks에서 시작해 2022년 CNCF Graduated 프로젝트가 되었습니다. ArgoCD가 단일 애플리케이션이라면, Flux는 GitOps Toolkit이라는 독립적인 컨트롤러들의 집합입니다.

  • 필요한 컨트롤러만 선택적으로 설치할 수 있는 모듈러 아키텍처
  • 모든 설정이 CRD로 관리되는 Kubernetes Native 접근
  • 컨테이너 이미지 자동 업데이트(Image Automation) 내장
  • 네임스페이스 기반의 Multi-tenancy 지원

참고: Weaveworks가 2024년 폐업했지만, Flux는 CNCF Graduated 프로젝트로서 커뮤니티와 CNCF의 지원 아래 활발히 개발되고 있습니다.

두 도구의 설계 철학 차이는 아키텍처 그림에서 바로 드러납니다.

flowchart TB
    subgraph ArgoCD [ArgoCD - 모놀리식]
        A_ALL[단일 애플리케이션\nAPI Server + Repo Server + Controller]
        A_UI[강력한 Web UI]
        A_APP[Application CRD]
    end

    subgraph Flux [Flux - 마이크로서비스]
        F_SOURCE[Source Controller]
        F_KUSTOMIZE[Kustomize Controller]
        F_HELM[Helm Controller]
        F_NOTIFY[Notification Controller]
        F_IMAGE[Image Automation]
    end

    A_ALL --> A_UI
    A_ALL --> A_APP

    F_SOURCE --> F_KUSTOMIZE
    F_SOURCE --> F_HELM
    F_KUSTOMIZE --> F_NOTIFY
    F_HELM --> F_NOTIFY

GitOps Toolkit 컨트롤러 구성

flowchart TB
    subgraph Sources [Source Controllers]
        Git[GitRepository]
        Helm[HelmRepository]
        OCI[OCIRepository]
        Bucket[Bucket]
    end

    subgraph Reconcilers [Reconciler Controllers]
        Kustomize[Kustomization]
        HelmRelease[HelmRelease]
    end

    subgraph Automation [Automation Controllers]
        ImageRepo[ImageRepository]
        ImagePolicy[ImagePolicy]
        ImageUpdate[ImageUpdateAutomation]
    end

    subgraph Notifications [Notification Controller]
        Alert[Alert]
        Provider[Provider]
        Receiver[Receiver]
    end

    Git --> Kustomize
    Git --> HelmRelease
    Helm --> HelmRelease
    OCI --> Kustomize
    OCI --> HelmRelease

    ImageRepo --> ImagePolicy
    ImagePolicy --> ImageUpdate
    ImageUpdate --> Git

    Kustomize --> Alert
    HelmRelease --> Alert
    Alert --> Provider
컨트롤러 역할 CRD
Source Controller Git, Helm, OCI에서 아티팩트 가져오기 GitRepository, HelmRepository, OCIRepository, Bucket
Kustomize Controller Kustomize 매니페스트 적용 Kustomization
Helm Controller Helm 릴리스 관리 HelmRelease
Notification Controller 이벤트 알림 Alert, Provider, Receiver
Image Automation 컨테이너 이미지 업데이트 자동화 ImageRepository, ImagePolicy, ImageUpdateAutomation

Source Controller

외부 소스에서 아티팩트를 가져와 클러스터 내에서 사용 가능하게 만듭니다. 가장 기본이 되는 GitRepository부터 봅니다.

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: my-app
  namespace: flux-system
spec:
  # 동기화 주기
  interval: 1m

  # Git 저장소 URL
  url: https://github.com/myorg/my-app.git

  # 브랜치/태그/커밋
  ref:
    branch: main
    # 또는
    # tag: v1.0.0
    # commit: abc123

  # 인증 (필요시)
  secretRef:
    name: git-credentials

  # 특정 경로만 가져오기
  ignore: |
    # 전체 제외
    /*
    # 특정 디렉토리만 포함
    !/deploy/

인증은 Secret으로 연결합니다. HTTPS는 사용자명과 토큰, SSH는 개인키와 known_hosts를 담습니다.

# HTTPS 인증
apiVersion: v1
kind: Secret
metadata:
  name: git-credentials
  namespace: flux-system
type: Opaque
stringData:
  username: git
  password: <GitHub PAT 등 액세스 토큰>

HelmRepository는 Helm Chart Repository를, OCIRepository는 OCI Registry의 아티팩트를 소스로 씁니다.

apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
  name: bitnami
  namespace: flux-system
spec:
  interval: 1h
  url: https://charts.bitnami.com/bitnami

  # OCI Registry도 지원
  # type: oci
  # url: oci://ghcr.io/myorg/charts
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 5m
  url: oci://ghcr.io/stefanprodan/manifests/podinfo
  ref:
    tag: latest

팁: OCI 아티팩트는 컨테이너 레지스트리에 Kubernetes 매니페스트를 저장하는 방식입니다. Git 없이도 GitOps가 가능해집니다.

Kustomize Controller

GitRepository에서 가져온 매니페스트를 클러스터에 적용합니다. ArgoCD의 Application에 대응하는 것이 Kustomization CRD입니다.

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: my-app
  namespace: flux-system
spec:
  # 동기화 주기
  interval: 10m

  # 재시도 주기 (실패 시)
  retryInterval: 2m

  # 소스 참조
  sourceRef:
    kind: GitRepository
    name: my-app

  # 매니페스트 경로
  path: ./deploy/production

  # 타겟 네임스페이스 (모든 리소스에 적용)
  targetNamespace: production

  # Git에서 삭제된 리소스 정리
  prune: true

  # 타임아웃
  timeout: 5m

  # Health Check 대기
  wait: true
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: my-app
      namespace: production

Flux의 강점 중 하나는 Kustomization 간 의존성 관리입니다. ArgoCD의 Sync Waves가 리소스 단위 순서 제어라면, Flux의 dependsOn은 배포 단위 간 의존성을 선언합니다.

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: infrastructure
  namespace: flux-system
spec:
  interval: 10m
  sourceRef:
    kind: GitRepository
    name: infra
  path: ./infrastructure
  prune: true

---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 10m
  # infrastructure가 Ready 상태가 된 후에만 적용
  dependsOn:
    - name: infrastructure
  sourceRef:
    kind: GitRepository
    name: apps
  path: ./apps
  prune: true

Kustomize 빌드 후 변수를 치환하는 Post-build Substitution도 지원합니다.

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: my-app
  namespace: flux-system
spec:
  # ...
  postBuild:
    substitute:
      ENVIRONMENT: production
      REPLICAS: "3"
    substituteFrom:
      - kind: ConfigMap
        name: cluster-config
      - kind: Secret
        name: cluster-secrets
# deployment.yaml (치환 대상)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: ${REPLICAS}
  template:
    spec:
      containers:
      - name: app
        env:
        - name: ENVIRONMENT
          value: ${ENVIRONMENT}

Helm Controller

Helm 릴리스를 선언적으로 관리합니다. 업그레이드 실패 시 재시도와 롤백 정책까지 CRD로 선언할 수 있습니다.

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: nginx
  namespace: default
spec:
  # 동기화 주기
  interval: 10m

  # Helm Chart 소스
  chart:
    spec:
      chart: nginx
      version: "15.x"  # SemVer 범위 지원
      sourceRef:
        kind: HelmRepository
        name: bitnami
        namespace: flux-system

  # Values 설정
  values:
    replicaCount: 3
    service:
      type: ClusterIP

  # Values 파일 참조
  valuesFrom:
    - kind: ConfigMap
      name: nginx-values
      valuesKey: values.yaml
    - kind: Secret
      name: nginx-secrets
      valuesKey: credentials

  # Upgrade 설정
  upgrade:
    remediation:
      retries: 3
      remediateLastFailure: true

  # Rollback 설정
  rollback:
    recreate: true
    cleanupOnFail: true

HelmRepository 대신 GitRepository에서 직접 차트를 가져올 수도 있습니다. chart.spec.chart에 Git 내 경로(예: ./charts/my-app)를 지정하고 sourceRef를 GitRepository로 연결하면 됩니다.

Image Automation: Flux의 차별점

Flux는 컨테이너 이미지 버전 자동 업데이트를 핵심 기능으로 내장합니다. 레지스트리 스캔부터 Git 커밋까지 전체가 자동화됩니다.

sequenceDiagram
    participant Registry as Container Registry
    participant Image as ImageRepository
    participant Policy as ImagePolicy
    participant Update as ImageUpdateAutomation
    participant Git as Git Repository
    participant Flux as Flux Controllers
    participant K8s as Kubernetes

    Image->>Registry: 새 이미지 태그 스캔
    Registry-->>Image: v1.2.3 발견
    Image->>Policy: 정책과 매칭
    Policy->>Policy: SemVer 필터링
    Policy->>Update: 업데이트 트리거
    Update->>Git: manifest 수정 & commit
    Git->>Flux: 변경 감지
    Flux->>K8s: 배포

ImageRepository가 레지스트리에서 태그를 스캔하고, ImagePolicy가 어떤 태그를 선택할지 결정합니다.

apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
  name: my-app
  namespace: flux-system
spec:
  # 스캔할 이미지
  image: ghcr.io/myorg/my-app

  # 스캔 주기
  interval: 5m

  # 인증 (필요시)
  secretRef:
    name: ghcr-auth
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: my-app
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: my-app

  # 정책: SemVer 범위
  policy:
    semver:
      range: ">=1.0.0"

  # 또는: 알파벳순 최신
  # policy:
  #   alphabetical:
  #     order: asc

  # 또는: 숫자순 최신
  # policy:
  #   numerical:
  #     order: asc

ImageUpdateAutomation이 Git 저장소의 매니페스트를 실제로 수정하고 커밋합니다.

apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: my-app
  namespace: flux-system
spec:
  interval: 5m

  # 업데이트할 Git 저장소
  sourceRef:
    kind: GitRepository
    name: my-app

  # Git 설정
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        name: Flux
        email: flux@myorg.com
      messageTemplate: |
        Update images

        {{range .Changed.Changes -}}
        - {{.OldValue}} -> {{.NewValue}}
        {{end}}
    push:
      branch: main

  # 업데이트 대상 파일
  update:
    path: ./deploy
    strategy: Setters  # 마커 기반 업데이트

업데이트 위치는 매니페스트에 마커 주석으로 표시합니다.

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: ghcr.io/myorg/my-app:v1.0.0  # {"$imagepolicy": "flux-system:my-app"}

중요: ArgoCD는 Image Updater가 별도 프로젝트인 반면, Flux는 Image Automation이 핵심 기능으로 내장되어 있습니다.

Notification Controller

배포 이벤트를 외부 시스템으로 알립니다. Alert가 어떤 이벤트를 보낼지, Provider가 어디로 보낼지 정의합니다.

apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Alert
metadata:
  name: on-call-alerts
  namespace: flux-system
spec:
  # 심각도 필터
  eventSeverity: error

  # 모니터링 대상
  eventSources:
    - kind: Kustomization
      name: '*'
    - kind: HelmRelease
      name: '*'

  # 알림 대상
  providerRef:
    name: slack
apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Provider
metadata:
  name: slack
  namespace: flux-system
spec:
  type: slack
  channel: devops-alerts
  secretRef:
    name: slack-webhook

Slack, Discord, Microsoft Teams, GitHub, GitLab, PagerDuty, Datadog, Sentry 등 다양한 Provider를 지원합니다. 반대로 외부에서 Flux로 Webhook을 받는 Receiver도 있어, GitHub push 이벤트로 즉시 reconcile을 트리거할 수 있습니다.

Bootstrap: Flux가 자기 자신을 관리한다

Flux 설치의 권장 방법은 Bootstrap입니다. Flux 컴포넌트 자체를 Git에 커밋하고, Flux가 자기 자신을 GitOps로 관리하게 만듭니다.

# GitHub 저장소로 Bootstrap
flux bootstrap github \
  --owner=myorg \
  --repository=fleet-infra \
  --branch=main \
  --path=clusters/production \
  --personal
# 생성되는 구조
fleet-infra/
└── clusters/
    └── production/
        └── flux-system/
            ├── gotk-components.yaml  # Flux 컴포넌트
            ├── gotk-sync.yaml        # 자기 자신을 관리하는 Kustomization
            └── kustomization.yaml

Multi-tenancy 패턴

Flux는 네임스페이스 기반 격리를 지원합니다. 팀별 네임스페이스에 GitRepository와 Kustomization을 두고, 제한된 권한의 ServiceAccount로 reconcile을 수행하게 합니다.

# 팀 A 전용 소스와 Kustomization
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: team-a-apps
  namespace: team-a  # 팀 네임스페이스
spec:
  url: https://github.com/myorg/team-a-apps.git
  # ...

---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: team-a-apps
  namespace: team-a
spec:
  # 해당 팀 네임스페이스로 제한
  targetNamespace: team-a
  serviceAccountName: team-a-reconciler  # 제한된 권한의 SA
  # ...

실무 도입 시 고려사항

도구 선택과 별개로 저장소 구조와 Secrets 전략을 먼저 정해야 합니다.

레포지토리는 Monorepo와 Polyrepo 중에서 선택합니다.

# Monorepo 방식
gitops-repo/
├── apps/
│   ├── frontend/
│   ├── backend/
│   └── database/
├── infrastructure/
│   ├── monitoring/
│   └── ingress/
└── clusters/
    ├── dev/
    ├── staging/
    └── prod/

# Polyrepo 방식
frontend-gitops/      # 프론트엔드 팀 관리
backend-gitops/       # 백엔드 팀 관리
platform-gitops/      # 플랫폼 팀 관리

환경별 설정은 Kustomize의 base/overlays 패턴이나 Helm values로 분리합니다.

base/
├── deployment.yaml
├── service.yaml
└── kustomization.yaml

overlays/
├── dev/
│   ├── kustomization.yaml
│   └── replica-patch.yaml
├── staging/
│   └── kustomization.yaml
└── prod/
    ├── kustomization.yaml
    └── resource-patch.yaml

Secrets는 Git에 평문으로 저장하면 안 됩니다. 다음 중 하나를 선택합니다.

  • Sealed Secrets: 암호화된 형태로 Git에 저장
  • External Secrets Operator: AWS Secrets Manager 등 외부 저장소 참조
  • SOPS: 파일 레벨 암호화 (Flux가 네이티브 지원)

ArgoCD vs Flux: 무엇을 선택할까

기능 ArgoCD Flux
CNCF 단계 Graduated Graduated
아키텍처 모놀리식 (단일 애플리케이션) 마이크로서비스 (독립 컨트롤러)
설치 helm install 한 줄 flux bootstrap
리소스 사용 약 400MB 메모리 약 100MB 메모리
Web UI 강력한 내장 UI Weave GitOps (별도)
멀티 클러스터 중앙 ArgoCD가 관리 (Hub-Spoke) 각 클러스터에 Flux 설치 (분산)
이미지 자동화 Image Updater (별도) 내장
Helm 지원 Application에서 처리 HelmRelease CRD
OCI 지원 지원 네이티브 지원
Health Check 내장 (강력) wait/healthChecks
SOPS 지원 제한적 네이티브
학습 곡선 낮음 (UI 친화적) 중간 (CRD 이해 필요)

ArgoCD가 맞는 경우는 다음과 같습니다.

  • 운영팀이 시각적 모니터링을 선호해 UI가 중요할 때
  • 한 곳에서 모든 클러스터를 관리하는 중앙 집중 구조가 필요할 때
  • Sync Waves, Hooks 같은 복잡한 Sync 전략이 필요할 때
  • 팀의 학습 곡선을 줄이고 싶을 때

Flux가 맞는 경우는 다음과 같습니다.

  • Edge 등 리소스 제한 환경에서 경량화가 중요할 때
  • 이미지 자동 업데이트가 핵심 요구사항일 때
  • SOPS로 Secrets를 관리할 때
  • 각 클러스터의 독립성이 중요할 때
  • CRD 중심의 Kubernetes Native 접근을 선호할 때

정리

개념 설명
GitOps Git을 Single Source of Truth로 삼는 운영 방식
4가지 원칙 Declarative, Versioned, Pulled, Reconciled
Pull 모델 클러스터 내부 에이전트가 Git을 감시, 인바운드 연결 제거
ArgoCD API Server + Repo Server + Application Controller, Application CRD와 ApplicationSet 중심
Flux GitOps Toolkit 컨트롤러 집합, Kustomization/HelmRelease CRD와 Image Automation 중심
선택 기준 UI와 중앙 관리는 ArgoCD, 경량화와 모듈화는 Flux

두 도구 모두 CNCF Graduated 프로젝트로 성숙도는 충분하므로, 조직의 운영 모델에 맞는 쪽을 고르면 됩니다. 하편에서는 Kustomize vs Helm 환경별 설정 관리, Secrets Management, CI/CD 파이프라인 통합을 다룹니다.

참고 자료