13년차의 서버실

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

[태그:] 온프레미스 Kubernetes 클라우드 마이그레이션

  • [k8s] 온프레미스 Kubernetes 클라우드 마이그레이션: 결정 기준과 체크포인트

    [k8s] 온프레미스 Kubernetes 클라우드 마이그레이션: 결정 기준과 체크포인트

    [인프라] 온프레미스 Kubernetes 클라우드 마이그레이션: 결정 기준과 체크포인트

    온프레미스 Kubernetes 클라우드 마이그레이션 이야기가 나오면, 흔히 인프라 위치만 바꾸는 일처럼 들리죠. 그런데 실무에 들어가 보면 느낌이 꽤 다릅니다. 서버 몇 대 옮기는 문제가 아니라 운영 책임, 장애 양상, 배포 기준, 비용 구조가 한꺼번에 바뀌는 의사결정에 더 가깝거든요. 같은 Kubernetes라도 온프레미스와 클라우드 관리형 환경은 장애 지점이 다르고, 같은 YAML도 운영 의미가 달라지는 구간이 분명히 있습니다.

    이번 글에서는 온프레미스 Kubernetes 클라우드 마이그레이션을 검토할 때 먼저 봐야 할 판단 기준, 이전 전에 걸러야 하는 비호환 요소, 그리고 마이그레이션 당일보다 더 중요한 사전 검증 포인트를 실무 관점으로 정리해보겠습니다. 문서만 읽으면 잘 안 보이는 부분, 특히 "왜 여기서 일정이 자꾸 미끄러지는지"까지 같이 짚어보겠습니다.

    온프레미스 클러스터, 클라우드 관리형 Kubernetes, 하이브리드 연결 구조를 한눈에 보여주는 개요 이미지입니다.

    온프레미스 Kubernetes 클라우드 마이그레이션이 어려운 이유

    Kubernetes 자체는 이식성이 좋은 편입니다. 문제는 실제 운영 환경이 그렇지 않다는 데 있어요. 온프레미스에서는 익숙했던 L2/L3 네트워크, 방화벽 예외, 사내 DNS 포워더, LDAP, 사설 이미지 레지스트리, NFS나 Ceph 같은 스토리지, 고정 IP 화이트리스트가 클라우드로 들어가는 순간 전부 다시 검토 대상이 됩니다. 저는 이걸 클러스터 이동이라기보다 주변 의존성 노출 작업이라고 보는 편입니다.

    예를 들어 PVC가 모두 Bound 상태라고 해서 스토리지 이전 준비가 끝난 건 아니더라고요. 온프레미스에서 RWX로 쓰던 워크로드가 클라우드 블록 스토리지로 옮겨가면, 같은 YAML이어도 동시 마운트 전제가 깨질 수 있습니다. 반대로 stateless처럼 보이던 서비스가 실제로는 사내 DNS suffix, 내부 SMTP, 사설 인증서 체인에 강하게 묶여 있어서 클라우드에선 readinessProbe부터 실패하는 경우도 많습니다. 겉으로 보이는 리소스 종류보다 런타임에 어디로 붙는지가 훨씬 중요합니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 결정 기준

    방향을 잡을 때 저는 네 가지를 먼저 봅니다. 이 기준이 흐리면 설계가 아니라 희망사항만 남기 쉽습니다.

    • 운영 책임 경계: Control Plane, 업그레이드, 인증서, CNI, CSI, 장애 대응을 누가 맡을지
    • 데이터 중력: 데이터셋과 핵심 DB가 어디에 있어야 하는지, 애플리케이션이 그 데이터를 얼마나 자주 왕복하는지
    • 지연 시간과 네트워크 경로: 온프레미스 시스템, 레거시 장비, 사내 인증 체계와의 호출 빈도와 실패 허용 범위
    • 규제와 접근 통제: 로그 보존, 망 분리, 비밀정보 저장 위치, 감사 추적의 기준점

    실무에서는 기술 우수성보다 우선순위가 더 중요합니다. 운영 부담을 줄이는 게 1순위인지, 아니면 데이터를 가까이 두는 게 1순위인지부터 정해야 해요. 둘을 동시에 최대화하려 하면 하이브리드가 나오는데, 그 순간 인증, 라우팅, 관측성이 다중 경로가 됩니다. 하이브리드는 좋은 절충안이라기보다, 대개 명확한 제약 때문에 선택하는 구조에 가깝습니다.

    선택지 적합한 상황 피해야 하는 상황 성능/비용/안정성에 주는 영향 최종 판단 기준
    온프레미스 유지 공장 설비, 내부 장비, 초저지연 연동, 데이터 상주 요구가 강할 때 플랫폼 운영 인력이 이미 부족하고 업그레이드가 밀려 있을 때 지연 시간은 유리하지만 운영 복잡도와 업그레이드 리스크를 계속 짊어지게 됨 플랫폼 팀이 클러스터 수명주기를 끝까지 책임질 수 있는지
    관리형 Kubernetes로 이전 Control Plane 운영 부담을 줄이고 배포 표준화가 필요할 때 핵심 DB와 장비가 온프레미스에 있고 왕복 호출이 잦을 때 운영 안정성은 올라가지만 네트워크 경로와 egress 비용이 새로 생길 수 있음 장애 원인의 상당수를 플랫폼이 아니라 애플리케이션 쪽으로 좁히고 싶은지
    하이브리드 클라우드 데이터는 남겨두되 웹/API 계층만 단계적으로 이전해야 할 때 조직 합의가 안 돼서 임시 절충안으로 선택할 때 초기 전환은 유연하지만 네트워크, 인증, 모니터링 경로가 가장 복잡해짐 단계적 이전의 필요성이 복잡도 증가를 정당화하는지
    리플랫폼 후 이전 hostPath, 정적 IP, 특정 Ingress annotation, 온프레미스 스토리지 전제가 강할 때 시간이 없다는 이유로 구조 개선 없이 그대로 들고 가려 할 때 초기 일정은 늘어나도 장기적으로 운영 단순화와 재현성이 좋아짐 지금의 편의를 위해 미래 장애를 예약하는 구조인지

    제 권고는 비교적 분명합니다. 온프레미스 의존성이 데이터와 장비에 붙어 있으면 유지 또는 제한적 하이브리드, 문제의 본질이 플랫폼 운영 부담이면 관리형 Kubernetes가 맞습니다. 둘 다 애매하면 마이그레이션 설계부터 시작하지 말고, 먼저 의존성 인벤토리와 트래픽 흐름도를 만드는 게 훨씬 낫습니다.

    K8s 이전 전략, 이렇게 나누면 덜 꼬입니다

    마이그레이션은 세 단계로 나눠야 덜 꼬입니다. 이걸 한 번에 밀어붙이면 꼭 뒤에서 문제가 터지더라고요.

    1. 발견(Discovery): 현재 클러스터 리소스, 외부 의존성, 네트워크 경로, 보안 정책 수집
    2. 분리(Decoupling): 클라우드 비호환 요소 제거, 환경 차이 분리, 매니페스트 표준화
    3. 이전(Migration): 워크로드 배포, 데이터 동기화, 트래픽 전환, 롤백 절차 검증

    여기서 가장 자주 실패하는 건 발견 단계를 "대충 리소스 목록 뽑는 작업"으로 축소하는 경우입니다. 실제로는 이 단계에서 이전 난도 분류까지 끝내야 해요. 같은 Deployment라도 한쪽은 ConfigMap과 Secret만 있으면 바로 올라가고, 다른 한쪽은 내부 LDAP, SMB 마운트, 고정 IP 허용 목록, 사내 인증서 체인이 없으면 부팅조차 안 됩니다. 예전에 배치 잡 하나를 단순 API 보조 작업으로 봤다가, 내부 파일 서버 경로가 빠져 있다는 걸 뒤늦게 발견해 일정이 밀린 적이 있습니다. YAML은 얌전했는데 런타임 의존성은 전혀 얌전하지 않았던 케이스였죠.

    실전 구현 1: 현재 의존성부터 수집하기

    아래 명령은 단순 인벤토리 수집용이 아닙니다. "무엇이 이동을 어렵게 만드는지"를 분류하기 위한 출발점에 가깝습니다. 리소스 수보다 위험 신호를 읽는 용도로 보셔야 합니다.

    kubectl get nodes -o wide
    kubectl get ns
    kubectl get deploy,statefulset,daemonset -A -o wide
    kubectl get svc,ingress,endpointslices -A
    kubectl get pvc,pv -A
    kubectl get storageclass
    kubectl get networkpolicy -A
    kubectl get poddisruptionbudget -A
    kubectl get sa,role,rolebinding,clusterrole,clusterrolebinding -A
    kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations
    kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\t"}{range .spec.volumes[*]}{.name}{":"}{.hostPath.path}{":"}{.persistentVolumeClaim.claimName}{" "}{end}{"\n"}{end}'

    이 결과를 볼 때 바로 표시해두면 좋은 위험 항목은 아래와 같습니다.

    • StatefulSet, PersistentVolumeClaim, PersistentVolume: 데이터 이전과 복구 절차 검증이 필요합니다.
    • hostPath: 관리형 환경이나 강화된 보안 정책에서는 제약을 받거나 의미가 달라질 수 있습니다.
    • DaemonSet: 노드 접근, 로그 수집, 보안 에이전트처럼 클라우드 노드 정책과 충돌하기 쉽습니다.
    • webhook: admission webhook가 필수 경로에 있고 failurePolicy: Fail로 운영되면 배포 자체가 막힐 수 있습니다.
    • serviceAccount와 RBAC: 클라우드 IAM 연계나 워크로드 아이덴티티 체계로 바뀌면 권한 모델이 달라집니다.
    • NetworkPolicy: CNI 구현 차이 때문에 같은 정책이라도 적용 범위와 디버깅 포인트가 달라질 수 있습니다.

    이 단계에서 꼭 같이 봐야 하는 게 파드 스펙에 박힌 온프레미스 전제입니다. 저는 아래처럼 한 번 더 필터링해서 눈에 띄는 항목을 추립니다.

    kubectl get deploy,statefulset,daemonset -A -o yaml | egrep 'hostPath:|nodeSelector:|tolerations:|topologySpreadConstraints:|storageClassName:|loadBalancerIP:|externalIPs:|dnsConfig:|dnsPolicy:|ingressClassName:'
    kubectl get ingress -A -o yaml | egrep 'kubernetes.io/ingress.class|nginx.ingress.kubernetes.io|alb.ingress.kubernetes.io|traefik.ingress.kubernetes.io'

    예를 들어 loadBalancerIP나 externalIPs에 기대는 매니페스트는 클라우드에서 그대로 동작하지 않는 경우가 많습니다. 또 nodeSelector가 온프레미스 물리 노드 이름이나 특정 랙 구성을 전제로 작성돼 있으면 스케줄링이 바로 깨질 수 있어요. 이런 항목은 나중에 급히 손보는 게 아니라, 발견 단계에서 리플랫폼 대상으로 태깅해두는 편이 훨씬 편합니다.

    판단 기준도 리소스 종류보다 서비스 특성에 두는 게 좋습니다. 예를 들어 PVC가 붙은 핵심 업무 워크로드는 단순 재배포 대상이 아니라 데이터 정합성 검증 대상입니다. 반대로 외부 상태 저장소를 이미 쓰는 API 서버는 선행 이전 후보가 됩니다. 이 분류만 제대로 해도 일정 산정이 훨씬 현실적으로 바뀝니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 의존성 매핑 이미지

    네임스페이스, 인그레스, 스토리지, 네트워크 정책을 기준으로 이전 대상과 위험 요소를 분류하는 이미지입니다.

    실전 구현 2: 매니페스트를 클라우드 친화적으로 정리하기

    온프레미스에서 잘 돌던 매니페스트를 그대로 들고 가면, 처음엔 배포가 되는 것처럼 보여도 운영 구간에서 문제가 납니다. 특히 StorageClass, Ingress, anti-affinity, 보안 컨텍스트, 서비스 어카운트 연계는 거의 항상 재검토 대상이에요. 이 단계의 목표는 단순히 배포 가능이 아니라 새 환경에서 설명 가능한 동작입니다. 이 기준을 잡아두면 나중에 장애가 나도 훨씬 빨리 좁혀집니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sample-api
      namespace: prod
    spec:
      replicas: 3
      revisionHistoryLimit: 5
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 0
          maxSurge: 1
      selector:
        matchLabels:
          app: sample-api
      template:
        metadata:
          labels:
            app: sample-api
        spec:
          serviceAccountName: sample-api
          terminationGracePeriodSeconds: 30
          securityContext:
            runAsNonRoot: true
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                matchLabels:
                  app: sample-api
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
                - weight: 100
                  podAffinityTerm:
                    topologyKey: kubernetes.io/hostname
                    labelSelector:
                      matchLabels:
                        app: sample-api
          containers:
            - name: app
              image: registry.example.com/sample-api:1.0.0
              imagePullPolicy: IfNotPresent
              ports:
                - name: http
                  containerPort: 8080
              env:
                - name: TZ
                  value: Asia/Seoul
              readinessProbe:
                httpGet:
                  path: /ready
                  port: http
                initialDelaySeconds: 5
                periodSeconds: 10
                timeoutSeconds: 2
                failureThreshold: 3
              livenessProbe:
                httpGet:
                  path: /health
                  port: http
                initialDelaySeconds: 15
                periodSeconds: 20
                timeoutSeconds: 2
                failureThreshold: 3
              startupProbe:
                httpGet:
                  path: /ready
                  port: http
                failureThreshold: 30
                periodSeconds: 5
              resources:
                requests:
                  cpu: "250m"
                  memory: "256Mi"
                limits:
                  cpu: "1"
                  memory: "512Mi"
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: sample-api
      namespace: prod
    spec:
      selector:
        app: sample-api
      ports:
        - name: http
          port: 80
          targetPort: http
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: sample-api
      namespace: prod
    spec:
      ingressClassName: nginx
      rules:
        - host: sample-api.example.com
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: sample-api
                    port:
                      number: 80

    이 예시에서 실무적으로 중요한 포인트는 세 가지입니다. 첫째, readinessProbe와 startupProbe를 분리해서 초기 기동이 느린 애플리케이션이 롤링 업데이트 중 불필요하게 죽지 않게 해야 합니다. Kubernetes 공식 문서 기준으로도 startupProbe가 성공하기 전에는 readiness와 liveness가 동작하지 않기 때문에, 느린 기동 앱에서 꽤 유용하더라고요. 둘째, anti-affinity와 topologySpreadConstraints를 넣어 두지 않으면 이전 직후 일부 노드에 파드가 몰려 장애 복원력이 떨어질 수 있습니다. 셋째, 예전 annotation 중심 Ingress 설정 대신 가능하면 ingressClassName처럼 명시적 필드를 쓰는 편이 환경 이식성에 유리합니다.

    Helm이나 Kustomize를 쓸 때도 무조건 환경별 파일을 늘리기보다, 변해야 하는 축만 분리하는 쪽이 좋습니다. 예를 들어 온프레미스와 클라우드 차이가 스토리지 클래스, 인그레스 클래스, 서비스 타입뿐이라면 그 값만 오버레이로 빼는 식이죠. YAML 전체를 환경별로 복제하면 당장은 편하지만 6개월만 지나도 drift가 생깁니다. 운영팀이 제일 힘들어하는 건 복잡한 기술 자체보다, 서로 조금씩 다른 매니페스트인 경우가 많습니다.

    실전 구현 3: 이전 전 검증 체크리스트 만들기

    배포 전에 검증 항목을 코드화하지 않으면, 마이그레이션 당일 사람 기억력에 의존하게 됩니다. 이건 진짜 위험합니다. 최소한 아래 정도는 사전 검증 명령으로 굳혀두는 편이 좋습니다.

    kubectl diff -f k8s/
    kubectl apply --server-side --dry-run=server -f k8s/
    kubectl auth can-i create deployments --as system:serviceaccount:prod:deployer -n prod
    kubectl auth can-i get secrets --as system:serviceaccount:prod:deployer -n prod
    kubectl wait --for=condition=Available deployment/sample-api -n prod --timeout=180s
    kubectl top pod -A
    kubectl top node
    kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -n 50

    핵심은 명령 자체보다 해석 기준입니다.

    • kubectl diff에서 스토리지, 셀렉터, 서비스 타입 차이가 크면 단순 이전보다 구조 정리가 먼저입니다.
    • --dry-run=server가 실패하면 API 버전 문제만 보지 말고 admission policy, CRD 누락, webhook 응답 실패도 같이 의심해야 합니다.
    • auth can-i가 막히면 RBAC만의 문제가 아니라 클라우드 IAM 연동 방식 차이일 수 있습니다.
    • kubectl top는 평균값보다 편차를 봐야 합니다. 일부 노드에 몰리면 스케줄링 정책 문제일 가능성이 큽니다. 다만 이 명령은 Metrics Server가 구성돼 있어야 동작합니다.
    • events는 단발성 경고보다 반복 패턴이 중요합니다. 같은 Warning이 주기적으로 나오면 구조 문제일 때가 많습니다.

    실제 시나리오를 하나 들어보면 더 명확합니다. 온프레미스에 있던 API가 클라우드로 올라간 뒤 readiness는 통과하는데 사용자 응답만 느려지는 경우가 있습니다. 이때 많은 팀이 애플리케이션 성능 저하로 접근하는데, 제가 본 사례 대부분은 앱 코드보다 외부 DB 왕복 경로 증가, NAT 경유, 사내 DNS 포워딩 지연, 프록시 재시도가 원인이었습니다. 앱 로그만 보면 안 보이고, 네트워크 관점으로 봐야 잡히는 문제였죠.

    하이브리드 클라우드 기반 온프레미스 Kubernetes 클라우드 마이그레이션 네트워크 이미지

    온프레미스 DB, 클라우드 Kubernetes, VPN 또는 전용선 연결, Ingress와 egress 흐름을 설명하는 이미지입니다.

    자주 터지는 문제와 해결법

    이 구간은 문서보다 경험 차이가 크게 나는 부분입니다. 여기서 시간을 아끼려는 시도가 오히려 더 비싸게 돌아오는 경우를 많이 봤습니다.

    1. 스토리지 클래스 이름만 바꾸고 끝난 줄 아는 경우

    PVC가 Bound 되었다고 애플리케이션이 같은 성격의 스토리지를 얻은 건 아닙니다. ReadWriteOnce, ReadWriteMany, 파일시스템 특성, 스냅샷, 확장 방식, 마운트 지연이 전부 다를 수 있어요. 온프레미스에서 NFS나 CephFS 기반 RWX로 쓰던 구성이 클라우드 블록 스토리지로 바뀌면, 동일 노드 외 동시 접근 전제가 깨질 수 있습니다. 근본 원인은 Kubernetes라기보다 백엔드 스토리지 모델 차이에 있습니다.

    이럴 때는 YAML 수정 전에 두 가지를 먼저 확인하는 편이 낫습니다. 첫째, 애플리케이션이 정말 공유 파일시스템 semantics를 필요로 하는지. 둘째, 백업과 복구가 volume snapshot 중심인지 파일 복제 중심인지. 이걸 구분하지 않으면 이전 후 복구 절차가 오히려 후퇴할 수 있습니다.

    2. 서비스 디스커버리와 DNS 지연

    이전 후 "애플리케이션은 살아 있는데 호출이 이상하게 느리다"는 사례의 상당수가 DNS입니다. CoreDNS upstream, search domain, 사내 DNS 포워더, 외부 인증 서버 해석 경로가 바뀌면 앱은 멀쩡해도 응답 시간이 흔들립니다. 특히 내부 FQDN이 아닌 짧은 호스트명을 쓰던 서비스는 환경이 바뀌는 순간 문제가 확 드러납니다. 근본 원인은 앱 로직이 아니라 이름 해석 경로의 숨은 결합인 경우가 많습니다.

    kubectl -n kube-system get configmap coredns -o yaml
    kubectl run dns-debug --rm -it --image=busybox:1.36 --restart=Never -- nslookup internal.example.local
    kubectl exec -n prod deploy/sample-api -- cat /etc/resolv.conf
    kubectl exec -n prod deploy/sample-api -- wget -S -O- http://sample-api.prod.svc.cluster.local/health

    이 네 가지를 보면 search domain, nameserver, 내부 서비스 해석, 외부 도메인 질의가 어디서 막히는지 꽤 빨리 드러납니다. 저도 앱 팀이 성능 문제라고 가져오면 먼저 /etc/resolv.conf와 DNS 응답 경로부터 봅니다. 의외로 거기서 끝나는 경우가 정말 많더라고요.

    3. 보안 정책 충돌

    온프레미스에서는 관성적으로 허용되던 privileged 컨테이너, hostNetwork, hostPath, 루트 권한 실행이 관리형 환경이나 강화된 정책 환경에선 제약을 받기 쉽습니다. Pod Security Admission, admission webhook, 조직 보안 정책이 한꺼번에 걸리면 파드가 Pending도 아니고 아예 생성 거부될 수 있습니다. 이런 문제의 근본 원인은 "클라우드가 엄격해서"라기보다 기존 워크로드가 노드 내부 구조를 전제로 하고 있었기 때문인 경우가 많습니다.

    이 경우엔 예외를 늘리기보다 정말 필요한 권한인지 먼저 줄이는 편이 낫습니다. 이전 프로젝트에서 hostPath 의존 로그 수집기를 그대로 들고 가려다가 시간을 많이 썼는데, 결국 표준 로그 수집 방식으로 바꾸고 나서야 운영이 단순해졌습니다. 예외 허용은 빨라 보이지만 이후 감사와 장애 대응이 계속 무거워집니다.

    4. 관측성 누락

    마이그레이션 당일에는 "왜 느리지", "왜 일부만 실패하지", "왜 롤아웃이 안 끝나지" 같은 질문이 동시에 쏟아집니다. 이때 메트릭, 로그, 트레이스 중 하나라도 비어 있으면 원인 파악 속도가 급격히 떨어집니다. 특히 하이브리드에서는 온프레미스 구간과 클라우드 구간을 한 화면에서 연결해보지 못하면 책임 공방이 먼저 시작되기 쉽습니다. 근본 원인은 도구 부재보다 관측 기준점이 분산된 설계에 있습니다.

    기준은 단순합니다. 이전 전부터 적어도 애플리케이션 로그, Kubernetes 이벤트, 기본 자원 메트릭, 외부 의존성 호출 경로를 함께 볼 수 있어야 합니다. 배포만 성공하고 관측이 비어 있으면, 사실상 프로덕션 디버깅을 라이브로 시작하는 셈이거든요.

    비용보다 먼저 봐야 할 것: 네트워크와 운영 복잡도

    많은 팀이 클라우드 이전을 논의할 때 바로 인스턴스 비용부터 계산합니다. 물론 중요합니다. 다만 실제로 일정과 장애를 더 크게 흔드는 건 컴퓨트 단가보다 네트워크 경로와 운영 복잡도입니다. 온프레미스 DB를 계속 쓰면서 앱만 클라우드로 올리면, 성능 문제는 CPU가 아니라 왕복 지연에서 먼저 드러나는 경우가 많습니다. 그리고 하이브리드 구조에선 누가 어떤 구간을 책임지는지도 흐려지기 쉽습니다.

    질문 A를 택할 때 B를 택할 때 실무 추천
    DB를 어디에 둘 것인가 온프레미스 유지: 데이터 상주와 장비 연동에 유리 클라우드 이전: 앱과 DB를 가깝게 두기 쉬움 앱-DB 왕복이 잦으면 가능한 한 같은 쪽에 두는 편이 안전합니다.
    Control Plane을 누가 운영할 것인가 직접 운영: 유연성은 크지만 업그레이드 부담이 큼 관리형 사용: 표준화와 운영 단순화에 유리 플랫폼 운영 인력이 부족하면 관리형이 대체로 맞습니다.
    트래픽을 한 번에 바꿀 것인가 빅뱅 전환: 구조는 단순하지만 실패 비용이 큼 점진 전환: 검증은 쉬우나 경로 복잡도가 증가 stateless부터 점진 전환, 상태 저장 계층은 별도 계획이 현실적입니다.
    환경별 매니페스트를 어떻게 관리할 것인가 완전 분리: 빠르게 시작 가능 공통 베이스 + 차이만 오버레이: 관리 일관성 확보 장기 운영을 생각하면 차이만 분리하는 쪽이 drift를 줄입니다.

    검증과 결과 확인은 이렇게 보시면 됩니다

    이전 완료를 배포 성공으로만 판단하면 안 됩니다. 실제로 봐야 하는 건 애플리케이션이 새 환경에서 안정적으로 서비스되고, 장애 원인을 추적할 수 있는 상태인지입니다.

    1. 모든 파드가 Running인지보다 Ready 상태가 안정적으로 유지되는지
    2. 재시작, Pending, 이미지 풀 실패, 스케줄링 실패가 없는지
    3. Ingress를 통한 실제 사용자 경로와 내부 서비스 간 호출이 모두 정상인지
    4. 백업, 복구, 롤백 절차가 새 환경에서 재현 가능한지
    kubectl get pods -A
    kubectl get events -A --sort-by=.metadata.creationTimestamp
    kubectl rollout status deploy/sample-api -n prod
    kubectl logs deploy/sample-api -n prod --tail=100
    kubectl describe pod -n prod -l app=sample-api
    kubectl get endpoints -n prod sample-api -o yaml

    해석 기준은 조금 더 구체적이어야 합니다.

    • Pending가 남으면 리소스 부족보다 먼저 스토리지 바인딩, 노드 셀렉터, taint/toleration, zone 제약을 의심합니다.
    • CrashLoopBackOff면 이미지보다 환경 변수, Secret, 외부 시스템 연결 실패를 먼저 봅니다.
    • rollout status가 지연되면 readinessProbe 경로, 서비스 엔드포인트, Ingress 백엔드 연결을 함께 확인합니다.
    • describe pod에서 이벤트 순서를 보면 이미지 풀 실패인지, 볼륨 마운트 실패인지, probe 실패인지 구분이 빨라집니다.
    • endpoints가 비어 있으면 서비스 셀렉터와 파드 라벨 불일치부터 의심하는 게 빠릅니다.

    제가 자주 하는 질문도 있습니다. "지금 장애는 앱이 못 뜨는 문제인가, 떴는데 연결이 느린 문제인가, 연결은 되는데 권한이 막히는 문제인가?" 이 세 가지를 빨리 분리하면 대응 속도가 확 달라집니다. 마이그레이션 실패는 대개 기능 실패보다 분류 실패에서 길어집니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 검증 대시보드 이미지

    파드 상태, 이벤트, 에러 로그, 롤아웃 상태를 함께 보는 검증 단계 이미지입니다.

    하이브리드 클라우드가 맞는 경우, 아닌 경우

    하이브리드는 멋있어 보이지만, 운영팀 관점에선 가장 신중해야 하는 선택입니다. 두 환경의 장점을 동시에 쓰는 구조라기보다, 두 환경의 제약을 동시에 관리하는 구조가 되기 쉽기 때문이죠. 그래도 분명 맞는 경우는 있습니다.

    • 하이브리드가 맞는 경우: 데이터는 사내에 남겨야 하지만, 웹/API 계층의 배포 속도와 확장성이 더 시급할 때
    • 하이브리드가 피곤한 경우: 조직 합의가 안 돼서 임시 절충안으로 택할 때
    • 완전 이전이 맞는 경우: 플랫폼 운영 인력이 부족하고 관리형 Kubernetes 이점을 크게 볼 수 있을 때
    • 온프레미스 유지가 맞는 경우: 장비 연동, 내부 전용망, 초저지연 요구가 비즈니스 핵심일 때

    현업에서 내리는 권고는 이렇습니다. 하이브리드는 전환 과정 때문에 택하는 것이지, 영구 기본값으로 두면 운영 비용이 커질 가능성이 높습니다. 단계적 이전이 목적이라면 기간, 책임 경계, 최종 목표 상태를 미리 정해두는 편이 좋습니다. 목표 없는 하이브리드는 거의 항상 복잡도만 남깁니다.

    자주 묻는 질문

    무중단으로 꼭 옮겨야 하나요?

    항상 그렇진 않습니다. 세션 상태가 외부 저장소에 있고, 데이터 동기화와 트래픽 전환 절차가 준비돼 있으면 점진 전환이 가능합니다. 반대로 상태 저장 워크로드는 억지 무중단보다 짧고 통제된 점검 시간이 더 안전할 때가 많습니다. 저는 "무중단"보다 롤백 가능한 전환을 더 높은 우선순위로 둡니다.

    기존 YAML을 그대로 재사용해도 되나요?

    일부는 가능합니다. 다만 StorageClass, Ingress, 서비스 타입, 보안 컨텍스트, 노드 스케줄링, 아이덴티티 연계는 거의 항상 다시 봐야 했습니다. 같은 Kubernetes여도 스토리지와 네트워크, 인증 모델이 다르면 운영 의미가 달라지거든요.

    무엇부터 옮기는 게 좋을까요?

    보통은 stateless API, 내부 도구, 배치 워크로드부터 시작하는 편이 좋습니다. 반대로 공유 파일시스템 의존 서비스, 내부 장비와 강결합된 서비스, 핵심 DB 연동이 많은 서비스는 뒤로 미루는 게 안전합니다. 실패 비용이 낮은 워크로드에서 먼저 학습한 뒤 어려운 대상을 다루는 편이 전체 일정이 덜 흔들립니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 선택 기준 요약 이미지

    각 선택지별 추천 상황과 주의사항을 짧게 비교하는 요약 이미지입니다.

    어떤 경우에 어떤 선택을 하면 되냐면

    온프레미스 Kubernetes 클라우드 마이그레이션의 정답은 하나가 아닙니다. 다만 선택 기준은 분명해야 합니다. 사내 데이터 의존과 장비 연동이 강하면 온프레미스 유지 또는 제한적 하이브리드가 맞습니다. 반대로 플랫폼 운영 부담이 이미 팀 생산성을 갉아먹고 있다면, 관리형 Kubernetes로 빨리 표준화하는 편이 낫습니다. 애매한 상태로 둘 다 잡으려 하면 실제로는 운영 복잡도만 늘어나는 경우가 많습니다.

    실무에서 권하는 판단 순서는 이렇습니다. 첫째, 데이터와 네트워크 경로를 기준으로 애플리케이션을 분류합니다. 둘째, 운영 책임을 줄이는 게 핵심이면 관리형으로 갑니다. 셋째, 하이브리드는 단계적 이전이 꼭 필요할 때만 씁니다. 넷째, hostPath, RWX 의존, 정적 네트워크 전제가 많다면 리플랫폼을 먼저 합니다. 이 기준이 서면 마이그레이션은 훨씬 덜 추상적이고, 일정과 위험도도 현실적으로 보이기 시작합니다.

    관련해서 다음 글에서는 Kubernetes Ingress와 Load Balancer를 클라우드 환경에서 어떻게 재설계할지를 더 깊게 다뤄보겠습니다. 결국 잘 된 이전은 화려한 아키텍처보다, 의존성 인벤토리, 검증 가능한 체크리스트, 단순한 운영 구조에서 나옵니다. 이 부분은 여러 번 해봐도 결국 같은 결론으로 돌아오더라고요.