Published on

GitHub Actions Kubernetes 배포 stuck in Progress 디버깅

Authors

GitHub Actions로 Kubernetes에 배포할 때 가장 사람을 지치게 만드는 증상 중 하나가 워크플로가 in progress 상태로 끝없이 멈춰 있는 상황입니다. 로컬에서 같은 kubectl apply 는 금방 끝나는데 CI에서는 멈추거나, kubectl rollout status 가 영원히 반환되지 않거나, 반대로 아무 로그도 없이 잡이 계속 실행되는 식입니다.

이 글은 “왜 멈췄는지”를 감으로 찍는 대신, 멈춤 지점을 관측 가능하게 만들고, 원인을 분류한 뒤, 타임아웃과 진단 로그를 워크플로에 내장해서 재발을 막는 방법을 정리합니다.

관련해서 배포 스크립트가 조용히 실패하거나 중간 상태로 빠지는 문제는 쉘 옵션 설정에서 출발하는 경우가 많습니다. 배포 스크립트 안전장치가 필요하다면 bash set -euo pipefail로 스크립트 폭발 막기도 함께 참고하세요.

1) “Progress에 멈춤”을 3가지로 분류하기

GitHub Actions UI에서 in progress 로 보일 때 실제로는 아래 중 하나입니다.

  1. 러너 단계에서 멈춤: 설치, 인증, 네트워크, kubectl 실행 자체가 블로킹
  2. Kubernetes 롤아웃 대기에서 멈춤: kubectl rollout status 또는 kubectl wait 가 조건을 만족 못해 무기한 대기
  3. 배포는 끝났는데 잡이 안 끝남: 백그라운드 프로세스, tail, watch, 포트포워딩 등이 종료되지 않음

가장 먼저 해야 할 일은 “어디에서 멈추는지”를 로그로 확정하는 것입니다. 다음 섹션의 워크플로 패턴을 먼저 적용하면, 이후 디버깅이 훨씬 빨라집니다.

2) GitHub Actions에서 멈춤 지점을 확정하는 워크플로 패턴

핵심은 3가지입니다.

  • 각 단계에 명시적 타임아웃을 둔다
  • 실패 시에도 진단을 남기도록 if: always() 로 덤프 스텝을 둔다
  • kubectl 명령은 --request-timeout--timeout 을 분리해 건다

예시: 안전한 Kubernetes 배포 Job 템플릿

아래 예시는 EKS, GKE, AKS 어디든 적용 가능한 형태입니다(인증 부분만 교체).

name: deploy
on:
  push:
    branches: ["main"]

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - uses: actions/checkout@v4

      - name: Install kubectl
        uses: azure/setup-kubectl@v4
        with:
          version: "v1.29.0"

      - name: Set up kubeconfig
        run: |
          set -euo pipefail
          mkdir -p "$HOME/.kube"
          echo "${KUBECONFIG_B64}" | base64 -d > "$HOME/.kube/config"
        env:
          KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}

      - name: Preflight (cluster reachability)
        run: |
          set -euo pipefail
          kubectl version --short --request-timeout=10s
          kubectl get nodes --request-timeout=10s
          kubectl get ns --request-timeout=10s

      - name: Apply manifests
        run: |
          set -euo pipefail
          kubectl apply -f k8s/ --request-timeout=30s

      - name: Wait rollout
        run: |
          set -euo pipefail
          kubectl rollout status deployment/myapp -n myns --timeout=180s
          kubectl get pods -n myns -o wide

      - name: Debug dump (always)
        if: always()
        run: |
          set +e
          echo "==== kubectl get all ===="
          kubectl get all -n myns -o wide --request-timeout=20s
          echo "==== describe deployment ===="
          kubectl describe deployment/myapp -n myns --request-timeout=20s
          echo "==== recent events ===="
          kubectl get events -n myns --sort-by=.lastTimestamp --request-timeout=20s | tail -n 200
          echo "==== pods describe ===="
          kubectl get pods -n myns -o name --request-timeout=20s | xargs -n 1 kubectl describe -n myns
          echo "==== pods logs (best effort) ===="
          kubectl get pods -n myns -o jsonpath='{.items[*].metadata.name}' --request-timeout=20s | tr ' ' '\n' | while read -r p; do
            echo "---- logs: $p ----"
            kubectl logs -n myns "$p" --all-containers=true --tail=200 --request-timeout=20s || true
          done

