13년차의 서버실

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

[태그:] DevOps

  • Ansible로 홈랩 자동화 입문 — 인벤토리와 첫 플레이북

    홈랩에 서버가 몇 대만 넘어가도 “하나하나 SSH 접속해서 똑같은 설정” 반복이 지옥이 됩니다. 저는 그래서 Ansible을 씁니다. 제어 노드 한 대에서 여러 서버를 동시에, 일관되게 설정하는 자동화 도구죠. 홈랩에 Ansible 학습 환경을 꾸리고 쓰는 법을 입문자 눈높이로 정리합니다.

    1. Ansible이 좋은 이유

    • 에이전트 불필요: 관리 대상에 뭘 설치할 필요 없이 SSH만 되면 됩니다.
    • 멱등성(idempotent): 같은 플레이북을 몇 번 돌려도 결과가 같습니다(이미 된 건 건너뜀).
    • YAML: 사람이 읽는 선언형 문법이라 진입장벽이 낮습니다.

    2. 홈랩 학습 토폴로지

    저는 제어 노드 1대 + 관리 대상 여러 대로 학습 랩을 구성했습니다.

    역할 설명
    제어 노드(control) Ansible 설치, 여기서 명령 실행
    관리 대상(node1~3) SSH로 제어받는 서버들

    Proxmox에서 cloud-init 템플릿으로 노드 몇 대를 찍어내면 학습 환경이 몇 분 만에 완성됩니다.

    3. 설치와 인벤토리

    # 제어 노드에 설치
    sudo apt install -y ansible
    
    # SSH 키를 관리 대상에 배포(비밀번호 없이 접속)
    ssh-copy-id user@node1

    인벤토리는 “누구를 관리할지” 목록입니다.

    # inventory.ini
    [webservers]
    node1 ansible_host=10.0.0.11
    node2 ansible_host=10.0.0.12
    
    [all:vars]
    ansible_user=user
    # 연결 확인 (전체에 ping)
    ansible -i inventory.ini all -m ping

    4. 첫 플레이북

    플레이북은 “무엇을 할지”를 YAML로 적은 것입니다. 패키지 설치 + 서비스 보장 예시입니다.

    # site.yml
    - hosts: webservers
      become: true
      tasks:
        - name: nginx 설치
          apt:
            name: nginx
            state: present
            update_cache: true
    
        - name: nginx 실행 보장
          service:
            name: nginx
            state: started
            enabled: true
    ansible-playbook -i inventory.ini site.yml

    이 한 번으로 모든 webservers에 nginx가 설치·실행됩니다. 다시 돌려도 “이미 됨”으로 건너뛰는 게 멱등성입니다.

    5. 실전에서 자주 쓰는 모듈

    모듈 용도
    apt/dnf 패키지 관리
    copy/template 파일·설정 배포(변수 치환)
    service/systemd 서비스 제어
    user 계정 관리
    lineinfile 설정 파일 한 줄 수정

    6. 홈랩에서의 활용

    • 초기 세팅 표준화: 새 VM마다 계정·SSH·방화벽·패키지를 플레이북 하나로.
    • 일괄 업데이트: apt upgrade를 전 서버에 동시에.
    • 설정 드리프트 방지: 플레이북이 곧 “원하는 상태” 문서이자 복구 수단.

    7. 정리

    Ansible은 인벤토리(누구를) + 플레이북(무엇을), 이 두 가지가 전부입니다. 에이전트 없이 SSH만으로, 멱등하게, 여러 서버를 한 번에 — 홈랩이 2~3대만 넘어가도 체감 효과가 큽니다. cloud-init으로 노드를 찍고 Ansible로 설정하는 조합이면, 서버를 “손으로” 만지는 일이 확 줄어듭니다.

  • [k8s] Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    [k8s] Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    Kubernetes 프로브 트러블슈팅이 중요한 이유

    Kubernetes 프로브 트러블슈팅은 단순히 /health 경로가 200을 반환하는지 확인하는 일이 아닙니다. 운영에서는 이 설정 하나가 재시작 루프, 트래픽 블랙홀, 배포 지연, 의존성 장애 전파를 만들기도 하고 막기도 하거든요. 13년 넘게 서비스를 운영하면서 느낀 건, 프로브는 애플리케이션의 상태를 보는 기능이라기보다 쿠버네티스가 장애에 어떻게 반응할지 정하는 정책에 가깝다는 점입니다.

    대표적인 장면은 비슷합니다. 앱 로그만 보면 큰 문제가 없는데 kubectl get pods의 RESTARTS가 계속 올라갑니다. 또는 배포는 끝났는데 Service 뒤에서 파드가 빠져 502, 503이 발생합니다. 이때 무작정 앱을 재배포하거나 노드를 의심하면 시간이 정말 빨리 녹습니다. 먼저 봐야 할 것은 kubelet 이벤트입니다. Liveness probe failed 뒤에 Killing container가 붙는지, Readiness probe failed만 반복되는지에 따라 원인이 완전히 갈립니다.

    위 흐름에서 핵심은 하나입니다. Liveness 실패는 재시작을 부르고, Readiness 실패는 트래픽 제외를 부릅니다. 둘 다 헬스 체크처럼 보이지만 운영 결과는 다릅니다. 그래서 프로브를 설계할 때는 “정상인가?”보다 “실패했을 때 쿠버네티스가 무엇을 해야 하는가?”를 먼저 정해야 합니다.

    Liveness Probe 실패와 Readiness Probe 오류의 차이

    Liveness Probe는 컨테이너가 복구 불가능한 상태에 빠졌는지 판단합니다. 실패가 임계치에 도달하면 kubelet이 해당 컨테이너를 재시작합니다. Readiness Probe는 지금 요청을 받아도 되는지 판단합니다. 실패해도 컨테이너는 계속 실행되지만, 해당 파드는 Service 엔드포인트에서 제외됩니다. Startup Probe는 초기 기동 중인 앱을 보호합니다. Startup Probe가 설정되면 성공하기 전까지 Liveness와 Readiness가 실행되지 않으므로, 기동이 느린 앱에서 조기 재시작을 막는 데 유용합니다.

    운영에서 많이 터지는 실수는 외부 의존성까지 Liveness에 넣는 겁니다. DB가 잠깐 느려졌을 뿐인데 모든 파드가 동시에 재시작되면, 커넥션 재생성·캐시 워밍업·JIT/클래스 로딩 같은 비용이 한꺼번에 몰립니다. 장애를 복구하는 게 아니라 장애에 연료를 붓는 모양이 되더라고요. DB, Redis, 메시지 브로커, 외부 API는 대부분 Readiness에서 다루는 편이 안전합니다.

    구분 Liveness Probe Readiness Probe Startup Probe 운영 판단 기준
    목적 컨테이너를 재시작해야 하는지 판단 트래픽을 받을 준비가 됐는지 판단 초기 기동이 끝났는지 판단 실패 시 원하는 조치가 재시작인지, 트래픽 차단인지부터 정합니다.
    실패 시 동작 컨테이너 재시작 Service 엔드포인트에서 제외 성공 전까지 다른 프로브 보류, 실패 시 컨테이너 재시작 RESTARTS, Events, EndpointSlice를 같이 봐야 오판이 줄어듭니다.
    적합한 검사 이벤트 루프 멈춤, 데드락, 기본 핸들러 무응답 DB 연결, 캐시 준비, 필수 설정 로딩, 마이그레이션 대기 JVM/Node.js/.NET 앱의 긴 부팅, 모델 로딩, 초기 캐시 구성 외부 의존성 실패가 곧 프로세스 사망을 의미하지는 않습니다.
    과도하게 엄격할 때 재시작 루프, 콜드 스타트 비용 증가 정상 파드가 트래픽을 못 받아 가용 용량 감소 기동 실패 감지가 늦어짐 엄격함은 안정성이 아니라 운영 정책입니다. 비용을 함께 계산해야 합니다.
    주로 확인할 증거 Killing container, BackOff, --previous 로그 Ready 0/1, Endpoint/EndpointSlice 누락 기동 로그, 초기화 완료 시점, Startup 실패 이벤트 로그보다 먼저 Events를 보면 방향을 빨리 잡습니다.

    쿠버네티스 헬스 체크 기본 설정 예시

    아래 예시는 세 프로브를 분리한 Deployment입니다. 실제 애플리케이션 이미지가 /livez, /readyz, /startupz 엔드포인트를 제공한다는 전제로 봐주세요. /livez는 프로세스와 HTTP 서버가 응답 가능한지만 확인하고, /readyz는 실제 요청 처리 준비 상태를 확인합니다. /startupz는 초기화가 끝나기 전 Liveness가 끼어들지 않도록 막습니다. 운영 매니페스트에서는 이 정도 분리가 나중에 장애 분석 시간을 꽤 줄여줍니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: probe-demo
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: probe-demo
      template:
        metadata:
          labels:
            app: probe-demo
        spec:
          terminationGracePeriodSeconds: 30
          containers:
            - name: app
              image: your-registry.example.com/probe-demo:1.0.0
              ports:
                - name: http
                  containerPort: 8080
              startupProbe:
                httpGet:
                  path: /startupz
                  port: http
                periodSeconds: 5
                timeoutSeconds: 2
                failureThreshold: 30
              livenessProbe:
                httpGet:
                  path: /livez
                  port: http
                periodSeconds: 10
                timeoutSeconds: 2
                failureThreshold: 3
              readinessProbe:
                httpGet:
                  path: /readyz
                  port: http
                periodSeconds: 5
                timeoutSeconds: 2
                successThreshold: 1
                failureThreshold: 3

    값을 고를 때는 감이 아니라 앱의 실제 시간을 기준으로 잡습니다. 기동 로그에서 “서버 포트 바인딩 완료”, “마이그레이션 완료”, “캐시 로딩 완료”, “요청 처리 가능” 시점을 분리해 보세요. initialDelaySeconds를 길게 늘리는 방식은 단순하지만, 앱이 빨리 떠도 무조건 기다립니다. 반면 startupProbe는 성공하는 순간 다음 단계로 넘어가므로, 느린 기동과 빠른 회복을 같이 다루기 좋습니다.

    프로브 파라미터는 서로 곱해져 운영 결과를 만듭니다. 예를 들어 periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 2라면 실패를 확정하기까지 여러 번의 검사 주기가 필요합니다. 대략 30초 안팎으로 생각할 수 있지만, 정확한 감지 시간은 스케줄링과 응답 지연에 영향을 받습니다. 고정 수치처럼 외우기보다 “얼마나 빨리 빼고 얼마나 늦게 죽일 것인가”라는 정책으로 보는 편이 낫습니다.

    매니페스트에서 확인할 필드는 httpGet.path, port, periodSeconds, timeoutSeconds, failureThreshold, successThreshold입니다. 참고로 successThreshold는 Liveness와 Startup에서는 1이어야 하고, Readiness에서만 1보다 크게 둘 수 있습니다. Readiness 복귀를 일부러 보수적으로 만들고 싶을 때만 조정하세요.

    실전 디버깅: Kubernetes 프로브 트러블슈팅 명령어

    장애가 났을 때 저는 애플리케이션 로그보다 먼저 쿠버네티스 이벤트를 봅니다. 이벤트는 kubelet이 왜 컨테이너를 죽였는지, 왜 Ready에서 제외했는지 비교적 솔직하게 남깁니다. 아래 순서대로 보면 “앱 문제인지, 프로브 설정 문제인지, Service 라우팅 문제인지”가 빠르게 갈립니다.

    1. 파드의 Ready 상태와 재시작 횟수를 확인합니다.
    2. Events에서 Liveness probe failed, Readiness probe failed, Startup probe failed를 찾습니다.
    3. 현재 로그와 이전 컨테이너 로그를 비교합니다.
    4. Service, Endpoint, EndpointSlice에 파드 IP가 들어갔는지 확인합니다.
    5. 클러스터 내부에서 DNS, 포트, HTTP 경로를 직접 호출합니다.
    NS=default
    APP=probe-demo
    POD=$(kubectl get pod -n "$NS" -l app="$APP" -o jsonpath='{.items[0].metadata.name}')
    
    kubectl get pods -n "$NS" -l app="$APP" -o wide
    kubectl describe pod "$POD" -n "$NS"
    kubectl logs "$POD" -n "$NS" --all-containers=true --tail=200
    kubectl logs "$POD" -n "$NS" --previous --tail=200
    kubectl get events -n "$NS" --sort-by=.lastTimestamp
    kubectl get svc "$APP" -n "$NS"
    kubectl get endpoints "$APP" -n "$NS"
    kubectl get endpointslice -n "$NS" -l kubernetes.io/service-name="$APP"

    해석은 이렇게 합니다. Liveness probe failed 다음에 Killing container가 이어지면 재시작 원인은 Liveness입니다. Readiness probe failed만 반복되고 RESTARTS가 늘지 않으면 파드는 살아 있지만 Service 트래픽에서 제외된 겁니다. connection refused는 포트 미오픈, 잘못된 포트명, 앱 기동 전 검사 시작, 또는 앱이 127.0.0.1에만 바인딩된 경우를 의심합니다. HTTP probe failed with statuscode: 404는 대개 경로 오타입니다. context deadline exceeded나 timeout 계열 메시지는 앱 내부 지연, CPU throttling, I/O 대기, GC 지연까지 같이 봐야 합니다.

    kubectl run curl-debug \
      -n default \
      --image=curlimages/curl:8.5.0 \
      --restart=Never \
      --rm -it \
      -- sh
    
    # 디버그 파드 안에서 실행
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/readyz
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/livez
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local:80/readyz

    여기서 중요한 건 “Service DNS로 되는가”와 “Pod IP로 직접 되는가”를 나눠 보는 겁니다. Service DNS는 실패하는데 Pod IP 직접 호출은 성공하면 Service selector나 EndpointSlice 쪽을 봅니다. Pod IP 직접 호출도 실패하면 컨테이너 포트, 앱 바인딩 주소, 경로, 응답 시간을 봅니다. 프로브는 kubelet이 Pod 네트워크의 Pod IP로 직접 보내는 검사라서, Ingress에서 보이는 증상과 다를 수 있습니다.

    재현 가능한 시나리오: Readiness Probe 오류 만들고 확인하기

    테스트 클러스터에서 일부러 잘못된 Readiness 경로를 넣어보면 동작이 선명해집니다. 아래 Pod는 nginx를 띄우지만 존재하지 않는 /not-ready를 Readiness로 검사합니다. 컨테이너는 실행되지만 Ready가 되지 않고, Service 엔드포인트에도 들어가지 않는 상태를 만들 수 있습니다. 이거 한 번 직접 보면 실장애 때 감이 훨씬 빨리 옵니다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: bad-readiness
      labels:
        app: bad-readiness
    spec:
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /not-ready
              port: http
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: bad-readiness
    spec:
      selector:
        app: bad-readiness
      ports:
        - name: http
          port: 80
          targetPort: http
    kubectl apply -f bad-readiness.yaml
    kubectl get pod bad-readiness -w
    kubectl describe pod bad-readiness
    kubectl get endpoints bad-readiness
    kubectl get endpointslice -l kubernetes.io/service-name=bad-readiness
    kubectl delete -f bad-readiness.yaml

    정상적인 관찰 결과는 이렇습니다. kubectl get pod에서 READY가 0/1로 보이고, describe Events에는 Readiness 실패가 찍힙니다. 하지만 RESTARTS는 증가하지 않습니다. Service의 Endpoint 또는 EndpointSlice도 비어 있거나 해당 파드 IP가 빠져 있어야 합니다. 이 작은 실험을 해두면 “왜 컨테이너는 Running인데 트래픽이 안 가지?”라는 질문에 훨씬 빨리 답할 수 있습니다.

    반대로 Liveness 경로를 잘못 지정하면 결과가 달라집니다. READY 이전에 컨테이너가 재시작될 수 있고, Events에는 Liveness 실패와 컨테이너 종료 메시지가 같이 나타납니다. 같은 404라도 Readiness에서는 트래픽 제외, Liveness에서는 재시작입니다. 이 차이를 머리에 넣어두면 디버깅 방향이 흔들리지 않습니다.

    제가 겪은 Liveness Probe 실패 패턴

    가장 위험했던 패턴은 종합 건강검진식 Liveness였습니다. /health 요청 하나에 DB 쿼리, Redis ping, 외부 API 호출, 디스크 체크를 전부 넣는 방식입니다. 겉보기에는 꼼꼼합니다. 그런데 DB가 순간적으로 느려지면 모든 파드가 Liveness 실패로 재시작됩니다. 재시작된 파드는 다시 DB 커넥션을 만들고, 캐시를 채우고, 준비되지 않은 상태에서 또 검사를 받습니다. 문제의 뿌리는 DB 지연인데 증상은 애플리케이션 재시작 폭풍으로 바뀝니다.

    제가 쓰는 기준은 꽤 엄격합니다. Liveness는 “이 프로세스를 죽이면 나아지는가?”에 예라고 답할 수 있을 때만 실패시킵니다. 외부 의존성이 실패했지만 프로세스가 요청 큐를 비우고 회복할 수 있다면 Liveness가 아니라 Readiness입니다. 이 기준이 약간 보수적으로 보여도, 운영에서는 불필요한 재시작을 줄이는 쪽이 대체로 더 싸고 안전했습니다.

    • Liveness: 기본 HTTP 핸들러, 이벤트 루프, 내부 상태 플래그처럼 가볍고 로컬인 검사만 둡니다.
    • Readiness: DB 연결, 브로커 구독, 필수 설정 로딩, 캐시 워밍업처럼 트래픽 처리 조건을 둡니다.
    • Startup: 앱 부팅이 느리거나 초기화 단계가 여러 개인 서비스에서 Liveness 개입을 늦춥니다.
    • 외부 API: 핵심 경로가 아니라면 Readiness에서도 과하게 엄격하게 묶지 않습니다. 서킷 브레이커와 graceful degradation이 더 나은 경우가 많습니다.
    startupProbe:
      httpGet:
        path: /startupz
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 30
    livenessProbe:
      httpGet:
        path: /livez
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /readyz
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 3

    이 구성을 그대로 복사하기보다, 먼저 앱의 실제 기동 경로를 관찰하세요. 포트가 열리는 시점과 요청을 안전하게 처리할 수 있는 시점은 다를 수 있습니다. 특히 Java, .NET, 대형 Node.js 앱처럼 초기 로딩이 긴 서비스는 포트가 먼저 열리고 내부 초기화가 나중에 끝나는 경우가 있습니다. 이때 Liveness를 빨리 켜면 멀쩡히 뜨는 중인 앱을 kubelet이 반복해서 죽입니다.

    검증/결과: 무엇을 보고 정상이라고 판단할까

    프로브 설정을 바꾼 뒤 “배포 성공”만 보고 끝내면 위험합니다. 제가 확인하는 기준은 네 가지입니다. 파드가 Ready인지, 재시작이 멈췄는지, Service 엔드포인트에 포함됐는지, 그리고 실패 이벤트가 더 이상 증가하지 않는지입니다. 이 확인 루틴은 짧지만, 운영에서는 진짜 편하더라고요.

    NS=default
    APP=probe-demo
    
    kubectl rollout status deployment/$APP -n $NS
    kubectl get pods -n $NS -l app=$APP -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount,PHASE:.status.phase,NODE:.spec.nodeName'
    kubectl get endpoints $APP -n $NS -o wide
    kubectl get endpointslice -n $NS -l kubernetes.io/service-name=$APP -o wide
    kubectl describe deployment $APP -n $NS
    kubectl get events -n $NS --field-selector involvedObject.kind=Pod --sort-by=.lastTimestamp

    판단은 이렇게 가져갑니다. READY가 모든 컨테이너 수와 일치하면 요청을 받을 준비가 된 상태입니다. RESTARTS가 계속 늘면 Liveness 실패 또는 애플리케이션 크래시를 봅니다. EndpointSlice에 파드 IP가 없으면 Readiness 실패, Service selector 불일치, 포트명 불일치, 또는 라벨 문제를 봅니다. connection refused는 포트와 바인딩 주소, timeout은 앱 지연과 리소스 압박, 404는 경로 불일치부터 확인하세요.

    Kubernetes 프로브 트러블슈팅을 위한 kubectl 진단 결과 화면

    운영 대시보드가 있다면 프로브 실패율만 따로 보는 것도 좋습니다. 다만 프로브 자체가 너무 자주 실행되면 노드와 애플리케이션에 부하가 됩니다. 특히 exec 프로브는 컨테이너 안에서 프로세스를 실행하므로, 파드 수가 많고 주기가 짧으면 비용이 눈에 띄게 늘 수 있습니다. HTTP나 TCP로 충분한 경우라면 굳이 셸 명령을 매번 실행하지 않는 편이 낫습니다.

    Kubernetes 프로브 모범 사례와 선택 기준

    제가 팀에 리뷰할 때 가장 많이 보는 항목은 세 가지입니다. 첫째, Liveness가 외부 의존성을 검사하지 않는가. 둘째, Readiness가 실제 트래픽 처리 가능성을 반영하는가. 셋째, 느린 기동을 initialDelaySeconds 땜질이 아니라 Startup Probe로 풀었는가. 이 세 가지만 잡아도 Kubernetes 프로브 트러블슈팅의 절반은 줄어듭니다.

    상황별 추천

    • 애플리케이션 시작이 느리다면: Startup Probe를 추가하고 Liveness의 initialDelaySeconds를 과하게 늘리지 않습니다.
    • DB가 잠깐 끊길 수 있다면: DB 검사는 Readiness Probe에 둡니다. Liveness에서 DB를 때리지 않습니다.
    • 프로세스가 가끔 멈춘다면: 로컬 핸들러 기반의 가벼운 Liveness Probe로 재시작하게 합니다.
    • 경로가 자주 바뀐다면: 비즈니스 API와 헬스 체크 엔드포인트를 분리하고, 라우팅 리팩터링에 영향받지 않는 고정 경로를 씁니다.
    • 서비스 메시나 Ingress를 쓴다면: kubelet의 Pod 프로브, Service 라우팅, 외부 로드밸런서 헬스 체크를 서로 다른 계층으로 보고 분리해서 검증합니다.
    • 파드 수가 많다면: 짧은 periodSeconds와 무거운 exec 프로브 조합을 피합니다. 헬스 체크도 트래픽입니다.

    자주 묻는 질문

    Q. Liveness와 Readiness를 같은 경로로 써도 되나요?
    가능은 하지만 운영 기본값으로 추천하지 않습니다. 같은 경로가 DB나 외부 API를 검사하면 Readiness 실패로 끝날 문제가 Liveness 재시작으로 커질 수 있습니다. 정말 같은 경로를 써야 한다면 그 경로는 로컬 상태만 검사해야 합니다.

    Q. TCP Probe와 HTTP Probe 중 뭘 써야 하나요?
    HTTP 서비스라면 HTTP Probe가 보통 더 낫습니다. 상태 코드와 경로로 의도를 표현할 수 있기 때문입니다. TCP Probe는 포트가 열렸는지만 확인하므로, 앱이 내부 초기화 중이어도 성공할 수 있습니다.

    Q. Exec Probe는 언제 쓰나요?
    HTTP 엔드포인트를 만들기 어려운 배치성 프로세스나 레거시 앱에서 컨테이너 내부 상태 파일·명령 결과를 확인할 때 씁니다. 다만 명령 실행 비용, 이미지 안의 바이너리 존재 여부, 셸 의존성을 반드시 확인해야 합니다.

    Q. gRPC 서비스는 어떻게 하나요?
    gRPC Health Checking Protocol을 구현했다면 Kubernetes v1.27부터 안정화된 gRPC probe를 검토할 수 있습니다. gRPC probe는 HTTP/TCP probe와 달리 named port를 지원하지 않으므로 숫자 포트를 명시해야 합니다. 서비스 이름과 포트 설정을 명확히 하고, HTTP 게이트웨이와 혼동하지 않도록 매니페스트 리뷰를 거치는 편이 좋습니다.

    Liveness Probe Readiness Probe Startup Probe 선택 기준 요약

    공식 동작 기준은 Kubernetes 문서의 Configure Liveness, Readiness and Startup Probes와 Liveness, Readiness, and Startup Probes를 기준으로 확인하되, 실제 값은 서비스의 기동 로그와 장애 패턴에 맞춰 조정하세요. 문서의 예시는 출발점이지 운영 정책 그 자체는 아닙니다. 관련 배포 전략이나 장애 대응 문서가 있다면 내부 링크로 함께 묶어두면 독자가 다음 글로 자연스럽게 이동합니다.

    마무리: 운영에서 덜 아프게 쓰는 결론

    프로브 설계의 기준은 명확합니다. 재시작하면 좋아지는 문제만 Liveness에 넣고, 트래픽만 빼면 되는 문제는 Readiness에 넣고, 느린 기동은 Startup Probe로 보호하세요. 이 원칙을 어기면 프로브는 장애 감지 장치가 아니라 장애 증폭 장치가 됩니다.

    일반 웹 서비스라면 저는 이렇게 시작합니다. Liveness는 /livez에서 프로세스와 기본 핸들러만 확인합니다. Readiness는 /readyz에서 DB 연결, 필수 설정, 내부 큐 상태처럼 요청 처리 조건을 확인합니다. 기동이 느린 서비스는 /startupz를 두고, Startup Probe가 성공하기 전까지 Liveness가 개입하지 않게 합니다. 배포 후에는 kubectl describe pod, kubectl logs --previous, kubectl get endpoints, kubectl get endpointslice를 함께 확인합니다. 이 네 가지를 습관처럼 보면 Kubernetes 프로브 트러블슈팅에서 가장 흔한 삽질을 꽤 많이 줄일 수 있습니다.

  • [Cloud] Terraform State 파일 오류 디버깅: Lock·Drift 해결 전략

    [Cloud] Terraform State 파일 오류 디버깅: Lock·Drift 해결 전략

    Terraform State 파일 오류 디버깅: Lock·Drift 해결 전략

    Terraform State 파일 오류는 대개 Terraform이 기억하는 세계와 실제 클라우드에 존재하는 세계가 어긋날 때 시작됩니다. 에러 메시지는 lock, drift, import, backend처럼 흩어져 나오지만, 현장에서 보면 원인은 보통 세 가지입니다. 같은 state를 동시에 만졌거나, 콘솔·스크립트·다른 파이프라인이 리소스를 바꿨거나, 내가 생각한 backend가 아닌 다른 state를 보고 있는 경우입니다.

    저도 홈랩의 Proxmox VM과 AWS 테스트 계정을 Terraform으로 같이 관리하다가 state가 꼬인 적이 있습니다. plan은 말이 안 되는 삭제를 보여주고, apply는 lock에 막히고, 이미 콘솔에서 지운 인스턴스를 Terraform은 아직 관리 중이라고 믿고 있더라고요. 그때 배운 건 단순합니다. Terraform 디버깅은 명령어를 많이 아는 게임이 아니라, state, configuration, real infrastructure 중 어느 쪽이 틀렸는지 분리하는 작업입니다.

    이 글은 공식 문서 요약보다 운영 중 손을 멈춰야 할 지점, 바로 실행해도 되는 명령, 마지막까지 미뤄야 할 명령을 구분하는 실전 기준에 가깝습니다. 특히 state rm, import, state mv, force-unlock은 모두 강력하지만 잘못 쓰면 문제를 감추거나 더 키울 수 있습니다. 내부 위키에 IaC 배포 절차나 장애 복구 문서가 있다면 이 글과 함께 연결해 두면 리뷰 때 진짜 편합니다.

    Terraform State 파일 오류 이해를 위한 state 관리 흐름 다이어그램

    Terraform CLI, 원격 백엔드, 실제 클라우드 리소스, state lock이 어떻게 연결되는지 보여주는 개요 이미지입니다.

    Terraform State 파일 오류는 “지도”보다 “소유권 장부” 문제입니다

    Terraform state를 단순히 현재 인프라의 지도라고만 설명하면 절반만 맞습니다. 실무에서는 state를 Terraform이 어떤 실제 객체를 어느 리소스 주소로 관리하는지 기록한 소유권 장부로 보는 편이 더 정확합니다. 예를 들어 aws_instance.web이라는 주소가 실제 AWS 인스턴스 ID i-...에 묶여 있다면, Terraform은 그 주소를 기준으로 변경·삭제·재생성 판단을 합니다.

    문제는 이 장부가 코드와 자동으로 완벽히 동기화되지 않는다는 점입니다. Terraform은 .tf 파일, state, provider가 API로 읽어온 실제 리소스 정보를 비교해서 plan을 만듭니다. 그래서 “코드는 맞는데 plan이 이상하다”는 말은 충분히 가능합니다. 코드가 틀린 게 아니라 state가 다른 환경을 가리키거나, 실제 리소스가 콘솔에서 바뀌었거나, provider가 읽어온 값이 이전 실행과 달라졌을 수 있거든요.

    • Configuration: 우리가 원하는 선언입니다. .tf 파일, 변수, module 호출이 여기에 해당합니다.
    • State: Terraform이 이미 관리한다고 믿는 객체 목록과 속성입니다.
    • Real infrastructure: AWS, Azure, GCP, Kubernetes, vSphere 등에 실제로 존재하는 리소스입니다.
    • Provider: 실제 API를 조회하고 생성·수정·삭제를 수행하는 계층입니다. provider 버전 차이도 plan 결과를 바꿀 수 있습니다.

    제가 운영 환경에서 먼저 확인하는 건 코드가 아니라 내가 지금 올바른 backend와 workspace를 보고 있는지입니다. prod 작업이라고 믿고 있었는데 staging state를 보고 있으면, 이후의 모든 분석은 정교한 착각이 됩니다.

    흔한 Terraform State 파일 오류 유형과 근본 원인

    증상만 보고 바로 명령어를 치면 위험합니다. 같은 plan 이상이라도 원인이 drift인지, backend 오지정인지, 리팩터링 후 주소 변경인지에 따라 복구 방법이 달라집니다.

    오류 유형 대표 증상 근본 원인 먼저 볼 것 권장 대응
    State Lock Error acquiring the state lock 다른 Terraform 실행이 state 쓰기 권한을 잡았거나 이전 실행이 비정상 종료됨 CI 실행 상태, lock ID, 실행자 정보, backend lock 저장소 실행 중 작업을 확인한 뒤 고아 lock일 때만 force-unlock
    Drift 예상하지 못한 ~, -, -/+가 plan에 표시됨 콘솔 수동 변경, 외부 자동화, provider 기본값 변화 terraform plan -refresh-only, 클라우드 변경 이력 코드로 흡수할지, 실제 값을 되돌릴지 결정
    State와 실제 리소스 불일치 삭제된 리소스를 계속 추적하거나 기존 리소스를 새로 만들려 함 수동 삭제, import 누락, state rm 오남용, workspace 혼동 terraform state list, terraform state show, 실제 리소스 ID state rm, import, apply 중 선택
    Backend 설정 오류 init 실패, 전혀 다른 리소스가 plan에 표시됨 S3 key, workspace, profile, region, backend-config 불일치 .terraform/terraform.tfstate의 backend 메타정보, terraform workspace show terraform init -reconfigure 후 plan 재검증
    리팩터링 후 주소 변경 동일 리소스를 삭제 후 새로 만들려 함 resource 이름, module 경로, for_each key 변경 이전 주소와 새 주소, plan의 -/+ 여부 moved block 또는 terraform state mv

    Terraform 디버깅 루틴: apply 전에 보는 7개 체크포인트

    문제가 터졌을 때 저는 apply를 바로 다시 실행하지 않습니다. 첫 번째 실행이 실패한 이유를 모른 채 두 번째 실행을 하면 state가 더 헷갈려집니다. 아래 루틴은 로컬 backend, S3 backend, HCP Terraform/Terraform Enterprise 모두에서 큰 틀은 같습니다.

    1. Terraform 버전과 provider lock 파일을 확인합니다.
    2. 현재 workspace가 의도한 환경인지 확인합니다.
    3. backend가 어느 state 객체를 보고 있는지 확인합니다.
    4. state에 들어 있는 리소스 주소를 목록화합니다.
    5. 실제 리소스 조회와 refresh-only plan으로 drift를 분리합니다.
    6. 삭제 또는 재생성 액션이 있으면 영향 큰 리소스부터 읽습니다.
    7. state 변경 명령은 백업 또는 원격 backend 버전 복구 가능성을 확인한 뒤 실행합니다.
    # 0. 버전과 provider 잠금 파일 확인
    terraform version
    ls -la .terraform.lock.hcl
    
    # 1. 내가 보고 있는 workspace 확인
    terraform workspace show
    terraform workspace list
    
    # 2. backend 재초기화가 필요한 상황인지 확인
    terraform init -reconfigure
    
    # 3. state에 등록된 주소 확인
    terraform state list
    terraform state list 'module.network.*'
    
    # 4. 특정 리소스가 어떤 실제 ID에 연결되어 있는지 확인
    terraform state show aws_instance.web
    
    # 5. 실제 인프라를 읽어 state와의 차이만 확인
    terraform plan -refresh-only
    
    # 6. 일반 plan은 파일로 저장해서 리뷰 가능하게 남김
    terraform plan -out=tfplan
    terraform show tfplan

    terraform plan -refresh-only는 인프라를 바꾸겠다는 뜻이 아니라 실제 객체를 읽어서 state가 어떻게 갱신될지를 보여주는 모드입니다. 다만 terraform apply -refresh-only까지 실행하면 state가 실제 값에 맞게 업데이트될 수 있습니다. 운영에서는 plan과 apply의 차이를 분명히 나눠야 합니다. 조회만 하려면 plan -refresh-only에서 멈추고, state 동기화까지 의도한 경우에만 apply 단계로 갑니다.

    반대로 terraform plan -refresh=false는 실제 리소스 조회를 건너뜁니다. 빠를 수는 있지만 오래된 state를 기준으로 plan을 만들기 때문에 drift 디버깅에는 부적합합니다. 저는 대규모 환경에서 provider API 호출 비용이나 속도가 부담될 때 임시 비교용으로만 쓰고, 운영 적용 판단에는 거의 쓰지 않습니다.

    Terraform 디버깅 명령어로 state 상태를 점검하는 화면

    터미널에서 workspace, state list, refresh-only plan을 순서대로 확인하는 장면을 표현한 이미지입니다.

    IaC 상태 관리의 핵심: 원격 백엔드와 S3 key

    팀 작업에서는 로컬 state보다 원격 backend가 기본값에 가깝습니다. 로컬 파일은 간단하지만 동시 실행 제어, 접근 제어, 백업, 감사 추적이 약합니다. AWS 환경이라면 S3 backend를 많이 씁니다. 현재 Terraform S3 backend는 use_lockfile = true로 S3 기반 state locking을 사용할 수 있고, DynamoDB 기반 locking은 deprecated 상태라 신규 구성에서는 S3 lockfile을 우선 검토하는 편이 좋습니다.

    선택지 언제 적합한가 장점 주의점
    Local state 개인 실험, 폐기 가능한 샌드박스 구성이 단순하고 빠름 협업, 백업, lock, 접근 제어에 취약
    S3 backend + use_lockfile AWS에서 신규 원격 state를 단순하게 운영할 때 DynamoDB 테이블 없이 lock 구성 가능 Terraform 버전과 S3 lock 파일 권한을 함께 확인해야 함
    S3 backend + DynamoDB lock 기존 조직 표준이 이미 DynamoDB lock으로 굳어졌을 때 운영 사례가 많고 기존 자동화와 연동 쉬움 deprecated 방식이므로 마이그레이션 계획이 필요함
    HCP Terraform/Terraform Enterprise 정책, 승인, 실행 이력, 팀 권한을 한곳에서 관리할 때 협업 기능과 실행 관리가 강함 조직 계정·권한·네트워크 정책 설계가 필요
    terraform {
      backend "s3" {
        bucket       = "my-terraform-state-bucket"
        key          = "prod/network/terraform.tfstate"
        region       = "ap-northeast-2"
        encrypt      = true
        use_lockfile = true
      }
    }

    여기서 가장 자주 터지는 건 bucket보다 key입니다. 같은 bucket 안에 dev/network/terraform.tfstate, staging/network/terraform.tfstate, prod/network/terraform.tfstate를 넣는 구조는 흔합니다. 그래서 key 경로를 한 글자만 잘못 넣어도 전혀 다른 환경의 장부를 펼쳐놓고 작업하게 됩니다. 제 기준으로 prod backend는 코드 리뷰에서 bucket보다 key를 더 오래 봅니다.

    terraform init -reconfigure \
      -backend-config="bucket=my-terraform-state-bucket" \
      -backend-config="key=prod/network/terraform.tfstate" \
      -backend-config="region=ap-northeast-2" \
      -backend-config="encrypt=true" \
      -backend-config="use_lockfile=true"
    
    terraform init -reconfigure \
      -backend-config="bucket=my-terraform-state-bucket" \
      -backend-config="key=prod/network/terraform.tfstate" \
      -backend-config="region=ap-northeast-2" \
      -backend-config="dynamodb_table=terraform-locks" \
      -backend-config="encrypt=true"

    init -reconfigure는 현재 디렉터리의 backend 설정을 다시 읽게 합니다. state를 다른 위치로 옮기는 migration과는 다릅니다. Terraform이 state migration 여부를 묻는 상황에서는 메시지를 끝까지 읽어야 합니다. 운영 state를 잘못 옮기면 plan 이상보다 복구가 더 피곤해집니다.

    State Lock 해결: force-unlock은 마지막에 씁니다

    State Lock 해결을 검색하면 대부분 terraform force-unlock LOCK_ID가 먼저 보입니다. 하지만 이 명령은 lock을 푸는 도구이지, 현재 실행 중인 apply가 안전하게 끝났다는 증거가 아닙니다. 누군가 실제로 apply 중인데 강제로 풀면 두 개의 실행이 같은 state를 쓰려고 할 수 있습니다.

    ps aux | grep '[t]erraform'
    terraform workspace show
    terraform force-unlock LOCK_ID
    terraform force-unlock -force LOCK_ID

    제가 force-unlock을 허용하는 기준은 꽤 보수적입니다. 첫째, 현재 로컬·CI/CD·동료 작업 중 실행 중인 plan 또는 apply가 없어야 합니다. 둘째, lock 에러의 실행자·시간·operation 정보가 현재 살아 있는 작업과 맞지 않아야 합니다. 셋째, unlock 후 바로 apply하지 않고 plan부터 다시 봐야 합니다.

    상황 해야 할 일 하지 말아야 할 일
    CI job이 아직 실행 중 job 종료 또는 취소 결과를 기다림 lock ID가 보인다는 이유만으로 force-unlock
    노트북이 꺼져 apply가 중단됨 클라우드 리소스 생성 상태와 state 반영 여부를 확인 unlock 후 바로 apply 재시도
    오래된 고아 lock으로 확인됨 terraform force-unlock LOCK_ID 후 plan 검증 backend 저장소를 직접 수정하는 것으로 시작
    lock 저장소 권한 오류 S3 또는 DynamoDB 권한, profile, region을 확인 lock 기능을 꺼서 우회

    S3 lockfile을 쓴다면 lock 파일에 대한 s3:GetObject, s3:PutObject, s3:DeleteObject 권한을 확인해야 합니다. DynamoDB lock을 쓰는 기존 구성이라면 lock 테이블 권한과 region을 봐야 합니다. lock 문제를 Terraform 문제가 아니라 IAM 문제로 풀어야 하는 경우도 꽤 많습니다.

    State 불일치 복구: rm, import, mv를 섞어 쓰지 마세요

    Terraform State 파일 오류 중 가장 헷갈리는 구간입니다. state rm, import, state mv는 모두 state를 만지지만 목적이 다릅니다. 저는 이 셋을 “추적 끊기, 추적 시작하기, 주소 바꾸기”로 외웁니다. 이 구분만 잡아도 사고 확률이 확 줄더라고요.

    상황 사용 명령 의미 사용하면 안 되는 경우
    실제 리소스는 삭제됐는데 state에만 남음 terraform state rm Terraform의 추적만 제거 리소스를 실제로 삭제하려는 목적이면 부적합
    실제 리소스가 있는데 Terraform이 모름 terraform import 기존 객체를 state 주소에 연결 코드가 없거나 주소가 틀린 상태에서 성급히 실행
    리소스 이름·module 경로만 바뀜 terraform state mv 또는 moved block 같은 실제 객체를 새 Terraform 주소로 이동 실제 객체를 교체해야 하는 변경에는 부적합
    설정에서 더 이상 관리하지 않음 removed block 또는 state rm Terraform 관리 대상에서 제외 팀이 변경 이력을 코드로 남겨야 하는데 임시 명령만 실행
    terraform state list
    terraform state show aws_instance.web
    terraform state rm aws_instance.web
    terraform import aws_instance.web i-0123456789abcdef0
    terraform state mv aws_instance.web module.compute.aws_instance.web

    state rm은 실제 리소스를 삭제하지 않습니다. 이 사실은 단순하지만 사고를 많이 막아줍니다. 반대로 terraform destroy는 실제 리소스를 삭제할 수 있습니다. “state에서 빼고 싶다”와 “클라우드에서 없애고 싶다”는 완전히 다른 요구입니다.

    리팩터링이라면 가능하면 코드에 moved block을 남기는 방식을 선호합니다. 이유는 간단합니다. terraform state mv는 실행한 사람의 터미널 기록에만 맥락이 남지만, moved block은 저장소에 의도가 남습니다.

    moved {
      from = aws_instance.web
      to   = module.compute.aws_instance.web
    }
    Terraform State 파일 오류 복구를 위한 rm import mv 흐름도

    state에서 제거, 가져오기, 주소 이동이 각각 어떤 상황에 맞는지 정리한 흐름도 이미지입니다.

    재현 시나리오: 콘솔에서 삭제된 EC2를 다시 만들려는 경우

    가장 흔하고 교육용으로도 좋은 시나리오입니다. Terraform으로 EC2를 만들었는데 누군가 AWS 콘솔에서 직접 삭제했다고 가정하겠습니다. 이후 terraform plan을 실행하면 Terraform은 state에 남아 있는 인스턴스 ID를 기준으로 실제 객체를 조회합니다. provider는 “없다”고 답하고, Terraform은 설정 파일에 리소스가 여전히 있으니 다시 만들 계획을 세웁니다.

    1. Terraform으로 aws_instance.web를 생성합니다.
    2. AWS 콘솔 또는 외부 스크립트로 해당 인스턴스를 삭제합니다.
    3. terraform plan -refresh-only로 state와 현실의 차이를 봅니다.
    4. 그 인스턴스가 계속 필요한지, 이미 폐기한 리소스인지 결정합니다.
    5. 필요한 리소스면 terraform apply로 재생성을 검토하고, 폐기한 리소스면 코드와 state를 함께 정리합니다.
    terraform state show aws_instance.web
    terraform plan -refresh-only
    terraform plan -out=tfplan
    terraform show tfplan
    terraform apply tfplan
    terraform state rm aws_instance.web
    terraform plan

    여기서 자주 하는 실수는 state rm을 먼저 실행하는 겁니다. 설정 파일에 리소스 블록이 그대로 남아 있으면 다음 plan에서 Terraform은 “state에는 없지만 코드에는 있으니 새로 만들자”고 판단합니다. 그래서 정말 폐기하려면 코드 제거와 state 정리가 같이 가야 합니다. 필요한 리소스라면 반대로 state를 지우지 말고 Terraform이 재생성하도록 plan을 검토하는 편이 자연스럽습니다.

    plan 출력 해석: 기호보다 리소스 종류가 더 중요합니다

    Terraform plan의 기호는 기본 문법입니다. 하지만 운영 판단은 기호만으로 하지 않습니다. 같은 ~라도 태그 변경과 데이터베이스 엔진 옵션 변경은 무게가 다릅니다. 같은 -/+라도 무상태 인스턴스와 영구 디스크는 위험도가 다릅니다.

    • +: 새 리소스 생성입니다. 비용과 quota를 확인합니다.
    • ~: 기존 리소스 변경입니다. in-place 변경인지, provider가 어떤 필드를 바꾸는지 봅니다.
    • -: 리소스 삭제입니다. 운영에서는 승인 없이 진행하지 않습니다.
    • -/+: 삭제 후 재생성입니다. 네트워크, DB, 디스크, IAM, Kubernetes namespace에 보이면 멈춥니다.
    terraform plan -out=tfplan
    terraform show tfplan
    terraform show -json tfplan > tfplan.json
    jq '.resource_changes[] | select(.change.actions | index("delete")) | {address, actions: .change.actions}' tfplan.json

    저는 plan 리뷰 때 리소스를 세 그룹으로 나눕니다. 첫째, 재생성되면 장애가 될 수 있는 리소스입니다. VPC, subnet, route table, DB, disk, IAM role, cluster 같은 것들입니다. 둘째, 비용이 바로 붙는 리소스입니다. 인스턴스, NAT gateway, load balancer, managed database가 여기에 들어갑니다. 셋째, 변경되어도 영향이 제한적인 메타데이터입니다. 태그나 설명이 대표적입니다. 이 분류를 해두면 plan 리뷰가 감상이 아니라 판단이 됩니다.

    Terraform plan 결과로 State Lock 해결과 변경 위험을 해석하는 대시보드

    생성, 변경, 삭제, 재생성 항목을 색상별로 구분해 보여주는 Terraform plan 결과 해석 이미지입니다.

    성능·비용·안정성에 영향을 주는 결정 포인트

    State 디버깅은 단순 복구 작업처럼 보이지만, backend 설계와 운영 습관은 비용과 안정성에 영향을 줍니다. 숫자를 지어낼 필요는 없습니다. 어떤 결정이 어떤 방향의 비용을 만드는지만 알아도 충분히 실수를 줄일 수 있습니다.

    결정 안정성 영향 비용·성능 영향 추천 기준
    state를 환경별로 분리할지 여부 장애 범위를 줄임 state 수가 늘어 관리 포인트 증가 prod, staging, dev는 최소한 key 또는 workspace로 분리
    하나의 거대한 state 사용 한 번의 plan 영향 범위가 커짐 provider 조회가 많아져 plan이 느려질 수 있음 네트워크, 플랫폼, 앱 계층을 무리 없이 나눔
    -refresh=false 사용 drift를 놓칠 수 있음 조회가 줄어 빨라질 수 있음 운영 apply 판단에는 사용하지 않음
    state lock 비활성화 동시 apply 충돌 위험 증가 구성은 단순해짐 팀 환경에서는 lock 없는 운영을 피함
    원격 state 암호화·버전 관리 복구와 보안에 유리 스토리지 정책 관리 필요 민감 값 가능성을 전제로 암호화와 접근 제어 적용

    특히 state를 너무 크게 키우는 패턴은 나중에 발목을 잡습니다. 모든 리소스를 하나의 root module과 하나의 state에 넣으면 처음엔 편합니다. 하지만 plan이 느려지고, 작은 변경도 큰 영향 범위를 갖고, lock 대기 시간이 길어집니다. 그렇다고 너무 잘게 쪼개면 remote state 참조와 의존성 관리가 늘어납니다. 저는 “같이 생성되고 같이 롤백되어도 괜찮은 단위”를 state 분리 기준으로 잡습니다.

    자주 묻는 질문: Terraform State 파일 오류 FAQ

    Q1. terraform.tfstate 파일을 직접 수정해도 되나요?

    가능은 하지만 운영에서는 거의 마지막 수단입니다. JSON이라 열어볼 수는 있어도 Terraform 내부 구조, provider schema, 민감 값 처리, serial 값이 얽혀 있습니다. 먼저 terraform state 하위 명령을 쓰고, 원격 backend라면 버전 복구 가능성을 확인한 뒤 진행하세요.

    Q2. state 파일을 Git에 올려도 되나요?

    일반적으로 올리지 않는 편이 맞습니다. state에는 리소스 속성뿐 아니라 민감한 값이 평문에 가까운 형태로 남을 수 있습니다. 팀 환경에서는 원격 backend, 암호화, 접근 제어, 감사 가능한 실행 경로를 쓰는 쪽이 안전합니다.

    Q3. lock 오류가 나면 무조건 force-unlock 하면 되나요?

    아닙니다. lock은 귀찮은 장벽이 아니라 state 동시 수정을 막는 안전장치입니다. 실행 중인 작업이 없고 고아 lock이라고 판단될 때만 terraform force-unlock LOCK_ID를 사용하세요.

    Q4. import만 하면 코드도 자동으로 완성되나요?

    terraform import는 기존 리소스를 state에 연결하는 명령입니다. 코드 작성과 속성 정리는 별도 작업으로 보는 편이 안전합니다. import 후에는 반드시 terraform plan으로 코드와 실제 리소스 차이를 맞춰야 합니다.

    Q5. 리소스 이름만 바꿨는데 왜 삭제 후 생성이 뜨나요?

    Terraform 주소가 바뀌었기 때문입니다. aws_instance.web를 aws_instance.app으로 바꾸면 Terraform은 기본적으로 다른 리소스로 봅니다. 같은 실제 객체를 유지하려면 moved block이나 terraform state mv로 주소 이동을 알려줘야 합니다.

    운영에서 바로 쓰는 판단 기준

    Terraform State 파일 오류를 만나면 명령어보다 순서가 중요합니다. 먼저 workspace와 backend를 확인합니다. 그다음 state list, state show, plan -refresh-only로 state와 현실의 차이를 분리합니다. lock 에러라면 실행 중인 작업이 있는지 확인한 뒤 고아 lock일 때만 해제합니다.

    이럴 땐 이렇게 하시면 됩니다. 콘솔에서 지운 리소스를 Terraform이 다시 만들려 한다면, 계속 필요한 리소스인지 먼저 결정하세요. 필요하면 plan 검토 후 apply, 필요 없으면 코드 제거와 state 정리를 같이 합니다. 이름이나 module 경로만 바꿨다면 삭제·재생성을 허용하지 말고 moved block 또는 state mv를 씁니다. lock이 걸렸다면 Terraform이 나를 괴롭히는 게 아니라 state를 보호하는 중이라고 보고, 실행 중인 작업부터 찾습니다.

    제가 운영에서 절대 넘기지 않는 신호는 -와 -/+입니다. 특히 네트워크, 데이터베이스, 디스크, IAM, 클러스터 리소스에 보이면 손을 멈추고 이유를 설명할 수 있어야 합니다. 설명할 수 없는 apply는 복구 계획이 없는 변경과 비슷합니다.

    정확한 옵션과 backend 동작은 HashiCorp의 Terraform CLI 문서, S3 backend 문서, state 명령 문서를 기준으로 확인하는 습관을 권합니다. 문서는 명령 문법을 확인하는 곳이고, 운영 판단은 현재 state와 실제 인프라를 놓고 해야 합니다. 이 둘을 분리해서 보면 Terraform state 문제는 훨씬 덜 무섭습니다.

  • [Cloud] GitLab CI 마이그레이션 회고: 전환 결정 기준

    [Cloud] GitLab CI 마이그레이션 회고: 전환 결정 기준

    GitLab CI 마이그레이션 회고: 전환 결정 기준

    GitLab CI 마이그레이션, YAML 변환보다 먼저 바뀌는 것

    GitLab CI 마이그레이션을 몇 번 해보면 초반 착각이 거의 비슷합니다. 기존 Jenkinsfile이나 사내 CI 스크립트를 .gitlab-ci.yml로 옮기면 끝날 것 같지만, 실제로 흔들리는 지점은 YAML 문법이 아니라 실행 환경, 권한 경계, 캐시 수명, 배포 승인 습관입니다.

    제가 제일 경계하는 실패는 빨간 파이프라인이 아닙니다. 차라리 실패는 빨리 보이거든요. 더 위험한 건 성공처럼 보이는데 산출물이 예전과 다른 경우입니다. 예를 들어 기존 빌드 서버에는 전역 패키지, 로컬 캐시, SSH known_hosts, 사내 CA 인증서가 이미 깔려 있었는데 GitLab Runner의 Docker Executor에서는 전부 사라질 수 있습니다.

    그래서 저는 전환을 시작할 때 ‘CI 도구를 바꾼다’고 보지 않습니다. 빌드 지식을 어디에 둘 것인가, 운영 배포 권한을 누가 어떤 조건에서 행사할 것인가, 실패 로그를 누가 재현 가능한 방식으로 읽을 것인가를 다시 정하는 작업으로 봅니다. 결국 GitLab CI는 도구 선택보다 팀의 배포 습관을 저장소 중심으로 다시 쓸 준비가 되어 있는지의 문제에 가깝더라고요.

    GitLab CI 마이그레이션 전체 흐름 아키텍처 다이어그램

    기존 CI 도구에서 GitLab CI로 넘어갈 때 함께 이동하는 요소를 한눈에 보는 개요 다이어그램입니다.

    GitLab CI 마이그레이션이 바꾸는 운영 경계

    GitLab CI/CD의 핵심은 저장소 안의 .gitlab-ci.yml을 기준으로 Job, Stage, Pipeline을 선언하는 데 있습니다. 그런데 실무에서 중요한 변화는 정의 파일의 위치가 아니라 책임의 위치입니다. 예전에는 빌드 서버 안에 있던 지식이 저장소로 들어오고, 운영팀 개인 계정에 묶여 있던 배포 절차가 Job과 Environment로 드러납니다.

    저는 전환 전에 기존 CI를 다음 네 덩어리로 분해합니다. 이 과정을 생략하면 나중에 ‘왜 Jenkins에서는 됐는데 GitLab에서는 안 되죠?’라는 질문만 반복됩니다.

    • 실행 환경: OS 패키지, 런타임 버전, Docker-in-Docker 사용 여부, 사내 인증서, DNS, 프록시 설정입니다.
    • 상태 저장 지점: 캐시, 아티팩트, 빌드 번호, 릴리스 노트, 컨테이너 이미지 태그가 어디에 남는지입니다.
    • 권한 경계: 배포 토큰, 레지스트리 인증, 클라우드 IAM, SSH 키, protected branch/tag 조건입니다.
    • 사람의 개입: 운영 배포 승인, 장애 시 재실행 기준, 롤백 명령을 누가 실행하는지입니다.

    이 네 가지가 정리되어 있으면 GitLab CI 문법은 금방 따라옵니다. 반대로 이게 흐릿하면 문법을 아무리 예쁘게 써도 파이프라인은 오래 못 갑니다. 관련해서는 이전에 정리한 CI/CD 체크리스트 글과 함께 보면 결정 기준을 더 빨리 잡을 수 있습니다.

    마이그레이션 결정 요소: 저는 이 표를 먼저 채웁니다

    도구 비교표보다 먼저 보는 건 팀의 현재 배포 습관입니다. 같은 GitLab CI라도 저장소가 이미 GitLab에 있는 팀과, Jenkins 플러그인에 배포 지식이 잔뜩 들어 있는 팀의 난이도는 완전히 다릅니다.

    결정 요소 이럴 땐 전환 우선 이럴 땐 보류 또는 Shadow 운영 실패 모드
    저장소 위치 코드와 MR 리뷰가 이미 GitLab 중심입니다. 여러 SCM에 코드가 흩어져 있고 미러링 정책이 없습니다. 커밋 기준과 빌드 기준이 달라 추적성이 깨집니다.
    Runner 운영 Docker 또는 Kubernetes 기반 격리 실행을 운영할 사람이 있습니다. 빌드 서버가 한 대뿐이고 전역 설치 도구에 강하게 의존합니다. 특정 서버에서만 되는 빌드가 됩니다.
    Secret 관리 토큰 목록과 소유자가 파악되어 있고 변수 범위를 재설계할 수 있습니다. 개인 계정 SSH 키, 오래된 API 토큰, 스크립트 내 평문 값이 섞여 있습니다. Job은 성공하지만 과도한 권한으로 배포됩니다.
    배포 승인 운영 배포 전 승인자와 브랜치 정책이 명확합니다. main push가 곧 운영 반영인데 롤백 절차가 문서화되어 있지 않습니다. 마이그레이션 첫 주에 자동 배포 사고가 납니다.
    외부 연동 컨테이너 레지스트리, 패키지 저장소, 클라우드 계정 접근 방식이 표준화되어 있습니다. Jenkins 플러그인이나 사내 스크립트가 인증을 대신 처리합니다. YAML에는 문제가 없는데 인증 단계에서 계속 pending 또는 401이 납니다.

    제 기준은 단순합니다. GitLab CI로 옮겼을 때 배포 이력과 권한이 더 잘 보이면 전환 가치가 큽니다. 반대로 기존 CI가 레거시 시스템의 접착제 역할을 하고 있다면, 전환이 아니라 먼저 분해가 필요합니다.

    실전 구현: 기존 파이프라인을 작게 쪼개 옮기는 순서

    처음부터 운영 배포까지 한 번에 옮기면 원인 추적이 지옥이 됩니다. 저는 테스트 전용 파이프라인, 산출물 생성, 수동 배포, 기존 CI 제거 순서로 갑니다. 느려 보이지만 장애 복구 시간을 줄여줍니다.

    1. 기존 CI 작업을 빌드, 테스트, 패키징, 이미지 빌드, 배포, 알림으로 나눕니다.
    2. 각 단계가 읽는 파일, 쓰는 산출물, 필요한 환경 변수를 표로 만듭니다.
    3. Runner Executor를 정합니다. 서버 상태 의존을 줄이고 싶으면 Docker Executor를 먼저 봅니다.
    4. 배포 없는 테스트 Job부터 GitLab CI에 붙입니다.
    5. 아티팩트와 캐시를 분리합니다. 결과물은 artifacts, 재사용 의존성은 cache입니다.
    6. 운영 배포는 rules, when: manual, protected branch/tag를 함께 설계합니다.

    아래 예시는 Node.js 프로젝트를 전제로 한 출발점입니다. 의도적으로 only 대신 rules를 썼습니다. GitLab 문서에서도 새 조건 설계는 rules 사용을 권장하고, only/except는 deprecated 키워드로 안내합니다.

    stages:
      - test
      - build
      - deploy
    
    default:
      image: node:20
      interruptible: true
      before_script:
        - node --version
        - npm --version
    
    cache:
      key:
        files:
          - package-lock.json
      paths:
        - .npm/
      policy: pull-push
    
    test:
      stage: test
      script:
        - npm ci --cache .npm --prefer-offline
        - npm test
      artifacts:
        when: always
        paths:
          - junit.xml
        reports:
          junit: junit.xml
        expire_in: 1 week
    
    build:
      stage: build
      script:
        - npm ci --cache .npm --prefer-offline
        - npm run build
      artifacts:
        paths:
          - dist/
        expire_in: 1 week
      needs:
        - job: test
          artifacts: false
    
    deploy_production:
      stage: deploy
      image: alpine:latest
      needs:
        - job: build
          artifacts: true
      script:
        - echo 'deploy script goes here'
      environment:
        name: production
      rules:
        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
          when: manual
        - when: never

    여기서 needs는 단순한 보기 좋은 문법이 아닙니다. Stage 순서를 무작정 기다리지 않고 필요한 Job 관계를 명시합니다. 다만 남발하면 의존 그래프가 복잡해져 장애 때 읽기 어려워집니다. 빌드 시간이 길고 독립 테스트가 많은 저장소에는 적극적으로 쓰고, 작은 서비스에는 Stage만으로 단순하게 유지하는 편이 낫더라고요.

    GitLab CI 마이그레이션에서 Runner와 설정 파일 구성 관계도

    .gitlab-ci.yml, Runner, Job, Artifact, Cache가 어떻게 연결되는지 보여주는 구성 다이어그램입니다.

    GitLab CI Runner 등록과 검증: 토큰 방식부터 확인하세요

    Self-managed Runner를 쓴다면 등록 단계에서 예전 블로그 글을 그대로 따라 하면 막힐 수 있습니다. Runner registration token 방식은 deprecated 상태이며, 현재 GitLab Runner 등록 흐름은 GitLab UI나 API에서 Runner를 만든 뒤 발급되는 runner authentication token을 사용하는 쪽이 기준입니다. 명령어 예시도 --registration-token이 아니라 --token을 기준으로 잡는 게 안전합니다.

    sudo gitlab-runner register \
      --url https://gitlab.example.com \
      --token glrt-REPLACE_WITH_RUNNER_AUTH_TOKEN \
      --executor docker \
      --docker-image alpine:latest \
      --description docker-runner-01

    Runner 태그와 protected 설정은 GitLab UI에서 관리되는 경우가 많습니다. 등록 명령만 성공했다고 Job이 실행되는 건 아닙니다. Pipeline이 계속 pending이면 저는 아래 순서로 봅니다.

    sudo gitlab-runner status
    sudo gitlab-runner verify
    sudo gitlab-runner list
    sudo gitlab-runner --debug run

    verify가 통과하면 GitLab 서버와 Runner의 통신은 일단 됩니다. 그래도 Job이 안 잡히면 네트워크보다 태그 매칭, protected branch/tag, Runner scope, locked 설정을 먼저 봅니다. Job에 tags가 있는데 Runner에 같은 태그가 없으면 정상적으로 대기합니다. 이건 장애가 아니라 스케줄링 조건 불일치입니다.

    test_with_tagged_runner:
      stage: test
      tags:
        - docker
        - linux
      image: alpine:latest
      script:
        - echo 'runner tag matched'
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

    Executor 선택도 비용과 안정성에 직접 영향을 줍니다. Shell Executor는 빠르고 단순하지만 호스트 오염 위험이 큽니다. Docker Executor는 격리가 좋아 마이그레이션 검증에 유리하지만 이미지 pull, 캐시, Docker 데몬 접근 정책을 설계해야 합니다. Kubernetes Executor는 확장성은 좋지만 클러스터 운영 역량이 없으면 CI 문제가 곧 플랫폼 문제가 됩니다.

    트러블슈팅: 겉으로는 CI 문제, 뿌리는 운영 습관 문제

    로컬에서는 되는데 CI에서 깨지는 상황은 대부분 암묵적 환경 때문입니다. 제가 실제로 본 문제들은 문법 오류보다 환경 차이, 권한 범위, 캐시 오염이 훨씬 많았습니다. 이거 진짜 한 번 겪으면, 다음 마이그레이션부터는 환경 목록부터 보게 됩니다.

    1. 변수 이름은 같지만 범위가 달랐습니다

    기존 CI에서 전역 토큰 하나를 모든 프로젝트가 공유하던 팀이 있었습니다. GitLab으로 옮기면서 프로젝트 변수에 넣었는데, 실제 배포 Job은 하위 템플릿 프로젝트에서 실행되고 있었습니다. 이름은 같은데 스코프가 달라 값이 비어 있었던 겁니다. 해결은 Group Variables로 올릴 값과 Project Variables로 제한할 값을 분리하고, 운영 배포용 변수는 protected branch/tag에서만 노출되도록 하는 것이었습니다.

    git grep -n -E 'DEPLOY|TOKEN|PASSWORD|SECRET|PRIVATE_KEY' -- . ':!node_modules' ':!dist'
    git grep -n -E 'ssh|scp|rsync|kubectl|helm|aws|gcloud|az' -- . ':!node_modules' ':!dist'

    이 명령은 마이그레이션 전에 꼭 돌려봅니다. 결과가 많이 나온다는 건 나쁜 게 아닙니다. 숨어 있던 배포 지식을 드러낸 겁니다. 다만 평문 Secret이 나오면 즉시 회수, 재발급, 변수화까지 같이 해야 합니다. 단순히 YAML에서 지우는 건 보안 조치가 아닙니다.

    2. 캐시가 실패를 숨겼습니다

    node_modules를 통째로 캐시하면 처음엔 빨라 보입니다. 그런데 lock file이 바뀌었는데도 오래된 의존성이 남거나, Runner OS/아키텍처가 바뀌며 네이티브 모듈이 깨지는 경우가 있습니다. 그래서 저는 패키지 매니저 캐시 디렉터리를 캐시하고, 설치 결과 디렉터리를 산출물처럼 다루지 않습니다. npm이면 .npm/, pip이면 wheel/cache 계열처럼 재다운로드 비용을 줄이는 쪽이 낫습니다.

    3. 아티팩트와 캐시를 섞었습니다

    캐시는 다음 파이프라인에도 재사용될 수 있는 가속 장치입니다. 아티팩트는 이번 파이프라인의 결과물입니다. 배포에 필요한 dist/, 패키지 파일, 테스트 리포트는 cache가 아니라 artifacts에 둬야 합니다. 반대로 의존성 다운로드 캐시는 artifacts에 넣으면 보관 비용과 다운로드 시간이 불필요하게 커집니다.

    4. 배포 자동화를 너무 빨리 켰습니다

    마이그레이션 첫 주에는 운영 배포를 when: manual로 둡니다. 저는 이 기간에 로그 포맷, 배포 산출물, 롤백 명령, 승인자 권한을 확인합니다. 자동화는 마지막에 켜도 늦지 않습니다. 검증 안 된 자동 배포는 속도가 아니라 부채입니다.

    GitLab CI 마이그레이션 검증 기준

    마이그레이션 완료 기준을 ‘성공 표시가 떴다’로 잡으면 위험합니다. 저는 같은 커밋에서 같은 산출물이 나오는지, 누가 배포를 눌렀는지, 실패 로그만 보고 원인을 좁힐 수 있는지를 봅니다.

    검증 항목 확인 방법 통과 기준
    재현성 같은 commit SHA로 기존 CI와 GitLab CI 산출물 파일 목록을 비교합니다. 파일 누락, 이름 규칙, 환경별 설정 차이가 설명 가능합니다.
    추적성 Pipeline, Job, Environment, Artifact 링크로 배포 이력을 따라갑니다. 운영 반영 커밋과 실행자를 역추적할 수 있습니다.
    권한 분리 protected branch/tag, protected variables, manual job 권한을 확인합니다. 일반 개발자가 운영 Secret을 읽거나 배포하지 못합니다.
    실패 해석 실패 명령 바로 위의 환경 출력, 버전, 인증 로그를 봅니다. 네트워크, 인증, 의존성, 스크립트 오류 중 하나로 빠르게 좁힙니다.

    로그를 볼 때는 마지막 줄만 보면 안 됩니다. npm ci가 실패했다면 registry 인증, lock file, 네트워크, Node 버전 차이를 봅니다. docker build가 실패했다면 Dockerfile 단계와 build context를 확인합니다. kubectl apply가 실패했다면 kubeconfig 권한, namespace, server-side validation을 분리해서 봅니다.

    git diff --name-only origin/main...HEAD
    find dist -type f | sort > dist-files.txt
    sha256sum package-lock.json dist-files.txt
    printenv | sort | grep -E '^CI_|^GITLAB_|^NODE_|^NPM_'

    로컬에서 Runner를 흉내 내는 방식은 빠른 확인에는 도움이 되지만, GitLab 서버의 모든 동작을 완전히 재현한다고 믿으면 안 됩니다. 특히 protected variables, merge request pipeline 조건, 환경 승인, Runner 스케줄링은 실제 GitLab Pipeline에서 확인해야 합니다.

    GitLab CI 파이프라인 검증과 로그 분석 화면

    파이프라인 성공 여부, 실패 로그, Artifact 확인 흐름을 보여주는 검증 화면 예시입니다.

    CI/CD 전환 전략: 병행 운영이 느린 게 아니라 싸게 먹힙니다

    제가 가장 효과를 본 방식은 기존 CI를 바로 끄지 않는 겁니다. GitLab CI를 먼저 읽기 전용으로 붙이고, 같은 커밋에 대해 테스트와 산출물을 비교합니다. 이 기간은 낭비가 아닙니다. 기존 자동화에 숨어 있던 전역 의존성, 권한 누수, 오래된 배포 스크립트를 찾는 보험입니다.

    1. Inventory 단계: 기존 Job, Secret, 산출물, 외부 연동을 목록화합니다.
    2. Read-only 단계: GitLab CI에서 테스트만 돌리고 배포는 하지 않습니다.
    3. Shadow 단계: 기존 CI와 GitLab CI를 같은 커밋에서 같이 돌립니다.
    4. Manual Deploy 단계: 운영 배포를 수동 승인 Job으로 연결합니다.
    5. Primary 단계: GitLab CI를 기본 파이프라인으로 바꾸고 기존 CI 트리거를 중지합니다.
    6. Cleanup 단계: 기존 토큰, 빌드 서버 권한, 스케줄러, 웹훅을 제거합니다.

    성능과 비용을 볼 때도 단순히 ‘GitLab CI가 빠른가’로 보면 답이 흐립니다. 비용을 좌우하는 건 Runner 대수보다 실패 재실행 횟수, 이미지 pull 정책, 캐시 설계, 병렬화된 Job의 대기 시간입니다. 작은 서비스는 단순한 Stage 기반 파이프라인이 더 안정적이고, 큰 모노레포는 rules:changes, needs, child pipeline을 검토할 가치가 있습니다. 다만 child pipeline은 관측 포인트가 늘어나므로 팀이 로그를 읽을 준비가 된 뒤에 넣는 편이 좋습니다.

    자주 묻는 질문

    GitLab CI 마이그레이션은 언제 하는 게 좋나요?

    저는 코드가 GitLab에 있고, Merge Request 중심으로 리뷰하며, 빌드와 배포 이력을 커밋 기준으로 추적하고 싶을 때 추천합니다. 특히 운영 배포 승인과 결과물을 GitLab 안에서 보고 싶은 팀은 전환 효과가 큽니다.

    Jenkins에서 바로 GitLab CI로 옮겨도 괜찮나요?

    가능하지만 Jenkinsfile을 줄 단위로 번역하면 실패하기 쉽습니다. 먼저 stage 구조, 플러그인 의존성, credential 사용 위치, workspace에 남는 파일을 분해해야 합니다. Jenkins 플러그인이 해주던 일을 GitLab CI에서는 YAML, Runner 설정, 외부 CLI, protected variables 조합으로 다시 설계하는 경우가 많습니다.

    가장 먼저 확인할 설정은 뭔가요?

    Runner 태그, protected branch/tag, CI/CD Variables 범위, artifacts 경로, cache key를 먼저 봅니다. 제 경험상 초반 문제의 상당수는 이 다섯 가지에서 나왔습니다.

    Shell Executor와 Docker Executor 중 무엇을 골라야 하나요?

    기존 빌드 서버의 상태를 그대로 써야 하고 격리 요구가 낮으면 Shell Executor가 빠른 출발점이 될 수 있습니다. 하지만 마이그레이션 품질을 높이고 싶다면 Docker Executor가 낫습니다. 매번 비슷한 컨테이너 환경에서 실행되기 때문에 ‘그 서버에만 깔린 무언가’를 빨리 찾아냅니다.

    GitLab CI 마이그레이션 결정 기준 요약 인포그래픽

    전환 여부를 판단하기 위한 저장소, Runner, 보안, 배포 승인 기준을 요약한 인포그래픽입니다.

    마무리: GitLab CI 마이그레이션은 언제 밀어붙일까

    GitLab CI 마이그레이션은 파이프라인 파일 하나를 만드는 작업이 아닙니다. 빌드 지식, 배포 권한, 장애 대응 루틴을 저장소 중심으로 다시 쓰는 일입니다. 그래서 저는 도구의 인기보다 운영 방식이 더 투명해지는지를 기준으로 봅니다.

    GitLab에 코드가 있고, MR 리뷰가 일상이고, 운영 배포 이력까지 한곳에서 보고 싶다면 GitLab CI로 옮기세요. 이 경우에는 테스트 전용 파이프라인부터 시작해 manual deploy까지 붙이는 방식이 가장 무난합니다. 반대로 Jenkins 플러그인, 개인 SSH 키, 사내 배포 서버의 전역 상태에 강하게 묶여 있다면 바로 전환하지 마세요. 먼저 Shadow Pipeline으로 기존 결과와 GitLab CI 결과를 비교하고, Secret과 배포 권한을 정리한 뒤에 주 파이프라인으로 승격하는 편이 안전합니다.

    제 경험상 성공적인 전환의 신호는 ‘파이프라인이 빨라졌다’보다 ‘실패했을 때 누구나 같은 방식으로 원인을 좁힐 수 있다’에 가깝습니다. 속도는 그다음에 옵니다. 파이프라인은 옮기는 게 아니라, 팀의 배포 습관을 코드로 다시 쓰는 일입니다.

    참고한 1차 문서: GitLab Runner 등록 문서, GitLab CI deprecated keywords, GitLab CI caching 문서

  • [CI/CD 보안] Trivy로 컨테이너 보안 취약점 자동 탐지 구축하기

    [CI/CD 보안] Trivy로 컨테이너 보안 취약점 자동 탐지 구축하기

    [CI/CD 보안] Trivy로 컨테이너 보안 취약점 자동 탐지 구축하기

    안녕하세요, 13년차 인프라 엔지니어입니다. 요즘 같은 DevOps 환경에서는 CI/CD 파이프라인이 선택이 아닌 필수가 되었죠. 그런데 이렇게 빠르게 빌드하고 배포하는 과정에서 보안(Security)은 제대로 챙기고 계신가요?

    현업과 홈랩에서 일하다 보니 느끼는 게, 보안은 항상 뒷전으로 밀리거나 나중에 터지고 나서야 허둥지둥 해결하는 경우가 많더라고요. 특히 컨테이너 이미지는 한 번 빌드되면 어떤 취약점이 있는지 제대로 확인하지 않고 프로덕션 환경에 배포되는 일이 허다합니다. 그러다 터지면… 상상하기도 싫죠.

    이런 문제를 미리 막고 싶어서, CI/CD 파이프라인에 보안 취약점 자동 탐지(Automated Vulnerability Scanning)를 도입하는 방법을 계속 고민해왔습니다. 그리고 찾은 게 바로 Trivy(트리비)입니다. 오늘은 Trivy를 활용해서 CI/CD 파이프라인에 컨테이너 이미지 보안 스캔을 자동화하는 방법을 제 경험을 바탕으로 솔직하게 풀어보려 합니다.

    참고: 본 글은 보안 학습과 자신이 관리하는 시스템 방어를 위한 교육 목적입니다. 타인의 시스템에 무단 접근하는 행위는 법률상 불법이며 처벌 대상이니 꼭 기억해두세요.

    CI/CD 파이프라인에 Trivy가 통합되어 컨테이너 이미지의 보안 취약점을 자동 탐지하는 아키텍처 다이어그램

    그림 1: Trivy를 활용한 CI/CD 보안 파이프라인 개요

    Trivy란 무엇인가? 컨테이너 보안 스캔 도구 개론

    Trivy는 Aqua Security에서 개발한 오픈소스 도구로, 컨테이너 이미지(Container Image), 파일 시스템(Filesystem), Git 저장소(Git Repository) 등 다양한 대상에서 보안 취약점(Security Vulnerabilities)과 잘못된 설정(Misconfigurations)을 찾아줍니다. 가볍고 빠르면서도 정확도가 높아서 많은 개발팀이 애용하고 있거든요.

    쉽게 말해, 우리가 만든 컨테이너 이미지 안에 혹시 오래된 라이브러리나 알려진 취약점이 있는 패키지가 포함되어 있지는 않은지, 혹은 Dockerfile이나 Kubernetes 설정 파일에 보안상 위험한 설정이 있지는 않은지 꼼꼼하게 검사해주는 보안 스캐너라고 생각하시면 됩니다. 저도 처음엔 반신반의했는데, 써보고 나서는 정말 감탄했어요.

    왜 CI/CD 파이프라인에 보안 스캔을 넣어야 할까요?

    DevOps 환경에서는 개발 단계에서부터 보안을 고려하는 Shift-Left Security(시프트 레프트 보안)가 중요합니다. 나중에 터지고 나서 고치려면 시간과 비용이 훨씬 많이 들거든요. 저도 예전에 프로덕션에 배포된 서비스에서 심각한 취약점이 발견돼서 밤샘 작업을 한 기억이 생생합니다.

    CI/CD 파이프라인에 Trivy 같은 도구를 넣으면:

    • 조기 발견 및 대응: 개발 초기에 취약점을 발견해서 빠르게 수정할 수 있습니다.
    • 자동화된 검증: 매번 수동으로 검사할 필요 없이, 코드가 푸시될 때마다 자동으로 보안 검증이 이루어집니다.
    • 보안 수준 향상: 잠재적인 보안 위협을 줄여 전체 시스템의 보안 견고성(Security Robustness)을 높일 수 있습니다.
    • 규제 준수: PCI-DSS, HIPAA 같은 특정 산업군의 보안 규제를 준수하는 데 도움이 됩니다.

    Trivy 실전 구축! CI/CD 파이프라인에 녹여내기

    이제 가장 중요한 실전 구현입니다. 저는 주로 GitLab CI/CD를 사용하는데요, 여기서는 GitLab CI를 예시로 보여드리겠습니다. 다른 CI/CD 도구(GitHub Actions, Jenkins 등)에서도 원리는 비슷하니 응용하시면 됩니다.

    1. Trivy 설치 및 기본 스캔 (로컬 환경)

    먼저 로컬에서 Trivy가 잘 작동하는지 확인해봐야겠죠? 설치는 정말 간단합니다. 저는 주로 Homebrew를 쓰지만, 다양한 설치 방법이 있어요.

    # macOS (Homebrew) 또는 Linux (apt, yum 등 각 배포판 패키지 매니저 활용)
    brew install aquasecurity/trivy/trivy
    
    # 또는 Docker로 실행 (설치 없이 바로 사용 가능)
    docker run --rm aquasecurity/trivy:latest --version
    

    설치가 완료되면, 이제 컨테이너 이미지를 스캔해봅시다. 저는 테스트용으로 NGINX 공식 이미지를 스캔해볼게요.

    trivy image nginx:latest
    

    명령어를 실행하면 NGINX 이미지에 포함된 패키지들의 취약점 목록이 쭉 나올 겁니다. 심각도(Severity)별로 분류되어 있어서 어떤 것부터 고쳐야 할지 한눈에 파악하기 좋더라고요. 처음엔 이 많은 취약점들을 어떻게 다 봐야 하나 당황했는데, 실제로는 Critical이나 High 레벨부터 우선순위를 두고 보면 됩니다.

    2. GitLab CI/CD 파이프라인에 Trivy 통합

    이제 로컬에서 잘 작동하는 Trivy를 CI/CD 파이프라인에 넣어봅시다. 제 경험상, 컨테이너 이미지를 빌드한 직후, 그리고 레지스트리(Registry)로 푸시하기 전에 스캔하는 것이 가장 효율적이었습니다. 이렇게 하면 취약한 이미지가 레지스트리에 올라가는 것을 사전에 차단할 수 있거든요.

    `.gitlab-ci.yml` 파일에 다음과 같은 내용을 추가할 수 있습니다.

    stages:
      - build
      - scan
      - deploy
    
    variables:
      DOCKER_IMAGE_NAME: my-app
      DOCKER_IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
      # CI_REGISTRY는 GitLab 내장 Registry 주소입니다.
      FULL_IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH/$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG
    
    build_image:
      stage: build
      image: docker:latest
      services:
        - docker:dind
      script:
        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
        - docker build -t $FULL_IMAGE_NAME .
        - docker push $FULL_IMAGE_NAME
      only:
        - main
        - merge_requests
    
    scan_image_with_trivy:
      stage: scan
      image: 
        name: aquasecurity/trivy:latest
        entrypoint: [""]
      variables:
        # Trivy가 취약점 DB를 다운로드 받을 디렉토리. CI/CD 캐시 활용을 위해 설정
        TRIVY_CACHE_DIR: ".trivycache"
      script:
        # 빌드된 이미지를 스캔하기 위해 Docker Registry에 로그인
        - trivy --version
        - trivy image --ignore-unfixed --severity CRITICAL,HIGH --exit-code 1 $FULL_IMAGE_NAME
      cache:
        key: "$CI_COMMIT_REF_SLUG-trivy-cache"
        paths:
          - "$TRIVY_CACHE_DIR"
        policy: pull-push
      only:
        - main
        - merge_requests
    
    deploy:
      stage: deploy
      script:
        - echo "Deploying $FULL_IMAGE_NAME"
        # 여기에 실제 배포 로직 (Kubernetes, Ansible 등)을 작성합니다.
      only:
        - main
    

    위 YAML 코드를 보시면, `scan_image_with_trivy`라는 새로운 stage를 추가했습니다. 여기서 주목할 부분은:

    • image: aquasecurity/trivy:latest: Trivy 공식 Docker 이미지를 사용해서 별도 설치 없이 바로 실행합니다.
    • --ignore-unfixed: 아직 패치가 나오지 않은 취약점은 결과에서 제외합니다. (이걸 안 하면 리포트가 너무 길어져서 피로도가 높더라고요.)
    • --severity CRITICAL,HIGH: Critical(치명적)과 High(높음) 심각도의 취약점만 보고합니다. 처음부터 모든 취약점을 잡으려다가는 배보다 배꼽이 더 커질 수 있습니다. 현실적으로 가장 위험한 것부터 처리하는 게 중요하더라고요.
    • --exit-code 1: Critical 또는 High 심각도의 취약점이 발견되면, 파이프라인을 실패(Exit Code 1)시킵니다. 이게 핵심입니다! 자동으로 취약한 이미지가 다음 단계로 넘어가지 못하게 막는 거죠.
    • cache: Trivy가 취약점 데이터베이스(DB)를 다운로드하는 시간을 줄이기 위해 캐시를 활용했습니다. CI/CD 환경에서는 캐시 활용이 빌드 시간을 줄이는 데 아주 중요하거든요.
    GitLab CI/CD에서 Trivy 스캔 작업이 실행되고 CRITICAL, HIGH 취약점이 표시된 결과 화면

    그림 2: GitLab CI/CD에서 Trivy 스캔 작업 실행 및 결과

    Trivy 도입 시 주의사항 및 트러블슈팅

    Trivy CI/CD 파이프라인을 구축하면서 겪었던 몇 가지 삽질 경험과 해결책을 공유합니다. 혹시 비슷한 문제를 겪으신다면 도움이 될 거예요!

    1. False Positive (오탐) 문제

    Trivy도 완벽하지 않습니다. 때로는 실제로는 문제가 없는데 취약점으로 보고하는 오탐(False Positive)이 발생하기도 합니다. 특히 개발 초기 단계에서는 이런 오탐 때문에 파이프라인이 계속 실패하면 개발자들의 불만이 커질 수 있거든요.

    • 해결책: .trivyignore 파일을 사용해서 특정 취약점 ID를 무시하거나, --ignore-unfixed 옵션을 활용하여 아직 패치되지 않은 취약점을 제외할 수 있습니다. 예를 들어, CVE-2023-12345라는 특정 취약점 ID를 무시하고 싶다면 .trivyignore 파일에 해당 ID를 한 줄에 하나씩 작성하면 됩니다.

    2. 너무 많은 취약점 보고서

    처음에는 모든 심각도를 스캔했더니 보고서가 너무 길고, 뭘 먼저 고쳐야 할지 막막하더라고요. 모든 취약점을 한 번에 다 고치려는 건 현실적으로 어렵습니다.

    • 해결책: 앞서 보여드린 것처럼 --severity CRITICAL,HIGH 옵션을 사용해서 가장 위험한 취약점부터 우선적으로 처리하도록 정책을 세우는 것이 좋습니다. 점진적으로 심각도 기준을 높여가는 거죠.

    3. Trivy DB 업데이트 실패

    CI/CD 환경에서 네트워크 문제나 프록시 설정 때문에 Trivy가 취약점 데이터베이스를 업데이트하지 못하는 경우가 있었습니다. 최신 DB가 아니면 정확한 스캔이 불가능하죠.

    • 해결책: CI/CD Runner가 외부 인터넷에 접근 가능한지 확인하고, 필요한 경우 프록시 환경 변수(HTTP_PROXY, HTTPS_PROXY)를 설정해줘야 합니다. GitLab CI의 경우, variables 섹션에서 설정할 수 있습니다.
    • 또한, trivy image 명령 전에 trivy sbom으로 SBOM(Software Bill Of Materials)을 생성하고, 이를 기반으로 trivy image --input sbom.json 형태로 스캔하는 방식도 고려해볼 수 있습니다. 이는 네트워크 제한 환경에서 유용합니다.

    CI/CD 파이프라인 보안 검증: 실제 스캔 결과 및 적용

    위 설정대로 파이프라인을 구축하고 나면, 이제 새로운 코드가 푸시되거나 Merge Request(머지 리퀘스트)가 생성될 때마다 자동으로 Trivy 스캔이 실행됩니다. 만약 Critical, High 심각도의 취약점이 발견되면, 스캔 단계에서 파이프라인이 실패하고, 개발자에게 알림이 갑니다. 개발자는 이 알림을 보고 취약점을 수정하거나, 정당한 오탐(False Positive)인 경우 무시 규칙을 추가할 수 있습니다.

    실제 GitLab CI/CD 파이프라인에서 성공적으로 스캔이 완료된 모습이나, 혹은 취약점 때문에 파이프라인이 실패한 모습을 보면 정말 뿌듯하더라고요. 저는 주로 이런 식으로 결과를 확인합니다.

    Trivy 스캔 결과가 심각도별로 요약되고 해결 방안을 제시하는 대시보드 리포트

    그림 3: Trivy 스캔 결과 대시보드 예시

    아래는 Trivy의 주요 스캔 대상과 그 특징을 비교한 표입니다. 상황에 따라 적절한 스캔 대상을 선택하는 것이 중요하죠.

    스캔 대상 (Scan Target) 설명 (Description) 주요 활용 사례 (Key Use Cases) 장점 (Pros) 고려사항 (Considerations)
    image (컨테이너 이미지) Docker 이미지, OCI 이미지 등 컨테이너 이미지 내부의 패키지 취약점 스캔 CI/CD 파이프라인에서 이미지 빌드 후 즉시 검사, 배포 전 최종 검증 가장 일반적이고 강력한 컨테이너 보안, 배포 전 위험 제거 스캔 시간이 다소 길 수 있음 (레이어 분석), 이미지 레이어 최적화 필요
    fs (파일 시스템) 로컬 파일 시스템 또는 압축 파일 내의 취약점 및 설정 오류 스캔 개발 중인 프로젝트 코드 스캔, 특정 디렉토리/파일 검사 빠른 스캔, 개발 초기 단계에서 피드백 제공, CI/CD 전 로컬 검증 전체 컨테이너 환경을 반영하기 어려움, 의존성 설치 환경에 따라 결과 상이
    repo (Git 저장소) Git 저장소의 설정 파일(Dockerfile, Kubernetes YAML)에서 잘못된 설정 스캔 IaC(Infrastructure as Code) 보안 검증, 초기 개발 단계에서 설정 오류 방지 코드 레벨에서 보안 취약점 조기 발견, 설정 파일 검증 코드 내부의 라이브러리 취약점은 탐지 불가, 설정 파일 문법 의존적
    Trivy의 컨테이너 이미지, 파일 시스템, Git 저장소 스캔 대상별 특징 비교 인포그래픽

    그림 4: Trivy 스캔 대상별 특징 비교

    마무리하며: Trivy를 통한 지속적인 보안 확보

    Trivy를 CI/CD 파이프라인에 통합하는 것은 컨테이너 기반 환경에서 보안을 강화하고, 개발 및 운영 효율성을 높이는 중요한 단계라고 생각합니다. 저도 처음엔 ‘이걸 언제 다 구축하나’ 싶었는데, 한 번 구축해두니 정말 든든하더라고요. 든든한 방패를 얻은 기분이랄까요?

    물론 Trivy 하나만으로 모든 보안 위협을 막을 수는 없습니다. 하지만 가장 기본적인 방어선을 구축하고, 자동화된 방식으로 지속적인 보안 검증을 수행한다는 점에서 그 가치는 충분하다고 봅니다. Critical, High 레벨의 취약점만이라도 배포 전에 걸러낼 수 있다면, 야간 비상 호출(On-call) 횟수를 확 줄일 수 있을 거예요. 저도 덕분에 잠을 좀 더 잘 수 있게 됐습니다.

    만약 여러분의 CI/CD 파이프라인에 아직 자동화된 보안 스캔이 없다면, 지금 당장 Trivy를 도입해보시길 강력히 추천합니다. 초기에는 오탐이나 너무 많은 보고서 때문에 조금 번거로울 수도 있지만, 꾸준히 규칙을 개선하고 피드백을 반영하다 보면 훨씬 견고하고 효율적인 보안 프로세스를 만들 수 있을 겁니다. 다음번에는 Trivy를 활용한 SBOM(Software Bill Of Materials) 생성 및 관리 방법에 대해 이야기해볼까 합니다. 기대해주세요!

  • [k8s] Kubernetes Liveness Probe: Readiness·Startup 차이와 설정 가이드

    [k8s] Kubernetes Liveness Probe: Readiness·Startup 차이와 설정 가이드

    Kubernetes Liveness Probe: Readiness, Startup 차이와 운영 가이드

    Kubernetes Liveness Probe를 처음 만졌을 때 가장 크게 데인 지점은, 프로브를 “상태 확인”이 아니라 “불안하면 일단 재시작” 버튼처럼 썼다는 점이었습니다. 앱이 조금 늦게 뜨기만 해도 죽은 것으로 판정했고, 결과는 뻔했죠. Pod(파드, 쿠버네티스의 최소 배포 단위)는 부팅 도중 반복 재시작했고, 실제 버그보다 운영 설정이 더 큰 장애를 만들더라고요.

    운영에서 프로브는 단순 헬스 체크가 아닙니다. 저는 이걸 장애 감지 장치보다 복구 정책 스위치에 가깝게 봅니다. Kubernetes Liveness Probe를 실패시키면 kubelet이 해당 컨테이너를 다시 시작하고, Readiness Probe가 실패하면 Service 트래픽 대상에서 빠지며, Startup Probe는 느린 기동 구간을 보호합니다. 같은 probe라도 실패 뒤에 이어지는 동작이 완전히 다르기 때문에, 이 차이를 설계하지 않고 문법만 맞추면 장애가 길어집니다.

    이번 글에서는 홈랩과 실무에서 반복해서 검증했던 기준으로, 언제 Liveness를 써야 하는지, 언제 오히려 빼는 게 나은지, Readiness를 어디까지 엄격하게 볼지, Startup Probe를 어떤 식으로 계산해 붙일지를 실행 가능한 예시와 함께 정리해보겠습니다. 배포 전략이나 Service 동작 원리가 헷갈린다면 블로그의 관련 글도 함께 읽어보시면 흐름이 더 잘 잡힙니다.

    Kubernetes Liveness Probe를 포함한 프로브 전체 흐름 아키텍처 이미지

    liveness, readiness, startup probe가 각각 어떤 순간에 동작하고 서비스 트래픽에 어떤 영향을 주는지 보여주는 개요 다이어그램입니다.

    Kubernetes Liveness Probe가 왜 중요한가

    Liveness Probe(라이브니스 프로브, 생존 확인)는 “이 프로세스를 계속 살려둘 가치가 있는가”를 묻습니다. 핵심은 회복 불가능성입니다. 요청이 일시적으로 느린 상태, DB가 잠깐 흔들린 상태, 외부 API가 timeout 나는 상태는 대개 liveness의 대상이 아니거든요. 반대로 이벤트 루프가 멈췄다거나, 스레드 데드락으로 더 이상 요청을 처리할 수 없고 자체 회복도 기대하기 어렵다면 liveness를 실패시켜 재기동시키는 편이 낫습니다.

    Readiness Probe(레디니스 프로브, 요청 처리 준비 상태 확인)는 질문이 다릅니다. 지금 이 Pod로 트래픽을 보내도 되나?를 판단하죠. 살아는 있지만 아직 준비가 안 됐거나, 의존 서비스가 불안정해 새 요청을 받으면 실패율만 올릴 상황이라면 readiness를 false로 두고 엔드포인트에서 빠지는 쪽이 안전합니다.

    Startup Probe(스타트업 프로브, 초기 기동 확인)는 더 실무적입니다. 느리게 뜨는 앱에게 “운영 중 기준”을 너무 일찍 들이대지 않게 해 줍니다. JVM 앱, 대용량 캐시를 적재하는 API, 마이그레이션 이후에만 정상 동작하는 서비스는 startup probe가 없으면 정상 기동 중에도 liveness에 맞아 죽기 쉽습니다.

    현장에서 가장 자주 보는 오해는 이것입니다. “헬스 체크는 엄격할수록 좋다.” 실제 운영은 반대인 경우가 많습니다. 프로브는 엄격함보다 의미 분리가 더 중요합니다. Liveness는 재시작을 정당화할 수 있을 때만, Readiness는 트래픽 차단이 이득일 때, Startup은 초기화 변동 폭을 흡수할 때 써야 합니다.

    Kubernetes Liveness Probe와 Probe 3종 역할 구분표

    Probe 종류 실무 질문 실패 시 쿠버네티스 동작 넣어야 하는 경우 빼거나 약하게 둬야 하는 경우
    Liveness Probe 프로세스가 회복 불가능하게 멈췄는가 컨테이너 재시작 deadlock, event loop 정지, 내부 워커 hang 외부 의존성 장애에 자주 흔들리는 앱, 자체 hang 가능성이 낮은 단순 API
    Readiness Probe 지금 요청을 받아도 되는가 Service 엔드포인트에서 제외 DB 연결 전, 캐시 워밍업 중, 소비자 초기화 전, 배포 중 drain 필요 상태 판단에 무거운 쿼리나 외부 API 호출이 필요한 경우
    Startup Probe 초기 부팅이 아직 끝나지 않았는가 기동 구간 보호, 실패 지속 시 재시작 JVM, 모델 로딩, schema migration, 큰 플러그인 초기화 기동 시간이 매우 짧고 변동 폭이 작은 앱

    운영 판단을 더 거칠게 요약하면 아래처럼 보시면 됩니다.

    • Liveness: 계속 두면 더 나빠질 프로세스만 다시 띄웁니다.
    • Readiness: 살아 있어도 지금은 손님 받지 말자는 신호입니다.
    • Startup: 부팅 중인 앱을 운영 기준으로 성급하게 심판하지 않기 위한 완충 장치입니다.

    이 차이를 놓치면 증상이 묘해집니다. Pod는 Running인데 요청은 503이 쌓이거나, DB가 잠깐 느려졌을 뿐인데 liveness가 재시작 루프를 만들어 장애 반경을 넓혀 버리죠. 프로브 설계의 핵심은 “정상/비정상 판정”보다 실패 도메인을 어디서 끊을지에 있습니다.

    Kubernetes 헬스 체크 방식도 성격이 다릅니다

    Kubernetes에서는 보통 HTTP GET, TCP Socket, Exec 세 방식으로 검사합니다. 셋 다 쓸 수 있다고 해서 셋 다 좋은 건 아닙니다. 중요한 건 무엇을 검증하고 무엇을 버리는지를 알고 선택하는 겁니다.

    • HTTP GET: 가장 해석이 쉽습니다. <code>/live, /ready처럼 의미를 분리하기 좋고, 애플리케이션 레벨 판단이 가능합니다.
    • TCP Socket: 포트가 연결 가능한지만 확인합니다. 포트는 열려 있는데 실제 요청 처리는 불안정한 상황은 못 잡을 수 있습니다.
    • Exec: 컨테이너 안에서 명령을 실행하므로 유연하지만, 프로세스 생성 비용과 스크립트 실패 모드까지 같이 관리해야 합니다.

    웹 애플리케이션이라면 대체로 Readiness는 HTTP, Liveness는 아주 가볍고 보수적으로, Startup은 실제 기동 시간을 기준으로 별도 구성하는 쪽을 권합니다. 반대로 배치 워커처럼 HTTP 서버가 없고, 작업 큐 polling 상태나 pid 파일 확인이 더 직접적인 경우에는 exec probe가 더 맞을 수 있습니다.

    중요한 판단 하나만 더 짚으면, Liveness에서 외부 의존성을 확인하는 순간 프로브는 자가치유 장치가 아니라 장애 증폭 장치가 되기 쉽습니다. DB, Redis, Kafka, 외부 API는 readiness 쪽에서 다루고, liveness는 프로세스 내부 생존성만 보는 편이 운영 사고가 적었습니다.

    실전 구현: Deployment에 Liveness, Readiness, Startup Probe 넣기

    재현 가능한 예시로 가보겠습니다. 아래 YAML은 HTTP 엔드포인트를 분리한 애플리케이션 기준입니다. 데모용 구조지만 실무에서도 그대로 많이 씁니다. 포인트는 세 가지입니다. startup은 부팅 보호, readiness는 트래픽 허용 판단, liveness는 내부 고장 감지입니다.

    1. 애플리케이션에서 /live와 /ready를 분리합니다.
    2. /ready는 요청 처리에 필요한 최소 의존성이 준비됐을 때만 200을 반환하게 합니다.
    3. /live는 외부 의존성을 빼고, 프로세스가 hang 상태인지 중심으로 판단합니다.
    4. 기동 시간이 흔들리는 앱이면 startup probe로 운영 구간 판정을 늦춥니다.
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: probe-demo
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: probe-demo
      template:
        metadata:
          labels:
            app: probe-demo
        spec:
          containers:
            - name: app
              image: your-registry/your-app:1.0.0
              ports:
                - containerPort: 8080
              startupProbe:
                httpGet:
                  path: /live
                  port: 8080
                periodSeconds: 5
                timeoutSeconds: 2
                failureThreshold: 24
              readinessProbe:
                httpGet:
                  path: /ready
                  port: 8080
                periodSeconds: 5
                timeoutSeconds: 2
                failureThreshold: 3
                successThreshold: 1
              livenessProbe:
                httpGet:
                  path: /live
                  port: 8080
                periodSeconds: 10
                timeoutSeconds: 2
                failureThreshold: 3

    여기서 많이 놓치는 계산이 있습니다. startupProbe의 허용 시간은 보통 failureThreshold * periodSeconds로 먼저 읽으면 편합니다. 위 예시는 5초마다 검사하고 24번까지 실패를 허용하니, 대략 120초까지는 기동 보호 구간이라고 해석할 수 있습니다. 앱이 어떤 날은 15초, 어떤 날은 80초 걸린다면 initialDelaySeconds 하나로 버티는 것보다 startup probe가 훨씬 안전합니다.

    startupProbe, readinessProbe, livenessProbe가 YAML 안에서 어떤 식으로 연결되고 순서대로 동작하는지 보여주는 구성 이미지입니다.

    적용 후에는 아래 순서로 보는 게 제일 빠릅니다. 배포, 상태 변화, 이벤트, 직전 로그를 묶어서 확인해야 원인이 잘 보입니다.

    kubectl apply -f probe-demo.yaml
    kubectl rollout status deployment/probe-demo
    kubectl get pods -l app=probe-demo -w
    kubectl describe pods -l app=probe-demo

    kubectl get pods -w는 READY와 RESTARTS 변화를 실시간으로 보기 좋고, kubectl describe pods -l app=probe-demo는 probe 실패 메시지와 kubelet 이벤트를 확인할 때 특히 유용합니다. 앱 로그만 보고 있으면 왜 재시작됐는지 놓치는 경우가 많습니다.

    Kubernetes Liveness Probe 파라미터는 이렇게 읽으시면 됩니다

    • initialDelaySeconds: 검사 시작 전 대기 시간입니다. 다만 느린 앱 보호를 이 값 하나로 해결하려 들면 운영 단계의 장애 감지가 늦어집니다.
    • periodSeconds: 검사 주기입니다. 너무 짧으면 노이즈와 부하가 늘고, 너무 길면 장애 감지가 늦습니다.
    • timeoutSeconds: 응답 대기 시간입니다. readiness가 자주 timeout 난다면 엔드포인트 자체가 무겁다는 신호일 수 있습니다.
    • failureThreshold: 연속 실패 허용 횟수입니다. 일시적 지연과 실제 장애를 구분하는 완충 장치로 보면 이해가 쉽습니다.
    • successThreshold: 연속 성공 횟수입니다. readiness의 flap을 줄일 때 의미가 있고, liveness와 startup에서는 1이어야 합니다.

    제 경험상 기동이 느린 앱에 initialDelaySeconds만 크게 주는 방식은 나중에 운영 감지를 둔하게 만듭니다. 시작 구간과 정상 운영 구간은 실패 의미가 다르기 때문에, startup probe로 분리하는 편이 더 깔끔하더라고요.

    헬스 엔드포인트는 이렇게 나누는 편이 덜 사고 납니다

    문법보다 더 중요한 건 애플리케이션이 무엇을 200으로 돌려주느냐입니다. 같은 Spring Boot나 FastAPI라도 엔드포인트 설계가 엉성하면 probe는 멀쩡해 보이는데 운영은 계속 흔들립니다. 저는 아래처럼 역할을 나누는 편을 선호합니다.

    엔드포인트 포함할 것 빼야 할 것 이유
    /live 메인 루프 정상 동작, 내부 큐 정지 여부, 프로세스 핵심 스레드 상태 DB ping, 외부 API 호출, 느린 파일 시스템 검사 재시작이 의미 있는 내부 고장만 잡아야 하기 때문
    /ready DB 연결 풀 준비, 필수 캐시 준비, 메시지 소비자 초기화 완료 비핵심 외부 연동, 과도한 상세 진단, 고비용 쿼리 트래픽 수용 가능 여부만 빠르게 판단해야 하기 때문

    예를 들어 주문 API가 있고, 요청 한 건을 처리하려면 DB 연결과 내부 캐시 로드가 필수라고 해보겠습니다. 이 경우 /ready는 DB pool이 usable인지, 필수 캐시가 준비됐는지만 보면 됩니다. 반면 분석용 외부 API가 잠깐 느린 건 새 주문 수락과 직접 관계가 없다면 readiness에 넣지 않는 편이 낫습니다. 모든 연동을 readiness에 다 넣으면 안전해 보일 수는 있어도, 실제로는 트래픽을 과하게 차단하게 됩니다.

    실무에서 많이 하는 실수 4가지

    여기서부터가 설정 문법보다 훨씬 중요합니다. 대부분의 장애는 YAML 오타보다 잘못된 운영 가정에서 나옵니다.

    1. Liveness Probe에 외부 의존성을 넣는 경우

    예를 들어 /healthz가 DB 질의를 수행하고, DB 응답이 늦으면 liveness가 실패하도록 만들면 어떻게 될까요. 앱 프로세스는 멀쩡한데 외부 의존성 장애가 컨테이너 재시작으로 번집니다. 재시작 직후 연결 폭증까지 붙으면 DB는 더 버거워지고, 결국 앱과 DB가 같이 흔들립니다. 이 패턴은 진짜 오래 사람을 괴롭히더라고요. 재시작이 복구가 아니라 증폭이 되는 겁니다.

    분리 기준은 단순합니다.

    • Liveness: 프로세스 내부 상태만 확인
    • Readiness: 요청 처리에 반드시 필요한 의존성만 확인

    2. 느린 앱에 Startup Probe 없이 Liveness만 거는 경우

    JVM, 대형 모델 로딩, 플러그인 초기화, schema migration이 있는 앱은 부팅 시간이 환경마다 다릅니다. 이때 liveness가 먼저 붙으면 정상 기동 중인 프로세스를 죽입니다. 증상은 CrashLoopBackOff로 보이는데, 실제 원인은 코드 버그보다 프로브 타이밍인 경우가 꽤 많습니다.

    권하는 방식은 먼저 실제 기동 로그를 보고, 포트 오픈 시점이 아니라 요청 처리 준비 완료 시점을 기준으로 startup budget을 잡는 겁니다. 앱이 포트를 열었다고 준비가 끝난 건 아니니까요.

    3. Readiness 엔드포인트를 너무 무겁게 만든 경우

    Readiness는 자주 호출됩니다. 여기에 비싼 SQL, 다중 외부 API 확인, 긴 디스크 검사까지 넣으면 체크 자체가 시스템 부하가 됩니다. 더 나쁜 경우는 readiness endpoint가 느려서 timeout 나고, 그 timeout 때문에 엔드포인트에서 빠졌다가 붙었다가를 반복하는 겁니다. 서비스는 살아 있는데 트래픽 경로만 흔들리죠.

    실무에서는 readiness를 빠르고, 결정적이며, 요청 처리 가능 여부만 말하는 엔드포인트로 두는 게 안정적입니다.

    4. probe 실패 로그를 앱 로그만으로 해석하는 경우

    Pod가 재시작하면 애플리케이션 로그부터 보는 경우가 많습니다. 그런데 probe 실패의 1차 단서는 대개 kubelet 이벤트에 있습니다. “왜 죽였는지”는 kubectl describe pod에 더 잘 남습니다. 재시작된 후 현재 컨테이너 로그만 보면 이미 중요한 구간이 사라졌을 수도 있고요.

    kubectl describe pod <pod-name>
    kubectl logs <pod-name> --previous
    kubectl get events --sort-by=.metadata.creationTimestamp

    --previous는 직전 컨테이너 로그를 보기에 특히 중요합니다. probe 때문에 재시작된 직후에는 현재 로그가 너무 깨끗해서 오히려 단서가 안 남는 경우가 많습니다.

    트러블슈팅: CrashLoopBackOff가 probe 때문인지 확인하는 방법

    재현 가능한 시나리오 하나를 잡아보겠습니다. 앱이 시작하면서 DB migration을 수행하고, HTTP 포트는 먼저 뜨지만 실제 요청 처리는 migration 완료 후에만 가능하다고 해보죠. 이때 liveness가 너무 빨리 붙으면, Kubernetes는 애플리케이션이 아직 기동 중인데도 불안정하다고 판단해 재시작시킬 수 있습니다. 문제는 재시작하면 migration이 다시 시작된다는 점입니다. 이 루프가 반복되면 코드가 아니라 프로브가 장애 원인입니다.

    1. Pod 상태 확인: kubectl get pods -w에서 RESTARTS가 계속 증가하는지 봅니다.
    2. 이벤트 확인: kubectl describe pod에서 Liveness probe failed, Readiness probe failed, Startup probe failed 메시지를 찾습니다.
    3. 직전 로그 확인: kubectl logs --previous로 종료 직전 애플리케이션 상태를 봅니다.
    4. 기동 순서 분리: 포트 오픈 시점, migration 완료 시점, ready 응답 가능 시점을 로그로 나눠 읽습니다.
    5. Startup Probe 추가 또는 조정: 준비 완료 전까지 liveness 간섭을 차단합니다.

    이때 숫자를 감으로 찍지 말고, 실제 이벤트와 로그 타임라인을 맞춰 보셔야 합니다. 앱의 “실제 준비 완료”와 Kubernetes가 “준비됐다고 믿는 시점”이 어긋날 때 문제가 생깁니다. 먼저 그 간격을 맞춘 뒤 threshold를 조정해야 덜 헤맵니다. 순서가 반대면 결국 숫자 놀음이 되더라고요.

    kubectl get pod <pod-name> -o wide
    kubectl describe pod <pod-name> | sed -n '/Events:/,$p'
    kubectl logs <pod-name> --previous --timestamps
    kubectl logs <pod-name> --timestamps

    여기서 볼 포인트는 단순합니다. 이벤트 시각과 앱 로그 시각을 맞춰서, probe 실패가 먼저였는지, 앱 내부 오류가 먼저였는지를 분리하는 겁니다. 순서를 잘못 읽으면 원인과 결과를 바꿔 잡게 됩니다.

    검증: 설정 후 무엇을 보고 정상이라고 판단할까

    설정을 넣는 것과 운영이 안정화되는 것은 별개입니다. 배포 직후에는 아래 네 가지를 같이 보는 편이 좋습니다. 하나만 보면 착시가 생깁니다.

    • 배포 직후 READY 변화: Running보다 READY가 중요합니다. Running인데 READY가 0/1이면 서비스는 사실상 트래픽을 못 받습니다.
    • 엔드포인트 반영: readiness 실패 시 실제로 Service 엔드포인트에서 빠지는지 확인해야 합니다.
    • RESTARTS 안정화: 정상 상태에 들어간 뒤 RESTARTS가 더 늘지 않아야 합니다.
    • 이벤트 노이즈 여부: 간헐적 probe failure가 계속 쌓인다면, 배포 피크나 노드 부하 때 실제 장애로 이어질 수 있습니다.
    kubectl rollout status deployment/probe-demo
    kubectl get pods -l app=probe-demo -o wide
    kubectl get endpoints
    kubectl describe deployment probe-demo

    검증 기준을 더 분명하게 두자면 이렇습니다. READY는 기대 복제 수만큼 올라와야 하고, RESTARTS는 안정 구간에서 멈춰야 하며, endpoints에는 준비된 Pod만 남아야 합니다. Pod가 떠 있다는 사실만으로 정상 판정을 내리면 실제 장애를 놓치기 쉽습니다.

    Kubernetes Liveness Probe 검증과 Pod 상태 확인 이미지

    Pod 상태, 이벤트 로그, readiness 반영 여부를 함께 보는 검증 흐름 이미지입니다.

    운영 팁: 앱 유형별로 probe를 다르게 잡아야 합니다

    앱 유형 추천 구성 이유 보수적으로 볼 포인트
    가벼운 웹 API Readiness + 보수적 Liveness 기동이 빠르고 요청 처리 여부 판단이 단순함 Liveness가 굳이 필요 없는 서비스도 적지 않음
    기동이 느린 JVM 앱 Startup + Readiness + Liveness 초기화 시간 보호와 운영 구간 분리가 필요함 initialDelaySeconds 하나로 해결하려 하지 않기
    배치 워커 Exec 또는 최소 Liveness HTTP 엔드포인트가 없을 수 있음 작업 큐 적체를 liveness 실패로 바로 연결하지 않기
    외부 의존성이 많은 앱 Readiness 강화, Liveness 단순화 장애 전파를 재시작 루프로 만들지 않기 위함 핵심 의존성만 readiness에 포함하기

    혹시 “애플리케이션은 분명 떠 있는데 로드밸런서 뒤에서만 실패하는” 경험이 있었다면, 대체로 readiness가 너무 느슨하거나 너무 무거운 경우가 많습니다. 카프카 소비자가 붙은 앱에서도 비슷한 장면이 자주 나옵니다. HTTP 포트는 빨리 떠도 소비자 그룹 조인이 끝나기 전엔 실제 처리 품질이 불안정할 수 있거든요. 그때 /ready를 소비자 초기화 완료 이후에만 true로 바꾸면 배포 중 실패율이 꽤 줄어듭니다. 이거 실무에서 진짜 편하더라고요.

    자주 묻는 질문

    Readiness만 있으면 Liveness는 없어도 될까요?

    가능합니다. 이건 의외로 많은 팀이 과하게 쓰는 부분입니다. 애플리케이션이 hang 상태에 빠질 가능성이 낮고, 장애 시 프로세스가 명확하게 종료되며, readiness만으로도 트래픽 차단이 충분하다면 liveness를 아예 두지 않는 선택도 실무적으로 타당합니다. Liveness는 필수 옵션이 아니라 재시작이 실제 복구 수단일 때만 가치가 있습니다.

    TCP probe면 충분하지 않나요?

    정말 단순한 서비스라면 됩니다. 하지만 대부분의 웹 애플리케이션은 “포트가 열려 있다”와 “요청을 정상 처리할 수 있다”가 다릅니다. 그래서 운영 해석 가능성을 높이려면 HTTP 기반 엔드포인트 분리가 더 낫습니다.

    헬스 체크 엔드포인트는 하나로 합쳐도 될까요?

    작게 시작할 때는 가능합니다. 다만 운영 기간이 길어질수록 역할 분리가 이깁니다. /live, /ready를 분리하면 장애 원인과 후속 조치가 선명해집니다. 최소한 readiness와 liveness는 분리하는 쪽을 권합니다.

    Kubernetes Liveness Probe 선택 기준 요약 인포그래픽

    앱 유형별로 어떤 probe 조합을 선택하면 좋은지 한눈에 보는 요약 이미지입니다.

    마무리: Kubernetes Liveness Probe는 이렇게 고르면 덜 흔들립니다

    Kubernetes Liveness Probe는 만능 안정화 버튼이 아닙니다. 잘못 걸면 재시작이 복구가 아니라 장애 확산 경로가 됩니다. 반대로 Readiness Probe를 제대로 설계하면 준비 안 된 Pod로 트래픽이 들어가는 문제를 깔끔하게 줄일 수 있고, Startup Probe는 느린 기동 서비스를 불필요한 CrashLoopBackOff에서 지켜줍니다.

    기준은 꽤 분명합니다. 가벼운 API라면 Readiness를 먼저 정확히 만들고, Liveness는 꼭 필요한 경우에만 보수적으로 추가하세요. 느리게 뜨는 앱이라면 Startup Probe를 분리하세요. 외부 의존성이 많은 서비스라면 Liveness는 단순하게 두고 Readiness에서 트래픽 차단을 설계하세요. 운영 안정성은 “많이 검사하는 것”보다 무엇을 실패시켰을 때 어떤 후속 조치가 일어나는지를 정확히 설계할 때 올라갑니다.

  • [Cloud] Jenkins 장애 해결: CI/CD 파이프라인 디버깅 방법론

    [Cloud] Jenkins 장애 해결: CI/CD 파이프라인 디버깅 방법론

    Jenkins 장애 해결: CI/CD 파이프라인 장애 발생 시 디버깅 방법론

    Jenkins 장애 해결이 급한 순간은 늘 비슷하더라고요. 배포 직전인데 파이프라인이 멈추고, 로그는 길고, 팀 채팅방은 조용히 뜨거워집니다. 지속적 통합 환경에서는 작은 설정 하나가 전체 흐름을 막는 경우가 많거든요. 그래서 이번 글에서는 Jenkins CI/CD 파이프라인 장애가 났을 때 어디부터 보고, 무엇으로 판단할지 실무 순서대로 정리해보겠습니다.

    처음엔 저도 젠킨스 에러가 뜨면 Jenkins 자체 문제부터 의심했는데요. 실제로는 소스 저장소 인증, 에이전트 연결, 워크스페이스, 셸 환경 변수처럼 경계 지점에서 막히는 일이 훨씬 많았습니다. 중요한 포인트는 하나입니다. 증상만 보지 말고 실행 경로를 층별로 나눠서 확인해야 합니다.

    Jenkins 장애 해결을 위한 CI/CD 전체 아키텍처 다이어그램

    Jenkins 컨트롤러, 에이전트, Git 저장소, 빌드 도구, 배포 대상 시스템 사이의 장애 지점을 한눈에 보여주는 개요 이미지입니다.

    Jenkins 장애 해결은 왜 순서가 중요할까

    쉽게 말해 Jenkins는 혼자 일하지 않습니다. 컨트롤러가 잡(Job)을 받고, 에이전트가 실제 명령을 수행하고, Git 같은 외부 시스템에서 코드를 가져오고, Docker나 Maven, Gradle 같은 도구를 호출합니다. 여기서 하나라도 어긋나면 파이프라인 문제 해결이 꼬이기 시작하죠.

    실무에서 자주 보는 장애 구간은 대략 이렇습니다.

    • 시작도 못 하는 장애: 큐에만 쌓이고 실행되지 않음
    • 초반 실패: SCM checkout 실패, credential 문제, webhook 미동작
    • 중간 실패: 테스트, 빌드, 이미지 생성, 스크립트 문법 에러
    • 후반 실패: 아티팩트 업로드, 배포 권한, 대상 서버 연결 불가
    • 간헐 장애: 같은 커밋인데 어떤 때는 되고 어떤 때는 안 됨

    결국 Jenkins 장애 해결은 Jenkins 자체보다 연결된 구성 요소의 경계면을 보는 작업에 가깝습니다. 저도 예전엔 콘솔 출력만 붙잡고 오래 헤맨 적이 있었는데, 구조를 나눠서 보니까 훨씬 빨리 풀리더라고요.

    CI/CD 디버깅 기본 원칙: 한 번에 하나씩 잘라 보기

    팀에서 자주 맞추는 기준이 있습니다. 재현 가능한 최소 실패 지점(minimal failing step)을 먼저 만들자는 거예요. 로그가 2천 줄이어도 결국 실패는 한 단계에서 시작되거든요.

    1. 최근 변경이 어디인지 확인합니다. Jenkinsfile, credential, agent image, plugin, target server 중 무엇이 바뀌었는지 먼저 봅니다.
    2. 실패 지점을 단계 단위로 자릅니다. checkout, build, test, publish, deploy 순서로 어디서 처음 깨지는지 확인합니다.
    3. 컨트롤러와 에이전트를 분리해서 봅니다. UI에서 보이는 에러와 실제 실행 노드의 시스템 로그는 다를 수 있습니다.
    4. 같은 명령을 에이전트 셸에서 직접 실행합니다. Jenkins만 실패하는지, OS 레벨에서도 실패하는지 비교합니다.
    5. 마지막 성공 이력과 비교합니다. 같은 브랜치의 이전 성공 빌드가 가장 좋은 기준선입니다.

    여기서 중요한 포인트는 왜 안 되지?보다 어디까지는 됐지?라고 묻는 습관입니다. 이거 진짜 편하더라고요.

    Jenkins 장애 해결 1차 점검: 서비스 상태와 기본 로그

    Jenkins 장애 해결에서 제일 먼저 할 일은 화려한 분석이 아닙니다. 서비스가 살아 있는지, 최근 로그에 뻔한 실패가 있는지부터 확인해야 합니다. 의외로 Java 프로세스 메모리 문제, 디스크 공간 부족, 권한 문제 같은 기본기가 원인인 경우가 많거든요.

    1. systemd 서비스 상태 확인

    sudo systemctl status jenkins
    sudo journalctl -u jenkins -n 200 --no-pager
    sudo journalctl -u jenkins -f

    Linux 패키지 설치 환경에서는 Jenkins 공식 문서 기준으로 journalctl -u jenkins가 기본 로그 확인 방법입니다. 여기서 볼 건 단순합니다. 프로세스가 반복 재시작하는지, 플러그인 로딩 실패가 있는지, 포트 바인딩 실패, Permission denied 같은 메시지가 있는지 먼저 보세요. 마지막 한 줄만 보지 말고, 실패 직전 수십 줄을 같이 보는 게 훨씬 정확합니다.

    2. Jenkins 홈 디렉터리와 디스크 확인

    sudo du -sh /var/lib/jenkins
    sudo df -h
    sudo ls -ld /var/lib/jenkins

    일반적인 Linux 패키지 설치에서는 /var/lib/jenkins가 자주 쓰이는 Jenkins 홈 디렉터리입니다. 다만 로그 파일 경로는 설치 방식에 따라 다를 수 있어서, Linux에서는 먼저 journalctl을 우선으로 보는 편이 안전합니다. 디스크가 꽉 차면 빌드 중단, 워크스페이스 정리 실패, 플러그인 캐시 이상처럼 애매한 증상으로 보일 때가 많습니다.

    3. 웹 응답과 큐 상태 확인

    curl -I http://127.0.0.1:8080/login
    curl -s http://127.0.0.1:8080/queue/api/json
    curl -s http://127.0.0.1:8080/computer/api/json

    /queue/api/json은 작업이 왜 대기 중인지 볼 때 유용합니다. 대기 사유를 설명하는 값이 내려오는 경우가 있어서, 적절한 라벨의 에이전트가 없거나 모든 실행기(executor)가 점유된 상황을 빨리 찾을 수 있죠. /computer/api/json도 에이전트 상태를 한 번에 점검할 때 꽤 편합니다.

    Jenkins 장애 해결을 위한 서비스 상태 및 로그 점검 이미지

    systemctl, journalctl, Jenkins 로그를 보며 서비스 상태와 에러 메시지를 추적하는 터미널 중심의 점검 장면입니다.

    파이프라인 문제 해결의 핵심: 콘솔 로그를 단계별로 읽는 법

    콘솔 로그는 다들 보지만, 읽는 순서가 제각각인 경우가 많습니다. 저는 아래 순서대로 봅니다.

    1. 첫 실패 지점을 찾습니다. 마지막 실패가 아니라 첫 실패입니다.
    2. 실행한 실제 명령을 찾습니다. Jenkins가 감싼 메시지 말고 sh, bat, git, docker 같은 실제 명령을 봅니다.
    3. 반환 코드(exit code)를 확인합니다. 같은 문구라도 종료 코드가 다르면 해석이 달라집니다.
    4. 환경 변수와 작업 디렉터리를 의심합니다. Jenkins 안에서만 실패하면 경로, 사용자, 셸 차이일 가능성이 큽니다.

    예를 들어 이런 Declarative Pipeline 조각이 있다고 해보겠습니다.

    pipeline {
      agent any
      stages {
        stage('Checkout') {
          steps {
            checkout scm
          }
        }
        stage('Build') {
          steps {
            sh 'pwd'
            sh 'printenv | sort'
            sh './gradlew clean build'
          }
        }
      }
      post {
        always {
          archiveArtifacts artifacts: 'build/reports/**', allowEmptyArchive: true
        }
      }
    }

    여기서 pwd와 printenv | sort를 자주 넣는 이유가 있습니다. 처음엔 투박해 보여도, 로컬에서는 되는데 Jenkins에서만 실패하는 문제를 잡아낼 때 꽤 강력하거든요. 특히 PATH, HOME, WORKSPACE 차이를 확인할 때 도움이 큽니다.

    또 하나 중요한 점은 SCM checkout 실패와 빌드 도구 실패를 섞어서 보지 않는 겁니다. checkout scm 이전에 실패하면 Git 접근, credential, 네트워크 문제일 가능성이 높고, 그 이후에 실패하면 빌드 스크립트나 런타임 문제로 좁혀집니다.

    CI/CD 디버깅 실전: 에이전트와 셸 환경 검증

    실전에서 정말 자주 만나는 케이스가 하나 있습니다. 파이프라인에서는 docker: command not found가 뜨는데, 운영자는 분명 에이전트에 Docker를 설치했다고 말하는 상황이죠. 저도 이런 경우를 여러 번 봤는데, 알고 보면 Jenkins가 실행되는 사용자와 사람이 SSH로 접속했을 때의 사용자 환경이 다른 경우가 많았습니다.

    이럴 때는 Jenkinsfile 안에서 추측만 하지 말고, 에이전트 셸에서 같은 사용자 맥락을 확인하는 게 빠릅니다.

    whoami
    id
    pwd
    echo "$PATH"
    command -v git
    command -v docker
    command -v java
    ls -la

    판단 기준은 이렇습니다.

    • command -v docker가 비어 있으면 PATH 또는 설치 경로 문제를 먼저 봅니다.
    • whoami 결과가 예상과 다르면 서비스 계정이 다를 수 있습니다.
    • 현재 디렉터리가 워크스페이스인지 확인합니다. 상대 경로 스크립트는 여기서 자주 깨집니다.
    • SSH 로그인 셸에서는 되는데 Jenkins에서 안 되면 .bashrc, .profile 같은 로그인 셸 초기화에 의존했을 가능성이 큽니다.

    에이전트가 컨테이너 기반이라면 한 번 더 들어가야 합니다. 이미지 자체에 도구가 빠졌거나, 엔트리포인트가 달라 환경 초기화가 예상과 다를 수 있거든요. 이런 경우는 Jenkins 문제라기보다 실행 런타임 문제에 더 가깝습니다.

    Jenkins 장애 해결 중 에이전트 환경 변수와 PATH 분석 다이어그램

    컨트롤러와 에이전트 사이에서 사용자 계정, PATH, 워크스페이스, 컨테이너 런타임 차이를 추적하는 장면을 설명하는 이미지입니다.

    자주 만나는 젠킨스 에러와 첫 대응 기준

    Jenkins 장애 해결에서 시간을 아끼려면, 증상별 첫 대응을 미리 정해두는 게 좋습니다. 아래 표는 현장에서 자주 쓰는 분류입니다.

    증상 의심 구간 먼저 볼 것 초기 대응
    빌드가 큐에서 안 나감 에이전트/라벨/실행기 /queue/api/json, /computer/api/json label 매칭, offline agent, executor 점유 상태 확인
    SCM checkout 실패 Git 접근/credential/네트워크 콘솔 로그의 git 명령, credential ID, known_hosts 토큰 권한, SSH 키, 저장소 URL, DNS 확인
    script returned exit code 1 빌드 스크립트 자체 실패한 실제 셸 명령 에이전트 셸에서 동일 명령 재실행
    Permission denied 파일 퍼미션/실행 권한 whoami, ls -l, mount 옵션 실행 비트, 소유권, workspace 권한 확인
    No space left on device 디스크/캐시/로그 df -h, du -sh, build history 불필요한 워크스페이스와 오래된 아티팩트 정리
    Agent disconnected 노드 연결/SSH 또는 inbound agent agent 로그, controller 로그 네트워크 단절, Java 실행 환경, 인증 정보 재확인
    HTTP 403/401 토큰/권한/CSRF API 호출 방식, crumb 필요 여부 인증 토큰, 권한 매트릭스, 요청 헤더 검토

    핵심은 숫자를 외우는 게 아니라 증상과 레이어를 연결하는 감각입니다. 예를 들어 Permission denied가 보여도 Jenkins 권한 모델 문제일 수 있고, 리눅스 파일 퍼미션 문제일 수도 있습니다. 문맥을 같이 봐야 헛수고를 줄일 수 있습니다.

    실제 삽질 포인트: 플러그인, 워크스페이스, 그리고 숨은 상태값

    이 섹션은 현장에서 자주 걸리는 포인트를 모은 겁니다. Jenkins는 눈에 보이는 설정 외에도 상태값 때문에 사람을 헷갈리게 할 때가 있더라고요.

    1. 플러그인 업데이트 직후 파이프라인 이상 동작

    플러그인 업데이트 후 특정 단계만 갑자기 깨질 수 있습니다. 특히 Pipeline, Git, Credentials 관련 플러그인이 바뀌면 증상이 미묘합니다. 로그를 보면 클래스 로딩 문제나 메서드 시그니처 불일치처럼 드러나는 경우가 있어서, 업데이트 직전 시점을 기준선으로 잡는 게 중요합니다.

    2. 워크스페이스 오염

    같은 잡이 브랜치나 조건에 따라 다른 파일을 남겨두면 간헐 장애가 납니다. 특히 생성 파일이 다음 빌드에 영향을 주는 프로젝트에서 자주 보이죠. 이런 때는 Jenkins의 cleanWs()나 Workspace Cleanup 플러그인처럼 범위가 명확한 정리 방식을 우선 권장합니다.

    post {
      always {
        cleanWs()
      }
    }

    셸에서 직접 삭제가 꼭 필요하다면 현재 디렉터리와 변수 값을 먼저 출력한 뒤, 삭제 범위를 다시 확인하세요. 이 단계에서 서두르면 더 큰 사고로 이어지기 쉽습니다.

    3. 숨은 환경 변수 차이

    로컬 셸에서는 프록시, 인증서, locale, JAVA_HOME이 잡혀 있는데 Jenkins에서는 빠져 있는 경우가 있습니다. 이건 CI/CD 디버깅에서 정말 흔합니다. 랜덤 장애처럼 보여도 사실은 환경 차이인 경우가 많습니다.

    4. 타임아웃과 외부 의존성

    테스트가 멈춘 것처럼 보여도 실제로는 외부 API 응답 대기일 수 있습니다. 이럴 땐 Jenkins 화면만 보지 말고 애플리케이션 로그, 대상 시스템 로그, 네트워크 경로, DNS, 프록시 유무까지 같이 봐야 합니다. Jenkins는 멈춘 원인이라기보다 멈춘 결과가 보이는 관측 지점일 때가 많거든요.

    검증 단계: 장애가 풀렸는지 어떻게 확인할까

    장애 복구 후에 초록불만 보고 끝내면 다음 주에 같은 문제를 다시 만날 수 있습니다. 그래서 검증은 세 가지로 나눠 보는 편이 좋습니다.

    1. 같은 커밋 재실행: 같은 입력에서 재현이 사라졌는지 봅니다.
    2. 바로 이전 성공 경로 비교: stage 흐름과 로그 패턴이 정상 범위인지 확인합니다.
    3. 외부 연동까지 확인: artifact, image, deploy, webhook, notification이 끝까지 이어지는지 봅니다.

    예를 들어 빌드는 성공했는데 아티팩트 보관이 비어 있으면, 저는 완전한 성공으로 보지 않습니다. 배포 단계가 생략됐는데 파이프라인만 녹색이어도 마찬가지예요. 무엇이 성공인지를 stage 단위로 정의해두면 재발 방지에도 도움이 됩니다.

    Jenkins UI로 확인해도 되지만, API로 확인하는 습관도 좋습니다.

    curl -s http://127.0.0.1:8080/job/my-pipeline/lastBuild/api/json
    curl -s http://127.0.0.1:8080/job/my-pipeline/lastSuccessfulBuild/api/json

    여기서는 result, number, building 같은 기본 상태와 함께, 단계 흐름과 산출물이 기대한 대로 나왔는지 같이 보세요. 숫자보다 성공 상태의 구조적 일관성을 확인하는 게 더 중요합니다.

    Jenkins 장애 해결 결과를 검증하는 빌드 대시보드 이미지

    성공과 실패가 섞인 스테이지 뷰, 빌드 이력, 아티팩트 보관 여부를 비교해 검증하는 결과 화면 이미지입니다.

    운영하면서 효과 있었던 예방책

    장애를 줄이려면 사후 대응만큼 예방도 중요합니다. 실제로 운영하면서 체감이 컸던 건 아래 네 가지였습니다.

    • Jenkinsfile에 진단용 출력 최소 세트를 넣어둡니다. pwd, whoami, printenv | sort 같은 기본 정보가 생각보다 자주 문제를 풀어줍니다.
    • 에이전트 이미지를 표준화합니다. 팀마다 다른 도구 버전과 PATH 구성은 결국 장애 원인이 됩니다.
    • 워크스페이스 정리 정책을 둡니다. 오래된 빌드와 캐시가 간헐 장애를 만듭니다.
    • 변경 이력 분리가 중요합니다. Jenkinsfile 변경, credential 변경, plugin 변경을 한 번에 몰아서 하지 않는 편이 좋습니다.

    이전 글에서 다룬 로그 읽기나 리눅스 디스크 점검 방법과 함께 보면 더 도움이 됩니다. 관련 운영 글도 내부 링크로 묶어두면 검색 유입과 체류 시간 둘 다 챙기기 좋습니다.

    FAQ: Jenkins 장애 해결에서 자주 받는 질문

    Q1. Jenkins UI에 에러가 너무 짧게 보일 때는요?

    컨트롤러 로그와 에이전트 로그를 분리해서 보시면 됩니다. UI는 요약일 뿐이라 실제 원인은 시스템 로그에 남는 경우가 많습니다.

    Q2. 파이프라인이 가끔만 실패하면 어디부터 봐야 하나요?

    간헐 실패는 외부 의존성, 워크스페이스 오염, 동시성, 네트워크 지연을 먼저 의심합니다. 같은 커밋 재실행과 깨끗한 워크스페이스 실행을 비교해보면 방향이 꽤 빨리 잡힙니다.

    Q3. Jenkins 장애 해결에서 가장 먼저 버려야 할 습관은 뭔가요?

    마지막 에러 한 줄만 보고 원인을 단정하는 습관입니다. 항상 첫 실패 지점을 찾는 쪽이 훨씬 정확합니다.

    Jenkins 장애 해결 유형별 대응 요약 인포그래픽

    증상별 원인 구간, 확인 명령어, 권장 대응 순서를 한 장으로 정리한 요약 이미지입니다.

    현장에서 바로 쓰는 Jenkins 장애 해결 방식

    배포가 급한 상황이라면 먼저 서비스 상태 확인 → 콘솔 로그 첫 실패 지점 확인 → 에이전트 셸에서 동일 명령 재실행, 이 세 단계부터 밟아보세요. 대부분 여기서 방향이 나옵니다. 복잡한 추측보다 이 순서가 훨씬 빠릅니다.

    반대로 같은 장애가 반복된다면 접근을 바꿔야 합니다. 에이전트 표준화, 워크스페이스 정리, Jenkinsfile 진단 출력 추가, 변경 이력 분리 쪽으로 가야 재발 방지 효과가 큽니다.

    결국 Jenkins 장애 해결은 화려한 비법보다 관찰 순서가 더 중요합니다. 젠킨스 에러를 보면 당황하기 쉽지만, 컨트롤러, 에이전트, 외부 연동, 실행 명령을 층으로 나눠 보면 생각보다 빨리 풀리더라고요. 파이프라인 문제 해결이 막막할 때는 오늘 적은 체크 순서대로 한 번 따라가 보세요.

  • [k8s] Karpenter 오토스케일링, 성능 벤치마크: 다양한 워크로드 환경 테스트

    [k8s] Karpenter 오토스케일링, 성능 벤치마크: 다양한 워크로드 환경 테스트

    [Kubernetes] Karpenter 성능 벤치마크로 보는 오토스케일링 테스트

    Karpenter 성능 벤치마크 이야기를 해보려고 합니다. Kubernetes(쿠버네티스) 운영을 조금만 오래 해보면, 결국 병목은 애플리케이션이 아니라 노드가 언제, 얼마나 빠르게 붙느냐에서 터지는 경우가 많거든요. 저도 홈랩과 실서비스에 가까운 테스트 환경에서 이것저것 만져보면서 느낀 게, 오토스케일링은 단순히 “늘어난다”가 아니라 얼마나 빨리 반응하고, 얼마나 낭비 없이 붙고, 붙은 뒤 얼마나 안정적으로 정리되느냐까지 같이 봐야 한다는 점이었습니다.

    특히 이번 글은 Karpenter 성능 벤치마크라는 키워드에 맞춰서, 단순 사용기가 아니라 benchmark(벤치마크) 관점으로 정리했습니다. 수치만 던지는 글이 아니라, 어떤 워크로드에서 무엇을 관찰해야 하는지, 그리고 Kubernetes 오토스케일링 성능을 볼 때 어디서 실수하기 쉬운지까지 같이 묶어보겠습니다. 혹시 노드 확장 속도 테스트를 해보려고 하는데 어디서부터 봐야 할지 막막하셨다면, 이 글이 꽤 도움이 될 겁니다.

    Karpenter 성능 벤치마크를 위한 쿠버네티스 오토스케일링 아키텍처 개요 이미지

    Karpenter, 스케줄러, 워크로드, 신규 노드 프로비저닝 흐름을 한눈에 보여주는 아키텍처 개요 이미지입니다.

    Karpenter란 무엇이고, 벤치마크에서 왜 중요할까요?

    쉽게 말해 Karpenter는 Pending Pod(스케줄되지 못하고 대기 중인 파드)를 보고, 거기에 맞는 노드를 빠르게 띄우는 방식의 노드 프로비저너입니다. 예전에는 HPA(Horizontal Pod Autoscaler, 파드 수 자동 조절)와 Cluster Autoscaler(클러스터 오토스케일러, 노드 수 자동 조절)를 묶어서 보는 경우가 많았는데요. Karpenter는 여기서 조금 더 직접적으로 “어떤 노드가 필요한지”를 계산해서 띄우는 쪽에 가깝습니다.

    제가 처음 이걸 봤을 때는 “결국 노드 늘리는 거 아닌가?” 싶었는데, 실제로 써보니까 차이가 꽤 크더라고요. 특히 아래 같은 부분이 벤치마크 포인트였습니다.

    • 프로비저닝 결정 속도: Pending Pod가 생긴 뒤 새 노드가 뜨기까지의 흐름
    • bin packing(빈 패킹, 자원 밀집 배치) 효율: CPU와 메모리 요청량에 맞춰 얼마나 낭비 없이 배치되는지
    • 워크로드 적합성: 짧게 치고 빠지는 배치성 작업과, 지속적으로 부하가 유지되는 서비스형 워크로드에서 반응이 다른지
    • 정리 속도: 부하가 빠졌을 때 불필요한 노드를 얼마나 깔끔하게 줄이는지

    여기서 중요한 포인트! Karpenter 효율성 검증은 “몇 초 빨랐다”보다, 스케줄링 실패 원인이 어디서 사라졌는지를 보는 게 더 중요합니다. 이미지 풀링(Image Pulling), CNI(Container Network Interface) 초기화, 리소스 요청값 과대설정 같은 변수가 너무 많거든요.

    벤치마크 설계: 어떤 워크로드로 테스트해야 하나요?

    벤치마크는 결국 설계가 반입니다. 제가 직접 해보니, 하나의 워크로드만 보면 결론이 자꾸 왜곡되더라고요. 그래서 최소한 아래 3종류는 나눠서 보는 걸 추천합니다.

    워크로드 유형 특징 관찰 포인트
    Burst API 짧은 시간에 요청 급증 노드 확장 속도 테스트, Pending Pod 해소 시점
    Batch Job 대량 Job이 한 번에 생성 bin packing 효율, 스케줄링 밀집도
    Steady Service 지속적인 중간 부하 과잉 프로비저닝 여부, 축소 안정성

    이렇게 나누는 이유가 있습니다. Burst API는 순간 반응 속도를 보기 좋고, Batch Job은 Karpenter가 노드를 얼마나 촘촘하게 맞춰 띄우는지 보기 좋습니다. Steady Service는 오히려 쓸데없이 많이 띄우지 않는지를 확인하기 좋습니다. 사실 운영에서는 이 마지막이 비용에 제일 크게 영향을 주더라고요.

    테스트 환경 준비: 관측 지표부터 맞춰야 합니다

    벤치마크 전에 먼저 해야 할 건 화려한 부하툴이 아니라 관측 기준 통일입니다. Prometheus(프로메테우스, 메트릭 수집), Grafana(그라파나, 시각화), 그리고 kubectl 이벤트 확인 정도는 꼭 준비해두세요. 저는 처음에 부하부터 때렸다가, 나중에 보니 노드는 떴는데 이미지 풀 때문에 파드가 늦게 뜬 거라 삽질 좀 했습니다 ㅎㅎ

    1. Kubernetes 클러스터 준비
    2. Karpenter 설치 및 기본 프로비저닝 정책 정의
    3. 메트릭 수집 도구 연결
    4. 부하 생성 도구 배치
    5. 이벤트 타임라인 기록
    helm repo add karpenter https://charts.karpenter.sh
    helm repo update
    
    kubectl create namespace karpenter
    helm upgrade --install karpenter karpenter/karpenter \
      --namespace karpenter \
      --set settings.clusterName=my-cluster

    설치 방식은 환경마다 조금 다를 수 있지만, 핵심은 설치 자체보다 Provisioner/NodePool 성격의 정책과 워크로드 리소스 요청값을 맞추는 겁니다.

    apiVersion: karpenter.sh/v1beta1
    kind: NodePool
    metadata:
      name: default
    spec:
      template:
        spec:
          requirements:
            - key: kubernetes.io/arch
              operator: In
              values: ["amd64"]
          expireAfter: 720h
      disruption:
        consolidationPolicy: WhenUnderutilized

    위 예시는 개념 예시입니다. 실제 필드나 리소스 종류는 사용 중인 Karpenter 버전에 따라 다를 수 있으니, 운영 반영 전에는 현재 문서를 꼭 맞춰보셔야 합니다. 버전 차이 무시하고 복붙했다가 안 되는 경우가 진짜 많거든요.

    Karpenter 성능 벤치마크에서 NodePool과 리소스 요청값 구성을 설명하는 이미지

    NodePool 정책, 리소스 요청/제한, 스케줄링 조건이 어떻게 연결되는지 보여주는 구성 다이어그램입니다.

    실전 구현: 다양한 워크로드로 Karpenter 성능 벤치마크 진행하기

    1. Burst API 워크로드

    짧은 시간에 Deployment(디플로이먼트) 복제 수를 확 올리거나, HPA와 부하 생성기를 함께 써서 순간 부하를 만드는 방식입니다.

    kubectl scale deployment sample-api --replicas=50
    kubectl get pods -w

    여기서 보는 건 단순히 파드가 Running 되는 순간이 아닙니다.

    • 처음 Pending이 발생한 시점
    • Karpenter가 노드 생성 결정을 내린 시점
    • 노드 Ready 시점
    • 파드가 실제 서비스 가능 상태가 된 시점

    제가 직접 해보니 이 네 구간을 나눠서 봐야 병목이 보였습니다. 노드 생성은 빨랐는데 DaemonSet 초기화 때문에 실제 서비스 반영이 늦는 경우도 있더라고요.

    2. Batch Job 워크로드

    Job을 여러 개 한 번에 던져보면 bin packing 효율을 확인하기 좋습니다.

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: cpu-batch-sample
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: worker
              image: busybox
              command: ["sh", "-c", "sleep 120"]
              resources:
                requests:
                  cpu: "500m"
                  memory: "256Mi"
    for i in $(seq 1 30); do
      kubectl create -f job.yaml
    done

    이 테스트는 생각보다 중요합니다. 왜냐하면 실제 운영에서는 API 서버보다 배치 잡이 더 난폭하게 클러스터를 흔드는 경우가 많거든요. 여기서 Karpenter가 과도하게 큰 노드를 띄우는지, 아니면 필요한 만큼만 맞춰 띄우는지를 체크하면 Karpenter 효율성 검증에 큰 도움이 됩니다.

    3. Steady Service 워크로드

    이건 부하를 오래 유지하면서 축소 동작까지 보는 테스트입니다.

    kubectl top nodes
    kubectl top pods -A
    kubectl get events -A --sort-by=.lastTimestamp

    중요한 건 “늘어나는 속도”만 보지 말고, 줄어들 때 서비스에 영향 없이 정리되는지도 같이 보셔야 합니다. 노드가 줄면서 PodDisruptionBudget(PDB, 파드 중단 예산)이나 스케줄링 제약과 충돌하면 오히려 운영 피로도가 확 올라갑니다.

    ⚠️ 실제로 많이 겪는 문제와 트러블슈팅

    이 섹션은 꼭 넣고 싶었습니다. 벤치마크가 이상하게 나오면 대부분 Karpenter 자체 문제라기보다 주변 설정 때문이더라고요.

    • 리소스 요청값이 비현실적일 때: requests가 과하게 크면 노드가 과잉 생성됩니다.
    • 이미지 풀 지연: 새 노드가 떠도 컨테이너 이미지 다운로드 때문에 체감은 느립니다.
    • DaemonSet 오버헤드 누락: CNI, 로깅 에이전트, 모니터링 에이전트 자원이 계산에서 빠지면 예상보다 적게 들어갑니다.
    • 스케줄링 제약 과다: nodeSelector, affinity, taint/toleration이 많으면 Karpenter가 선택할 수 있는 노드 풀이 좁아집니다.

    저도 처음엔 “왜 노드를 띄웠는데 Pending이 안 없어지지?” 싶었는데, 알고 보니 topology spread constraints와 리소스 요청이 같이 꼬였던 적이 있었습니다. 이럴 때는 아래 순서로 보면 빨리 풀립니다.

    1. Pending Pod describe로 실패 이유 확인
    2. Karpenter 로그에서 provisioning decision 확인
    3. 노드 Ready 이후 CNI/DaemonSet 상태 확인
    4. 이미지 풀 시간과 애플리케이션 startup probe 확인
    kubectl describe pod <pending-pod-name>
    kubectl logs -n karpenter deploy/karpenter
    kubectl get daemonsets -A
    kubectl describe node <new-node-name>

    💡 팁 하나 드리면, 노드 확장 속도 테스트를 할 때는 테스트용 이미지 크기를 최대한 줄이세요. 이미지가 크면 오토스케일링 성능이 아니라 레지스트리/네트워크 성능을 재게 됩니다.

    검증과 결과 해석: 숫자보다 흐름을 보세요

    이제 결과를 봐야죠. 다만 여기서 조심할 게 있습니다. 제가 일부러 구체 벤치마크 수치를 박지 않는 이유는, 인스턴스 타입, 네트워크, 이미지 캐시, CNI 구성에 따라 결과가 너무 달라지기 때문입니다. 대신 재현 가능한 관찰 포맷을 드리는 게 더 낫습니다.

    측정 항목 어떻게 확인하나 좋게 봐야 할 신호
    Pending 해소 시간 pod 상태 변화, 이벤트 타임라인 증가 구간이 짧고 일관적임
    노드 준비 시간 node Ready 시점 워크로드 증가 시 급격한 편차가 적음
    자원 밀집도 node/pod 사용률 비교 유휴 자원이 과도하게 남지 않음
    축소 안정성 scale-in 이후 재스케줄 상태 서비스 영향 없이 정리됨

    실제로 써보니까, Kubernetes 오토스케일링 성능은 “최대 속도”보다 “예상 가능한 속도”가 더 중요했습니다. 갑자기 한 번 엄청 빠른 결과가 나오는 것보다, 비슷한 조건에서 비슷하게 반응하는 쪽이 운영에는 훨씬 유리하거든요.

    Karpenter 성능 벤치마크 결과로 워크로드별 Pending Pod와 노드 준비 시간을 비교한 이미지

    Burst, Batch, Steady 워크로드별로 Pending Pod 해소 흐름과 노드 Ready 전환을 비교한 대시보드 이미지입니다.

    비교 정리: 어떤 환경에서 Karpenter가 특히 잘 맞을까요?

    제 기준으로는 아래 환경에서 특히 체감이 좋았습니다.

    • 트래픽 변동폭이 큰 서비스
    • 짧은 배치 작업이 자주 몰리는 플랫폼
    • 팀이 노드 타입과 비용 최적화까지 같이 보고 싶은 환경

    반대로, 부하가 거의 고정이고 노드 풀도 단순한 환경이라면 체감 이점이 크지 않을 수도 있습니다. 그래서 벤치마크는 꼭 “남들도 좋다더라”가 아니라 내 워크로드 기준으로 봐야 합니다. 여기서 중요한 포인트, 다시 한 번 말씀드리면 Karpenter 성능 벤치마크는 제품 홍보 자료처럼 보면 안 됩니다. 운영 제약, 워크로드 패턴, 팀의 관측 역량이 같이 들어가야 의미가 생깁니다.

    Karpenter 성능 벤치마크와 기존 오토스케일링 방식을 비교한 요약 이미지

    프로비저닝 방식, 반응성, 자원 활용 관점에서 Karpenter와 기존 접근을 비교한 요약 인포그래픽입니다.

    자주 묻는 질문

    Karpenter만 있으면 HPA는 필요 없나요?

    아닙니다. HPA는 파드 수를 늘리고, Karpenter는 그 파드가 올라갈 노드를 준비하는 역할에 가깝습니다. 둘은 대체 관계라기보다 연동 관계로 보는 게 맞습니다.

    벤치마크에서 가장 먼저 봐야 할 로그는 뭔가요?

    Pending Pod 이벤트와 Karpenter 컨트롤러 로그입니다. 여기서 프로비저닝 판단이 있었는지 먼저 확인해야 합니다.

    비용 절감 효과도 바로 검증할 수 있나요?

    가능은 하지만, 하루 이틀 테스트로 단정하긴 어렵습니다. 최소한 여러 워크로드 패턴을 반복해보고, 축소 정책까지 함께 봐야 의미가 있습니다.

    마무리: 빠른 확장보다 중요한 건 예측 가능한 확장입니다

    이번 글에서는 Karpenter 성능 벤치마크를 워크로드 유형별로 어떻게 봐야 하는지 정리해봤습니다. 제가 직접 해보니, 제일 중요한 건 화려한 벤치마크 숫자가 아니라 Pending 발생 → 노드 생성 결정 → 노드 Ready → 서비스 가능 이 흐름을 끊김 없이 읽어내는 것이었습니다. 드디어 됐다! 싶은 순간도 있었지만, 사실 그 전에 로그 보면서 한참 헤맨 적도 많았거든요.

    정리하자면 이렇습니다.

    • ✅ Burst, Batch, Steady 워크로드를 나눠서 봐야 합니다.
    • ✅ 노드 생성 속도만 보지 말고 이미지 풀과 DaemonSet 초기화도 같이 봐야 합니다.
    • ✅ Karpenter 효율성 검증은 절대 수치보다 일관성과 자원 활용 흐름이 중요합니다.

    다음 글에서는 HPA와 함께 붙였을 때의 상호작용, 그리고 실제 운영에서 자주 쓰는 관측 대시보드 구성도 따로 다뤄볼 예정입니다. 이전 글에서 다뤘던 클러스터 리소스 요청값 설계 내용도 같이 보시면 훨씬 이해가 쉬우실 겁니다.

  • [Kubernetes] ConfigMap 운영 중 흔히 겪는 문제와 해결 전략

    [Kubernetes] ConfigMap 운영 중 흔히 겪는 문제와 해결 전략

    [Kubernetes] ConfigMap 운영 중 흔히 겪는 문제와 해결 전략

    Kubernetes ConfigMap 문제 해결은 운영하다 보면 한 번쯤 꼭 붙잡게 되는 주제예요. 배포는 멀쩡히 끝났는데 환경 변수는 안 바뀌어 있고, YAML은 분명 맞는 것 같은데 애플리케이션이 예전 설정으로 뜨는 경우가 정말 많더라고요. 저도 처음엔 이게 뭔가 싶었거든요. 특히 홈랩에서 여러 서비스를 굴리면서 ConfigMap(컨피그맵, 설정 데이터를 담는 리소스)과 Secret(시크릿, 민감 정보 저장 리소스)을 섞어 쓰다 보니 어디서 꼬였는지 찾는 데 정말 시간이 걸렸어요. 이번 글에서는 제가 실제로 자주 봤던 ConfigMap 업데이트 이슈, 환경 변수 미적용 문제, 설정 오류 디버깅 흐름을 기준으로 정리해보겠습니다.

    핵심만 먼저 말씀드리면 이렇습니다. ConfigMap이 바뀌었다고 해서 모든 Pod(파드, 컨테이너 실행 단위)가 자동으로 기대한 방식으로 반영되지는 않습니다. 값을 어떤 방식으로 주입했는지에 따라 반영 방식이 다르고, 애플리케이션이 설정 재로딩(reload, 설정 다시 읽기)을 지원하는지도 함께 봐야 하거든요. 여기서 중요한 포인트! Kubernetes 자체 문제처럼 보여도 실제 원인은 애플리케이션 기동 방식이나 Deployment(디플로이먼트, 배포 리소스) 선언에 있는 경우가 정말 많아요.

    ConfigMap이 Pod에 주입되는 경로와, 업데이트 후 반영 지점을 한눈에 보여주는 개요 다이어그램입니다.

    1. 왜 ConfigMap 문제가 자주 생길까

    쉽게 말해 ConfigMap은 애플리케이션 이미지와 설정을 분리하려고 쓰는 도구예요. 이미지 자체를 다시 빌드하지 않고도 환경별 설정을 바꿀 수 있으니 정말 편하죠. 그런데 편한 만큼 함정도 있어요.

    • 환경 변수(env)로 주입했는지, 파일(volume)로 마운트했는지에 따라 동작이 달라집니다.
    • 애플리케이션이 시작할 때만 설정을 읽는지, 실행 중 재로딩을 지원하는지 다릅니다.
    • YAML 들여쓰기나 키 이름 오타처럼 아주 사소한 실수도 바로 장애로 이어져요.
    • 운영 중에는 여러 팀이 같은 설정을 만지다 보니 변경 이력 추적도 중요해집니다.

    실제로 써보니까 ConfigMap은 단순한 설정 저장소가 아니라, 배포 전략과 디버깅 방식까지 같이 설계해야 하는 운영 요소였어요. 처음에는 그냥 값만 넣으면 끝인 줄 알았는데, 나중에 보니 반영 타이밍 때문에 삽질 좀 했습니다 ㅎㅎ

    2. ConfigMap 개념 설명: 어디에 쓰는가

    ConfigMap은 문자열 기반 설정 데이터를 Kubernetes 클러스터 안에서 관리하기 위한 리소스예요. 보통 다음 두 가지 방식으로 많이 써요.

    주입 방식 설명 운영 시 주의점
    환경 변수(Environment Variables) 컨테이너 시작 시 값을 환경 변수로 주입 대체로 Pod 재시작 없이는 값 변경 체감이 어려워요
    볼륨 마운트(Volume Mount) ConfigMap 내용을 파일 형태로 컨테이너 내부에 제공 파일이 갱신돼도 애플리케이션이 재로딩하지 않으면 반영이 안 될 수 있어요

    여기서 많이 헷갈리는 부분이 있어요. ConfigMap 업데이트와 애플리케이션 설정 반영은 같은 일이 아닙니다. Kubernetes 입장에서는 데이터를 바꿨을 뿐이고, 그걸 애플리케이션이 언제 다시 읽을지는 별개의 문제거든요. 저도 처음엔 kubectl로 ConfigMap만 바꿔놓고 왜 서비스 동작이 그대로인지 한참 봤었어요.

    자주 쓰는 예시 ConfigMap

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: app-config
    data:
      APP_MODE: "production"
      LOG_LEVEL: "info"
      app.properties: |
        server.port=8080
        feature.toggle=true
    

    위 예시처럼 간단한 key-value도 넣을 수 있고, 설정 파일 전체를 문자열로 넣을 수도 있어요. 운영에서는 둘 다 많이 써요.

    3. 실전 구현: ConfigMap 생성과 Pod 연결

    이제 실전으로 가봅시다. Kubernetes ConfigMap 문제 해결의 시작은 항상 현재 어떤 방식으로 연결되어 있는지 확인하는 것이에요. 환경 변수인지, 파일 마운트인지, 둘 다인지 먼저 봐야 한다는 뜻입니다.

    3-1. ConfigMap 생성

    kubectl create configmap app-config \
      --from-literal=APP_MODE=production \
      --from-literal=LOG_LEVEL=info

    또는 선언형으로 관리하려면 YAML 파일을 두고 적용하는 방식이 더 좋아요. GitOps(깃옵스, Git 기반 운영) 흐름에도 잘 맞고요.

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: app-config
    data:
      APP_MODE: "production"
      LOG_LEVEL: "info"
    
    kubectl apply -f configmap.yaml

    3-2. 환경 변수로 주입

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: demo-app
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: demo-app
      template:
        metadata:
          labels:
            app: demo-app
        spec:
          containers:
            - name: app
              image: nginx
              env:
                - name: APP_MODE
                  valueFrom:
                    configMapKeyRef:
                      name: app-config
                      key: APP_MODE
                - name: LOG_LEVEL
                  valueFrom:
                    configMapKeyRef:
                      name: app-config
                      key: LOG_LEVEL
    

    이 방식은 선언이 명확해서 좋아요. 다만 환경 변수 미적용 이슈가 가장 자주 보이는 방식이기도 합니다. 왜냐하면 컨테이너는 보통 시작할 때 환경 변수를 읽고 끝이거든요.

    3-3. 파일로 마운트

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: demo-app-file
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: demo-app-file
      template:
        metadata:
          labels:
            app: demo-app-file
        spec:
          containers:
            - name: app
              image: nginx
              volumeMounts:
                - name: config-volume
                  mountPath: /etc/app-config
          volumes:
            - name: config-volume
              configMap:
                name: app-config
    

    파일 마운트 방식은 애플리케이션이 파일 변경을 감지하거나 재시작 시 다시 읽는 구조일 때 특히 유용해요. 근데 여기서도 안심하면 안 돼요. 애플리케이션이 그 파일을 다시 안 읽으면 소용이 없거든요.

    Kubernetes ConfigMap 업데이트 방식 비교 이미지

    Deployment에서 ConfigMap을 환경 변수와 볼륨으로 주입하는 구성을 비교하는 다이어그램입니다.

    4. ConfigMap 업데이트 시 가장 많이 터지는 문제

    이 섹션은 제가 운영하면서 가장 많이 봤던 증상들 위주로 적어볼게요. 혹시 이런 경험 있으신가요? 분명 ConfigMap은 바뀌었는데 서비스가 그대로인 상황 말이에요. 대부분 아래 범주 안에 들어가요.

    4-1. ConfigMap 업데이트 후 환경 변수 미적용

    가장 흔한 증상이에요. 원인은 단순한 편입니다. 환경 변수는 보통 컨테이너 시작 시점에 주입되기 때문이에요. 그래서 ConfigMap만 바꿔서는 이미 떠 있는 Pod 내부 환경 변수가 즉시 바뀌지 않아요.

    제가 직접 해보니 여기서 중요한 건 Kubernetes가 아니라 주입 방식이었어요. 해결은 보통 다음 중 하나입니다.

    1. Deployment를 다시 롤아웃해서 새 Pod를 띄웁니다.
    2. 설정 변경 시점에 맞춰 배포 파이프라인에서 재시작 절차를 함께 실행해요.
    3. 가능하면 파일 기반 설정 + 애플리케이션 재로딩 구조를 검토해야 해요.
    kubectl rollout restart deployment demo-app
    kubectl rollout status deployment demo-app

    4-2. 볼륨 파일은 바뀌었는데 애플리케이션 동작은 그대로

    이 경우는 더 헷갈려요. 컨테이너 안 파일을 열어보면 값이 바뀐 것 같은데, 서비스는 예전 설정대로 동작하거든요. 원인은 대개 애플리케이션이 시작할 때만 파일을 읽고 메모리에 들고 있기 때문이에요.

    이럴 때는 아래를 점검해야 해요.

    • 애플리케이션이 SIGHUP 같은 재로딩 신호를 받는지
    • 파일 변경 감지(watch)를 지원하는지
    • 설정 파일 경로가 실제 읽는 경로와 같은지
    • 서브패스(subPath) 사용 여부

    특히 subPath로 마운트한 파일은 기대한 방식의 갱신이 안 보이는 사례가 있어서 운영 시 더 조심하게 되더라고요. 저도 예전에 특정 설정 파일 하나만 예쁘게 꽂으려고 subPath를 썼다가, 왜 변경 반영이 안 되지 하고 한참 봤었어요.

    4-3. 키 이름 오타, 네임스페이스 착오

    은근히 자주 나와요. ConfigMap 이름은 맞는데 key가 다르거나, 적용한 namespace(네임스페이스, 리소스 격리 단위)가 달라서 Pod가 다른 리소스를 보고 있는 경우죠.

    kubectl get configmap app-config -n default -o yaml
    kubectl get deployment demo-app -n default -o yaml
    kubectl describe pod <pod-name> -n default

    이 세 가지를 같이 보면 생각보다 금방 풀려요. 저는 디버깅할 때 항상 ConfigMap 원본, Deployment 참조, 실제 Pod 상태를 한 세트로 봐요.

    5. 설정 오류 디버깅: 제가 실제로 보는 순서

    문제가 터졌을 때는 감으로 보면 더 꼬여요. 순서를 정해두는 게 좋습니다. 저는 아래 흐름으로 봐요.

    1. ConfigMap 값 확인
      kubectl get configmap으로 현재 값이 정말 바뀌었는지 확인합니다.
    2. Deployment 선언 확인
      env, envFrom, volumeMounts, volumes가 예상대로 연결됐는지 봐요.
    3. Pod 재생성 여부 확인
      환경 변수 방식이라면 새 Pod가 떴는지 확인합니다.
    4. 컨테이너 내부 확인
      printenv, cat 등으로 실제 반영 상태를 확인해요.
    5. 애플리케이션 로그 확인
      설정 파싱 오류나 fallback 값 사용 흔적이 없는지 봅니다.
    kubectl get configmap app-config -o yaml
    kubectl get deployment demo-app -o yaml
    kubectl get pods -l app=demo-app
    kubectl exec -it <pod-name> -- printenv | grep APP_MODE
    kubectl exec -it <pod-name> -- sh -c 'cat /etc/app-config/app.properties'
    kubectl logs <pod-name>

    여기서 중요한 포인트! 컨테이너 내부 확인 없이 추측으로 넘어가면 시간만 더 써요. 실제로 써보니까 운영 이슈는 눈으로 확인하는 게 제일 빠르더라고요.

    Kubernetes ConfigMap 문제 해결을 위한 설정 오류 디버깅 이미지

    kubectl 명령으로 ConfigMap 값, Pod 환경 변수, 마운트 파일을 순서대로 점검하는 디버깅 흐름 이미지입니다.

    6. 운영에서 쓰는 해결 전략 정리

    단순히 문제를 푸는 것보다, 다시 안 터지게 만드는 게 더 중요해요. 제가 지금은 아래 전략을 기본으로 가져가고 있습니다.

    상황 권장 전략 이유
    환경 변수 기반 설정 변경 후 롤아웃 재시작 절차 포함 Pod 재기동이 반영 경로가 되기 쉬워요
    파일 기반 설정 애플리케이션 재로딩 지원 여부 확인 파일 변경과 동작 반영은 별개예요
    설정 변경 잦음 선언형 관리와 변경 이력 추적 누가 무엇을 바꿨는지 파악하기 쉬워요
    장애 대응 검증 명령어를 런북(runbook, 운영 절차서)화 야간 대응 속도가 크게 향상돼요
    • ConfigMap 이름에 버전성 힌트를 두는 방식도 운영에 따라 정말 유용해요.
    • Deployment annotation 변경으로 롤링 업데이트를 유도하는 패턴도 자주 써요.
    • 애플리케이션 기본값(fallback) 존재 여부를 꼭 확인해야 해요. 설정이 안 먹었는데도 앱이 떠버리면 더 위험하거든요.
    • Secret과 ConfigMap 역할 분리도 중요해요. 민감 정보는 ConfigMap에 넣지 않는 게 기본입니다.

    그리고 k8s 설정 관리는 결국 사람 문제이기도 해요. 파일명, 키 네이밍, 네임스페이스 규칙을 팀 단위로 맞추지 않으면 나중에 디버깅 비용이 훨씬 커져요.

    7. 검증과 결과 확인: 바뀐 설정이 정말 적용됐는지

    배포가 끝났다고 끝난 게 아니에요. 저는 항상 아래 세 가지를 같이 봐요.

    1. 리소스 기준 검증: ConfigMap과 Deployment 선언 확인
    2. 컨테이너 기준 검증: 환경 변수 또는 파일 내용 확인
    3. 애플리케이션 기준 검증: 로그, 헬스체크, 실제 동작 확인
    kubectl rollout status deployment demo-app
    kubectl exec -it <pod-name> -- printenv | grep LOG_LEVEL
    kubectl exec -it <pod-name> -- sh -c 'ls -l /etc/app-config && cat /etc/app-config/app.properties'
    kubectl logs <pod-name>

    여기서 드디어 됐다! 하는 순간이 와요. 그런데 한 단계 더 가야 해요. 애플리케이션이 실제로 새 설정으로 동작하는지 확인해야 하거든요. 예를 들어 로그 레벨이 바뀌었는지, 특정 기능 토글이 반영됐는지, 외부 연결 정보가 새 값으로 적용됐는지까지 봐야 진짜 검증이에요.

    처음엔 kubectl 출력만 보고 끝냈었는데, 나중에 보니 앱은 예전 설정으로 캐시해서 쓰고 있더라고요. 그래서 지금은 결과 검증을 더 꼼꼼히 하고 있어요.

    Kubernetes ConfigMap 적용 결과 검증 대시보드 이미지

    롤아웃 완료, 환경 변수 확인, 로그 검증까지 끝난 상태를 보여주는 결과 확인 이미지입니다.

    8. 자주 묻는 질문 정리

    Q1. ConfigMap 업데이트만 하면 바로 반영되나요?

    항상 그렇지는 않아요. 주입 방식과 애플리케이션 동작 방식에 따라 다르거든요. 환경 변수 기반이면 보통 재시작 관점으로 보는 게 안전해요.

    Q2. 환경 변수 미적용 문제는 Kubernetes 버그인가요?

    대부분은 버그라기보다 동작 방식 이해 차이에서 나와요. 저도 처음엔 그렇게 오해했는데, 실제로는 컨테이너 재기동 시점과 설정 읽는 타이밍 문제인 경우가 정말 많았어요.

    Q3. 설정 오류 디버깅은 어디서부터 봐야 하나요?

    ConfigMap 원본, Deployment 참조, Pod 내부 상태를 순서대로 보세요. 이 흐름만 지켜도 절반은 빨리 해결돼요.

    Q4. k8s 설정 관리를 더 안정적으로 하려면?

    선언형 관리, 변경 이력 추적, 운영 런북 정리 이 세 가지가 효과적이에요. 이전 글에서 다뤘던 배포 점검 체크리스트와 함께 보면 더 도움이 될 거예요. 다음 글에서는 Secret 운영 패턴과 ConfigMap 분리 기준도 다뤄볼 계획입니다.

    9. 마무리: ConfigMap은 단순한 설정 파일이 아니었습니다

    Kubernetes ConfigMap 문제 해결은 결국 설정 저장보다 설정 반영 경로를 이해하는 일에 가까워요. 제가 직접 해보니 ConfigMap 업데이트 자체보다, 그 값이 언제 어떻게 애플리케이션에 전달되는지 파악하는 게 핵심이었어요. 특히 환경 변수 미적용, 설정 오류 디버깅, k8s 설정 관리 이 세 가지는 따로 떨어진 주제가 아니라 한 흐름으로 묶여 있더라고요.

    혹시 지금 운영 중인 서비스에서 ConfigMap 업데이트 후 반영이 이상하다면, 오늘 글의 순서대로만 점검해보세요. 꽤 빨리 원인을 좁힐 수 있을 거예요. 완벽한 정답 하나가 있다기보다, 주입 방식에 맞는 운영 전략을 정해두는 것이 제일 중요했어요. 저도 처음엔 많이 헷갈렸는데, 한 번 패턴이 잡히고 나니까 훨씬 덜 흔들리더라고요.

    Kubernetes ConfigMap 문제 해결 전략 요약 인포그래픽

    환경 변수 방식과 파일 마운트 방식의 차이, 그리고 운영 체크포인트를 요약한 인포그래픽입니다.

    정리하면 이렇습니다.

    • ConfigMap 업데이트와 애플리케이션 반영은 같은 일이 아니에요.
    • 환경 변수 방식은 재시작 관점을 기본으로 가져가는 게 안전해요.
    • 파일 마운트 방식은 애플리케이션 재로딩 지원 여부를 꼭 확인해야 해요.
    • 설정 오류 디버깅은 ConfigMap, Deployment, Pod 내부 확인 순서로 보면 빨라져요.

    운영은 결국 반복 가능한 습관 싸움이더라고요. 다음 글에서는 Secret과 ConfigMap 분리 기준, 그리고 배포 자동화 파이프라인에서 설정 반영을 안전하게 묶는 방법도 이어서 정리해보겠습니다.

  • [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 중심 관점에서 설명했습니다.

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

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