- Published on
GitHub Actions Kubernetes 배포 stuck in Progress 디버깅
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
GitHub Actions로 Kubernetes에 배포할 때 가장 사람을 지치게 만드는 증상 중 하나가 워크플로가 in progress 상태로 끝없이 멈춰 있는 상황입니다. 로컬에서 같은 kubectl apply 는 금방 끝나는데 CI에서는 멈추거나, kubectl rollout status 가 영원히 반환되지 않거나, 반대로 아무 로그도 없이 잡이 계속 실행되는 식입니다.
이 글은 “왜 멈췄는지”를 감으로 찍는 대신, 멈춤 지점을 관측 가능하게 만들고, 원인을 분류한 뒤, 타임아웃과 진단 로그를 워크플로에 내장해서 재발을 막는 방법을 정리합니다.
관련해서 배포 스크립트가 조용히 실패하거나 중간 상태로 빠지는 문제는 쉘 옵션 설정에서 출발하는 경우가 많습니다. 배포 스크립트 안전장치가 필요하다면 bash set -euo pipefail로 스크립트 폭발 막기도 함께 참고하세요.
1) “Progress에 멈춤”을 3가지로 분류하기
GitHub Actions UI에서 in progress 로 보일 때 실제로는 아래 중 하나입니다.
- 러너 단계에서 멈춤: 설치, 인증, 네트워크,
kubectl실행 자체가 블로킹 - Kubernetes 롤아웃 대기에서 멈춤:
kubectl rollout status또는kubectl wait가 조건을 만족 못해 무기한 대기 - 배포는 끝났는데 잡이 안 끝남: 백그라운드 프로세스, 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'
Events 에 Failed 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
describe 에 0/.. 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 version과kubectl 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 wait 와 rollout 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에서 멈췄다”가 아니라 “어떤 조건이 충족되지 않았다”로 문제를 다룰 수 있게 됩니다.