이 템플릿만 넣어도 “Progress에서 멈춤”이 아니라, 어느 단계에서 무엇을 기다리다 실패했는지가 남습니다.

3) kubectl rollout status 가 영원히 끝나지 않는 대표 원인

3.1 readinessProbe 실패로 Ready가 안 됨

배포는 성공했지만 Pod가 Ready가 되지 않으면 rollout status 는 계속 기다립니다.

  • 애플리케이션이 실제로는 기동 실패
  • readinessProbe 경로가 틀림
  • 의존 서비스(DB, 캐시) 연결 실패로 준비 상태 진입 불가
  • ServiceAccount 권한 문제로 내부 API 호출 실패

진단 순서:

  • kubectl describe pod 에서 Readiness probe failed 확인
  • 컨테이너 로그에서 기동 실패 원인 확인
  • kubectl get endpoints 로 엔드포인트가 비어 있는지 확인
kubectl get pods -n myns -o wide
kubectl describe pod -n myns myapp-xxxxx
kubectl logs -n myns myapp-xxxxx --all-containers=true --tail=200
kubectl get endpoints -n myns myapp

애플리케이션이 DB 커넥션 고갈이나 풀 설정 문제로 기동이 지연되는 케이스도 흔합니다. Java 기반이라면 Spring Boot 3에서 HikariCP 커넥션 고갈 원인 9가지에서 “기동은 되는데 준비 상태가 늦는” 패턴을 함께 점검해보면 좋습니다.

3.2 이미지 Pull 실패 또는 레지스트리 인증 문제

ImagePullBackOff 는 CI에서 자주 터집니다.

  • 새 태그가 아직 레지스트리에 push 되기 전인데 배포가 먼저 시작됨
  • imagePullSecrets 누락
  • 프라이빗 레지스트리 토큰 만료
  • 노드에서 egress 제한
kubectl get pods -n myns
kubectl describe pod -n myns myapp-xxxxx | sed -n '1,200p'

EventsFailed to pull image 가 찍히면 거의 확정입니다. 해결은 대개 “push 완료 후 배포” 보장(잡 의존성), imagePullSecrets 점검, 네트워크 정책 점검입니다.

3.3 스케줄링 불가로 Pending 지속

CI는 배포를 했지만 클러스터 리소스가 부족하면 Pod는 Pending에 머뭅니다.

  • CPU, 메모리 요청량 과다
  • 노드 셀렉터, 어피니티 조건 불만족
  • 테인트, 톨러레이션 불일치
  • PV 바인딩 실패
kubectl get pod -n myns myapp-xxxxx -o jsonpath='{.status.phase}'
kubectl describe pod -n myns myapp-xxxxx

describe0/.. nodes are available 메시지가 나오면 스케줄링 조건을 재검토해야 합니다.

3.4 롤링 업데이트가 교착 상태에 빠짐 (maxUnavailable, PDB)

의외로 많이 보는 케이스입니다.

  • maxUnavailable: 0 인데 새 Pod가 Ready가 안 되면 기존 Pod를 못 내림
  • PodDisruptionBudget이 너무 빡세서 교체가 진행되지 않음
kubectl get deploy -n myns myapp -o yaml | sed -n '1,220p'
kubectl get pdb -n myns
kubectl describe pdb -n myns your-pdb-name

이 경우는 readiness 문제를 먼저 해결하거나, 배포 전략을 조정해야 합니다.

4) GitHub Actions 단계 자체가 멈추는 원인 (클러스터 밖 문제)

4.1 kubeconfig 또는 인증 플로우가 대기 상태로 빠짐

  • 클라우드 CLI가 인터랙티브 인증을 요구
  • aws eks update-kubeconfig 가 네트워크 문제로 응답 없음
  • OIDC 토큰 교환이 실패했는데 재시도로 오래 끌림

대응:

  • 인증 커맨드에 타임아웃을 걸고, 실패 시 즉시 덤프
  • kubectl versionkubectl cluster-info 를 preflight로 분리
kubectl cluster-info --request-timeout=10s
kubectl get --raw='/readyz?verbose' --request-timeout=10s

EKS 환경에서 컨트롤 플레인 접근, 로드밸런서 컨트롤러, IAM 권한이 얽히면 증상이 더 복잡해질 수 있습니다. 특히 Ingress, ALB 연동 이슈가 동반된다면 EKS AWS Load Balancer Controller 설치 후 403 해결 같은 권한·인증 관점도 함께 확인하세요.

4.2 네트워크 hang: API 서버 접근은 되는데 응답이 느림

