13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] GitOps

  • [Cloud] Crossplane GitOps로 멀티 클라우드 인프라 운영 전략

    [Cloud] Crossplane GitOps로 멀티 클라우드 인프라 운영 전략

    Crossplane GitOps로 멀티 클라우드 인프라 운영 전략 잡기

    Crossplane GitOps를 붙이면 처음엔 다 비슷한 착각을 하더라고요. Git에 YAML만 넣으면 멀티 클라우드 운영이 정리될 거라고요. 그런데 실제 운영에선 YAML 문법보다 인터페이스를 어디까지 추상화할지, 어느 계정과 어느 클러스터에 누가 적용 권한을 가질지, 실패했을 때 어느 계층에서 멈췄는지 어떻게 판별할지가 훨씬 더 중요합니다.

    특히 AWS, GCP, Azure가 섞여 있고 환경마다 네트워크, 계정, 규정이 다르면 더 그렇습니다. 이때 Crossplane GitOps의 진짜 가치는 단순히 인프라를 Kubernetes 안으로 가져오는 데 있지 않습니다. 제가 현업에서 더 크게 느낀 건 개발팀이 보는 요청 인터페이스와 플랫폼 팀이 유지하는 구현 세부를 분리할 수 있다는 점이었어요. 멀티 클라우드 운영이 어려운 이유는 클라우드가 여러 개라서가 아니라, 요청 인터페이스와 실행 책임이 뒤엉키기 때문이거든요.

    그래서 이 글은 단순 설치 가이드로 가지 않겠습니다. Crossplane GitOps를 언제 쓰면 이득이 커지고, 언제는 오히려 운영 복잡도만 늘어나는지, 그리고 실제 설계에서 먼저 봐야 할 기준을 중심으로 정리해보겠습니다.

    Crossplane GitOps 기반 멀티 클라우드 관리 아키텍처 다이어그램

    Git 저장소, Argo CD, Crossplane, 여러 클라우드 또는 클러스터가 어떻게 연결되는지 한눈에 보여주는 아키텍처 이미지 위치입니다.

    Crossplane GitOps가 멀티 클라우드에서 유리한 이유, 그리고 아닌 경우

    제가 이 조합을 높게 보는 이유는 하나입니다. 요청은 공통 API로 받고, 실행은 환경별 구현으로 분기할 수 있기 때문입니다. 개발팀은 비슷한 형식의 리소스를 요청하고, 플랫폼 팀은 뒤에서 AWS용 Composition을 태울지 GCP용 Composition을 태울지 고릅니다. 이 구조가 잡히면 클라우드 차이가 사용자 경험으로 바로 새지 않아요.

    • 적합한 경우: 개발팀 셀프서비스가 필요하고, 플랫폼 팀이 공통 API를 설계할 역량이 있을 때
    • 적합한 경우: 이미 Argo CD나 Flux 같은 GitOps 흐름이 있고, 인프라도 같은 승인 체계로 묶고 싶을 때
    • 보류할 경우: 팀마다 요구사항 차이가 커서 공통 인터페이스가 거의 안 나올 때
    • 보류할 경우: 단일 클라우드 비중이 높고 Terraform 워크스페이스 수준으로도 운영이 충분히 정리될 때

    여기서 핵심 판단 기준은 반복되는 요청이 실제로 존재하느냐입니다. Namespace, Object Storage, 기본 권한 연결처럼 반복 요청이 많고 정책을 통일해야 하는 자원은 Crossplane GitOps와 잘 맞습니다. 반대로 일회성 예외 구성, 팀별 편차가 큰 네트워크 토폴로지, 사람 승인 없이는 못 건드리는 보안 자원은 억지로 추상화하면 디버깅 비용만 올라갑니다.

    Crossplane GitOps 설계 전에 먼저 못 박아야 하는 결정

    Crossplane GitOps가 꼬이는 팀은 대개 YAML을 늦게 배워서가 아니라 결정 순서가 뒤집혀서 그렇습니다. 이걸 정하지 않고 XRD부터 만들면 나중에 스키마를 몇 번씩 뜯어고치게 됩니다. 이거 생각보다 꽤 아픕니다.

    의사결정 항목 선택지 이럴 때 권장 트레이드오프
    제어면 토폴로지 중앙 Crossplane 1개 플랫폼 팀이 강하고 공통 정책을 한곳에서 관리해야 할 때 장애 반경이 커지고 ProviderConfig 권한 경계 설계가 더 중요해집니다.
    제어면 토폴로지 환경별 Crossplane 분리 계정·망 분리가 엄격하고 운영 책임이 환경별로 나뉠 때 중복 배포와 업그레이드 비용이 늘지만 장애 격리가 쉽습니다.
    API 노출 방식 Namespaced XR Crossplane v2 신규 설계를 시작할 때 최신 권장 방식이지만 기존 Claim 중심 운영 모델과는 설계 습관이 달라집니다.
    API 노출 방식 Claim v1 스타일 API를 이미 쓰고 있거나 호환 운영이 필요할 때 Crossplane v2 신규 기본 모델은 아니므로 장기적으로는 XR 중심 전환을 검토해야 합니다.
    추상화 단위 얇은 API 셀프서비스를 빨리 열고 장애 분석 시간을 줄여야 할 때 리소스 수는 늘지만 실패 지점이 선명합니다.
    클라우드 분기 방식 Composition 분리 AWS/GCP/Azure 구현 차이가 큰 경우 구현은 길어지지만 디버깅과 변경 영향도 파악이 쉽습니다.
    자격 증명 모델 ProviderConfig 환경별 분리 계정, 프로젝트, 구독이 다르고 감사 경계가 중요할 때 Secret 회전과 참조 관계를 별도 운영해야 합니다.

    제가 실제로는 이렇게 봅니다. 멀티 클라우드라고 해서 처음부터 공통 API를 넓게 잡지 마세요. 공통분모가 작으면 추상화도 작아야 합니다. Namespace, Bucket, 기본 IAM 연동처럼 공통성이 높은 것부터 묶고, 네트워크처럼 클라우드별 의미가 달라지는 영역은 늦게 가져오는 편이 훨씬 덜 아픕니다.

    실전 구현 1: 저장소 분리와 기본 설치는 이렇게 가져가면 덜 꼬입니다

    실무에서는 저장소 구조가 운영 모델을 거의 결정합니다. 저는 보통 platform, providers, claims-or-xrs를 분리합니다. 이유는 단순합니다. 변경 주기와 승인 권한이 다르기 때문입니다. 플랫폼 팀이 XRD와 Composition을 바꾸는 행위와, 서비스 팀이 요청 리소스 하나 추가하는 행위를 같은 PR 정책으로 묶으면 금방 병목이 생깁니다.

    1. platform: XRD, Composition, Function
    2. providers: Provider, ProviderConfig, Secret 참조 정책
    3. claims-or-xrs: 팀별 요청 리소스

    최신 Crossplane v2 기준으로는 Namespaced XR이 기본이고, Claim은 신규 기본 모델이 아닙니다. 다만 기존 v1 스타일 API를 유지하는 환경이라면 Claim 패턴을 계속 쓸 수는 있습니다. 그래서 신규 도입 팀이라면 XR 중심으로 시작하고, 기존 팀이라면 Claim을 호환 운영 대상으로 보는 편이 더 안전합니다.

    Crossplane 자체 설치는 현재 문서 기준으로 Helm chart가 가장 깔끔합니다. 그리고 Patch & Transform을 쓸 거라면 Composition Function도 같이 올려두는 편이 좋습니다. 나중에 예제를 확장할 때 진짜 편하더라고요.

    helm repo add crossplane-stable https://charts.crossplane.io/stable
    helm repo update
    
    helm install crossplane \
      --namespace crossplane-system \
      --create-namespace \
      crossplane-stable/crossplane
    
    kubectl get pods -n crossplane-system
    
    kubectl apply -f - <<'EOF'
    apiVersion: pkg.crossplane.io/v1
    kind: Function
    metadata:
      name: function-patch-and-transform
    spec:
      package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2
    EOF
    
    kubectl get functions

    GitOps 도구는 Crossplane를 애플리케이션 하나로 보지 않는 편이 좋습니다. 제가 권하는 최소 단위는 core, platform, workloads 세 층입니다. 이유는 순서와 장애 반경이 다르기 때문입니다. XRD가 아직 준비되지 않았는데 XR이나 Claim이 먼저 들어오면, YAML은 정상인데 동기화가 실패하는 황당한 상황이 바로 나옵니다.

    Argo CD를 쓰면 sync wave 또는 앱 분리로 순서를 고정하는 편이 안전합니다. 또 Crossplane 공식 가이드처럼 annotation 기반 리소스 추적을 쓰고, ProviderConfigUsage는 UI에서 제외하는 구성이 운영 노이즈를 줄이는 데 도움이 됩니다.

    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: crossplane-platform
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://git.example.com/platform/infra.git
        targetRevision: main
        path: crossplane/platform
      destination:
        server: https://kubernetes.default.svc
        namespace: crossplane-system
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
          - ApplyOutOfSyncOnly=true
          - SkipDryRunOnMissingResource=true

    여기서 포인트는 세 가지입니다. CreateNamespace=true는 기본 편의고, SkipDryRunOnMissingResource=true는 CRD가 먼저 생기는 구조에서 불필요한 실패를 줄여줍니다. 그리고 앱이 커지기 시작하면 ApplyOutOfSyncOnly=true를 고려할 만합니다. Claim이나 XR 수가 많아질수록 매 sync마다 전체 리소스를 다시 건드리는 비용이 꽤 느껴지거든요.

    Crossplane GitOps 저장소 구조와 Argo CD 동기화 흐름 이미지

    플랫폼 정의, Provider 설정, Claim 또는 XR 배포가 어떤 순서로 Git에서 흘러가는지 보여주는 구성도 위치입니다.

    실전 구현 2: XR 하나로 클라우드별 구현 갈라타기

    최신 Crossplane v2 신규 설계라면 예시는 Claim보다 Namespaced XR로 잡는 편이 맞습니다. 여러 클러스터에 걸쳐 Namespace를 만드는 패턴은 작지만 실무적인 예제예요. 추상화가 맞는지 틀린지는 거대한 데이터베이스보다 이런 작은 자원에서 먼저 드러나는 경우가 많습니다.

    먼저 provider-kubernetes와 ProviderConfig를 준비합니다. 서로 다른 대상 클러스터에 apply하는 흐름이므로 kubeconfig Secret 경계를 분명히 두는 게 핵심입니다. 운영에서 제일 흔한 사고는 권한이 부족해서 실패하는 경우보다, 잘못된 대상 클러스터에 정상 적용되는 경우입니다. 그래서 ProviderConfig 이름에 환경과 대상 의미를 같이 넣는 걸 권합니다.

    apiVersion: pkg.crossplane.io/v1
    kind: Provider
    metadata:
      name: provider-kubernetes
    spec:
      package: xpkg.crossplane.io/crossplane-contrib/provider-kubernetes:v1.3.0
    ---
    apiVersion: kubernetes.m.crossplane.io/v1alpha1
    kind: ProviderConfig
    metadata:
      name: aws-dev-cluster
      namespace: crossplane-system
    spec:
      credentials:
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: aws-dev-cluster-kubeconfig
          key: kubeconfig
    ---
    apiVersion: kubernetes.m.crossplane.io/v1alpha1
    kind: ProviderConfig
    metadata:
      name: gcp-prod-cluster
      namespace: crossplane-system
    spec:
      credentials:
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: gcp-prod-cluster-kubeconfig
          key: kubeconfig

    그다음 XRD와 Composition입니다. 아래 예시는 사용자에게는 동일한 PlatformNamespace만 보이고, 실제 구현은 AWS용과 GCP용 Composition으로 갈라집니다. 만약 기존 운영이 Claim 중심이라면 이 패턴을 v1 스타일 API와 함께 유지할 수는 있지만, 신규 도입이라면 XR로 시작하는 편이 덜 헷갈립니다.

    apiVersion: apiextensions.crossplane.io/v2
    kind: CompositeResourceDefinition
    metadata:
      name: platformnamespaces.platform.example.org
    spec:
      scope: Namespaced
      group: platform.example.org
      names:
        kind: PlatformNamespace
        plural: platformnamespaces
      versions:
      - name: v1alpha1
        served: true
        referenceable: true
        schema:
          openAPIV3Schema:
            type: object
            properties:
              spec:
                type: object
                properties:
                  parameters:
                    type: object
                    properties:
                      name:
                        type: string
                      provider:
                        type: string
                        enum:
                        - aws
                        - gcp
                    required:
                    - name
                    - provider
                required:
                - parameters
    ---
    apiVersion: apiextensions.crossplane.io/v1
    kind: Composition
    metadata:
      name: platformnamespace-aws
      labels:
        provider: aws
    spec:
      compositeTypeRef:
        apiVersion: platform.example.org/v1alpha1
        kind: PlatformNamespace
      mode: Pipeline
      pipeline:
      - step: patch-and-transform
        functionRef:
          name: function-patch-and-transform
        input:
          apiVersion: pt.fn.crossplane.io/v1beta1
          kind: Resources
          resources:
          - name: namespace
            base:
              apiVersion: kubernetes.m.crossplane.io/v1alpha1
              kind: Object
              metadata:
                namespace: crossplane-system
              spec:
                forProvider:
                  manifest:
                    apiVersion: v1
                    kind: Namespace
                    metadata:
                      name: placeholder
                providerConfigRef:
                  name: aws-dev-cluster
            patches:
            - type: FromCompositeFieldPath
              fromFieldPath: spec.parameters.name
              toFieldPath: spec.forProvider.manifest.metadata.name
    ---
    apiVersion: apiextensions.crossplane.io/v1
    kind: Composition
    metadata:
      name: platformnamespace-gcp
      labels:
        provider: gcp
    spec:
      compositeTypeRef:
        apiVersion: platform.example.org/v1alpha1
        kind: PlatformNamespace
      mode: Pipeline
      pipeline:
      - step: patch-and-transform
        functionRef:
          name: function-patch-and-transform
        input:
          apiVersion: pt.fn.crossplane.io/v1beta1
          kind: Resources
          resources:
          - name: namespace
            base:
              apiVersion: kubernetes.m.crossplane.io/v1alpha1
              kind: Object
              metadata:
                namespace: crossplane-system
              spec:
                forProvider:
                  manifest:
                    apiVersion: v1
                    kind: Namespace
                    metadata:
                      name: placeholder
                providerConfigRef:
                  name: gcp-prod-cluster
            patches:
            - type: FromCompositeFieldPath
              fromFieldPath: spec.parameters.name
              toFieldPath: spec.forProvider.manifest.metadata.name
    ---
    apiVersion: platform.example.org/v1alpha1
    kind: PlatformNamespace
    metadata:
      namespace: platform-team
      name: observability
    spec:
      crossplane:
        compositionSelector:
          matchLabels:
            provider: aws
      parameters:
        name: observability
        provider: aws

    여기서 제가 꼭 보는 판단 기준이 있습니다. 분기 기준을 사용자에게 직접 노출할지, 운영 정책으로 숨길지입니다. 팀이 아직 멀티 클라우드 배치 정책을 이해하고 선택할 단계가 아니면 provider 같은 필드를 API에 열어주지 않는 편이 낫습니다. 반대로 플랫폼 팀이 모든 배치 결정을 대신하면서 병목이 심하면, 제한된 선택지를 노출하는 게 더 현실적일 때도 있어요.

    Crossplane GitOps의 Composition 선택과 멀티 클라우드 배포 흐름

    AWS용 Composition과 GCP용 Composition이 같은 요청 인터페이스를 받아 서로 다른 대상 클러스터로 배포하는 흐름을 설명하는 이미지 위치입니다.

    실전에서 자주 터지는 문제와 근본 원인

    운영에서 힘든 지점은 YAML 자체가 아닙니다. 대부분의 장애는 Composition 선택, ProviderConfig 인증, 외부 API 응답, GitOps 적용 순서에서 생깁니다. 저도 초기에 이 구간에서 제일 오래 헤맸습니다.

    1. CRD와 요청 리소스를 같은 배포 단위로 넣었더니 가끔만 성공

    증상: 어떤 날은 되고 어떤 날은 안 됩니다. Argo CD에서는 리소스가 다 보이는데, 실제 sync에서는 XR이나 Claim이 먼저 들어가며 the server could not find the requested resource 비슷한 오류가 납니다.

    근본 원인: 선언은 동시에 넣었지만, 컨트롤 플레인 입장에서는 XRD 등록과 이를 참조하는 리소스 생성이 완전히 다른 단계이기 때문입니다.

    실무 처방: 앱을 분리하거나 sync wave를 명시하세요. 이 문제는 설치 순서라기보다 배포 계약 문제에 가깝습니다.

    2. ProviderConfig 인증 문제인데 요청 리소스만 들여다봄

    증상: XR은 생성됐는데 Ready가 끝까지 올라오지 않습니다.

    근본 원인: kubeconfig Secret 참조 오류, 잘못된 컨텍스트, 대상 클러스터 RBAC 부족, 만료된 토큰 중 하나인 경우가 많습니다.

    제가 보는 순서는 위에서 아래가 아니라 아래에서 위입니다. 요청 리소스에서 시작해 결국 Managed Resource와 Provider 로그까지 내려가야 원인이 보입니다.

    kubectl get platformnamespaces.platform.example.org -A
    kubectl get objects.kubernetes.m.crossplane.io -A
    kubectl describe object.kubernetes.m.crossplane.io <object-name> -n crossplane-system
    kubectl describe providerconfig.kubernetes.m.crossplane.io aws-dev-cluster -n crossplane-system
    kubectl -n crossplane-system logs -l pkg.crossplane.io/provider=provider-kubernetes --tail=200
    • XR는 존재하는데 Object가 없으면 Composition 선택이나 함수 입력을 먼저 봅니다.
    • Object는 있는데 Ready=False면 외부 클러스터 적용 실패 가능성이 큽니다.
    • cannot get credentials, secret not found류 메시지는 거의 ProviderConfig 또는 Secret 참조 문제입니다.
    • forbidden가 보이면 kubeconfig는 읽었지만 대상 클러스터 RBAC가 부족한 경우가 많습니다.

    3. 요청 API를 너무 두껍게 만들어서 장애 반경이 커짐

    이건 구조적 문제입니다. Namespace, RoleBinding, NetworkPolicy, ExternalSecret, StorageClass 참조까지 한 API에 다 넣으면 처음엔 멋있어 보여요. 그런데 하나만 실패해도 전체 요청 실패로 뭉개집니다. 멀티 클라우드에서는 실패 원인이 클라우드별로 달라서, 한 API는 한 책임 원칙을 거의 고정으로 가져가는 편이 훨씬 낫습니다.

    • 네임스페이스 생성은 네임스페이스 생성
    • 권한 바인딩은 별도 API
    • 스토리지는 스토리지
    • 데이터 서비스는 데이터 서비스

    4. Composition 변경이 기존 XR 전체에 바로 전파됨

    증상: 플랫폼 팀이 Composition을 수정했는데, 의도하지 않게 기존 요청들도 동작이 바뀝니다.

    근본 원인: Crossplane는 Composition 변경을 revision으로 추적하고, XR은 기본적으로 최신 revision을 자동 추종할 수 있기 때문입니다.

    실무 처방: 변경 위험이 큰 리소스는 compositionUpdatePolicy: Manual을 검토하세요. 데이터 서비스, 네트워크, 권한 모델처럼 기존 인스턴스가 한꺼번에 바뀌면 안 되는 자원은 자동 추종보다 수동 승격이 훨씬 안전합니다.

    apiVersion: platform.example.org/v1alpha1
    kind: PlatformNamespace
    metadata:
      namespace: platform-team
      name: observability
    spec:
      crossplane:
        compositionUpdatePolicy: Manual
        compositionRef:
          name: platformnamespace-aws
      parameters:
        name: observability
        provider: aws

    5. Git에서 지웠더니 실제 리소스 삭제까지 이어져 버림

    GitOps를 인프라에 붙일 때 가장 민감한 건 생성보다 삭제입니다. Namespace나 버킷처럼 파급효과가 큰 자원은 PR 하나의 merge가 곧바로 파괴 작업으로 이어지지 않게 설계해야 합니다.

    실무 처방: 삭제 위험이 큰 자원은 저장소를 분리하거나, PR 승인 정책을 강화하거나, Argo CD의 삭제 확인 옵션을 검토하세요. Crossplane GitOps는 생성 자동화보다 삭제 통제를 먼저 설계한 팀이 훨씬 안정적이었습니다.

    검증 포인트: 무엇을 봐야 진짜 성공인지

    배포가 끝났다고 끝이 아닙니다. 저는 항상 세 층을 분리해서 봅니다. Git 상태, Crossplane 상태, 실제 대상 상태입니다. 이 셋 중 하나라도 빠지면 동기화는 됐는데 서비스는 안 되는 애매한 상태가 생깁니다.

    1. GitOps 도구에서 애플리케이션 sync와 health가 정상인지 확인
    2. XR, Composition, Managed Resource가 모두 생성됐는지 확인
    3. 조건 값에서 Synced와 Ready를 구분해 읽기
    4. 대상 클러스터 또는 클라우드에 실제 자원이 생겼는지 직접 검증
    kubectl get platformnamespace observability -n platform-team -o yaml
    kubectl get platformnamespaces.platform.example.org -A -o custom-columns=NAME:.metadata.name,SYNCED:.status.conditions[?(@.type=="Synced")].status,READY:.status.conditions[?(@.type=="Ready")].status
    kubectl get objects.kubernetes.m.crossplane.io -A -o custom-columns=NAME:.metadata.name,SYNCED:.status.conditions[?(@.type=="Synced")].status,READY:.status.conditions[?(@.type=="Ready")].status
    kubectl --kubeconfig ./aws-dev-cluster.kubeconfig get ns observability

    Argo CD가 Healthy여도 외부 클러스터 리소스 생성까지 보장하지는 않습니다. 반대로 Crossplane Object가 Synced라고 해도 대상 클러스터에서 admission webhook이나 정책 엔진이 막고 있을 수 있습니다. 그래서 대시보드도 정상 개수보다 Ready=False 목록과 최근 이벤트 위주로 보는 편이 훨씬 실용적입니다.

    Crossplane GitOps 운영 검증 체크리스트와 선택 가이드 인포그래픽

    검증 순서, 상태값 판독 기준, 상황별 추천 선택을 정리한 요약 인포그래픽 위치입니다.

    운영 권장안: 이런 팀이면 이렇게 가는 편이 낫습니다

    • 클라우드가 1~2개이고 플랫폼 팀이 작다: API는 얇게 두고, ProviderConfig만 환경별로 분리하세요. 초반에는 추상화의 아름다움보다 장애 분석 속도가 더 중요합니다.
    • 플랫폼 팀과 개발팀이 분리돼 있다: XRD 스키마부터 먼저 고정하세요. 요청 저장소와 platform 저장소 권한을 분리하는 게 운영 비용을 줄입니다.
    • 클라우드마다 자원 모델 차이가 크다: 억지 공통분모를 만들지 말고 Composition을 클라우드별로 나누세요. 구현 중복보다 잘못된 추상화가 더 비쌉니다.
    • 변경 승인과 감사 추적이 중요하다: 사용자 요청 merge와 Composition 변경을 같은 승인 흐름에 두지 마세요. 위험도가 다릅니다.
    • 삭제 사고가 치명적이다: GitOps 자동화 범위에서 삭제를 별도로 취급하세요. 생성 자동화보다 삭제 통제가 먼저입니다.

    반대로 요청 패턴이 아직 안정되지 않았거나, 예외가 표준보다 많거나, 운영팀이 Kubernetes CRD 디버깅 경험이 부족하다면 조금 멈추는 게 맞습니다. Crossplane가 만능 해답은 아니거든요.

    제가 실제로는 보통 이렇게 시작합니다. Namespace → Object Storage → 기본 권한 연결 → 데이터 서비스 순서입니다. 공통성이 높은 자원부터 열고, 실패 비용이 큰 자원은 뒤로 미루는 편이 운영 충격이 적었습니다.

    자주 묻는 질문과 마지막 판단 기준

    Crossplane GitOps가 Terraform을 완전히 대체하나요?

    항상 그렇지는 않습니다. 다만 플랫폼 API를 Kubernetes 안에 두고, 서비스 팀 요청을 PR 기반으로 받으며, 멀티 클라우드 구현을 뒤로 숨겨야 한다면 Crossplane GitOps가 훨씬 자연스럽습니다. 반대로 공통 API보다 프로젝트별 세밀한 조정이 더 중요하면 Terraform이 더 단순할 때도 많습니다.

    멀티 클라우드 운영을 위해 꼭 복잡한 추상화가 필요한가요?

    아닙니다. 오히려 복잡한 추상화를 기본값으로 두지 않는 편이 낫습니다. 반복되는 요청이 명확할 때만 추상화하고, 반복되지 않으면 직결 구성이 더 낫습니다.

    처음 도입하는 팀이라면 어디까지를 1차 목표로 잡는 게 좋을까요?

    제가 추천하는 1차 목표는 간단합니다. 사용자가 같은 형식의 XR 또는 Claim을 제출하고, 플랫폼 팀이 ProviderConfig와 Composition으로 대상 환경을 통제할 수 있는 상태까지입니다. 거기서 이미 운영 가치가 나옵니다. 그다음에 Composition Revision, Secret 회전, 환경 승격, 정책 검증을 붙이는 순서가 훨씬 안정적입니다.

    관련 글도 함께 보시면 흐름이 더 잘 잡힙니다. Argo CD sync wave 설계 가이드, Kubernetes GitOps 운영 체크리스트도 이어서 확인해보세요.

    마지막 판단 기준을 한 줄로 줄이면 이겁니다. Crossplane GitOps는 인프라 자동화 도구라기보다 플랫폼 인터페이스 설계 도구에 가깝습니다. AWS와 GCP를 함께 다루는 팀이라면 API는 좁고 명확하게, Composition은 클라우드별로 분리해서 시작하세요. 반대로 단일 클라우드에 가깝거나 반복 요청이 적다면 굳이 복잡한 추상화를 도입하지 않는 편이 더 낫습니다.

  • [Cloud] ArgoCD로 Kubernetes에 Ollama 배포: 6개월 운영 후기 및 최적화 전략

    [Cloud] ArgoCD로 Kubernetes에 Ollama 배포: 6개월 운영 후기 및 최적화 전략

    목차

    [Cloud] ArgoCD로 Kubernetes에 Ollama 배포: 6개월 운영 후기 및 최적화 전략

    ArgoCD Ollama 배포를 처음 붙일 때만 해도, 솔직히 저는 “로컬에서 잘 돌던 걸 굳이 Kubernetes(쿠버네티스, 컨테이너 오케스트레이션 플랫폼)까지 올려야 하나?” 싶었습니다. 그런데 팀이나 홈랩에서 여러 워크로드를 같이 운영하다 보면 얘기가 달라지더라고요. 모델 파일은 크고, 노드는 자꾸 바뀌고, 누가 어떤 설정을 바꿨는지 추적도 필요합니다. 결국 ArgoCD(아르고CD, GitOps 배포 도구)와 Ollama(올라마, 로컬/서버 환경에서 LLM을 실행하는 런타임) 조합으로 정리해 두니 운영 피로도가 확 줄었더라고요.

    이번 글은 제가 홈랩과 테스트 클러스터에서 6개월 정도 굴려보면서 정리한 ArgoCD Ollama 배포 운영 후기입니다. 단순 설치 가이드가 아니라, 어디서 삽질했는지, 어떤 최적화가 체감이 컸는지, 그리고 Kubernetes GitOps 관점에서 무엇을 꼭 잡아야 하는지 중심으로 풀어보겠습니다. 비슷하게 LLM 클라우드나 사내 추론 환경을 작게 시작해 보려는 분들께 꽤 현실적인 기준점이 될 겁니다.

    ArgoCD가 Git 저장소의 선언형 설정을 읽어 Kubernetes 클러스터에 Ollama를 배포하고, 내부 서비스와 스토리지를 연결하는 전체 구조 예시입니다.

    왜 ArgoCD Ollama 배포가 생각보다 중요했는지

    쉽게 말해, Ollama 하나만 띄우는 건 어렵지 않습니다. 문제는 운영이거든요. 처음엔 Docker 하나로 시작했었습니다. 그때는 편했습니다. 근데 모델을 추가하고, 스토리지를 옮기고, 노드를 교체하고, Ingress(인그레스, 외부 트래픽 진입점) 뒤에 붙이고, 다시 재현하려고 하니 슬슬 꼬이기 시작했습니다.

    제가 직접 해보니 진짜 차이는 설치가 아니라 재현성(reproducibility, 같은 상태를 다시 만드는 능력)에서 나왔습니다. Git에 원하는 상태를 남기고, ArgoCD가 그 상태를 계속 맞춰 주니까 “어제는 됐는데 오늘은 왜 안 되지” 같은 상황이 확 줄더라고요. 특히 LLM 워크로드는 모델 파일과 디스크 사용량이 크기 때문에, 사람 손으로 운영하면 금방 흔들립니다.

    • 변경 이력 추적: 누가 리소스 제한을 바꿨는지 Git commit으로 남습니다.
    • 복구 속도: 노드가 바뀌어도 선언형 매니페스트로 다시 맞추기 쉽습니다.
    • 운영 표준화: dev, lab, prod 비슷한 구조로 가져가기 좋습니다.
    • 드리프트 방지: kubectl로 급하게 만진 설정이 오래 남지 않습니다.

    핵심 개념 정리: Kubernetes GitOps와 Ollama를 같이 볼 때

    여기서 중요한 포인트가 있습니다. Ollama는 애플리케이션이고, ArgoCD는 상태 관리자입니다. 이 둘의 역할을 섞어서 생각하면 금방 헷갈립니다. 저도 처음엔 ArgoCD가 모델까지 알아서 관리해 주는 느낌으로 생각했었는데, 실제로는 그렇지 않더라고요.

    1. ArgoCD는 원하는 상태를 맞추는 도구입니다

    Git 저장소에 있는 YAML이 기준입니다. Deployment(디플로이먼트, 파드 배포 정의), Service(서비스, 네트워크 노출), PersistentVolumeClaim(PVC, 영구 스토리지 요청) 같은 리소스를 계속 감시하고 맞춰 줍니다.

    2. Ollama는 모델 실행 환경입니다

    모델 파일을 저장하고, 요청을 받아 추론을 수행합니다. 그래서 CPU/GPU보다도 처음엔 디스크와 네트워크가 더 자주 병목이 되기도 합니다. 특히 모델을 여러 번 다시 내려받는 구조가 되면 운영이 굉장히 피곤해집니다.

    3. 모델 관리와 애플리케이션 관리를 분리해야 덜 꼬입니다

    제가 6개월 운영하면서 얻은 결론은 이겁니다. 애플리케이션 배포와 모델 프리로드(preload, 미리 받아두기)를 같은 단계로 억지로 묶지 않는 게 좋습니다. 처음엔 한 번에 끝내고 싶어서 initContainer(초기화 컨테이너) 쪽으로 몰아넣었는데, 재배포 때마다 모델 다운로드가 걸려서 시간도 오래 걸리고 실패 지점도 늘었습니다.

    구분 역할 운영 팁
    ArgoCD Git 기준 상태 동기화 자동 동기화와 self-heal은 켜되, 삭제 정책은 팀 기준에 맞춰 보수적으로
    Kubernetes 파드 실행, 스토리지, 네트워크 관리 리소스 요청값과 스토리지 클래스부터 먼저 정리
    Ollama LLM 실행과 모델 저장 모델 캐시 경로와 영구 볼륨 전략이 핵심
    GitOps 운영 변경 이력과 재현성 확보 핫픽스 후에는 반드시 Git 원복 반영

    실전 구현: 제가 쓰는 ArgoCD Ollama 배포 기본 구조

    이제 본론입니다. 아래 구조는 제가 홈랩에서 가장 무난하게 굴렸던 방식입니다. 아주 화려하진 않지만, 유지보수는 편했습니다. 처음엔 이게 뭔가 싶었는데, 결국 오래 가는 건 단순한 구조더라고요.

    디렉터리 구조

    platform/
      argocd/
        applications/
          ollama.yaml
      apps/
        ollama/
          namespace.yaml
          pvc.yaml
          deployment.yaml
          service.yaml
          kustomization.yaml

    1. Namespace와 PVC부터 고정합니다

    Ollama는 모델 파일이 남아야 의미가 있습니다. 그래서 저는 거의 항상 PVC를 먼저 잡습니다. 여기서 대충 가면 나중에 재배포할 때 모델을 다시 받느라 시간 다 씁니다.

    apiVersion: v1
    kind: Namespace
    metadata:
      name: ollama
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: ollama-data
      namespace: ollama
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 100Gi

    2. Deployment는 최대한 단순하게 갑니다

    실제로 써보니까 처음부터 옵션을 너무 많이 넣는 것보다, 먼저 떠야 합니다. 그리고 그다음에 리소스 제한과 노드 스케줄링을 붙이는 쪽이 덜 위험했습니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: ollama
      namespace: ollama
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: ollama
      template:
        metadata:
          labels:
            app: ollama
        spec:
          containers:
            - name: ollama
              image: ollama/ollama:latest
              ports:
                - containerPort: 11434
              volumeMounts:
                - name: ollama-data
                  mountPath: /root/.ollama
              resources:
                requests:
                  cpu: "2"
                  memory: "8Gi"
                limits:
                  cpu: "4"
                  memory: "16Gi"
          volumes:
            - name: ollama-data
              persistentVolumeClaim:
                claimName: ollama-data
    apiVersion: v1
    kind: Service
    metadata:
      name: ollama
      namespace: ollama
    spec:
      selector:
        app: ollama
      ports:
        - name: http
          port: 11434
          targetPort: 11434

    3. Kustomize와 ArgoCD Application으로 묶습니다

    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    namespace: ollama
    resources:
      - namespace.yaml
      - pvc.yaml
      - deployment.yaml
      - service.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: ollama
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://git.example.com/platform.git
        targetRevision: main
        path: apps/ollama
      destination:
        server: https://kubernetes.default.svc
        namespace: ollama
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
    1. Git 저장소에 Ollama 매니페스트를 커밋합니다.
    2. ArgoCD에 Application을 등록합니다.
    3. 첫 Sync 후 파드와 PVC가 정상 생성되는지 확인합니다.
    4. 그다음 모델 다운로드 전략을 별도로 붙입니다.
    ArgoCD Ollama 배포 설정과 동기화 구성을 표현한 이미지

    ArgoCD에서 애플리케이션이 Synced 상태로 보이고, Ollama Deployment와 PVC가 함께 연결된 구성을 확인하는 장면을 설명하는 이미지 자리입니다.

    4. 초기 검증 명령어

    kubectl get pods -n ollama
    kubectl get pvc -n ollama
    kubectl get svc -n ollama
    kubectl logs -n ollama deploy/ollama

    여기까지 오면 기본 배포는 끝입니다. 드디어 됐다! 싶죠. 근데 진짜 운영은 지금부터입니다 ㅎㅎ

    6개월 운영하면서 효과 컸던 최적화 전략

    이 섹션이 사실 핵심입니다. 단순 배포보다 중요한 건 계속 안정적으로 돌리는 법이거든요. 제가 직접 해보니 아래 네 가지가 체감이 제일 컸습니다.

    스토리지와 모델 캐시를 먼저 설계합니다

    Ollama는 모델 파일 크기 특성상 스토리지 전략이 중요합니다. 저는 초반에 스토리지를 임시 볼륨처럼 다뤘다가, 노드 교체 시 모델을 다시 받느라 시간을 꽤 날렸습니다. 그 뒤로는 모델 저장 경로를 PVC에 고정하고, 이미지 재배포와 모델 캐시를 분리했습니다.

    • PVC 고정: 파드가 재생성돼도 모델 캐시는 유지
    • 노드 디스크 여유 확인: 디스크 압박은 CPU 부족보다 더 먼저 터질 때가 많음
    • 백업 기준 마련: 모델 자체보다 설정과 프롬프트 자산 백업 기준 분리

    리소스 요청값은 보수적으로 시작합니다

    처음부터 크게 잡으면 클러스터 전체 밸런스가 깨집니다. 반대로 너무 작게 잡으면 파드가 뜨더라도 응답이 흔들립니다. 저는 초반에 메모리 요청값을 낮게 잡았다가, 다른 워크로드와 겹치는 시간대에 지연이 튀는 걸 봤습니다. 이후엔 실제 사용 패턴을 보고 천천히 올렸습니다.

    모델 프리로드는 배포 단계와 분리합니다

    이거 진짜 편하더라고요. 배포는 배포대로 끝내고, 모델 다운로드는 Job(잡, 일회성 실행 리소스)이나 운영 스크립트로 분리하니까 실패 지점이 줄었습니다. 특히 ArgoCD Ollama 배포를 여러 환경에 복제할 때 차이가 컸습니다.

    kubectl exec -n ollama deploy/ollama -- ollama pull llama3
    kubectl exec -n ollama deploy/ollama -- ollama list

    모델명은 실제 사용 환경에 맞게 바꾸시면 됩니다. 중요한 건 “어디에서 다운로드를 책임질지”를 명확히 하는 겁니다.

    Ingress와 프록시 타임아웃을 반드시 확인합니다

    LLM 요청은 일반 API보다 응답 시간이 길 수 있습니다. 그래서 Ingress나 리버스 프록시 설정이 보수적이면 중간에서 연결이 끊깁니다. 저는 처음에 앱이 느린 줄 알고 한참 봤는데, 실제 원인은 앞단 타임아웃이었습니다. 이런 건 로그를 함께 봐야 보입니다.

    ⚠️ 실제로 겪었던 문제와 해결법

    이 부분은 좀 현실적으로 적어보겠습니다. 문서만 보면 다 쉬워 보이는데, 운영에선 꼭 예상 밖 포인트가 나오더라고요.

    문제 1. 파드는 떴는데 모델이 매번 다시 내려받아졌습니다

    원인: 모델 저장 경로가 영구 볼륨에 제대로 붙지 않았거나, 다른 경로를 보고 있었습니다.

    해결: 컨테이너 내부 경로와 volumeMount를 다시 확인했습니다. 그리고 재배포 후에도 같은 PVC가 붙는지 꼭 확인했습니다.

    문제 2. ArgoCD는 Synced인데 실제 동작은 불안정했습니다

    원인: Git 기준 리소스 상태와 애플리케이션 런타임 상태는 다를 수 있습니다. Synced는 선언형 상태 일치이지, 성능 보장까지 해주진 않거든요.

    해결: readiness/liveness보다 먼저 실제 요청 테스트와 로그 수집을 붙였습니다. ArgoCD 상태만 보고 안심하면 안 됩니다.

    문제 3. 노드 이동 후 성능 체감이 달라졌습니다

    원인: 같은 Kubernetes라도 노드 디스크 성능, 메모리 여유, 다른 워크로드 간섭이 다릅니다.

    해결: nodeSelector(노드 셀렉터, 특정 노드 선택)나 taint/toleration(테인트/톨러레이션, 스케줄링 제어)을 검토했고, 최소한 LLM 워크로드가 너무 자주 이사 다니지 않게 잡았습니다.

    문제 4. 급한 핫픽스가 Git과 어긋났습니다

    원인: 운영 중 kubectl edit로 바로 고친 뒤 Git에 반영하지 않았습니다. 며칠 후 ArgoCD 재동기화에서 다시 원래 값으로 돌아가더라고요. 네, 이거 은근 자주 나옵니다.

    해결: 핫픽스 후 바로 Git PR로 반영하는 습관을 들였습니다. Kubernetes GitOps는 결국 Git이 진실 공급원(single source of truth)이니까요.

    ArgoCD Ollama 배포 트러블슈팅 흐름을 설명하는 이미지

    운영 중 자주 만나는 문제인 스토리지 마운트 오류, Git 드리프트, 프록시 타임아웃을 단계별로 추적하는 트러블슈팅 흐름을 설명하는 이미지 자리입니다.

    검증과 결과: 무엇을 기준으로 성공이라고 봤는가

    저는 “파드가 떴다”를 성공으로 보지 않았습니다. 진짜 중요한 건 재배포 후에도 동일하게 동작하는지, 그리고 운영자가 덜 불안한지였거든요.

    제가 보는 검증 체크리스트

    1. ArgoCD에서 애플리케이션이 지속적으로 Synced/Healthy로 유지되는가
    2. 파드 재생성 후에도 기존 모델 캐시가 유지되는가
    3. 간단한 추론 요청이 내부 네트워크에서 안정적으로 응답하는가
    4. 노드 변경이나 롤링 업데이트 후에도 서비스 재현성이 유지되는가
    5. 운영 변경 사항이 모두 Git commit으로 추적되는가
    kubectl rollout restart deploy/ollama -n ollama
    kubectl get pods -n ollama -w
    kubectl exec -n ollama deploy/ollama -- ollama list

    실제로 써보니까, 위 세 줄만으로도 꽤 많은 걸 확인할 수 있었습니다. 롤링 후 파드가 다시 뜨고, 기존 모델이 그대로 보이면 일단 큰 산은 넘은 겁니다. 여기에 사내 서비스나 실험용 앱에서 실제 API 호출까지 붙여보면 더 좋고요.

    ArgoCD Ollama 배포 운영 결과와 안정성을 보여주는 대시보드 이미지

    재배포 이후에도 Ollama 모델 캐시가 유지되고, ArgoCD와 Kubernetes 상태가 안정적으로 보이는 운영 결과 대시보드 이미지 자리입니다.

    운영 관점에서 느낀 장단점

    장점은 명확합니다. ArgoCD Ollama 배포 구조를 한 번 정리해 두면 환경 복제와 복구가 빨라집니다. 특히 여러 사람이 만지는 환경에서는 “누가 뭘 바꿨는지”가 보이는 것만으로도 가치가 큽니다.

    • 장점: 선언형 관리, 재현성, 운영 표준화, 장애 복구 속도
    • 단점: 스토리지와 네트워크를 모르면 초반 진입 장벽이 있음
    • 주의점: Synced 상태와 서비스 품질은 별개라서 모니터링이 반드시 필요

    반대로 단점도 있습니다. 작은 단일 서버 환경에서는 오히려 Kubernetes가 과할 수 있습니다. 그래서 저는 항상 “정말 GitOps가 필요한 규모인가”를 먼저 봅니다. 혼자 잠깐 실험하는 정도라면 Docker Compose로 시작하는 게 더 낫기도 합니다. 하지만 팀이 붙고, 재현성이 필요하고, LLM 클라우드 실험을 계속 이어갈 생각이라면 이야기가 달라집니다.

    정리와 다음 단계

    정리하자면, 6개월 운영 기준으로 가장 중요했던 건 세 가지였습니다. 스토리지 고정, Git 기준 운영, 배포와 모델 관리를 분리. 이 세 가지만 지켜도 안정감이 꽤 올라갑니다. 저도 처음엔 이것저것 한 번에 자동화하려다가 삽질 좀 했습니다 ㅎㅎ 근데 결국 오래 살아남는 구성은 단순하고, 역할이 분리된 구성이더라고요.

    혹시 지금 ArgoCD로 LLM 워크로드를 올리려는 중이신가요? 그렇다면 먼저 작은 범위로 시작해 보세요. Ollama 하나, PVC 하나, Service 하나부터요. 그리고 동작이 확인되면 그다음에 Ingress, 인증, 모니터링을 붙이시면 됩니다. 이전 글에서 다룬 Kubernetes 스토리지 운영 팁과도 연결되는 부분이고, 다음 글에서는 ArgoCD Ollama 배포 뒤에 인증 프록시와 관측성(observability, 관측 가능성) 붙이는 이야기도 정리해보겠습니다.

    배포 전후 운영 복잡도, 모델 캐시 유지 여부, GitOps 적용 효과를 한눈에 비교하는 요약 인포그래픽 이미지 자리입니다.

    자주 묻는 질문

    Q. Ollama를 Kubernetes에 꼭 올려야 하나요?

    아닙니다. 단일 사용자, 단일 서버라면 더 단순한 방법이 맞을 수 있습니다. 다만 재현성과 팀 협업이 중요해지면 Kubernetes와 GitOps가 힘을 발휘합니다.

    Q. GPU가 없으면 의미가 없나요?

    꼭 그렇진 않습니다. 다만 모델 크기와 응답 기대치에 따라 체감이 다릅니다. 저는 처음 검증은 CPU 환경부터 시작했고, 그 과정에서 오히려 스토리지와 운영 흐름을 먼저 정리할 수 있었습니다.

    Q. 운영 후기 기준으로 가장 먼저 볼 지표는 뭔가요?

    저는 순서가 이렇습니다. 파드 상태, PVC 유지 여부, 실제 요청 성공, 재배포 후 재현성. 화려한 대시보드보다 이 네 가지가 먼저입니다.

  • [k8s] Kubeadm vs ClusterAPI: Kubernetes 클러스터 프로비저닝 도구 비교

    [k8s] Kubeadm vs ClusterAPI: Kubernetes 클러스터 프로비저닝 도구 비교

    Kubeadm vs ClusterAPI: Kubernetes 클러스터 프로비저닝 도구 비교

    Kubeadm과 ClusterAPI를 비교할 때, 많은 분들이 같은 지점에서 막혀 있더라고요. “클러스터 하나 빨리 올리면 되는 건가?”, 아니면 “여러 환경에서 반복 가능하게 클러스터 구축 체계를 만들어야 하나?” 같은 고민 말이에요. 저도 홈랩에서 처음엔 kubeadm(쿠버네티스 클러스터 초기화 도구)만으로 시작했었는데, 노드 수가 늘고 재현 가능한 배포가 필요해지니까 Cluster API(CAPI, 쿠버네티스 방식으로 클러스터 자체를 관리하는 프로젝트) 쪽이 갑자기 눈에 들어오더라고요. 처음엔 이게 뭔가 싶었습니다. 컨트롤러도 많고, 프로바이더도 많고, YAML도 길고요. 근데 구조를 한 번 이해하고 나니 왜 운영팀들이 이 조합을 진지하게 보는지 감이 왔습니다.

    이 글에서는 단순히 기능 나열만 하지 않고, Kubeadm vs ClusterAPI를 실제 운영 관점에서 풀어보겠습니다. 쉽게 말해, kubeadm은 클러스터를 시작하게 해주는 공구에 가깝고, ClusterAPI는 클러스터 생애주기 자체를 선언형으로 다루는 운영 프레임워크에 가깝습니다. 여기서 중요한 포인트! 둘은 경쟁 관계이면서도, 실전에서는 같이 엮이는 경우가 꽤 많습니다.

    Kubeadm ClusterAPI 비교를 보여주는 쿠버네티스 클러스터 프로비저닝 아키텍처 이미지

    kubeadm은 개별 클러스터 부트스트랩 흐름에, Cluster API는 관리 클러스터에서 워크로드 클러스터를 선언형으로 제어하는 흐름에 초점이 있습니다.

    Kubernetes 클러스터 프로비저닝, 뭐가 다른가요?

    쉽게 말해 kubeadm은 노드에 들어가서 클러스터를 만드는 도구입니다. 컨트롤 플레인(클러스터 제어 영역) 초기화, 조인 토큰 생성, 인증서 배포 같은 부트스트랩 작업을 잘 해주죠. 반면 Cluster API는 클러스터를 쿠버네티스 리소스처럼 다루는 방식입니다. 즉, “이런 모양의 클러스터를 만들어줘”라고 선언하면 컨트롤러가 그 상태를 맞추려고 움직입니다.

    제가 직접 해보니 kubeadm은 구조가 비교적 단순해서 학습 진입 장벽이 낮았습니다. 특히 베어메탈(가상화 없이 물리 서버 직접 운영)이나 홈랩처럼 손으로 만질 수 있는 환경에서는 진짜 빠릅니다. 반대로 Cluster API는 관리 클러스터(다른 클러스터를 제어하는 클러스터)라는 개념부터 잡아야 해서 초반 허들이 있습니다. 대신 익숙해지면 반복 배포, 업그레이드, 스케일링, 클러스터 폐기까지 흐름이 꽤 깔끔해집니다.

    • kubeadm: 단일 클러스터 프로비저닝에 강함
    • Cluster API: 여러 클러스터의 선언형 운영과 생애주기 관리에 강함
    • CAPI + kubeadm: Cluster API가 kubeadm 기반 부트스트랩을 활용하는 조합이 현실

    Kubeadm vs ClusterAPI 핵심 구조 비교

    비교 항목 kubeadm Cluster API
    주요 목적 쿠버네티스 클러스터 초기 구성 클러스터 생성, 변경, 업그레이드, 삭제까지 생애주기 관리
    운영 방식 명령형(명령 실행 중심) 선언형(원하는 상태 기술)
    주요 실행 위치 대상 노드 관리 클러스터의 컨트롤러
    멀티 클러스터 운영 수동 설계 필요 기본 개념 자체가 멀티 클러스터 친화적
    인프라 연동 직접 구성 필요 Infrastructure Provider(인프라 프로바이더)로 연동
    업그레이드 자동화 운영 스크립트나 절차 설계 필요 버전 선언 후 컨트롤러 기반 롤링 변경 가능
    초기 난이도 상대적으로 낮음 상대적으로 높음

    여기서 중요한 포인트! Cluster API가 kubeadm을 대체하는 느낌으로 보일 수 있는데, 실제로는 Cluster API 안에서 kubeadm bootstrap provider를 사용하는 사례가 많습니다. 즉, kubeadm은 여전히 중요한 구성요소고, Cluster API는 그걸 더 큰 자동화 체계 안에 넣는 느낌이라고 보시면 이해가 편합니다.

    언제 kubeadm이 더 잘 맞을까요?

    • 처음 쿠버네티스 구조를 익히는 단계일 때
    • 노드 수가 적고 클러스터 수가 많지 않을 때
    • 베어메탈이나 제한된 홈랩 환경에서 빠르게 검증할 때
    • 구성 과정을 손으로 제어해야 안심되는 팀 문화일 때

    언제 Cluster API가 빛날까요?

    • 동일한 형태의 클러스터를 반복 생성해야 할 때
    • 개발, 스테이징, 운영 환경을 비슷한 클러스터 구축 템플릿으로 관리할 때
    • 클러스터 업그레이드와 노드 교체를 체계화하고 싶을 때
    • GitOps(Git 기반 선언형 운영)와 잘 엮고 싶을 때

    실전 구현 1: kubeadm으로 클러스터 올리기

    먼저 kubeadm 흐름부터 보겠습니다. 실제로 써보니까 kubeadm은 클러스터 내부 동작을 이해하는 데 정말 좋았습니다. 인증서, kubelet(노드 에이전트), CNI(컨테이너 네트워크 플러그인) 같은 개념이 어디서 필요한지 몸으로 익히게 되거든요. 다만 그만큼 수동 단계도 많습니다. 삽질 좀 했습니다 ㅎㅎ

    1. 런타임(container runtime)과 kubelet, kubeadm, kubectl 설치
    2. 컨트롤 플레인 노드에서 초기화
    3. CNI 설치
    4. 워커 노드 조인
    5. 상태 검증
    sudo kubeadm init \
      --pod-network-cidr=192.168.0.0/16
    
    mkdir -p $HOME/.kube
    sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
    sudo chown $(id -u):$(id -g) $HOME/.kube/config
    
    kubectl get nodes

    초기화가 끝나면 바로 Ready가 안 뜰 수 있습니다. 여기서 당황 많이 하시더라고요. 대부분은 아직 CNI가 없어서 그렇습니다.

    kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/master/manifests/calico.yaml
    kubectl get pods -A

    워커 노드 조인은 `kubeadm token create –print-join-command`로 받은 명령을 그대로 실행하면 됩니다.

    kubeadm token create --print-join-command

    이 방식의 장점은 명확합니다. 어디서 뭐가 실패하는지 눈에 보입니다. 반대로 단점도 명확해요. 같은 클러스터를 세 번, 네 번 반복해서 만들다 보면 사람이 실수할 지점이 계속 생깁니다. 바로 이 지점에서 “클러스터 구축 도구를 더 선언형으로 가져가야 하나?”라는 생각이 들기 시작하더라고요.

    실전 구현 2: Cluster API로 선언형 클러스터 만들기

    이제 Cluster API 쪽입니다. 처음엔 관리 클러스터가 또 필요하다고 해서 부담스러웠는데, 실제 구조를 이해하고 나니 왜 필요한지 납득됐습니다. Cluster API는 몇 가지 핵심 컴포넌트로 나뉩니다.

    • Core Provider: Cluster API 핵심 리소스와 컨트롤러
    • Bootstrap Provider: 노드 초기화 데이터 생성(kubeadm 활용)
    • Control Plane Provider: 컨트롤 플레인 구성 관리
    • Infrastructure Provider: 실제 VM, 인스턴스, 머신, 네트워크 리소스 생성

    기본 흐름은 이렇습니다. 관리 클러스터에 Cluster API 컴포넌트를 설치하고, 그 위에 워크로드 클러스터(실제 애플리케이션이 올라갈 대상 클러스터)의 정의를 YAML로 적용합니다.

    clusterctl init --infrastructure <provider-name>

    프로바이더 이름은 사용하는 환경에 따라 달라집니다. 예를 들어 퍼블릭 클라우드, 프라이빗 클라우드, 베어메탈 쪽에서 선택지가 갈리죠. 여기서는 원리를 이해하는 데 집중하겠습니다.

    Cluster API 관리 클러스터와 워크로드 클러스터 구조를 설명하는 Kubeadm ClusterAPI 비교 이미지

    관리 클러스터에서 컨트롤러가 동작하고, 선언된 리소스를 바탕으로 워크로드 클러스터의 머신과 컨트롤 플레인을 조립하는 구조입니다.

    apiVersion: cluster.x-k8s.io/v1beta1
    kind: Cluster
    metadata:
      name: demo-cluster
    spec:
      controlPlaneRef:
        apiVersion: controlplane.cluster.x-k8s.io/v1beta1
        kind: KubeadmControlPlane
        name: demo-control-plane
      infrastructureRef:
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
        kind: GenericInfrastructureCluster
        name: demo-infra
    ---
    apiVersion: controlplane.cluster.x-k8s.io/v1beta1
    kind: KubeadmControlPlane
    metadata:
      name: demo-control-plane
    spec:
      replicas: 1
      version: v1.29.0
      kubeadmConfigSpec:
        clusterConfiguration: {}
        initConfiguration: {}
        joinConfiguration: {}
    ---
    apiVersion: cluster.x-k8s.io/v1beta1
    kind: MachineDeployment
    metadata:
      name: demo-md-0
    spec:
      clusterName: demo-cluster
      replicas: 2
      selector:
        matchLabels: {}
      template:
        spec:
          version: v1.29.0
          clusterName: demo-cluster

    위 YAML은 Cluster API 구조 예시입니다. 실제 배포에서는 인프라 프로바이더 리소스가 더 들어가고, 환경 변수나 자격 증명도 필요합니다. 중요한 건 클러스터를 명령으로 만드는 게 아니라 상태로 선언한다는 점입니다. 이게 Cluster API의 본질이에요.

    kubectl apply -f cluster.yaml
    kubectl get cluster
    kubectl get machinedeployments
    kubectl get machines

    제가 직접 해보니 여기서 재미있는 포인트가 있었습니다. kubeadm은 한 번 만들고 나면 이후 운영 자동화는 별도로 설계해야 하는데, Cluster API는 처음부터 “운영까지 생각한 구조”라는 느낌이 강합니다. 대신 디버깅은 더 추상적입니다. 노드 한 대에서 끝나는 문제가 아니라 컨트롤러, 프로바이더, 템플릿이 다 얽혀 있거든요.

    주의사항과 트러블슈팅: 실제로 많이 막히는 지점

    ⚠️ 여기 진짜 중요합니다. Kubeadm ClusterAPI 비교에서 많은 글이 장점만 말하는데, 실전에서는 막히는 지점이 선택 기준이 되더라고요.

    1. kubeadm은 CNI 전까지 정상처럼 보여도 정상 아닐 수 있습니다

    `kubeadm init`이 끝났다고 클러스터가 다 살아난 건 아닙니다. `kubectl get nodes`에서 `NotReady`가 보이면 CNI부터 의심하세요. 저도 처음엔 인증서 문제인 줄 알고 엉뚱한 로그만 한참 봤었습니다.

    kubectl describe node
    kubectl get pods -A
    journalctl -u kubelet

    2. Cluster API는 실패 지점이 더 분산됩니다

    예를 들어 머신이 안 생기면 인프라 프로바이더 문제인지, bootstrap 데이터 생성 문제인지, control plane reconciliation 문제인지 나눠서 봐야 합니다.

    kubectl get pods -A
    kubectl get clusters,machines,machinedeployments,kubeadmcontrolplanes
    kubectl describe machine <machine-name>
    kubectl logs -n capi-system deployment/capi-controller-manager

    혹시 이런 경험 있으신가요? 리소스는 분명히 생성됐는데 상태가 계속 `Provisioning`에서 안 넘어가는 경우요. 이럴 땐 대부분 인프라 쪽 자격 증명, 이미지 템플릿, 네트워크 전제조건이 빠져 있는 경우가 많았습니다.

    3. kubeadm은 단순한 대신 표준화가 흔들리기 쉽습니다

    운영자가 둘 이상만 돼도 설치 옵션, OS 준비 상태, 네트워크 플러그인 선택이 조금씩 달라집니다. 결국 문서화와 스크립트화가 필수입니다. 즉, kubeadm이 간단하다고 해서 운영 체계까지 단순한 건 아니더라고요.

    4. Cluster API는 관리 클러스터 자체의 안정성이 중요합니다

    관리 클러스터가 흔들리면 워크로드 클러스터 변경 작업도 영향받습니다. 그래서 초기에 “관리 클러스터를 어디에 둘 것인가”를 꽤 신중하게 봐야 합니다. 홈랩에서는 한 대에 몰아넣으면 편하긴 한데, 장애 분리 측면에서는 아쉬움이 남습니다.

    검증과 결과 확인: 뭐가 더 운영 친화적인가

    검증은 단순히 `kubectl get nodes`만 보면 부족합니다. 생성, 변경, 교체, 삭제 흐름까지 봐야 합니다. 이 부분이 Kubernetes 클러스터 프로비저닝 도구를 비교할 때 핵심이거든요.

    1. 클러스터 최초 생성 시간과 절차 복잡도 확인
    2. 워커 노드 확장 방식 확인
    3. 버전 변경 시 운영 개입량 확인
    4. 장애 시 복구 절차 문서화 가능성 확인
    kubectl get nodes -o wide
    kubectl get cluster
    kubectl get machines
    kubectl get events --sort-by=.lastTimestamp
    Kubeadm ClusterAPI 비교 결과를 검증하는 쿠버네티스 상태 확인 이미지

    노드 Ready 상태, Machine 리소스 변화, 이벤트 흐름을 통해 선언한 상태와 실제 상태가 맞아가는지 확인하는 장면을 상상하시면 됩니다.

    실제로 써보니까 결과는 꽤 분명했습니다. 하나의 클러스터를 깊게 이해하고 빠르게 구축하려면 kubeadm이 좋았고, 반복 가능한 다수의 클러스터를 체계적으로 관리하려면 Cluster API가 훨씬 운영 친화적이었습니다. 특히 업그레이드나 머신 교체 시나리오를 생각하면 차이가 더 벌어집니다.

    상황별 선택 가이드: 클러스터 구축 도구 어떤 게 맞을까

    상황 추천 이유
    홈랩에서 쿠버네티스 구조 학습 kubeadm 구성 요소를 직접 체감하기 좋음
    단일 운영 클러스터를 신중하게 수동 관리 kubeadm 절차가 명확하고 제어감이 높음
    여러 환경에 동일한 클러스터 배포 Cluster API 선언형 템플릿 재사용이 쉬움
    업그레이드/교체 자동화를 운영 정책으로 추진 Cluster API 생애주기 관리 철학에 잘 맞음
    베어메탈 자동화까지 포함한 확장 설계 환경에 따라 다름 인프라 프로바이더 성숙도와 운영 역량이 중요
    • 빠른 시작과 학습이 목표면 kubeadm
    • 반복성과 선언형 운영이 목표면 Cluster API
    • 둘 중 하나만 고집할 필요는 없음: kubeadm으로 원리 이해 후 CAPI로 확장하는 경로가 현실적
    Kubeadm ClusterAPI 비교 선택 기준을 정리한 요약 인포그래픽

    팀 규모, 클러스터 수, 자동화 요구사항, 관리 방식에 따라 어떤 도구가 더 어울리는지 한눈에 정리한 비교 이미지입니다.

    자주 묻는 질문

    Q1. Cluster API가 있으면 kubeadm은 이제 안 써도 되나요?

    그렇진 않습니다. 오히려 Cluster API 내부에서 kubeadm 기반 부트스트랩을 활용하는 경우가 흔합니다. kubeadm은 여전히 중요한 기반 기술입니다.

    Q2. 소규모 팀도 Cluster API를 바로 도입해야 할까요?

    반드시 그렇진 않습니다. 클러스터 수가 적고 운영자가 구조를 아직 익히는 단계라면 kubeadm이 더 현실적일 수 있습니다. 자동화 비용보다 복잡도 비용이 더 클 수 있거든요.

    Q3. GitOps와 더 잘 맞는 쪽은 무엇인가요?

    Cluster API가 더 자연스럽습니다. 클러스터 정의 자체를 리소스로 관리하니까 Git 저장소 기반 운영 모델과 궁합이 좋습니다.

    마무리: 현실적인 접근

    정리해보면 이렇습니다. Kubeadm vs ClusterAPI 비교는 단순한 우열 비교가 아니라, 운영 성숙도와 목표가 다른 두 접근입니다. 제가 직접 해보니 kubeadm은 쿠버네티스를 제대로 이해하게 해주는 좋은 선생님이었고, Cluster API는 그 다음 단계에서 운영 자동화를 체계로 바꿔주는 도구였습니다. 둘 중 하나가 무조건 정답은 아니었습니다.

    개인적으로는 이런 순서를 추천드립니다. 처음엔 kubeadm으로 컨트롤 플레인 초기화, CNI 연결, 노드 조인, 인증서 구조를 손으로 한 번 겪어보세요. 그다음 반복 배포가 필요해지는 시점에 Cluster API를 붙이면 훨씬 덜 헷갈립니다. 저도 처음엔 YAML만 보고 멍했었는데, 기반 개념을 알고 나니까 드디어 됐다! 싶은 순간이 오더라고요.

    다음 글에서는 kubeadm으로 만든 클러스터를 운영 표준화하는 방법이나, Cluster API와 GitOps를 연결하는 흐름을 따로 다뤄보겠습니다. 이전 글에서 다룬 네트워크 플러그인 비교나 Ingress(외부 트래픽 진입점) 구성 글과 함께 보시면 더 이해가 잘 되실 겁니다.

    처음엔 수동 구축으로 원리를 익히고, 이후 선언형 운영으로 넘어가는 학습 및 운영 로드맵을 보여주는 마무리 이미지입니다.

  • [Kubernetes] Kubernetes CRD 심층 분석: 커스텀 리소스 정의 및 활용 전략

    [Kubernetes] Kubernetes CRD 심층 분석: 커스텀 리소스 정의 및 활용 전략

    [Kubernetes] Kubernetes CRD 심층 분석: 커스텀 리소스 정의 및 활용 전략

    Kubernetes CRD는 쿠버네티스(Kubernetes, 컨테이너 오케스트레이션 플랫폼)를 단순히 “파드를 띄우는 도구”에서 끝내지 않고, 우리 조직의 운영 규칙을 클러스터 안으로 끌고 들어오게 만드는 핵심 확장 포인트입니다. 처음엔 저도 이게 그냥 YAML 하나 더 만드는 기능인가 싶었는데, 실제로 써보니까 운영 지식을 API로 승격시키는 도구에 가깝더라고요. 특히 반복되는 인프라 작업이 많거나, 특정 서비스 배포 규칙을 팀 표준으로 강제하고 싶을 때 Kubernetes CRD의 진가가 확실히 드러납니다.

    제가 홈랩과 실무 환경에서 Kubernetes를 오래 만지면서 느낀 건, 사람이 문서 보고 수동으로 맞추는 방식은 결국 어딘가에서 틀어진다는 점이었습니다. “이 서비스는 TLS가 꼭 있어야 한다”, “이 애플리케이션은 백업 정책이 필요하다”, “이 데이터베이스는 복구 절차가 정해져 있다” 같은 규칙을 문서로만 남겨두면요. 언젠가는 빠집니다. 근데 Kubernetes CRD와 Operator 패턴(Operator Pattern, 운영 자동화 패턴)을 같이 쓰면, 그 규칙을 선언형(Declarative, 원하는 상태를 선언하는 방식)으로 관리할 수 있거든요.

    쿠버네티스 API 서버, etcd, CustomResourceDefinition, 커스텀 컨트롤러의 관계를 한눈에 보여주는 개요 이미지 위치입니다.

    Kubernetes CRD란 무엇인가요?

    쉽게 말해 CRD(CustomResourceDefinition, 커스텀 리소스 정의)는 쿠버네티스에 새로운 리소스 타입을 추가하는 방법입니다. Deployment, Service, Ingress(인그레스, 외부 트래픽 진입점)처럼 원래 있던 리소스 말고, 예를 들어 Database, BackupPolicy, AppRelease 같은 우리만의 리소스를 만들 수 있는 거죠.

    여기서 사람들이 많이 헷갈리는 포인트가 하나 있습니다. CRD 자체는 결국 스키마(schema, 데이터 구조 정의)를 등록하는 일이거든요. 즉, API 서버가 “아, 이제 이런 모양의 리소스도 받을 수 있구나” 하고 이해하게 만드는 단계예요. 그런데 실제로 그 리소스를 보고 뭔가 동작하게 만드는 건 보통 컨트롤러(Controller, 상태를 감시하고 맞추는 제어 루프)가 담당합니다. 저도 처음엔 CRD만 만들면 자동으로 뭔가 굴러갈 줄 알았는데, 아무 일도 안 일어나서 삽질 좀 했습니다 ㅎㅎ

    기본 구성 요소

    • CRD: 새로운 리소스 타입의 정의
    • Custom Resource: 실제로 생성하는 인스턴스
    • Controller: 원하는 상태와 현재 상태를 맞추는 로직
    • Operator: 특정 운영 지식을 코드로 구현한 컨트롤러 묶음
    항목 역할 비유
    CRD 새 API 타입 등록 양식(template) 만들기
    Custom Resource 실제 선언 데이터 양식에 값 채워 제출
    Controller 선언을 실제 상태로 반영 담당자가 요청 처리
    Operator 도메인 운영 자동화 숙련 엔지니어의 절차를 자동화

    왜 Kubernetes 확장에 CRD를 쓰는가

    Kubernetes 확장이라고 하면 예전에는 API Aggregation(집계 API)처럼 더 무거운 접근도 있었지만, 대부분의 팀에게는 CRD가 훨씬 현실적입니다. 이유는 분명합니다.

    1. 기존 kubectl과 선언형 워크플로우를 그대로 활용할 수 있습니다.
    2. RBAC(Role-Based Access Control, 역할 기반 접근 제어), admission, GitOps와 잘 맞습니다.
    3. 도메인 개념을 YAML로 표준화할 수 있습니다.
    4. Operator 패턴과 결합하면 반복 작업을 코드로 고정할 수 있습니다.

    예를 들어 운영팀에서 매번 데이터베이스 인스턴스를 만들 때 스토리지 클래스, 백업 주기, 리소스 제한, 비밀 정보(secret) 연결을 공통 규칙으로 적용한다고 해보겠습니다. 이걸 문서로 적어두는 대신 Database라는 커스텀 리소스를 만들고, 컨트롤러가 이를 보고 StatefulSet, Service, Secret 참조, 백업 Job을 구성하게 만들 수 있습니다. 이게 바로 커스텀 리소스와 Operator 패턴이 실무에서 힘을 발휘하는 지점입니다.

    혹시 이런 경험 있으신가요? 배포 문서는 있는데 팀원마다 해석이 조금씩 달라서 결과가 달라지는 상황이요. 저는 그걸 몇 번 겪고 나서, 사람이 읽는 문서와 시스템이 읽는 선언을 분리해야겠다고 생각했습니다. Kubernetes CRD는 바로 그 중간을 메워줍니다.

    실전 구현 1: CRD 정의하기

    이제 가장 단순한 예제로 가보겠습니다. AppClaim이라는 커스텀 리소스를 만들어서, 애플리케이션 배포에 필요한 최소 정보를 선언한다고 가정해보죠. 여기서는 이해를 위해 구조를 단순화했습니다.

    1. API 그룹(group)과 버전(version)을 정합니다.
    2. 리소스 이름과 복수형(plural)을 정합니다.
    3. OpenAPI v3 스키마로 필드를 정의합니다.
    4. 추가 프린터 컬럼으로 kubectl 가독성을 높입니다.
    apiVersion: apiextensions.k8s.io/v1
    kind: CustomResourceDefinition
    metadata:
      name: appclaims.platform.example.com
    spec:
      group: platform.example.com
      scope: Namespaced
      names:
        plural: appclaims
        singular: appclaim
        kind: AppClaim
        shortNames:
          - ac
      versions:
        - name: v1alpha1
          served: true
          storage: true
          schema:
            openAPIV3Schema:
              type: object
              properties:
                spec:
                  type: object
                  required:
                    - image
                    - replicas
                  properties:
                    image:
                      type: string
                    replicas:
                      type: integer
                      minimum: 1
                    port:
                      type: integer
                      minimum: 1
                      maximum: 65535
                    expose:
                      type: boolean
                status:
                  type: object
                  properties:
                    phase:
                      type: string
                    readyReplicas:
                      type: integer
          subresources:
            status: {}
          additionalPrinterColumns:
            - name: Image
              type: string
              jsonPath: .spec.image
            - name: Replicas
              type: integer
              jsonPath: .spec.replicas
            - name: Phase
              type: string
              jsonPath: .status.phase

    이 YAML의 핵심은 spec과 status를 분리한 거예요. spec은 사용자가 원하는 상태이고, status는 컨트롤러가 관찰한 결과입니다. 이 구분이 흐려지면 나중에 운영이 엄청 꼬입니다. 저도 예전에 status 비슷한 값을 spec에 억지로 넣었다가 reconcile loop(리컨실 루프, 상태 동기화 루프)가 지저분해져서 다시 갈아엎은 적이 있습니다.

    kubectl apply -f appclaim-crd.yaml
    kubectl get crd appclaims.platform.example.com

    정상 적용되면 이제 클러스터는 AppClaim이라는 새 리소스 타입을 이해합니다. 여기서 중요한 포인트! 아직은 타입만 생긴 상태입니다. 배포는 자동으로 안 됩니다.

    Kubernetes CRD YAML을 작성하고 적용하는 실전 구성 이미지

    CRD YAML을 작성하고 kubectl로 적용하는 실전 구성 이미지를 넣는 위치입니다.

    실전 구현 2: 커스텀 리소스 생성과 컨트롤러 연결

    다음은 실제 인스턴스를 하나 만들어보겠습니다.

    apiVersion: platform.example.com/v1alpha1
    kind: AppClaim
    metadata:
      name: demo-web
    spec:
      image: nginx:stable
      replicas: 2
      port: 80
      expose: true
    kubectl apply -f demo-appclaim.yaml
    kubectl get appclaims
    kubectl describe appclaim demo-web

    이제 컨트롤러가 이 리소스를 감시하면서 Deployment와 Service를 생성하도록 만들 수 있습니다. 실제 프로덕션에서는 Go와 controller-runtime을 많이 쓰지만, 여기서는 흐름 이해가 목적이니 의사 코드 수준으로 보겠습니다.

    def reconcile(appclaim):
        desired_deployment = build_deployment(appclaim.spec)
        desired_service = build_service(appclaim.spec)
    
        apply_object(desired_deployment)
    
        if appclaim.spec.get("expose"):
            apply_object(desired_service)
    
        update_status(
            appclaim,
            phase="Ready",
            ready_replicas=get_ready_replicas(appclaim.metadata.name)
        )

    실제로 써보니까 이 구조가 왜 좋은지 금방 체감됩니다. 사용자는 AppClaim만 만들면 되고, 배포 세부 구현은 컨트롤러가 맡습니다. 즉, 사용자 인터페이스는 단순하게, 운영 로직은 중앙집중화되는 거죠. 팀 규모가 커질수록 이 차이가 큽니다.

    Operator 패턴과의 연결

    Operator 패턴은 단순 생성 자동화를 넘어섭니다. 설치, 업그레이드, 백업, 복구, 장애 대응 같은 운영 절차를 컨트롤러 안에 녹이는 방식입니다. 대표적으로 데이터베이스, 메시지 큐, 모니터링 스택 같은 상태 저장 워크로드에서 자주 쓰입니다. Kubernetes CRD는 이 Operator의 입력 포맷이 되는 경우가 많고요.

    만약 앱 수준이 아니라 데이터베이스 운영 자동화까지 가고 싶다면, 다음 글에서 StatefulSet과 Operator 설계 포인트도 이어서 다뤄볼 예정입니다. 이전 글에서 다룬 Helm 차트 구조화 내용이 있다면 그것도 같이 보시면 흐름이 더 잘 잡힙니다.

    ⚠️ 주의사항과 트러블슈팅

    여기서부터가 진짜 실무 구간입니다. CRD는 만들기보다 버전 관리와 호환성에서 더 많이 흔들립니다. 제가 직접 해보니 아래 이슈는 거의 한 번씩 다 밟게 되더라고요.

    1. v1alpha1을 너무 오래 끌고 가는 문제

    처음엔 빠르게 실험하려고 v1alpha1로 시작합니다. 저도 그랬고요. 근데 이 상태로 사용자 수가 늘어나면 필드 변경이 무서워집니다. 기존 리소스와의 호환성을 고민해야 하거든요. 가능하면 초기에 필드 의미를 명확히 하고, status 구조도 대충 넣지 말고 의도를 분명히 하세요.

    2. 스키마 검증을 느슨하게 잡는 문제

    “컨트롤러에서 알아서 처리하면 되지”라고 생각하고 schema를 대충 열어두면 나중에 이상한 값이 다 들어옵니다. port에 문자열이 들어오거나, replicas가 0이 되거나, 필수 필드가 비는 식이죠. 그러면 에러 위치가 API 서버가 아니라 애플리케이션 쪽으로 밀려서 디버깅이 더 어려워집니다.

    3. status 업데이트 충돌

    컨트롤러가 status를 자주 갱신하면 resourceVersion 충돌이 날 수 있습니다. 이럴 때는 status subresource를 쓰고, 재시도 로직과 idempotent(멱등성 있는) 설계를 같이 가져가야 합니다. “한 번 더 실행돼도 결과가 같아야 한다” 이 원칙이 정말 중요합니다.

    4. finalizer 처리 누락

    외부 리소스를 만들었다면 finalizer(파이널라이저, 삭제 전 정리 훅)를 빼먹으면 안 됩니다. 삭제 이벤트가 왔을 때 외부 DNS, 스토리지, 인증서 같은 부수 리소스를 정리하지 않으면 유령 자원이 남습니다. 저도 예전에 테스트 환경에서 볼륨이 계속 쌓여서 왜 그런가 봤더니 finalizer 처리 누락이 원인이더라고요.

    kubectl get appclaim demo-web -o yaml
    kubectl api-resources | grep appclaim
    kubectl explain appclaim.spec

    위 명령어 세 개는 문제 생겼을 때 정말 자주 씁니다. 특히 kubectl explain은 Kubernetes CRD가 의도한 구조대로 등록됐는지 빠르게 확인할 때 유용합니다. 💡 작은 팁인데, 추가 프린터 컬럼을 잘 만들어두면 운영 가시성이 크게 좋아집니다.

    Kubernetes CRD 트러블슈팅과 검증 흐름을 보여주는 이미지

    스키마 검증 오류, 컨트롤러 동기화, kubectl 진단 흐름을 설명하는 트러블슈팅 다이어그램 위치입니다.

    검증과 결과 확인

    그럼 무엇을 검증해야 “Kubernetes CRD가 잘 동작한다”고 볼 수 있을까요? 단순히 리소스가 생성됐다는 것만으로는 부족합니다. 저는 보통 아래 순서로 봅니다.

    1. CRD가 정상 등록됐는지 확인합니다.
    2. Custom Resource 생성이 스키마에 맞게 통과하는지 봅니다.
    3. 컨트롤러가 원하는 하위 리소스를 생성하는지 확인합니다.
    4. status가 실제 상태를 반영하는지 확인합니다.
    5. 삭제 시 정리 로직이 정상 동작하는지 검증합니다.
    kubectl get crd
    kubectl get appclaims
    kubectl get deployment,service
    kubectl describe appclaim demo-web
    kubectl delete appclaim demo-web

    정상 흐름이라면 AppClaim을 만들었을 때 Deployment와 Service가 생성되고, 준비가 끝나면 status.phase 같은 필드가 업데이트됩니다. 삭제 시에는 관련 리소스가 정리돼야 하고요. 이 지점에서 드디어 “아, 이제 사람이 수작업으로 안 해도 되네” 하는 느낌이 옵니다. 🎉 이거 진짜 편하더라고요.

    검증 항목 확인 방법 기대 결과
    CRD 등록 kubectl get crd 리소스 타입 노출
    스키마 검증 잘못된 필드로 생성 시도 API 서버에서 거부
    하위 리소스 생성 Deployment/Service 조회 의도한 객체 생성
    Status 반영 kubectl describe 상태 필드 갱신
    삭제 정리 delete 후 잔여 리소스 확인 누수 없이 정리

    커스텀 리소스 상태와 생성된 워크로드를 검증하는 결과 화면 이미지를 넣는 위치입니다.

    언제 CRD를 쓰고, 언제 쓰지 말아야 하나

    모든 문제를 CRD로 풀 필요는 없습니다. 이건 정말 중요합니다. 저도 한때 뭐든지 커스텀 리소스로 만들면 멋있어 보인다고 생각했었는데, 나중엔 운영 복잡도만 올라간 적이 있었습니다.

    • CRD를 쓰면 좋은 경우: 반복되는 운영 절차가 있고, 선언형 API로 표준화할 가치가 있을 때
    • CRD가 과한 경우: 단순 스크립트 한 번이면 끝나는 작업일 때
    • Operator가 필요한 경우: 설치 이후에도 지속적인 상태 관리, 복구, 업그레이드가 필요할 때
    • Helm만으로 충분한 경우: 정적 템플릿 배포가 중심이고 런타임 제어가 크지 않을 때

    한 줄로 정리하면 이렇습니다. 새로운 API가 팀의 언어가 될 수준인지 먼저 보세요. 그 정도가 아니면 단순화하는 편이 낫습니다.

    정리와 FAQ

    Kubernetes CRD는 단순 기능 추가가 아니라, 팀의 운영 지식을 쿠버네티스 API로 모델링하는 방법입니다. CRD, 커스텀 리소스, Kubernetes 확장, Operator 패턴을 한 세트로 이해하면 왜 많은 플랫폼 팀이 이 접근을 쓰는지 감이 오실 겁니다. 저도 처음엔 용어가 너무 많아서 복잡했는데, 결국 핵심은 하나였습니다. “원하는 상태를 잘 정의하고, 그걸 시스템이 계속 맞추게 하자.” 이 철학만 잡히면 훨씬 편해집니다. ✅

    다음 단계로는 컨트롤러 프레임워크(controller-runtime), 상태 전이 설계, CRD 버전 업그레이드 전략까지 이어서 보시면 좋습니다. 다음 글에서 다룰 예정입니다.

    Kubernetes CRD와 Operator 패턴 역할 비교 요약 이미지

    CRD, 커스텀 리소스, 컨트롤러, Operator의 역할을 요약 비교하는 인포그래픽 위치입니다.

    자주 묻는 질문

    Q. CRD만 만들면 자동화가 되나요?
    아닙니다. CRD는 타입 등록이고, 실제 자동화는 보통 컨트롤러가 담당합니다.

    Q. CRD와 Operator는 같은 건가요?
    같지 않습니다. CRD는 API 정의이고, Operator는 운영 지식을 담은 자동화 로직입니다. 둘이 함께 쓰이는 경우가 많습니다.

    Q. 기존 Deployment나 Helm으로 충분한데도 Kubernetes CRD가 필요할까요?
    반복 운영 규칙을 API로 표준화할 필요가 없다면 굳이 안 써도 됩니다. 과한 설계는 오히려 부담이 됩니다.

    Q. 처음 만들 때 가장 중요한 한 가지는 뭔가요?
    spec과 status의 역할을 명확히 나누고, 스키마 검증을 초기에 엄격하게 잡는 겁니다. 여기서 흔들리면 나중에 계속 고생합니다.

  • [인프라] Crossplane 장애 사례로 배우는 멀티 클라우드 인프라 관리

    [인프라] Crossplane 장애 사례로 배우는 멀티 클라우드 인프라 관리

    [인프라] Crossplane 장애 사례로 배우는 멀티 클라우드 인프라 관리

    Crossplane 장애 사례를 한 번이라도 겪어보신 분들은 아실 겁니다. 처음엔 “쿠버네티스(Kubernetes, 컨테이너 오케스트레이션 플랫폼)처럼 리소스를 선언형으로 관리하면 인프라도 깔끔해지겠네?” 싶거든요. 저도 홈랩하고 업무 환경에서 멀티 클라우드 관리 구조를 정리하려고 Crossplane을 붙였었는데, 막상 운영에 들어가니까 컨트롤 플레인(Control Plane, 전체 상태를 조정하는 중앙 제어 계층) 특성 때문에 장애가 생각보다 교묘하게 터지더라고요. 특히 인프라스트럭처 코드(Infrastructure as Code, 코드로 인프라를 정의하는 방식)와 쿠버네티스 API 감각이 섞이면서 원인을 잘못 짚으면 복구가 더 늦어집니다.

    이번 글은 제품 소개보다는 troubleshooting 중심입니다. 제가 직접 해보니 Crossplane은 잘만 쓰면 멀티 클라우드 관리 복잡도를 꽤 줄여주는데, 장애가 났을 때는 “어디서 상태가 꼬였는지”를 읽는 눈이 정말 중요하더라고요. 그래서 오늘은 Crossplane 장애 사례를 바탕으로 어떤 식으로 문제가 드러났고 어떻게 풀어갔는지, 그리고 운영하면서 꼭 챙겨야 할 포인트를 정리해보겠습니다.

    Crossplane 장애 사례를 이해하기 위한 멀티 클라우드 관리 아키텍처 개요

    Crossplane이 여러 클라우드 리소스를 쿠버네티스 컨트롤 플레인으로 관리하는 전체 흐름을 보여주는 이미지입니다.

    1. Crossplane을 왜 멀티 클라우드 관리에 쓰는가

    쉽게 말해 Crossplane은 쿠버네티스 API로 외부 인프라를 다루게 해주는 도구입니다. AWS, GCP, Azure 같은 퍼블릭 클라우드 자원을 쿠버네티스 리소스처럼 선언하고, 원하는 상태(desired state)와 실제 상태(actual state)를 맞추도록 계속 reconcile(리컨실, 상태를 일치시키는 반복 제어)하는 구조죠.

    이게 왜 좋냐면요. 클라우드마다 콘솔도 다르고 권한 체계도 다르고 Terraform 상태 파일 관리도 따로 고민해야 하는데, Crossplane은 적어도 운영 관점에서 제어면을 하나로 모으는 효과가 있습니다. 특히 팀 단위로 표준화된 Composite Resource(복합 리소스)나 Claim(클레임, 사용자 요청 객체)을 만들어두면 개발팀은 세부 클라우드 차이를 몰라도 공통 인터페이스로 인프라를 요청할 수 있거든요.

    • 장점 1: 멀티 클라우드 관리 진입점이 쿠버네티스로 통일돼요.
    • 장점 2: 인프라스트럭처 코드와 GitOps 흐름을 연결하기 좋습니다.
    • 장점 3: 플랫폼 팀이 정책과 표준 구성을 감싸서 제공하기 좋습니다.

    반대로 단점도 분명합니다. Crossplane 자체가 또 하나의 컨트롤 플레인이기 때문에 장애 포인트가 사라지는 게 아니라 다른 계층으로 이동하는 느낌이 있어요. 저도 처음엔 이게 뭔가 싶었는데, 실제로 써보니까 “리소스를 만드는 일”보다 “상태를 해석하는 일”이 더 중요하더라고요.

    2. 핵심 개념 정리: Provider, Composition, Reconciliation

    Crossplane 장애 사례를 이해하려면 구조를 먼저 아주 간단히 잡고 가는 게 좋아요.

    개념 쉽게 말하면 운영 시 체크 포인트
    Provider 클라우드 API와 통신하는 드라이버 인증 정보, 권한, CRD 설치 상태
    Managed Resource 실제 클라우드 리소스와 매핑되는 객체 Ready 조건, 외부 이름, 이벤트
    Composition 여러 리소스를 묶는 설계도 패치, 참조, 필드 연결 오류
    Claim 사용자가 요청하는 추상화된 리소스 상위 상태는 정상인데 하위가 실패할 수 있음
    Reconciliation 원하는 상태로 계속 맞추는 루프 반복 에러, 드리프트, 재시도 패턴

    여기서 중요한 포인트! 겉으로 보이는 Claim이 멀쩡해 보여도 하위 Managed Resource가 실패 중일 수 있어요. 반대로 하위 리소스 하나가 계속 에러를 내면서 전체 Composition이 완료되지 않는 경우도 흔합니다. 저는 초반에 상위 객체만 보고 “왜 안 되지?” 하다가 삽질 좀 했습니다 ㅎㅎ

    3. 실전 구현: 기본 구성과 관찰 포인트

    아래 예시는 개념 설명용으로 단순화한 구조입니다. 특정 클라우드 벤더 기능을 깊게 파기보다는 Crossplane troubleshooting 흐름을 보는 데 집중하시면 됩니다.

    3-1. Provider와 인증 Secret 준비

    1. Crossplane과 Provider가 설치되어 있는지 확인하세요.
    2. 클라우드 인증 정보가 담긴 Secret(시크릿, 민감 정보 저장 객체)을 만듭니다.
    3. ProviderConfig(프로바이더 설정)가 Secret을 올바르게 참조하는지 봅시다.
    kubectl get pods -n crossplane-system
    kubectl get providers
    kubectl get providerconfigs
    kubectl get secrets -n crossplane-system

    제가 실제로 써보니까 첫 장애는 생각보다 단순했어요. 리소스 생성 로직이 아니라 인증 Secret 네임스페이스(namespace, 쿠버네티스 논리적 격리 단위)가 어긋나 있었거든요. 이벤트를 보기 전까지는 Composition 문제인 줄 알았습니다.

    apiVersion: v1
    kind: Secret
    metadata:
      name: cloud-creds
      namespace: crossplane-system
    type: Opaque
    stringData:
      creds: |
        {
          "example": "replace-with-real-credentials"
        }
    ---
    apiVersion: pkg.crossplane.io/v1
    kind: Provider
    metadata:
      name: example-provider
    spec:
      package: xpkg.example/provider
    ---
    apiVersion: example.crossplane.io/v1beta1
    kind: ProviderConfig
    metadata:
      name: default
    spec:
      credentials:
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: cloud-creds
          key: creds

    3-2. Composite Resource와 Claim 정의

    플랫폼 팀이 표준 리소스를 만들 때는 보통 Composition을 씁니다. 예를 들어 네트워크, 데이터베이스, 스토리지를 조합해서 하나의 “애플리케이션용 환경”처럼 제공하는 식이죠.

    apiVersion: apiextensions.crossplane.io/v1
    kind: Composition
    metadata:
      name: xappenvs.platform.example.org
    spec:
      compositeTypeRef:
        apiVersion: platform.example.org/v1alpha1
        kind: XAppEnv
      resources:
        - name: bucket
          base:
            apiVersion: storage.example.crossplane.io/v1beta1
            kind: Bucket
            spec:
              forProvider:
                region: us-east-1
              providerConfigRef:
                name: default
          patches:
            - fromFieldPath: "spec.parameters.region"
              toFieldPath: "spec.forProvider.region"
    apiVersion: platform.example.org/v1alpha1
    kind: AppEnv
    metadata:
      name: demo-appenv
    spec:
      parameters:
        region: us-east-1

    여기서부터는 단순 생성보다 관찰이 중요해요.

    kubectl get appenv
    kubectl describe appenv demo-appenv
    kubectl get managed
    kubectl get events --sort-by=.metadata.creationTimestamp
    Crossplane 장애 사례 분석용 Claim과 Managed Resource 연결 구조

    Claim에서 Composition을 거쳐 실제 Managed Resource가 생성되는 연결 관계를 보여주는 구성도입니다.

    4. Crossplane 장애 사례 1: 리소스는 생성됐는데 Ready가 안 올라오는 경우

    이 사례는 꽤 자주 봐요. 클라우드 콘솔에서는 리소스가 보이는데 쿠버네티스 쪽 상태는 계속 Creating 또는 NotReady에 머무는 거죠. 처음엔 “분명 만들어졌는데 왜 실패지?” 싶었습니다.

    제가 겪었던 원인은 크게 세 가지였어요.

    • 외부 리소스 식별자(external-name) 불일치
    • Provider 권한 부족
    • 후속 조회 API 실패

    Crossplane은 생성만 하는 게 아니라 이후에도 상태를 조회하고 맞춰야 해요. 그래서 create 권한만 있고 read 또는 describe 계열 권한이 빠져 있으면 리소스는 생겨도 Ready 조건이 정상으로 못 올라올 수 있거든요.

    kubectl describe <managed-resource-kind> <resource-name>
    kubectl get <managed-resource-kind> <resource-name> -o yaml

    이때 꼭 볼 부분은 아래입니다.

    1. status.conditions: Ready, Synced 상태가 어떻게 찍히는지
    2. metadata.annotations: external-name 같은 외부 식별자
    3. Events: API 호출 실패 메시지

    실제로 써보니까 “리소스가 존재하니 성공”이라고 보면 안 되더라고요. Crossplane은 상태 일치가 끝나야 진짜 성공이에요.

    5. Crossplane 장애 사례 2: Composition 패치 오류로 엉뚱한 값이 들어간 경우

    이건 진짜 많이 헷갈려요. Claim에 값을 넣었는데 하위 리소스에 반영이 안 되거나 전혀 다른 필드로 들어가는 경우죠. 문법 에러가 아니라서 더 무섭습니다. YAML은 적용됐는데 결과가 이상하거든요.

    제가 처음 삽질했던 포인트는 fromFieldPath와 toFieldPath 오타였어요. 한 글자만 틀려도 조용히 의도와 다르게 흘러갈 수 있습니다. 그리고 일부 필드는 하위 리소스 스키마에 실제로 존재해야 하니까 Crossplane 문제처럼 보여도 사실은 CRD 필드 구조를 잘못 이해한 경우도 많아요.

    kubectl get composition
    kubectl describe composition xappenvs.platform.example.org
    kubectl get xr
    kubectl describe xr <composite-resource-name>

    제가 정리한 확인 순서는 이렇습니다.

    1. Claim의 spec 값이 기대한 형태인지 확인하세요.
    2. Composite Resource(XR)에 값이 전달됐는지 봅시다.
    3. Managed Resource spec에 최종 반영됐는지 확인해요.
    4. 필드 타입이 문자열인지 배열인지, 맵인지 다시 봅시다.

    근데 여기서 중요한 건 Crossplane은 선언형이라 “중간 단계”를 하나씩 따라가야 한다는 점이에요. Terraform처럼 plan 출력 하나 보고 감 잡는 방식과는 결이 좀 다르더라고요.

    5-1. 문제를 줄이는 운영 팁

    • Composition 변수명 규칙을 팀 내에서 고정하세요.
    • region, size, class 같은 공통 필드는 네이밍을 통일해요.
    • 복잡한 패치는 처음부터 크게 만들지 말고 작은 단위로 검증합니다.
    • 변경 후에는 테스트용 Claim을 바로 적용해 이벤트를 확인하세요.
    Crossplane 장애 사례를 추적하는 이벤트 로그 기반 트러블슈팅 장면

    Crossplane 리소스 이벤트와 상태 조건을 보면서 원인을 추적하는 troubleshooting 상황을 표현한 이미지입니다.

    6. Crossplane 장애 사례 3: 멀티 클라우드 관리 환경에서 권한과 한도 이슈가 섞여 터진 경우

    멀티 클라우드 관리가 어려운 이유는 에러 형태가 벤더마다 다르게 보인다는 데 있어요. 어떤 곳은 권한 부족이 명확하게 찍히고, 어떤 곳은 rate limit(요청 제한)이나 quota(할당량) 문제처럼 보이다가 결국 재시도만 반복하기도 하거든요.

    제가 겪은 케이스는 이랬습니다. 한 클라우드에서는 리소스가 잘 만들어졌는데 다른 쪽은 동일한 Claim 패턴으로 계속 실패했어요. 처음엔 Composition 차이인 줄 알았는데 알고 보니 Provider가 쓰는 계정의 권한 범위가 환경마다 달랐습니다. 즉, 코드가 아니라 운영 계정 표준화가 문제였던 거죠.

    증상 겉으로 보이는 현상 실제 원인 후보
    계속 Pending 상위 Claim만 오래 대기 하위 Managed Resource 생성 실패
    반복 재시도 이벤트가 주기적으로 누적 권한 부족, API 제한, 잘못된 참조
    일부만 생성 네트워크는 되고 DB는 실패 Composition 내 특정 리소스 설정 누락
    삭제 지연 오브젝트는 지웠는데 외부 리소스 잔존 finalizer, 외부 API 에러, 종속성 문제

    이런 상황에서는 쿠버네티스 내부만 보면 안 돼요. 클라우드 측 감사 로그나 API 에러도 같이 봐야 합니다. 컨트롤 플레인이 하나라고 해서 장애 원인까지 하나로 줄어드는 건 아니더라고요. 이건 정말 운영하면서 체감했습니다.

    7. ⚠️ 삭제가 안 되는 장애: Finalizer와 외부 리소스 정리 실패

    개인적으로 제일 식은땀 나는 건 삭제 문제였어요. 리소스를 지웠는데 오브젝트가 계속 Terminating 상태로 남아 있고 외부 클라우드 리소스도 깔끔하게 정리되지 않는 경우요. 비용도 문제고 나중에 이름 충돌이나 의존성 꼬임으로 이어지기도 합니다.

    Crossplane은 finalizer(파이널라이저, 삭제 전에 정리 작업을 보장하는 메커니즘)를 사용해서 외부 리소스를 정리한 뒤 객체를 제거해요. 따라서 외부 API 호출이 실패하거나 참조 관계가 꼬이면 삭제가 길어질 수 있습니다.

    kubectl get <managed-resource-kind> <resource-name> -o yaml
    kubectl describe <managed-resource-kind> <resource-name>
    kubectl get events --sort-by=.metadata.creationTimestamp

    여기서 제가 배운 건 무작정 finalizer를 건드리면 안 된다는 점이에요. 물론 정말 예외적인 복구 상황은 있겠지만 먼저 확인해야 합니다.

    1. 외부 리소스가 실제로 삭제 가능한 상태인지 봅시다.
    2. 연결된 종속 리소스가 남아 있는지 확인하세요.
    3. Provider 권한이 delete와 observe까지 포함하는지 다시 봐요.
    4. 이벤트 로그에서 반복되는 에러 메시지를 확인하세요.

    ⚠️ 운영 팁: 삭제 장애는 생성 장애보다 복구 비용이 커요. 그래서 테스트 환경에서 생성뿐 아니라 삭제 시나리오까지 꼭 검증해야 합니다. 저도 예전엔 만드는 데만 집중했었는데 실제로는 지우는 흐름이 더 중요하더라고요.

    8. 검증과 결과: 어디까지 자동화됐는지 확인하는 방법

    문제를 고치고 나면 “이제 됐다”로 끝내면 안 돼요. 다시 같은 Crossplane 장애 사례가 반복되는지 봐야 하거든요. 저는 아래 체크리스트로 검증합니다.

    1. Claim 생성부터 Ready까지 걸리는 흐름을 확인하세요.
    2. 하위 Managed Resource가 모두 Synced 상태인지 봅시다.
    3. 외부 클라우드 콘솔에서도 리소스 속성이 기대값과 같은지 확인해요.
    4. 삭제 테스트까지 수행하세요.
    5. 이벤트 로그에 경고가 남는지 다시 봅니다.
    kubectl get appenv
    kubectl get xr
    kubectl get managed
    kubectl get events --sort-by=.metadata.creationTimestamp

    제가 직접 해보니 결과가 눈에 보이게 달라졌어요. 장애가 완전히 사라진다기보다 문제가 생겨도 어디를 봐야 하는지 감이 생긴다는 게 커요. 이건 운영 피로도를 많이 줄여줍니다. 특히 멀티 클라우드 관리 환경에서는 “도구를 더 넣는 것”보다 “상태를 읽는 기준을 팀이 공유하는 것”이 훨씬 중요하더라고요.

    Crossplane 장애 사례 해결 후 안정화된 멀티 클라우드 관리 결과

    문제 해결 후 Crossplane 리소스들이 Ready 상태로 정렬되고 운영 지표가 안정된 결과를 보여주는 이미지입니다.

    9. 정리: Crossplane은 편한 도구가 아니라, 잘 설계해야 편해지는 도구입니다

    정리해보면 Crossplane은 멀티 클라우드 관리와 인프라스트럭처 코드 표준화에 꽤 강력해요. 다만 처음 붙일 때 “쿠버네티스로 클라우드를 다룬다”는 멋진 그림만 보면 안 돼요. 실제 운영에선 Provider 인증, Composition 패치, 상태 조건, 삭제 흐름, 권한 범위 같은 현실 이슈가 계속 튀어나오거든요.

    저도 처음엔 Crossplane 장애 사례를 겪을 때마다 도구 자체를 의심했었는데 실제로 써보니까 대부분은 관찰 포인트 부족이나 운영 표준 미정리에서 시작하더라고요. 드디어 됐다! 싶은 순간도 있었고, 반대로 “왜 어제 되던 게 오늘 안 되지?” 하면서 로그만 한참 본 날도 있었어요. 근데 그런 삽질이 쌓이니까 구조가 보이더군요.

    • 💡 기억할 점 1: 상위 Claim만 보지 말고 XR과 Managed Resource까지 따라가세요.
    • 💡 기억할 점 2: 이벤트와 조건(status.conditions)은 가장 먼저 봐야 합니다.
    • 💡 기억할 점 3: 생성 성공보다 삭제 성공까지 확인해야 진짜 운영 준비가 끝납니다.
    • 💡 기억할 점 4: 멀티 클라우드 관리는 도구보다 표준화가 먼저에요.

    혹시 지금 Crossplane 장애 사례 때문에 막혀 계신가요? 그러면 가장 먼저 객체 계층을 위에서 아래로, 그리고 이벤트를 시간순으로 보시는 걸 추천드립니다. 이 흐름만 익혀도 troubleshooting 속도가 꽤 빨라집니다. 이전 글에서 다뤘던 쿠버네티스 운영 체크리스트와도 연결되는 이야기고 다음 글에서는 Composition 설계를 어떻게 단순화하면 장애를 줄일 수 있는지 이어서 다뤄볼 예정입니다.

    장애 원인 파악 순서와 운영 체크포인트를 한눈에 정리한 요약 인포그래픽 이미지입니다.

  • [DevOps] Argo CD vs Spinnaker: CI/CD 파이프라인 구축 비교 분석

    [DevOps] Argo CD vs Spinnaker: CI/CD 파이프라인 구축 비교 분석

    [DevOps] Argo CD vs Spinnaker: CI/CD 파이프라인 구축 비교 분석

    제가 현업이랑 홈랩에서 이것저것 굴려보면서 느낀 게 하나 있습니다. Argo CD Spinnaker 비교는 단순히 “어느 툴이 더 좋냐”의 문제가 아니더라고요. 팀이 Kubernetes를 어디까지 표준으로 쓰고 있는지, 배포 승인을 얼마나 엄격하게 가져가는지, 그리고 운영자가 원하는 가시성(visibility)이 어느 정도인지에 따라 답이 꽤 달라집니다. CI/CD 파이프라인을 처음 설계할 때 이 부분을 대충 잡고 들어가면, 나중에 배포 흐름이 꼬여서 삽질 좀 하게 됩니다 ㅎㅎ

    특히 지속적 배포(Continuous Delivery)를 도입하려는 팀이라면 더 그렇습니다. 처음엔 저도 “둘 다 배포 툴 아닌가?” 싶었는데, 실제로 써보니까 운영 철학이 꽤 다르더라고요. 오늘은 클라우드 네이티브 환경에서 Argo CD와 Spinnaker를 어떻게 봐야 하는지, 어디서 갈리는지, 실전에서는 어떻게 접근하면 되는지를 경험 섞어서 정리해보겠습니다.

    Argo CD Spinnaker 비교 아키텍처 다이어그램

    Argo CD의 GitOps 흐름과 Spinnaker의 파이프라인 중심 배포 흐름을 한 장으로 비교하는 개요 이미지입니다.

    1. 왜 Argo CD vs Spinnaker 비교가 중요한가

    배포 자동화는 이제 선택이 아니라 기본이죠. 그런데 자동화라고 다 같은 자동화가 아닙니다. 어떤 팀은 Git 저장소만 바꾸면 자동 반영되는 구조가 편하고, 어떤 팀은 승인 단계, 카나리 배포(일부 트래픽만 먼저 보내는 배포), 멀티 클라우드 전략이 더 중요하거든요.

    여기서 중요한 포인트가 있습니다. Argo CD는 GitOps(Git을 단일 진실 원본으로 삼는 운영 방식)에 굉장히 강한 도구이고, Spinnaker는 복잡한 배포 파이프라인과 멀티 클라우드 전달 흐름에 강한 플랫폼이라는 점입니다. 이 차이를 모르고 고르면, 도입 초반엔 쉬워 보여도 운영이 무거워지거나 반대로 필요한 통제가 안 붙는 상황이 생깁니다.

    • Git 변경 기반 자동 동기화가 중요하다면 Argo CD 쪽이 잘 맞습니다.
    • 복수 환경 승인, 배포 전략, 외부 시스템 연동이 많다면 Spinnaker가 더 자연스럽습니다.
    • Kubernetes 중심인지, 여러 배포 타깃이 섞여 있는지도 판단 기준입니다.

    2. 개념부터 쉽게: Argo CD와 Spinnaker는 어떻게 다를까

    2-1. Argo CD: 선언형 배포와 GitOps 중심

    쉽게 말해 Argo CD는 “클러스터 상태는 Git에 적어둔 그대로여야 한다”는 철학으로 움직입니다. Git 저장소 안의 YAML 매니페스트(manifest, 리소스 정의 파일)나 Helm 차트(Chart, 쿠버네티스 패키지)를 보고, 실제 클러스터 상태와 비교한 뒤 차이가 있으면 맞춰주는 방식이죠.

    제가 직접 해보니 이 방식의 장점은 정말 명확했습니다. 배포 이력이 Git 커밋으로 남고, 누가 뭘 바꿨는지 추적하기가 편합니다. 드리프트(drift, 선언한 상태와 실제 상태가 어긋나는 현상)도 눈에 잘 보이고요. 대신 애플리케이션 전달 과정 전체를 설계하는 엔진이라기보다는, Kubernetes 배포 상태를 Git 기준으로 맞추는 데 최적화된 도구에 가깝습니다.

    2-2. Spinnaker: 파이프라인 오케스트레이션 중심

    Spinnaker는 접근이 조금 다릅니다. 이쪽은 “어떤 조건에서 어떤 순서로 배포를 진행할지”를 세밀하게 다루는 데 강합니다. 예를 들면 빌드 완료 후 테스트 실행, 승인, 스테이징 배포, 카나리 분석, 운영 반영 같은 흐름을 하나의 파이프라인으로 묶는 식이죠.

    처음엔 이게 뭔가 싶었는데, 실제로 써보니까 대규모 환경이나 규정이 많은 조직에서 왜 Spinnaker를 선호하는지 이해가 되더라고요. 반면 구성 요소가 많고 운영 복잡도도 꽤 있습니다. 작은 팀이 가볍게 시작하기엔 다소 무겁다고 느낄 수 있습니다.

    2-3. 핵심 차이 한눈에 보기

    항목 Argo CD Spinnaker
    핵심 철학 GitOps 기반 선언형 동기화 파이프라인 기반 지속적 배포
    주요 타깃 Kubernetes 중심 멀티 클라우드 및 다양한 배포 전략
    운영 복잡도 상대적으로 단순 상대적으로 높음
    강점 상태 일치, 변경 추적, Git 연동 승인 흐름, 카나리, 복합 파이프라인
    적합한 팀 플랫폼 표준이 Kubernetes인 팀 배포 정책과 절차가 복잡한 팀

    3. 어떤 팀에 어떤 도구가 맞는가

    이 부분은 제가 후배들한테도 자주 이야기하는데요, 도구를 고를 때 기능 목록보다 운영 모델을 먼저 봐야 합니다.

    • Argo CD가 잘 맞는 경우: Git 기반 운영을 표준화하고 싶을 때, Kubernetes 리소스가 배포의 중심일 때, 운영 단순성이 중요할 때
    • Spinnaker가 잘 맞는 경우: 승인 단계가 많을 때, 여러 클라우드나 배포 전략을 함께 다룰 때, 파이프라인 오케스트레이션이 핵심일 때
    • 둘을 함께 보는 경우: CI는 다른 툴에서 처리하고, CD만 역할 분리해서 설계할 때

    사실 Argo CD Spinnaker 비교에서 제일 많이 놓치는 지점이 여기입니다. 둘은 완전히 같은 문제를 푸는 경쟁 제품이라기보다, 겹치는 영역이 있으면서도 중심축이 다른 도구라고 보는 게 더 정확합니다.

    4. 실전 구현: Argo CD로 GitOps형 CI/CD 파이프라인 구성

    먼저 Argo CD 쪽부터 보겠습니다. 홈랩에서 테스트할 때 저는 보통 “애플리케이션 매니페스트를 Git에 넣고, Argo CD가 자동 동기화하게 만드는 흐름”으로 검증합니다. 이 구조는 이해도 쉽고, 실무 전환도 빠르거든요.

    4-1. Argo CD 설치

    1. 전용 네임스페이스(namespace, Kubernetes 논리 분리 공간)를 만듭니다.
    2. 공식 설치 매니페스트를 적용합니다.
    3. 초기 관리자 비밀번호를 확인하고 UI에 접속합니다.
    kubectl create namespace argocd
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    kubectl get pods -n argocd
    kubectl port-forward svc/argocd-server -n argocd 8080:443

    여기서 저는 처음에 포트포워딩(port-forwarding, 로컬 포트를 클러스터 서비스에 연결)만 열어놓고 왜 접속이 불안정하지 싶었는데, 로컬 브라우저 인증서 경고를 그냥 넘기지 않아서 그런 경우가 있더라고요. 사소한데 은근 많이 막힙니다.

    4-2. Git 저장소에 애플리케이션 매니페스트 준비

    Argo CD의 핵심은 Git이기 때문에, 배포 대상 매니페스트가 먼저 있어야 합니다. 가장 단순한 Deployment(디플로이먼트, 파드 복제와 롤링 업데이트 관리)와 Service(서비스, 네트워크 노출 객체) 예시는 아래처럼 둘 수 있습니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: demo-nginx
      namespace: demo
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: demo-nginx
      template:
        metadata:
          labels:
            app: demo-nginx
        spec:
          containers:
            - name: nginx
              image: nginx:stable
              ports:
                - containerPort: 80
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: demo-nginx
      namespace: demo
    spec:
      selector:
        app: demo-nginx
      ports:
        - port: 80
          targetPort: 80

    4-3. Argo CD Application 리소스 생성

    이제 Argo CD에 “어느 Git 저장소의 어느 경로를 어느 클러스터 네임스페이스에 반영할지” 알려주면 됩니다. 이 리소스가 사실상 배포 선언문 역할을 합니다.

    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: demo-nginx
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://github.com/example-org/example-manifests.git
        targetRevision: main
        path: apps/demo-nginx
      destination:
        server: https://kubernetes.default.svc
        namespace: demo
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
    kubectl apply -f application.yaml
    kubectl get applications -n argocd
    Argo CD Spinnaker 비교 중 Argo CD 동기화 설정 화면

    Git 저장소 경로, 대상 네임스페이스, 자동 동기화 옵션이 설정된 Argo CD Application 화면을 보여주는 위치입니다.

    여기서 prune은 Git에서 제거된 리소스를 클러스터에서도 정리하는 옵션이고, selfHeal은 누군가 클러스터에서 수동 변경해도 Git 기준으로 다시 되돌리는 기능입니다. 이거 진짜 편하더라고요. 운영자가 많아질수록 체감됩니다.

    5. 실전 구현: Spinnaker로 파이프라인 중심 배포 설계

    이번엔 Spinnaker 관점입니다. Spinnaker는 단순히 매니페스트를 맞추는 느낌보다, 배포 절차를 단계적으로 묶는 쪽에 가깝습니다. 그래서 예시도 “배포 흐름 설계” 관점으로 보는 게 이해가 쉽습니다.

    5-1. 추천 파이프라인 흐름

    1. CI 도구에서 이미지 빌드 및 레지스트리 푸시
    2. Spinnaker가 새 이미지 태그 감지
    3. 스테이징 환경 배포
    4. 수동 승인(Manual Judgment)
    5. 운영 환경 반영

    Spinnaker의 장점은 바로 이 지점입니다. 예를 들어 조직 정책상 운영 배포 전에 승인 절차가 꼭 필요하다면, Argo CD만으로는 별도 설계가 필요했던 부분을 Spinnaker는 비교적 자연스럽게 파이프라인에 녹일 수 있습니다.

    5-2. 파이프라인 단계 예시

    단계 설명 운영 포인트
    Trigger 이미지 변경 또는 이벤트 감지 CI와 연결 구조를 명확히 해야 함
    Deploy to Staging 스테이징 환경 우선 배포 운영과 최대한 동일한 조건 유지
    Manual Judgment 사람 승인 후 다음 단계 진행 승인 기준을 문서화해야 혼선이 적음
    Deploy to Prod 운영 환경 반영 롤백 기준과 모니터링 연동 중요

    실제로 써보니까 Spinnaker는 “배포 파이프라인을 플랫폼 차원에서 관리하고 싶다”는 팀에 꽤 매력적입니다. 대신 구성요소가 많아서 관리 비용이 올라갑니다. 작은 팀이면 이 장점이 부담으로 바뀌기도 하더라고요.

    6. ⚠️ 주의사항과 트러블슈팅: 제가 실제로 많이 부딪힌 포인트

    6-1. Argo CD에서 자주 겪는 문제

    • 드리프트 오해: 운영자가 클러스터에서 급한 수정 후 Git 반영을 빼먹으면 Argo CD가 다시 덮어씁니다.
    • 권한 문제: 네임스페이스 생성이나 특정 리소스 적용 시 RBAC(Role-Based Access Control, 역할 기반 권한 제어) 때문에 막히는 경우가 많습니다.
    • Helm 값 충돌: values 파일과 환경별 override가 섞이면 실제 반영값 추적이 어려워집니다.

    저도 처음엔 selfHeal이 멋져 보여서 다 켜놨었는데, 운영자가 수동 조치한 내용을 바로 되돌려버려서 당황한 적이 있습니다. 그래서 지금은 긴급 변경은 Git에 먼저 반영이라는 팀 규칙을 꼭 같이 둡니다.

    6-2. Spinnaker에서 자주 겪는 문제

    • 구성 복잡도: 서비스 수와 설정 포인트가 많아 초반 진입장벽이 있습니다.
    • 파이프라인 관리 비용: 애플리케이션이 늘어나면 표준화하지 않은 파이프라인이 금방 복잡해집니다.
    • 운영 관찰성 확보: 어느 단계에서 실패했는지 추적하려면 로그와 모니터링 체계를 같이 잡아야 합니다.

    근데 여기서 중요한 포인트! Spinnaker 자체가 나쁘다는 뜻이 아니라, 요구사항보다 플랫폼이 더 커지면 운영 피로도가 급격히 올라간다는 이야기입니다. 특히 팀 규모가 작을수록요.

    Argo CD Spinnaker 비교를 위한 배포 파이프라인 승인 흐름 이미지

    스테이징 배포, 수동 승인, 운영 반영으로 이어지는 Spinnaker 스타일의 파이프라인 흐름을 설명하는 이미지입니다.

    7. 검증과 결과: 어떤 선택이 더 현실적이었나

    제가 여러 환경에서 비교해보니 결과는 꽤 분명했습니다. Kubernetes가 배포 표준이고, Git 중심 운영 문화를 만들고 싶다면 Argo CD가 훨씬 빠르게 자리 잡습니다. 반대로 배포 절차가 길고 승인, 검증, 단계별 제어가 중요하다면 Spinnaker가 더 잘 맞습니다.

    검증할 때는 보통 아래 항목을 봤습니다.

    1. Git 변경 후 실제 반영까지 흐름이 단순한가
    2. 실패했을 때 원인 추적이 쉬운가
    3. 롤백(rollback, 이전 정상 상태로 되돌리기)이 명확한가
    4. 운영자 수가 늘어도 관리 규칙이 유지되는가

    Argo CD는 상태 비교가 직관적이라 운영 중 안심이 되더라고요. 반면 Spinnaker는 파이프라인 단계가 많을수록 통제력은 좋지만, 초반 설계 퀄리티가 정말 중요했습니다. 이건 진짜 경험 차이입니다.

    kubectl get applications -n argocd
    kubectl get pods -n demo
    kubectl rollout status deployment/demo-nginx -n demo

    위 명령으로 Argo CD 애플리케이션 상태, 실제 파드 상태, 롤아웃 완료 여부를 확인할 수 있습니다. 지속적 배포 환경에서는 “배포했다”보다 “원하는 상태로 수렴했는가”를 보는 게 더 중요하거든요.

    Argo CD Spinnaker 비교 결과를 보여주는 배포 검증 대시보드

    애플리케이션이 Synced, Healthy 상태인지와 실제 배포 결과를 함께 보여주는 검증용 이미지 자리입니다.

    8. Argo CD vs Spinnaker 최종 정리

    질문 추천
    우리는 Kubernetes 중심으로 단순하고 강한 CD가 필요한가? Argo CD
    GitOps 운영 모델을 팀 표준으로 만들고 싶은가? Argo CD
    승인, 카나리, 복합 배포 절차가 핵심인가? Spinnaker
    멀티 환경과 복잡한 전달 흐름을 한 플랫폼에서 통제하고 싶은가? Spinnaker

    짧게 정리하면 이렇습니다. Argo CD는 “원하는 상태를 Git에 적고 맞춰나가는 도구”이고, Spinnaker는 “배포 절차를 정교하게 설계하고 흘려보내는 플랫폼”입니다. 둘 다 훌륭하지만, 잘 맞는 환경이 다릅니다.

    혹시 이런 경험 있으신가요? 배포 자동화를 시작했는데, CI/CD 파이프라인이 오히려 더 복잡해져서 손이 더 많이 가는 상황이요. 저도 처음엔 헷갈렸는데, 결국 답은 기능 수보다 운영 방식에 있었습니다.

    Argo CD Spinnaker 비교 선택 기준 요약 인포그래픽

    팀 규모, Kubernetes 의존도, 승인 절차 복잡도에 따라 어떤 도구가 맞는지 요약한 비교 인포그래픽입니다.

    9. 마무리: 지금 시작한다면 저는 이렇게 고릅니다

    만약 지금 새로 시작하는 팀이고, 이미 Kubernetes를 기본 플랫폼으로 쓰고 있다면 저는 Argo CD부터 검토할 것 같습니다. 이유는 분명합니다. 도입 속도가 빠르고, GitOps 문화를 만들기 좋고, 운영 단순성도 꽤 좋거든요.

    반대로 조직 규모가 크고, 배포 승인과 전달 절차가 중요한 팀이라면 Spinnaker 쪽이 더 설득력 있습니다. 다만 이 경우엔 플랫폼 운영 책임까지 함께 고려해야 합니다. 단순히 배포 기능만 보고 들어가면 나중에 힘들 수 있습니다.

    다음 글에서는 Argo CD 기반으로 CI와 CD를 분리해서 운영하는 패턴, 예를 들어 GitHub Actions 같은 CI 도구와 연결하는 방식도 다뤄볼 예정입니다. 이전 글에서 다뤘던 Kubernetes 배포 기본 흐름과 함께 보시면 더 이해가 잘 되실 겁니다.

    자주 묻는 질문

    Argo CD가 CI까지 대체하나요?

    보통은 아닙니다. Argo CD는 CD에 더 가깝고, 빌드와 테스트는 별도 CI 도구가 맡는 경우가 많습니다.

    Spinnaker는 Kubernetes 환경에서만 쓰나요?

    아닙니다. 다만 이 글에서는 클라우드 네이티브와 Kubernetes 중심 관점에서 설명했습니다.

    둘 중 하나만 꼭 골라야 하나요?

    반드시 그렇진 않습니다. 팀 구조와 기존 플랫폼에 따라 역할을 나눠 설계하는 경우도 있습니다.

  • [k8s] ArgoCD 도입 실패 사례 분석: GitOps 전환 시 흔한 실수와 방지 전략

    [k8s] ArgoCD 도입 실패 사례 분석: GitOps 전환 시 흔한 실수와 방지 전략

    [DevOps] ArgoCD 도입 실패 사례 분석과 GitOps 실수 방지

    ArgoCD 도입 실패 이야기는 생각보다 흔합니다. 저도 처음엔 GitOps(깃옵스, Git을 단일 진실 공급원으로 삼는 운영 방식)만 붙이면 배포가 깔끔해질 줄 알았거든요. 그런데 실제로 해보니, ArgoCD(아르고CD, Kubernetes 선언형 배포 도구)를 넣는 순간 오히려 배포가 더 복잡해지는 팀도 많았습니다. 특히 CI/CD 전환 사례를 보면, 기술 문제가 아니라 역할 분리, 저장소 구조, 승인 흐름 같은 운영 설계에서 먼저 무너지는 경우가 많더라고요. 이번 글은 제가 현업과 홈랩에서 반복해서 본 ArgoCD 도입 실패 패턴을 중심으로, 왜 그런 일이 생기는지와 어떻게 피해야 하는지를 정리해보겠습니다.

    혹시 이런 경험 있으신가요? 배포 자동화는 넣었는데 누가 어떤 값을 바꿨는지 더 헷갈리고, 장애가 나면 Git이 문제인지 클러스터가 문제인지부터 추적하게 되는 상황 말입니다. 여기서 중요한 포인트는, ArgoCD 자체가 문제라기보다 GitOps 실수가 누적되면서 운영 복잡도가 폭발한다는 점입니다.

    ArgoCD 도입 실패와 GitOps 전환 흐름을 설명하는 아키텍처 이미지

    Git 저장소, CI 파이프라인, ArgoCD, Kubernetes 클러스터 사이의 흐름과 실패 포인트를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 ArgoCD 도입 실패가 반복될까요?

    쉽게 말해 ArgoCD는 배포 버튼을 없애는 도구가 아니라, 변경 관리 방식 자체를 바꾸는 도구입니다. 그래서 예전 CI/CD에서는 잘 통하던 습관이 GitOps로 오면 바로 문제를 만듭니다. 예를 들면 이런 식입니다.

    • 운영값을 Git이 아니라 사람 손으로 클러스터에서 직접 수정함
    • 애플리케이션 코드 저장소와 배포 매니페스트 저장소의 책임이 불명확함
    • 자동 동기화(auto-sync)를 켰는데 승인 절차는 그대로 수동 운영을 기대함
    • Helm(헬름, Kubernetes 패키지 관리 도구) 값 파일이 환경별로 제멋대로 늘어남
    • 장애가 나도 누가 마지막 변경을 만들었는지 추적이 어려움

    저도 처음엔 이게 뭔가 싶었는데, 결국 핵심은 하나였습니다. ArgoCD는 배포 도구이면서 동시에 운영 규율을 강제하는 도구라는 점입니다. 팀이 그 규율을 합의하지 않은 상태에서 도입하면, ArgoCD 문제점처럼 보이는 현상이 사실은 프로세스 문제로 터집니다.

    2. GitOps와 ArgoCD를 아주 쉽게 설명해보면

    GitOps는 “실제 운영 상태는 Git에 적힌 선언대로 맞춘다”는 철학입니다. ArgoCD는 그 선언과 클러스터 상태를 계속 비교해서 drift(드리프트, 선언과 실제 상태가 어긋난 상태)를 감지하고 맞춰주는 역할을 하죠.

    예전 방식은 대체로 이랬습니다. CI 서버가 빌드하고, 누군가 kubectl(쿠버네티스 CLI)이나 Helm으로 운영 클러스터에 직접 배포합니다. 반면 GitOps 방식은 배포 변경도 Pull Request(PR, 변경 제안 요청)로 남고, 승인 후 Git이 바뀌면 ArgoCD가 그걸 따라갑니다. 감사 추적(audit trail, 변경 이력 추적)이 되는 대신, 우회 수정이 어려워집니다. 이 점이 장점이자 초반 저항 포인트입니다.

    항목 기존 CI/CD GitOps + ArgoCD
    배포 트리거 파이프라인 또는 운영자 수동 실행 Git 변경 반영
    변경 이력 파이프라인 로그 중심 Git 커밋과 PR 중심
    긴급 수정 클러스터에서 직접 수정 가능 직접 수정 시 drift 발생
    권한 통제 클러스터 권한 중심 저장소 권한과 승인 흐름 중요
    실패 원인 스크립트 불안정, 환경 차이 저장소 구조, 책임 분리 실패

    그래서 ArgoCD 도입 실패를 줄이려면 설치보다 운영 모델을 먼저 설계해야 합니다. 이걸 건너뛰면 나중에 진짜 많이 돌아갑니다. 저도 삽질 좀 했습니다 ㅎㅎ

    3. 실제로 많이 본 실패 사례 4가지

    3-1. 저장소 구조를 너무 늦게 정한 경우

    가장 흔한 실수입니다. 앱 소스와 배포 설정이 한 저장소에 섞여 있거나, 반대로 너무 잘게 쪼개져서 어디를 기준으로 봐야 하는지 모호한 경우죠. 처음엔 편해 보였는데, 팀이 커질수록 충돌이 심해집니다. 누가 이미지 태그를 관리하고, 누가 replica 수를 바꾸고, 누가 Ingress(인그레스, 외부 트래픽 진입점)를 수정하는지 경계가 안 잡히거든요.

    3-2. 자동 동기화만 켜고 승인 체계를 안 만든 경우

    auto-sync는 정말 편합니다. 드디어 됐다! 싶은 순간이 오거든요. 근데 여기서 바로 사고가 납니다. 운영 반영 전 검토가 필요한 팀인데도 자동 배포를 먼저 켜버리면, 잘못된 값 하나가 바로 프로덕션으로 갑니다. 도구는 빨라졌는데 프로세스는 그대로인 상태죠.

    3-3. Secret 관리 방식을 정하지 않은 경우

    GitOps 실수에서 빠지지 않는 항목입니다. 민감 정보(Secret)를 평문으로 저장하면 안 되고, 그렇다고 운영자가 클러스터에만 몰래 만들어두면 Git과 실제 상태가 분리됩니다. 저는 이 부분에서 팀마다 가장 오래 멈추는 걸 많이 봤습니다. Sealed Secrets(시일드 시크릿)나 External Secrets Operator 같은 접근을 검토하되, 핵심은 “Git에 무엇을 남기고 실제 값은 어디서 주입할지”를 팀 단위로 합의하는 겁니다.

    3-4. Drift를 장애로 볼지, 운영 유연성으로 볼지 합의가 없는 경우

    운영자가 급한 패치를 직접 넣는 문화가 남아 있으면 ArgoCD는 계속 OutOfSync(아웃오브싱크) 상태를 만들 겁니다. 이걸 경고로 볼지, 바로 되돌릴지, 특정 리소스는 예외로 둘지 정하지 않으면 경보만 쌓이고 신뢰가 깨집니다. 이 지점이 대표적인 ArgoCD 문제점처럼 보이는데, 사실은 정책 부재에 가깝습니다.

    4. 실전 구현: 실패 확률을 낮추는 최소 전환 절차

    제가 직접 해보니, 처음부터 전 서비스에 GitOps를 거는 것보다 작은 서비스 하나로 운영 규칙을 검증하는 게 훨씬 낫더라고요. 아래는 많이 무리하지 않는 최소 절차입니다.

    1. 배포 대상 네임스페이스(namespace) 하나를 파일럿으로 고릅니다.
    2. 애플리케이션 코드 저장소와 배포 매니페스트 저장소의 책임을 분리합니다.
    3. ArgoCD 프로젝트(AppProject)로 허용 대상 클러스터/네임스페이스를 제한합니다.
    4. 자동 동기화는 바로 켜지 말고, 먼저 수동 sync로 운영 흐름을 익힙니다.
    5. 변경 승인 기준을 PR 템플릿과 리뷰 규칙으로 문서화합니다.
    6. Drift 발생 시 대응 절차를 정합니다.

    예시로는 아래처럼 시작하면 무난합니다.

    kubectl create namespace argocd
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    kubectl get pods -n argocd

    설치 자체보다 더 중요한 건 프로젝트 경계를 먼저 두는 일입니다.

    apiVersion: argoproj.io/v1alpha1
    kind: AppProject
    metadata:
      name: sample-project
      namespace: argocd
    spec:
      description: sample project for controlled GitOps rollout
      sourceRepos:
        - 'https://github.com/example/platform-manifests.git'
      destinations:
        - namespace: sample-app
          server: 'https://kubernetes.default.svc'
      clusterResourceWhitelist:
        - group: '*'
          kind: '*'

    그 다음 애플리케이션은 이렇게 붙일 수 있습니다.

    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: sample-app
      namespace: argocd
    spec:
      project: sample-project
      source:
        repoURL: 'https://github.com/example/platform-manifests.git'
        targetRevision: main
        path: apps/sample-app/overlays/prod
      destination:
        server: 'https://kubernetes.default.svc'
        namespace: sample-app
      syncPolicy: {}

    여기서 일부러 자동 동기화 설정을 비워둔 것이 포인트입니다. 처음엔 수동 sync로 팀이 변경 흐름을 이해하게 만드는 게 좋습니다. 자동화는 익숙해진 뒤에 켜도 늦지 않습니다.

    ArgoCD 도입 실패를 줄이기 위한 AppProject와 저장소 구조 구성 이미지

    AppProject 경계, Application 연결, PR 승인 후 반영되는 흐름을 시각적으로 설명하는 이미지입니다.

    5. CI/CD 전환 사례에서 특히 조심해야 할 체크포인트

    기존 파이프라인에서 이미지 빌드까지는 잘 돌아가는데 GitOps로 넘기면서 꼬이는 경우가 많습니다. 보통 CI는 artifact(아티팩트, 배포 가능한 결과물)를 만들고, CD는 배포를 실행합니다. GitOps에선 이 경계가 바뀝니다. CI가 직접 배포하지 않고, 이미지 태그나 차트 값을 Git에 반영하는 식으로 역할이 이동하죠.

    예를 들면 이런 방식입니다.

    # CI job example
    export IMAGE_TAG=${GIT_COMMIT_SHA}
    sed -i "s/tag: .*/tag: ${IMAGE_TAG}/" apps/sample-app/overlays/prod/values.yaml
    git add apps/sample-app/overlays/prod/values.yaml
    git commit -m "chore: deploy sample-app ${IMAGE_TAG}"
    git push origin main

    이 방식은 단순하지만, 바로 main 브랜치에 반영하면 위험합니다. 제가 추천하는 건 별도 배포 브랜치나 PR 자동 생성 방식입니다. 사람이 마지막으로 한 번 더 보고 머지하는 흐름이 초반 안정화에 꽤 도움이 됩니다.

    • 프로덕션 반영은 PR 승인 후에만 가능하게 설정
    • 리뷰어를 플랫폼 담당자와 서비스 담당자로 분리
    • 롤백 기준을 Git revert 중심으로 문서화
    • 수동 kubectl 적용은 예외 상황에서만 허용

    이런 기본선만 있어도 CI/CD 전환 사례에서 실패 확률이 꽤 내려갑니다.

    6. ⚠️ 트러블슈팅: 제가 실제로 많이 본 문제와 해결법

    여기서부터는 정말 많이 부딪히는 부분들입니다. 처음엔 저도 “왜 sync는 성공인데 앱은 안 뜨지?” 같은 상황을 자주 만났거든요.

    증상 원인 해결 방향
    OutOfSync가 계속 발생 운영자가 클러스터에서 직접 수정 직접 수정 금지 원칙 정리, 예외 리소스만 ignore 설정 검토
    Sync 성공인데 서비스 장애 매니페스트 문법은 맞지만 런타임 의존성 누락 readinessProbe, Secret 참조, ConfigMap 값 검증 강화
    배포가 너무 자주 일어남 이미지 태그나 공통 값 파일이 과도하게 변경됨 환경별 경로 분리, 변경 범위 최소화
    리뷰 병목 발생 모든 변경이 한 저장소에 몰림 팀 단위 경계 재설계, CODEOWNERS 활용
    롤백이 헷갈림 Git revert와 수동 핫픽스가 섞임 롤백 절차를 Git 기준으로 단일화

    6-1. ignoreDifferences 남용

    ArgoCD에는 특정 필드 차이를 무시하는 기능이 있습니다. 편하긴 한데, 이걸 남용하면 drift 감지 의미가 사라집니다. 정말 컨트롤러가 자동으로 바꾸는 필드처럼 불가피한 경우에만 제한적으로 쓰는 게 맞습니다.

    6-2. Helm 값 파일 난립

    환경이 늘수록 values 파일이 너무 많아집니다. dev, stage, prod는 그렇다 쳐도 팀별 패치, 긴급 패치, 지역별 패치가 늘어나면 나중에 아무도 구조를 설명 못 합니다. 저도 홈랩에서 비슷하게 풀었다가, 결국 Kustomize(커스터마이즈, 오버레이 기반 설정 관리)와 역할을 분리하면서 정리했었습니다.

    6-3. 알림 없이 운영

    Sync 실패나 Health degraded(헬스 저하) 이벤트를 아무도 못 받으면, ArgoCD UI를 매번 열어보기 전까지 장애를 놓치게 됩니다. 알림 체계는 초반부터 붙이는 게 좋습니다. 최소한 운영 채널로 실패 이벤트는 전달되게 구성해두세요.

    7. 검증: 전환이 잘 되고 있는지 어떻게 확인할까

    성공 기준도 미리 정의해야 합니다. 그냥 “ArgoCD가 떴다”로 끝내면 안 됩니다. 저는 보통 아래 항목으로 봅니다.

    1. 배포 변경이 모두 PR과 커밋으로 추적되는가
    2. 직접 클러스터 수정 비율이 줄고 있는가
    3. 장애 시 마지막 변경점 확인 시간이 짧아졌는가
    4. 롤백 절차가 Git revert 기준으로 일관되게 동작하는가
    5. 서비스별 소유권(owner)이 저장소 구조와 일치하는가

    ArgoCD UI에서 Health, Sync 상태를 보는 것도 좋지만, 그것만으론 부족합니다. 진짜 중요한 건 운영팀과 개발팀이 같은 변경 이력을 보고 같은 언어로 이야기하게 됐는지입니다. 이게 되면 도입이 절반은 성공한 겁니다. 실제로 써보니까 배포 속도보다 커뮤니케이션 비용이 더 크게 줄더라고요.

    ArgoCD 도입 실패 검증을 위한 Sync와 Health 상태 대시보드 이미지

    배포 상태 확인, 이상 징후 탐지, 롤백 판단 포인트를 한눈에 보여주는 결과 검증 이미지입니다.

    8. 정리 FAQ: 많이 받는 질문

    Q1. ArgoCD는 무조건 자동 동기화로 써야 하나요?

    아닙니다. 초반에는 수동 sync가 오히려 안전합니다. 팀이 GitOps 흐름에 익숙해지고 승인 절차가 자리 잡으면 그때 auto-sync를 검토해도 됩니다.

    Q2. 기존 CI/CD를 전부 버려야 하나요?

    그건 아닙니다. 빌드와 테스트는 CI가 계속 담당하고, 배포 실행만 Git 기반으로 넘기는 식이 일반적입니다.

    Q3. ArgoCD 문제점은 결국 도구 한계 아닌가요?

    일부는 맞지만, 현장에서 보이는 대부분은 정책과 구조 문제였습니다. 특히 저장소 책임 분리와 승인 체계가 약하면 같은 문제가 반복됩니다.

    Q4. 작은 팀도 GitOps가 필요할까요?

    작은 팀도 필요할 수 있습니다. 다만 처음부터 무겁게 가지 말고, 단일 서비스와 단순한 저장소 구조로 시작하는 편이 좋습니다.

    ArgoCD 도입 실패 방지 전략과 GitOps 실수 비교 요약 이미지

    도입 전후 비교, 실패 패턴, 예방 전략을 요약한 인포그래픽 형태의 정리 이미지입니다.

    9. 마무리: ArgoCD 도입 실패를 줄이는 진짜 핵심

    ArgoCD 도입 실패는 대개 설치 실패가 아니라 운영 모델 설계 실패입니다. GitOps는 도구를 붙이는 프로젝트가 아니라, 변경을 기록하고 승인하고 되돌리는 방식을 다시 정의하는 작업이거든요. 저도 처음엔 UI가 예쁘고 sync가 자동으로 돌아가니까 금방 안정화될 줄 알았는데, 실제론 저장소 구조와 팀 규칙을 먼저 잡아야 효과가 났습니다.

    정리하면 이렇습니다. GitOps 실수를 줄이려면 작은 범위로 시작하고, 수동 sync로 흐름을 익히고, 저장소 책임과 승인 규칙을 먼저 고정해야 합니다. 그리고 drift, Secret, 롤백 기준을 문서로 남겨야 합니다. 이 4가지만 해도 현장에서 체감 차이가 큽니다.

    다음 글에서는 App of Apps 패턴과 멀티 클러스터 운영에서 어디까지 표준화해야 하는지 다뤄볼 예정입니다. 이전 글에서 다뤘던 Kubernetes 배포 기본기와 함께 보시면 흐름이 더 잘 잡히실 겁니다. 혹시 지금 ArgoCD 도입 실패를 겪고 계시다면, 설치 로그보다 먼저 운영 규칙부터 점검해보세요. 그게 생각보다 훨씬 빠른 지름길입니다. 🎉

  • [k8s] Rancher 2.x에서 차세대로 마이그레이션: 실제 경험과 주요 변경점

    [k8s] Rancher 2.x에서 차세대로 마이그레이션: 실제 경험과 주요 변경점

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘은 많은 분들이 고민하고 계실 법한 주제, 바로 Rancher 2.x 환경을 차세대 쿠버네티스 관리 아키텍처로 전환하는 경험에 대해 이야기해볼게요. 제목에는 ‘Rancher 3.x’라는 표현을 썼지만, 사실 직접적인 ‘Rancher 3.x’라는 버전이 명확하게 출시된 것은 아닙니다. 대신, Rancher를 활용한 쿠버네티스 관리 방식이 점차 진화하면서, 기존 2.x 시절의 방식과는 크게 달라진 미래 지향적인 아키텍처로의 전환 과정을 ‘3.x 마이그레이션’이라는 개념으로 풀어보려고 해요. 즉, 단순히 버전을 올리는 것을 넘어, 더 효율적이고 안정적인 쿠버네티스 운영을 위한 근본적인 변화를 고민하는 시간이라고 보시면 되겠습니다.

    저도 홈랩에서부터 시작해서 프로덕션 환경까지 다양한 규모의 쿠버네티스 클러스터를 Rancher 2.x로 관리해왔어요. 처음에는 하나의 Rancher 서버로 모든 클러스터를 중앙에서 관리하는 방식이 정말 편하더라고요. 하지만 클러스터의 수가 늘어나고, 요구사항이 복잡해지면서 중앙 집중형 관리의 한계를 느끼게 됐습니다. 특히, Rancher 서버 자체의 안정성이나 업그레이드 부담, 그리고 GitOps(깃옵스)와 같은 최신 트렌드를 통합하는 데 대한 고민이 많았거든요. 그래서 ‘이제는 좀 더 분산되고 선언적인 방식으로 가야 하지 않을까?’ 하는 생각을 하게 됐습니다. 이런 고민을 하고 계신 분들이라면 오늘 제 이야기가 작은 팁이라도 될 수 있을 겁니다. 제가 직접 삽질해가며 얻은 경험들을 솔직하게 공유해볼게요!

    개념 설명: Rancher 2.x와 차세대 아키텍처의 차이

    먼저, 우리가 이야기하는 Rancher 2.x는 보통 RKE1(Rancher Kubernetes Engine 1) 기반의 쿠버네티스 클러스터들을 Rancher Management Server(랜처 관리 서버)가 중앙에서 프로비저닝하고 관리하는 형태를 의미합니다. 웹 UI를 통해 클러스터를 생성하고, 워크로드를 배포하며, 모니터링까지 한곳에서 할 수 있었죠. 정말 편리하고 직관적입니다. 하지만 단점도 명확해요.

    • 중앙 집중형 의존성: Rancher Management Server에 장애가 발생하면 모든 하위 클러스터 관리에 문제가 생길 수 있습니다.
    • 관리 복잡성 증가: 클러스터가 많아질수록 관리 서버 자체의 부담도 커집니다.
    • GitOps 통합의 어려움: UI 기반의 작업이 많아 GitOps 파이프라인과 완벽하게 통합하기 쉽지 않은 부분이 있었습니다.

    그렇다면 여기서 말하는 ‘차세대 아키텍처’ 혹은 ‘3.x스러운’ 접근 방식은 뭘까요? 저는 주로 RKE2(Rancher Kubernetes Engine 2)나 K3s(케이쓰리s)와 같은 경량화되거나 보안이 강화된 쿠버네티스 배포판을 활용하고, GitOps 원칙을 적극적으로 도입하여 선언적(Declarative)으로 클러스터와 애플리케이션을 관리하는 방식을 의미한다고 봐요. 이제 더 이상 Rancher Management Server가 클러스터의 모든 것을 직접 제어하기보다는, 클러스터 자체의 견고함과 Git을 통한 선언적 관리에 초점을 맞추는 거죠. Rancher는 이런 클러스터들을 통합적으로 보여주고 관리하는 ‘플랫폼’으로서의 역할에 더 집중하게 됩니다.

    Rancher 2.x 중앙 집중형 관리와 차세대 분산형 GitOps 아키텍처 비교 다이어그램

    Rancher 2.x의 중앙 집중형 관리 방식과 차세대 분산형 GitOps 아키텍처의 주요 차이점을 시각적으로 보여주는 다이어그램입니다.

    실전 구현: 차세대 클러스터 환경 구축 전략

    기존 Rancher 2.x 환경을 ‘마이그레이션’하는 것은 단순히 버전을 올리는 것이 아니라, 새로운 클러스터를 구축하고 워크로드를 이전하는 과정에 가깝습니다. 제가 홈랩에서 여러 번 시도해본 결과, 크게 두 가지 접근 방식이 있더라고요.

    1. 새로운 RKE2/K3s 클러스터 프로비저닝

    먼저, 기존 Rancher 2.x 관리 서버에서 새로운 RKE2나 K3s 클러스터를 프로비저닝하는 방법을 소개할게요. Rancher 2.6 버전부터는 RKE2/K3s 클러스터 생성을 공식적으로 지원해서 훨씬 수월해졌습니다.

    1. Rancher UI 접속: 기존 Rancher Management Server의 웹 UI에 접속합니다.
    2. 클러스터 생성 메뉴 진입: ‘클러스터’ 탭에서 ‘클러스터 생성’ 버튼을 클릭합니다.
    3. RKE2/K3s 선택: ‘Rancher Kubernetes Engine 2 (RKE2)’ 또는 ‘K3s’를 선택합니다. 저는 보안과 성능을 고려해 RKE2를 선호하는 편입니다.
    4. 노드 구성 및 옵션 설정: 컨트롤 플레인(Control Plane), etcd, 워커(Worker) 노드를 구성하고 필요한 네트워크, CNI(Container Network Interface, 컨테이너 네트워크 인터페이스) (예: Canal, Calico 등) 옵션을 설정합니다. 이때, 클러스터의 고가용성(High Availability, HA)을 위해 컨트롤 플레인 노드를 최소 3개 이상으로 구성하는 것이 중요합니다.
    5. 클러스터 생성: 모든 설정을 마치고 클러스터를 생성하면, Rancher가 백그라운드에서 노드에 에이전트를 설치하고 RKE2/K3s를 배포합니다.

    다음은 RKE2 클러스터를 생성할 때 필요한 기본적인 설정 예시입니다.

    # 클러스터 생성 시 RKE2/K3s 설정 예시 (Rancher UI에서 구성)
    kubernetes_version: v1.27.x-rke2r1 # 원하는 RKE2 버전 선택
    cni: canal # CNI 플러그인 선택 (calico, cilium 등)
    cloud_provider: 
      name: aws # 클라우드 환경에 맞춰 설정 (baremetal, azure, vsphere 등)
    agent_options:
      node_taints:
        - "CriticalAddonsOnly=true:NoExecute"
      labels:
        - "node-role.kubernetes.io/control-plane=true"
        - "node-role.kubernetes.io/etcd=true"
    # 노드 추가는 Rancher UI에서 직접 진행하거나, Machine Group을 통해 자동화 가능
    

    이렇게 새 클러스터를 만들고 나면, 이제 기존 워크로드들을 새로운 클러스터로 이전할 준비가 된 겁니다. 이게 생각보다 쉽지 않아요. 애플리케이션 의존성부터 데이터 마이그레이션까지 고려할 게 많거든요. 제가 초반에 이걸 간과해서 밤샘 삽질을 좀 했습니다. 😅

    Rancher UI에서 RKE2 클러스터를 생성하는 화면 예시

    Rancher UI를 통해 새로운 RKE2 클러스터를 생성하는 화면 예시입니다. 여기서 다양한 옵션을 설정할 수 있습니다.

    2. 워크로드 및 데이터 마이그레이션

    가장 중요한 단계입니다. 워크로드 마이그레이션은 서비스 중단 시간을 최소화하는 방향으로 계획해야 해요. 몇 가지 팁을 드리자면:

    • 상태 없는(Stateless) 애플리케이션 먼저: 웹 서버, API 게이트웨이 등 상태를 저장하지 않는 애플리케이션부터 이전하여 위험을 줄입니다. Helm(헬름)이나 GitOps 툴(예: Argo CD, Flux CD)을 활용하면 배포를 선언적으로 관리하고 쉽게 이전할 수 있어요.
    • 상태 있는(Stateful) 애플리케이션: 데이터베이스와 같은 상태 있는 애플리케이션은 velero(벨레로)와 같은 백업 및 복구 툴을 사용하거나, 클라우드 제공업체의 볼륨 스냅샷 기능을 활용하여 데이터를 이전합니다. 이 과정에서 네트워크 지연(Latency)이나 데이터 정합성(Data Consistency)에 문제가 없는지 꼼꼼히 확인해야 해요.
    • 네트워크 설정: Ingress(인그레스, 외부 트래픽 진입점) 컨트롤러, Service(서비스) 타입, ExternalDNS(외부 DNS) 설정 등을 새 클러스터에 맞게 재구성해야 합니다. 특히 DNS 레코드를 변경할 때는 TTL(Time-To-Live, 캐시 유지 시간)을 짧게 설정하여 롤백에 대비하는 것이 좋습니다.
    # Argo CD를 이용한 GitOps 배포 예시
    # 새 클러스터에 Argo CD 설치 후, Git 리포지토리 연결
    
    # 1. Argo CD 설치 (새 클러스터에)
    kubectl create namespace argocd
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    
    # 2. Argo CD CLI 설치 및 초기 비밀번호 확인
    # (생략)
    
    # 3. 애플리케이션 정의 (예: my-app.yaml)
    # applications/my-app.yaml 파일 생성
    # apiVersion: argoproj.io/v1alpha1
    # kind: Application
    # metadata:
    #   name: my-webapp
    #   namespace: argocd
    # spec:
    #   project: default
    #   source:
    #     repoURL: https://github.com/my-org/my-app-configs.git # 애플리케이션 설정이 있는 Git 레포지토리
    #     targetRevision: HEAD
    #     path: dev # Git 레포지토리 내 경로
    #   destination:
    #     server: https://kubernetes.default.svc
    #     namespace: my-webapp-prod # 배포할 네임스페이스
    #   syncPolicy:
    #     automated:
    #       prune: true
    #       selfHeal: true
    
    # 4. Argo CD에 애플리케이션 등록
    argocd app create my-webapp --repo https://github.com/my-org/my-app-configs.git --path dev --dest-server https://kubernetes.default.svc --dest-namespace my-webapp-prod
    

    이런 GitOps 툴을 활용하면, 기존 클러스터에서 사용하던 매니페스트(Manifest) 파일들을 Git 레포지토리에 올리고, 새 클러스터에 Argo CD를 설치하여 동기화하는 방식으로 쉽게 워크로드를 이전할 수 있어요. 코드로 인프라를 관리하는(Infrastructure as Code, IaC) 경험은 정말 혁신적입니다!

    ⚠️ 주의사항 및 트러블슈팅: 삽질 경험 공유

    제가 이 ‘차세대 전환’을 하면서 겪었던 몇 가지 삽질과 해결책들을 공유해볼게요.

    1. RKE1과 RKE2/K3s의 설정 차이: RKE1은 cluster.yml 파일을 기반으로 클러스터를 구성했지만, RKE2/K3s는 /etc/rancher/rke2/config.yaml (또는 k3s) 파일을 사용하거나 Helm 차트 등을 통해 더 세분화된 설정을 합니다. 기존 RKE1의 커스텀 설정(예: 추가 컨테이너 런타임 옵션, CNI 플러그인 설정)을 그대로 옮기려다 호환성 문제로 고생 좀 했어요. 새로운 배포판의 문서(Documentation)를 꼼꼼히 확인하는 것이 정말 중요합니다.
    2. StorageClass(스토리지 클래스) 호환성: 기존 클러스터에서 사용하던 PersistentVolume(영구 볼륨)과 PersistentVolumeClaim(영구 볼륨 클레임)은 StorageClass에 따라 다르게 동작할 수 있습니다. 특히 클라우드 환경이 변경되거나 스토리지 솔루션이 달라지면, 새로운 StorageClass를 정의하고 기존 PV/PVC를 마이그레이션해야 해요. 저는 홈랩에서 Longhorn(롱혼)을 사용하는데, 새 클러스터에 Longhorn을 재설치하고 데이터를 옮기는 과정에서 네트워크 설정 문제로 볼륨이 마운트되지 않아 애먹었습니다. 😅
    3. Cilium(실리움) CNI 도입 시 주의사항: 최근 Cilium이 성능과 보안 면에서 각광받고 있어서 저도 도입을 고려했는데, 기존 CNI(예: Canal)에서 Cilium으로 변경할 때는 네트워크 정책(Network Policy)과의 호환성, kube-proxy(쿠베 프록시) 없이 동작하는 모드(Direct Routing) 설정 등 고려할 사항이 많아요. 잘못하면 클러스터 내부 통신이 마비될 수 있으니 충분한 테스트 환경에서 검증해야 합니다.
    4. Rancher Management Server의 역할 변화: 기존에는 Rancher 서버가 모든 것을 다 해주는 느낌이었다면, 이제는 ‘관측성(Observability)’과 ‘정책 관리(Policy Management)’, 그리고 ‘클러스터 수명 주기 관리(Cluster Lifecycle Management)’에 더 집중하게 됩니다. 즉, 클러스터 내부의 복잡한 운영은 GitOps 툴과 같은 다른 도구들에게 맡기고, Rancher는 그 위에서 통합된 뷰를 제공하는 역할로 전환되는 거죠. 이 역할을 이해하지 못하면 ‘왜 Rancher가 예전처럼 클러스터 설정을 다 해주지 않지?’ 하고 헤맬 수 있습니다.

    검증 및 결과: 안정적인 차세대 환경 확인

    모든 워크로드 마이그레이션이 완료되었다면, 새로운 클러스터가 제대로 동작하는지 꼼꼼히 검증해야 합니다. 다음은 제가 주로 확인하는 항목들입니다.

    • 애플리케이션 정상 동작 확인: 모든 서비스의 엔드포인트에 접속하여 기능이 정상적으로 동작하는지 확인합니다. 특히 로깅(Logging)과 모니터링(Monitoring) 시스템이 제대로 연동되는지 확인하는 것이 중요해요.
    • 리소스 사용량 모니터링: Grafana(그라파나), Prometheus(프로메테우스) 등의 툴을 통해 CPU, 메모리, 네트워크, 디스크 I/O 등 클러스터 리소스 사용량을 면밀히 모니터링합니다. 기존 클러스터와 비교하여 비정상적인 패턴이 없는지 확인해야 합니다.
    • 클러스터 컴포넌트 상태: kubectl get pods -A 명령어로 모든 네임스페이스의 파드(Pod) 상태를 확인하고, kubectl get nodes로 노드 상태를 확인하여 문제가 없는지 점검합니다.
    • 데이터 정합성 검증: 상태 있는 애플리케이션의 경우, 이전된 데이터가 정확한지, 쓰기/읽기 작업이 문제없이 이루어지는지 반드시 확인해야 합니다.

    이 모든 검증을 마치고 나면, 드디어 새로운 차세대 Rancher 환경이 안정적으로 운영되는 것을 확인할 수 있어요. 이 뿌듯함이란! 🎉

    새롭게 마이그레이션된 Rancher 환경의 클러스터 대시보드

    새롭게 마이그레이션된 Rancher 환경에서 클러스터의 전반적인 상태를 보여주는 대시보드 화면입니다.

    마무리: 배운 점과 다음 단계

    Rancher 2.x에서 차세대 쿠버네티스 관리 환경으로의 전환은 단순히 소프트웨어 버전을 올리는 작업이 아니었습니다. 이는 클러스터 아키텍처, 운영 방식, 그리고 인프라 엔지니어의 역할에 대한 근본적인 재정의 과정이라고 생각해요. 저도 이 과정을 거치면서 많은 것을 배우고 삽질도 정말 많이 했습니다. 하지만 그 덕분에 GitOps의 중요성, RKE2/K3s의 장점, 그리고 Rancher가 나아가야 할 방향에 대해 더 깊이 이해하게 되었죠.

    Rancher 환경 전환 시 고려해야 할 핵심 사항 요약 인포그래픽

    Rancher 환경 전환 시 고려해야 할 핵심 사항들을 요약한 인포그래픽입니다.

    이번 마이그레이션을 통해 얻은 주요 교훈은 다음과 같습니다.

    • 계획의 중요성: 충분한 사전 계획 없이는 성공적인 마이그레이션은 불가능해요. 의존성 파악, 백업/복구 전략, 롤백 계획 등 모든 것을 미리 준비해야 합니다.
    • GitOps의 힘: 선언적인 코드로 인프라와 애플리케이션을 관리하는 GitOps는 복잡한 마이그레이션을 훨씬 수월하게 만들고, 향후 운영의 안정성을 높여줍니다.
    • 문서화의 중요성: 모든 변경 사항과 결정 사항을 문서화하여 팀원들과 공유하고, 향후 트러블슈팅에 활용해야 합니다.

    이제 새로운 클러스터에서 더 안정적이고 효율적인 쿠버네티스 운영을 할 수 있게 되었어요. 다음 단계로는 클러스터의 보안 강화(Security Hardening), 자동화된 재해 복구(Automated Disaster Recovery) 시스템 구축, 그리고 멀티 클러스터 환경에서의 서비스 메시(Service Mesh) 도입 등을 고민해볼 예정입니다. 여러분도 혹시 비슷한 고민을 하고 계시다면, 주저하지 말고 새로운 아키텍처로의 전환을 시도해보세요. 물론 삽질은 필수겠지만, 그만큼 얻는 것도 많을 겁니다! 궁금한 점이 있다면 언제든 댓글 남겨주세요. 다음 포스팅에서 또 재미있는 이야기로 찾아뵙겠습니다!

  • [k8s] Argo CD 멀티 클러스터 동기화 문제 해결: 실제 운영 사례와 디버깅 팁

    [k8s] Argo CD 멀티 클러스터 동기화 문제 해결: 실제 운영 사례와 디버깅 팁

    Argo CD 멀티 클러스터 동기화 문제 해결: 실제 운영 사례와 디버깅 팁

    안녕하세요, 13년차 서버실 지킴이입니다. 요즘 쿠버네티스(Kubernetes) 환경에서 GitOps(깃옵스)를 도입하는 기업들이 정말 많죠? 저도 홈랩과 회사 프로젝트에서 Argo CD(아르고 CD)를 활용해서 여러 쿠버네티스 클러스터를 관리하고 있는데요. 처음엔 “와, 이거 정말 편하겠다!” 싶었는데, 막상 실제 운영 환경에서 멀티 클러스터(Multi-Cluster) 동기화 문제를 만나면 머리가 지끈거릴 때가 많더라고요.

    특히 여러 클러스터에 동일한 애플리케이션을 배포하거나, 환경별로 미묘하게 다른 설정을 적용해야 할 때 ApplicationSet(애플리케이션셋)을 쓰면 정말 유용하거든요. 그런데 간혹 특정 클러스터만 동기화가 안 되거나, 알 수 없는 에러로 삽질을 거듭하곤 해요. 제가 직접 겪었던 Argo CD 멀티 클러스터 동기화 문제들과 그 해결 과정을 솔직하게 공유해볼까 해요. 혹시 비슷한 경험으로 고생하고 계시다면, 이 글이 작은 힌트라도 되었으면 좋겠네요! 💡

    Argo CD 멀티 클러스터 관리 아키텍처: Git을 중심으로 Argo CD가 여러 쿠버네티스 클러스터에 애플리케이션을 배포하고 동기화하는 흐름을 보여줍니다.

    Argo CD, GitOps, 그리고 멀티 클러스터, 이게 뭔가요?

    본격적인 문제 해결에 앞서, 핵심 개념들을 간단히 짚고 넘어갈게요. “아, 이건 다 아는데!” 하시는 분들도 계시겠지만, 혹시 처음 접하시는 분들을 위해 쉽게 풀어 설명해 드릴게요.

    • Argo CD (아르고 CD): 쿠버네티스 환경에서 GitOps를 구현하기 위한 대표적인 선언적(Declarative) CD(Continuous Delivery, 지속적 배포) 툴입니다. Git 저장소를 애플리케이션의 ‘원본 소스(Single Source of Truth)’로 삼아, 쿠버네티스 클러스터의 실제 상태가 Git에 정의된 상태와 일치하도록 지속적으로 동기화(Synchronization)를 시도하죠. 즉, Git에 코드를 푸시하면 Argo CD가 알아서 클러스터에 배포해주는 방식입니다.

    • GitOps (깃옵스): Git을 운영의 중심(Operational Hub)으로 삼아 인프라와 애플리케이션의 모든 상태를 관리하는 방식입니다. 모든 변경 사항이 Git에 기록되므로, 누가 언제 무엇을 변경했는지 추적하기 쉽고, 문제가 생겼을 때 쉽게 롤백(Rollback)할 수 있다는 장점이 있습니다. “Git에서 정의한 대로 클러스터 상태를 유지한다”는 철학이죠.

    • 멀티 클러스터 (Multi-Cluster): 여러 개의 쿠버네티스 클러스터를 동시에 관리하는 환경을 의미합니다. 개발, 스테이징, 프로덕션 환경을 분리하거나, 재해 복구(Disaster Recovery)를 위해 여러 리전에 클러스터를 운영할 때 등 다양한 이유로 멀티 클러스터 환경을 구축합니다. Argo CD는 하나의 중앙 집중식(Centralized) 컨트롤 플레인(Control Plane)으로 여러 원격 클러스터(Remote Cluster)를 관리할 수 있게 해줍니다.

    Argo CD 멀티 클러스터 설정과 흔한 문제 시나리오

    Argo CD를 이용해 멀티 클러스터를 관리하려면, 먼저 Argo CD 컨트롤 플레인이 설치된 클러스터(보통 ‘관리 클러스터’)에 다른 클러스터들을 등록해야 합니다. argocd cluster add 명령어가 이걸 해주죠. 이때 Argo CD가 원격 클러스터에 접근할 수 있는 ServiceAccount와 ClusterRoleBinding을 생성해줍니다.

    그리고 여러 클러스터에 애플리케이션을 배포할 때는 주로 ApplicationSet을 사용합니다. 예를 들어, 다음과 같은 ApplicationSet으로 여러 클러스터에 nginx-app을 배포한다고 가정해봅시다.

    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: my-nginx-appset
    spec:
      generators:
      - clusters: {}
      template:
        metadata:
          name: '{{name}}-nginx-app'
        spec:
          project: default
          source:
            repoURL: https://github.com/my-org/my-gitops-repo.git
            targetRevision: HEAD
            path: apps/nginx
          destination:
            server: '{{server}}'
            namespace: default
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
    

    이렇게 설정하면 Argo CD가 등록된 모든 클러스터에 apps/nginx 경로의 매니페스트를 배포하려고 시도합니다. 그런데 여기서 문제가 발생하곤 해요. 어떤 클러스터는 잘 동기화(SYNCED)되는데, 다른 클러스터는 계속 OutOfSync 상태이거나 Failed 상태를 벗어나지 못하는 거죠. “아니, 분명 다 똑같이 설정했는데 왜 이럴까?” 하는 생각이 절로 듭니다. 저도 처음엔 정말 막막했는데, 몇 가지 공통적인 원인이 있더라고요. 😅

    Argo CD UI의 ApplicationSet 대시보드: 문제 발생 상황

    Argo CD UI의 ApplicationSet 대시보드: 여러 클러스터에 배포된 애플리케이션들의 동기화 상태를 한눈에 볼 수 있으며, 문제가 발생한 애플리케이션을 쉽게 식별할 수 있습니다.

    ⚠️ 실제 운영 사례로 본 Argo CD 동기화 문제와 디버깅 팁

    제가 13년 동안 쌓은 삽질 경험을 바탕으로, Argo CD 멀티 클러스터 환경에서 자주 발생하는 동기화 문제와 그 해결책을 알려드릴게요. 하나씩 체크하다 보면 분명 원인을 찾을 수 있을 겁니다!

    1. RBAC (Role-Based Access Control) 권한 문제
      가장 흔하면서도 놓치기 쉬운 문제예요. Argo CD 컨트롤 플레인이 관리 대상 클러스터에 배포할 권한이 없는 경우입니다. argocd cluster add 명령어가 자동으로 필요한 권한을 생성해주지만, 간혹 수동으로 클러스터를 추가했거나, 보안 정책상 특정 권한이 누락된 경우가 있거든요.

      💡 디버깅 팁:

      • 관리 대상 클러스터에서 kubectl get serviceaccount argocd-manager -n argocd 명령어로 argocd-manager ServiceAccount가 잘 생성되었는지 확인하세요.
      • 이 ServiceAccount에 연결된 ClusterRoleBinding과 ClusterRole의 권한을 확인해보세요. 보통 cluster-admin 권한이 부여되는데, 만약 특정 리소스에 대한 권한만 있다면 해당 리소스 배포가 실패할 수 있거든요. kubectl describe clusterrole argocd-manager-role 명령으로 권한 목록을 볼 수 있습니다.
      • 특히 ServiceAccount가 아닌 다른 방식으로 인증(예: kubeconfig 파일)을 사용한다면, 해당 kubeconfig 파일에 정의된 사용자에게 충분한 권한이 있는지 확인해야 합니다.
    2. 네트워크 연결 문제
      Argo CD 서버가 관리 대상 클러스터의 쿠버네티스 API 서버에 접근할 수 없는 경우네요. 방화벽, VPN, VPC 피어링 등 네트워크 구성을 꼼꼼히 확인해야 합니다. “어제까진 잘 됐는데?” 하다가 네트워크 변경 때문에 막히는 경우가 꽤 있거든요.

      💡 디버깅 팁:

      • Argo CD UI에서 해당 클러스터의 상태가 Unavailable인지 확인하세요.
      • Argo CD 컨트롤러 파드(Pod)에서 관리 대상 클러스터의 API 서버로 curl 같은 명령어로 접속을 시도해보세요. 파드에 접속하는 방법은 kubectl exec -it <argocd-repo-server-pod> -n argocd -- bash 후 curl -k https://<cluster-api-server-ip>:<port>/healthz 와 같이 시도해볼 수 있습니다.
      • argocd cluster add 시 입력한 API 서버 주소가 올바른지 다시 한번 확인해보세요. 내부망 주소여야 할 때 외부망 주소를 입력하거나, 그 반대인 경우도 있거든요.
    3. ApplicationSet 설정 오류
      ApplicationSet의 generator나 template 설정이 잘못된 경우예요. 특히 cluster 필터링이나 Git 저장소 경로(path)를 잘못 지정했을 때 문제가 발생하곤 합니다.

      💡 디버깅 팁:

      • argocd appset get <application-set-name> 명령으로 ApplicationSet의 현재 상태와 생성된 Application 목록을 확인해보세요.
      • ApplicationSet의 template 섹션에서 source.path가 Git 저장소의 실제 경로와 일치하는지 확인하세요. 대소문자나 오타가 있을 수 있거든요.
      • generator에서 특정 클러스터만 선택하도록 설정했다면, 해당 클러스터의 라벨(labels)이 selector와 정확히 일치하는지 확인해보세요.
    4. 배포하려는 리소스 매니페스트 오류
      Git 저장소에 있는 쿠버네티스 매니페스트(YAML 파일) 자체에 문제가 있는 경우네요. 문법 오류, 잘못된 필드 이름, 존재하지 않는 StorageClass 참조 등이 있을 수 있거든요.

      💡 디버깅 팁:

      • Argo CD UI에서 Application 상태를 확인하고, Events(이벤트) 탭이나 Logs(로그) 탭을 자세히 살펴보세요. 어떤 리소스에서 어떤 에러가 발생했는지 상세하게 나옵니다. 예를 들어, "admission webhook denied the request" 같은 메시지는 Admission Controller(어드미션 컨트롤러)나 OPA Gatekeeper 같은 정책 엔진에 의해 배포가 거부되었음을 의미할 수 있어요.
      • 해당 클러스터에 직접 kubectl apply -f <problematic-manifest.yaml> --dry-run=client 로 배포를 시도하여 문법 오류 등을 미리 확인해보세요.
      • kubectl describe <resource-type> <resource-name> -n <namespace> 명령으로 대상 클러스터에 배포된(혹은 배포 실패한) 리소스의 상세 상태를 확인하세요.
    5. Argo CD 컨트롤러 파드 문제
      아주 드물지만, Argo CD 자체의 argocd-application-controller나 argocd-repo-server 파드에 문제가 생겨 동기화가 제대로 작동하지 않을 수 있어요. 리소스 부족, 네트워크 문제, 버그 등이 원인일 수 있거든요.

      💡 디버깅 팁:

      • kubectl get pods -n argocd 명령으로 Argo CD 관련 파드들이 모두 Running 상태인지 확인해보세요.
      • 문제 있어 보이는 파드의 로그를 확인해보세요: kubectl logs -f <argocd-pod-name> -n argocd. 특히 argocd-application-controller 로그에 동기화 관련 오류 메시지가 나타날 수 있거든요.
      • 파드를 재시작하거나, 리소스(CPU, Memory)를 늘려주는 것도 방법이 될 수 있습니다.

    ✅ 문제 해결 후 검증하기

    위의 팁들을 바탕으로 문제를 해결했다면, 이제 제대로 동기화가 되는지 확인해야겠죠? 드디어 SYNCED 상태를 봤을 때의 그 쾌감이란! 🎉

    1. Argo CD UI/CLI 확인
      가장 먼저 Argo CD UI에 접속해서 해당 Application 또는 ApplicationSet의 상태가 Synced 그리고 Healthy로 표시되는지 확인합니다. CLI를 선호한다면 argocd app list 나 argocd app get <application-name> 명령어를 사용해보세요.

    2. 대상 클러스터 직접 확인
      Argo CD UI/CLI에서 Synced로 표시되더라도, 혹시 모르니 해당 클러스터에 접속해서 kubectl get <resource-type> -n <namespace> 명령으로 실제로 리소스가 배포되었는지, 상태는 어떤지 직접 확인하는 게 좋습니다. 특히 Deployment나 StatefulSet의 파드들이 정상적으로 Running 상태인지 확인해 주세요.

    3. 새로운 변경사항 푸시 테스트
      Git 저장소에 간단한 변경사항(예: ConfigMap 내용 변경)을 푸시해서 Argo CD가 변경을 감지하고 자동으로 동기화하는지 확인해봅니다. 이 과정이 원활하게 진행된다면 성공적으로 문제를 해결한 거예요!

    Argo CD UI의 성공적인 동기화 대시보드

    성공적인 Argo CD 동기화 대시보드: 모든 애플리케이션이 문제없이 배포되고 동기화되어 안정적인 운영 상태를 보여줍니다.

    마무리하며: 삽질은 경험이 되고, 경험은 자산이 됩니다.

    오늘은 Argo CD 멀티 클러스터 동기화 문제 해결에 대한 저의 경험과 디버깅 팁을 공유해드렸습니다. 솔직히 저도 처음엔 정말 막막해서 밤늦게까지 삽질 좀 했거든요. “왜 안 되는 거지?” 하면서 클러스터 여기저기를 들쑤시고 다녔던 기억이 생생해요. 😂

    하지만 이런 삽질 과정이 결국 저의 노하우가 되고, 여러분 같은 동료 인프라 엔지니어분들께 도움이 되는 자산이 된다는 사실을 깨달았습니다. Argo CD는 정말 강력한 툴이지만, 그만큼 복잡한 환경에서는 예상치 못한 문제들이 발생할 수 있어요. 중요한 건 문제를 만났을 때 당황하지 않고, 차근차근 원인을 찾아 해결해나가는 자세라고 생각합니다.

    이번 글이 Argo CD 멀티 클러스터 환경에서 고군분투하는 분들께 조금이나마 도움이 되었기를 바랍니다. 다음 글에서는 Argo CD의 Progressive Delivery(점진적 배포) 전략인 Argo Rollouts에 대해 다뤄볼게요. 그때까지 GitOps 즐겁게 운영하시길 바랍니다! 궁금한 점이 있다면 언제든 댓글 남겨주세요! 😊

    Argo CD 멀티 클러스터 동기화 문제 해결 체크리스트: RBAC, 네트워크, ApplicationSet 설정, 매니페스트 오류, Argo CD 파드 상태 등 주요 점검 사항을 요약한 인포그래픽입니다.

  • [k8s] Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    [k8s] Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    안녕하세요, 13년차의 서버실 운영자입니다. 오늘은 제가 홈랩과 회사에서 Argo CD 멀티 클러스터 환경을 구축하면서 겪었던 일들과, 여러분이 효율적인 GitOps 베스트 프랙티스를 적용할 수 있도록 돕는 GitOps 체크리스트를 공유해드리려고 합니다.

    요즘 Kubernetes 다중 클러스터 관리는 선택이 아니라 필수가 되어가고 있죠. 개발, 스테이징, 프로덕션 환경이 따로 있거나, DR(재해 복구)을 위해 여러 리전에 클러스터를 두는 경우가 많습니다. 근데 이 클러스터들을 일일이 관리하는 게 보통 일이 아니더라고요. 직접 해보니 삽질 좀 했습니다. (ㅎㅎ)

    이런 고민을 하시는 분들을 위해, Argo CD를 활용한 멀티 클러스터 GitOps의 개념부터 실전 전략, 그리고 제가 직접 겪었던 트러블슈팅 경험까지 솔직하게 풀어보겠습니다. 이 글을 통해 여러분의 Argo CD 배포 전략이 한층 더 견고해지길 바랍니다. 드디어 배포가 편해지는 그 날을 위해!

    Argo CD 멀티 클러스터 GitOps 아키텍처 다이어그램

    Argo CD를 이용한 멀티 클러스터 GitOps의 전체 아키텍처는 위 그림처럼 구성할 수 있습니다. 중앙의 Argo CD가 여러 클러스터를 관리하는 형태죠.

    Argo CD와 GitOps, 그리고 멀티 클러스터 관리 핵심 개념

    우선, 핵심 개념부터 쉽고 편하게 짚고 넘어가 볼까요? 제가 처음 Argo CD를 접했을 때를 생각하면서 설명해 드릴게요.

    GitOps: Git이 진리의 원천입니다

    GitOps(깃옵스)는 말 그대로 Git을 운영(Operations)의 중심으로 삼는 방식입니다. 모든 인프라와 애플리케이션의 상태를 Git 리포지토리에 선언적으로 정의하고, 이 Git 리포지토리의 변경사항이 자동으로 실제 환경에 반영되도록 하거든요. 쉽게 말해, kubectl apply -f를 사람이 직접 하는 게 아니라, Git에 커밋만 하면 시스템이 알아서 해주는 거죠. 제가 처음 이걸 알았을 때, ‘와, 이거 진짜 편하겠다!’ 싶었어요.

    GitOps의 장점은 명확합니다:

    • 버전 관리(Version Control): 모든 변경 이력을 Git에서 확인할 수 있어요. 누가 언제 뭘 바꿨는지 투명하게 알 수 있죠.
    • 자동화(Automation): 수동 작업이 줄어들어 휴먼 에러를 최소화하고, 배포 속도를 높일 수 있습니다.
    • 복원력(Resilience): 문제가 발생하면 Git의 이전 상태로 쉽게 롤백(Rollback)할 수 있습니다.
    • 협업(Collaboration): 개발팀과 운영팀이 Git을 통해 더 효율적으로 협업할 수 있습니다.

    Argo CD: GitOps를 Kubernetes에 현실로 만들어주는 도구

    그럼 Argo CD(아르고 CD)는 뭘까요? Argo CD는 선언적(Declarative) GitOps 지속적 배포(Continuous Delivery) 도구거든요. Kubernetes(쿠버네티스) 애플리케이션의 배포와 라이프사이클 관리를 Git에서 정의된 상태에 따라 자동으로 동기화(Sync)해주는 역할을 해요. 제가 써보니까, 복잡한 배포 파이프라인을 구축하는 수고를 엄청나게 덜어주더라고요.

    멀티 클러스터 관리: 복잡성을 단순화하기

    여러 개의 Kubernetes 클러스터를 관리하는 건 마치 여러 대의 서버를 손으로 직접 관리하는 것과 비슷해요. 하나만 관리할 때는 괜찮지만, 두 개, 세 개… 점점 늘어나면 감당하기 어려워집니다. Kubernetes 다중 클러스터 관리는 이런 복잡성을 줄이고 일관성을 유지하기 위한 전략이 필요해요. Argo CD는 이 문제를 ApplicationSet(애플리케이션셋)이라는 기능을 통해 아주 우아하게 해결해 줍니다. 처음엔 이게 뭔가 싶었는데, 써보니 진짜 물건이더라고요!

    실전 구현: Argo CD 멀티 클러스터 환경 설정 베스트 프랙티스

    이제 이론을 바탕으로 실제로 어떻게 구성하는지 알아볼 시간입니다. 제가 홈랩에서 여러 시도를 해보고, 회사에서 적용하면서 얻은 노하우들을 풀어드릴게요.

    1. Argo CD Control Plane 설치

    먼저, Argo CD가 설치될 메인 클러스터, 즉 Control Plane(컨트롤 플레인) 클러스터가 필요합니다. 이곳에서 모든 배포를 관장하게 됩니다. 기본적인 Argo CD 설치는 공식 문서에 잘 나와 있는데, 간단히 요약해볼게요.

    
    kubectl create namespace argocd
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    

    설치 후에는 Argo CD UI에 접속하여 초기 비밀번호를 설정하고 로그인할 수 있습니다.

    2. Managed Clusters(관리 대상 클러스터) 등록

    다음으로, Argo CD가 관리할 다른 Kubernetes 클러스터들을 등록해야 합니다. 저는 개발, 스테이징, 프로덕션 클러스터를 각각 등록했거든요. Argo CD는 kubectl 명령어를 통해 쉽게 클러스터를 등록할 수 있도록 해줍니다.

    
    # 먼저 Argo CD CLI를 설치합니다.
    brew install argocd # macOS 기준
    
    # Argo CD API 서버에 로그인합니다.
    argocd login <ARGOCD_SERVER_IP_OR_HOSTNAME>
    
    # 관리 대상 클러스터의 kubeconfig 컨텍스트로 전환합니다.
    kubectl config use <MANAGED_CLUSTER_CONTEXT_NAME>
    
    # 현재 컨텍스트의 클러스터를 Argo CD에 등록합니다.
    argocd cluster add <MANAGED_CLUSTER_CONTEXT_NAME>
    

    이 명령어를 실행하면 Argo CD가 해당 클러스터에 필요한 RBAC(Role-Based Access Control) 권한을 자동으로 설정하고, 클러스터를 관리 목록에 추가합니다. 이게 정말 편리하더라고요. 제가 일일이 RBAC을 설정할 필요가 없어서 좋았어요.

    Argo CD UI에서 여러 Kubernetes 클러스터가 관리되는 모습

    Argo CD UI에 접속하면 이렇게 등록된 여러 클러스터들이 한눈에 보입니다. 각 클러스터의 상태도 직관적으로 확인할 수 있죠.

    3. ApplicationSet 활용: 멀티 클러스터 배포의 꽃!

    수동으로 각 클러스터에 Application(애플리케이션)을 생성하는 건 비효율적입니다. 여기서 ApplicationSet이 빛을 발합니다. ApplicationSet은 여러 클러스터에 동일하거나 유사한 형태의 Application을 자동으로 생성해주는 CRD(Custom Resource Definition)거든요. 제가 처음엔 Application을 복사 붙여넣기 했었는데, ApplicationSet을 알고 나서는 ‘아, 이래서 쓰는구나!’ 하고 무릎을 탁 쳤습니다.

    ApplicationSet을 사용하면 Git 리포지토리의 특정 경로에 있는 매니페스트를 여러 클러스터에 배포하거나, 클러스터 이름에 따라 다른 설정 값을 주입하는 등의 고급 배포 전략을 구현할 수 있습니다.

    예시 YAML 코드를 한번 볼까요? 저는 Git 제너레이터를 이용해서 특정 Git 경로의 폴더들을 클러스터로 배포하는 방식을 즐겨 사용합니다.

    
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: my-multi-cluster-app
    spec:
      generators:
      - git:
          repoURL: https://github.com/my-org/my-gitops-repo.git
          revision: HEAD
          directories:
          - path: clusters/dev/*
          - path: clusters/stg/*
          - path: clusters/prod/*
      template:
        metadata:
          name: '{{path.basename}}-app'
          labels:
            app.kubernetes.io/managed-by: argocd
        spec:
          project: default
          source:
            repoURL: https://github.com/my-org/my-gitops-repo.git
            targetRevision: HEAD
            path: '{{path}}'
          destination:
            server: '{{path.basename | clusterServer}}' # 클러스터 이름에 따라 서버 URL 매핑
            namespace: default
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
            syncOptions:
              - CreateNamespace=true
    

    위 예시에서는 clusters/dev, clusters/stg, clusters/prod 폴더 각각을 하나의 GitOps Application으로 인식하고, 해당 폴더 이름에 매핑되는 클러스터에 배포하도록 설정한 거예요. {{path.basename | clusterServer}}와 같은 템플릿 문법을 활용해서 클러스터의 서버 URL을 동적으로 주입할 수 있습니다. 물론 clusterServer 같은 함수는 직접 구현하거나, Argo CD의 List 제너레이터 등 다른 제너레이터를 활용하여 클러스터 정보를 직접 명시할 수도 있습니다.

    Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    이제 제가 경험하면서 중요하다고 느꼈던 Argo CD 멀티 클러스터 GitOps 베스트 프랙티스들을 체크리스트 형태로 정리해 보았습니다. 이대로만 따라 하셔도 웬만한 삽질은 피할 수 있을 거예요!

    1. ✅ 단일 Git 리포지토리(Single Source of Truth) 사용: 모든 클러스터와 애플리케이션의 상태는 하나의 Git 리포지토리에서 관리하는 것이 좋습니다. 폴더 구조를 잘 정리해서 혼란을 줄이세요. (예: repo/clusters/dev, repo/clusters/prod, repo/apps/my-app)
    2. ✅ ApplicationSet 적극 활용: 멀티 클러스터 배포의 핵심입니다. 클러스터별 배포, 환경별 차이점 관리 등을 ApplicationSet으로 자동화하세요.
    3. ✅ Helm(헬름) 또는 Kustomize(커스터마이즈)로 설정 관리: 환경(dev, stg, prod)별로 다른 설정이 필요할 때, Helm Values나 Kustomize Overlays를 사용하여 매니페스트의 차이를 관리합니다. 저는 Helm을 주로 사용하는데, 템플릿 기능이 강력해서 유연하게 대응할 수 있더라고요.
    4. ✅ RBAC(Role-Based Access Control) 최소 권한 원칙 준수: Argo CD 컨트롤 플레인과 각 관리 대상 클러스터 간의 통신 시, Argo CD가 필요한 최소한의 권한만 가지도록 RBAC을 설정해야 합니다. 보안은 아무리 강조해도 지나치지 않아요!
    5. ✅ Secret(시크릿) 관리 전략 수립: 민감 정보는 Git에 직접 올리지 않고, HashiCorp Vault, Sealed Secrets, External Secrets Operator 같은 도구를 사용하여 안전하게 관리해야 합니다.
    6. ✅ 자동 동기화(Automated Sync) 및 Self-Heal(자가 복구) 활성화: Git의 변경사항이 자동으로 클러스터에 반영되고, 클러스터의 상태가 Git과 다를 경우 자동으로 복구되도록 설정합니다. 이게 GitOps의 가장 큰 매력 중 하나죠.
    7. ✅ Rollback(롤백) 전략 수립 및 테스트: 문제가 발생했을 때 신속하게 이전 버전으로 롤백할 수 있도록 전략을 세우고, 실제 롤백 테스트를 주기적으로 수행하세요.
    8. ✅ 모니터링(Monitoring) 및 로깅(Logging) 구축: Argo CD의 상태, 배포 이력, 각 클러스터의 애플리케이션 상태 등을 모니터링하고 로깅 시스템에 연동하여 가시성을 확보해야 합니다. Prometheus(프로메테우스)와 Grafana(그라파나)는 국룰이죠.
    9. ✅ PreSync/PostSync Hooks(훅) 활용: 배포 전후에 특정 작업을 수행해야 할 경우, PreSync/PostSync Hooks를 사용하여 데이터베이스 마이그레이션이나 캐시 초기화 같은 작업을 자동화할 수 있습니다.

    ⚠️ 삽질 경험 & 트러블슈팅: 제가 겪었던 문제들

    13년차 엔지니어도 삽질은 피할 수 없는 운명입니다. 제가 Argo CD 멀티 클러스터를 구축하면서 겪었던 몇 가지 삽질과 해결책을 공유해 드릴게요.

    1. 네트워크 연결 문제: 방화벽은 항상 제 발목을 잡죠

    문제: Argo CD Control Plane에서 Managed Cluster로 접근이 안 되는 경우가 있었습니다. 클러스터가 다른 VPC나 다른 데이터센터에 있을 때 주로 발생하더라고요.

    해결: 뻔하지만 가장 중요한 건 방화벽(Firewall)과 네트워크 ACL(Access Control List) 설정입니다. Argo CD Control Plane이 Managed Cluster의 Kubernetes API 서버에 접근할 수 있도록 네트워크 경로를 열어줘야 합니다. 보통 443 포트를 사용하죠. 저는 보안 그룹 설정을 빼먹어서 한참 헤맸습니다. 😭

    2. RBAC 권한 부족: ‘Forbidden’ 메시지는 언제나 당황스럽습니다

    문제: Argo CD가 특정 클러스터에 애플리케이션을 배포하려는데 Forbidden 에러가 뜨는 경우가 있었습니다. ApplicationSet이 Application을 생성하지 못하거나, Application이 특정 리소스를 생성하지 못하는 상황이었죠.

    해결: Argo CD가 Managed Cluster에 설치하는 argocd-manager ServiceAccount(서비스 어카운트)의 RBAC 권한을 확인해야 합니다. ClusterRole 또는 Role이 필요한 verbs(get, list, watch, create, update, patch, delete)와 resources(pods, deployments, services 등)를 가지고 있는지 점검해야 합니다. 저는 CustomResourceDefinition(CRD)을 배포하려는데 권한이 없어서 한참 고생했어요. CRD는 특별한 권한이 필요하거든요.

    3. Sync Status ‘Out Of Sync’: Git과 실제 상태가 다를 때

    문제: Argo CD UI에서 분명히 Sync를 했는데 계속 Out Of Sync 상태로 남아있는 경우가 있었습니다. 특히 Helm 차트를 사용할 때 자주 발생했죠.

    해결: 여러 원인이 있을 수 있습니다. 첫째, Helm Hooks(훅)가 제대로 동작하지 않거나, helm.sh/hook-delete-policy 같은 주석이 설정되어 있지 않은 경우. 둘째, CRD가 먼저 배포되지 않은 상태에서 해당 CRD를 사용하는 리소스가 배포되려 할 때. 셋째, 클러스터 내부의 컨트롤러가 Git에 없는 변경을 일으키는 경우. 넷째, resource.customizations 설정을 통해 특정 리소스의 필드를 무시하도록 설정하는 방법도 있습니다. 저는 특히 CRD 배포 순서 때문에 많이 헷갈렸는데, PreSync Hook을 이용하거나, ApplicationSet에서 CRD Application을 먼저 배포하도록 순서를 조정해서 해결했습니다.

    검증 및 결과: 이제 배포가 이렇게 편해집니다!

    이런 삽질과 노하우를 바탕으로 Argo CD 멀티 클러스터 GitOps 환경을 제대로 구축하고 나니, 정말 배포의 패러다임이 바뀌는 것을 느꼈습니다. 제가 직접 Git에 커밋하고 푸시만 하면, 개발, 스테이징, 프로덕션 클러스터에 알아서 척척 배포되는 모습을 보면 정말 뿌듯하더라고요.

    • 🎉 일관성 있는 배포: 모든 클러스터에 동일한 방식으로 애플리케이션이 배포됩니다.
    • 🎉 배포 속도 향상: 수동 작업이 사라지고, Git 변경만으로 배포가 완료됩니다.
    • 🎉 가시성 확보: Argo CD UI에서 모든 클러스터의 애플리케이션 상태를 한눈에 파악할 수 있습니다.
    • 🎉 안정성 증가: 잘못된 배포는 Git 롤백으로 쉽게 되돌릴 수 있습니다.
    Argo CD 대시보드에서 멀티 클러스터 배포 성공 현황

    위 그림처럼 Argo CD 대시보드에서 여러 클러스터에 걸쳐 배포된 애플리케이션들의 상태를 실시간으로 확인할 수 있습니다. 모든 것이 ‘Synced’ 상태일 때의 그 쾌감이란! (웃음)

    마무리: 더 나은 GitOps 여정을 위해

    오늘은 Argo CD 멀티 클러스터 GitOps에 대한 저의 경험과 베스트 프랙티스 체크리스트를 공유해드렸습니다. 처음에는 복잡하게 느껴질 수 있지만, 한번 구축하고 나면 정말 강력한 배포 시스템을 손에 넣게 될 겁니다. 저도 처음엔 헷갈렸는데, 계속 시도하고 삽질하면서 익숙해지더라고요. 독자 여러분도 이 글을 통해 성공적인 GitOps 여정을 시작하시길 바랍니다.

    혹시 GitOps 환경에서 더 깊이 있는 보안 강화나, CI(Continuous Integration) 파이프라인과의 연동에 대해 궁금하시다면, 다음 글에서 다룰 예정이니 기대해주세요!

    Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 요약 체크리스트

    지금까지의 내용을 요약한 체크리스트입니다. 여러분의 GitOps 환경을 점검할 때 유용하게 활용되길 바랍니다.