러너에서 Kubernetes API 서버까지의 네트워크가 불안정하면 kubectl 이 오래 대기합니다. --request-timeout 을 반드시 사용해 “무한 대기”를 끊어야 합니다.

kubectl get pods -n myns --request-timeout=10s

추가로, 러너가 사설망에 있어야 하는데 퍼블릭 러너를 쓰는 구조라면(예: 프라이빗 엔드포인트), self-hosted runner 또는 터널링이 필요합니다.

5) 배포는 끝났는데 Job이 안 끝나는 경우

이 패턴은 kubectl 이 아니라 워크플로의 다른 커맨드가 “종료되지 않는” 문제입니다.

  • kubectl logs -f 로 follow를 걸어둠
  • kubectl port-forward 를 백그라운드로 띄워놓고 종료 처리를 안 함
  • helm upgrade--wait 가 무한대기에 가까운 시간으로 설정
  • tail -f 로 파일을 계속 읽음

해결 방법:

  • follow 옵션을 쓰지 말고 --tail 로 제한
  • 백그라운드 프로세스는 PID를 저장하고 종료 트랩을 둠
set -euo pipefail
kubectl port-forward -n myns svc/myapp 8080:80 &
PF_PID=$!
trap 'kill "$PF_PID" 2>/dev/null || true' EXIT

# 테스트 실행
curl -fsS http://127.0.0.1:8080/health

6) kubectl waitrollout status 를 “실패 친화적”으로 바꾸기

CI에서 중요한 건 성공 시나리오가 아니라 실패 시 빠르게 실패하고 증거를 남기는 것입니다.

권장 규칙:

  • rollout status 는 항상 --timeout 지정
  • kubectl 호출은 항상 --request-timeout 지정
  • 실패 시 describe, events, logs 를 자동 수집

예시:

set -euo pipefail

kubectl apply -f k8s/ --request-timeout=30s

if ! kubectl rollout status deploy/myapp -n myns --timeout=180s; then
  echo "Rollout failed. Dumping diagnostics..."
  kubectl get pods -n myns -o wide --request-timeout=20s || true
  kubectl get events -n myns --sort-by=.lastTimestamp --request-timeout=20s | tail -n 200 || true
  kubectl describe deploy/myapp -n myns --request-timeout=20s || true
  exit 1
fi

7) 자주 놓치는 디테일 체크리스트

7.1 네임스페이스 불일치

apply 는 기본 네임스페이스에 들어가고, rollout status 는 특정 네임스페이스를 보게 되면 “기다리는 대상이 없는 상태”가 됩니다. 명령마다 -n myns 를 일관되게 붙이거나, manifest에 metadata.namespace 를 명시하세요.

7.2 컨텍스트가 다른 클러스터를 보고 있음

kubeconfig에 컨텍스트가 여러 개면, CI가 의도치 않은 컨텍스트를 잡을 수 있습니다.

kubectl config get-contexts
kubectl config current-context
kubectl cluster-info

7.3 helm --wait--timeout 조합

Helm을 쓰면 helm upgrade --install --wait 가 내부적으로 리소스 Ready를 기다립니다. 이때 --timeout 을 지정하지 않으면 매우 오래 기다릴 수 있습니다.

helm upgrade --install myapp ./chart -n myns --create-namespace --wait --timeout 3m

7.4 GitHub Actions Job 타임아웃 미설정

워크플로 전체가 영원히 돌지 않게 jobs.<job>.timeout-minutes 는 기본으로 넣으세요. 특정 스텝만 제어하려면 커맨드 자체에 타임아웃을 넣는 방식이 더 확실합니다.

8) 결론: “멈춤”을 없애는 가장 현실적인 방법

GitHub Actions에서 Kubernetes 배포가 stuck in progress 로 보일 때, 대부분은 Kubernetes가 멈춘 게 아니라 CI가 무언가를 기다리도록 만들어진 상태입니다. 따라서 해결의 핵심은 다음 3가지입니다.

  • 대기 명령에 --timeout--request-timeout 을 반드시 설정
  • 실패하더라도 describe, events, logs 를 자동 수집
  • preflight로 “클러스터 접근 가능 여부”를 초반에 짧게 검증

이 3가지를 워크플로 템플릿으로 고정해두면, 다음부터는 “Progress에서 멈췄다”가 아니라 “어떤 조건이 충족되지 않았다”로 문제를 다룰 수 있게 됩니다.