티스토리 뷰

최근 다양한 배포 환경을 테스트하던 중, Argo Rollouts이 PodDisruptionBudgets (PDB)와 상호작용하는 과정에서 Kubernetes의 일반적인 Deployment와는 다르게 동작하는 흥미로운 이슈를 발견했습니다.

특히 Pod가 재시작(Restart)되는 과정에서 겪은 문제상황과 그 원인을 분석한 내용을 정리해 보았습니다.

테스트 환경

비교를 위해 두 가지 환경을 구성했습니다. 두 환경 모두 replicas: 1, minAvailable: 1 이라는 타이트한 제약 조건을 가지고 있습니다.

1. Standard Deployment & PDB

일반적인 Kubernetes Deployment 환경입니다.

deploy.yaml, pdb.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: nginx

 

2. Argo Rollout & PDB

Argo Rollout을 사용하며 Canary 전략(Weight 100)을 사용합니다.

rollout.yaml, pdb.yaml

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollout-nginx-testb
spec:
  strategy:
    canary:
      steps:
        - setWeight: 100
  replicas: 1
  selector:
    matchLabels:
      app: nginx-testb
  template:
    metadata:
      labels:
        app: nginx-testb
    spec:
      containers:
      - name: nginx-testb
        image: nginx:latest
        ports:
        - containerPort: 80
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-pdb-rollout
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: nginx-testb

 

 

발생한 이슈: 재시작 실패 (Restart Failure)

먼저 Rollout 리소스에 대해 일반적인 kubectl rollout restart 명령어를 시도했을 때, 다음과 같은 에러가 발생했습니다.

error: no kind "Rollout" is registered for version "argoproj.io/v1alpha1" in scheme "pkg/scheme/scheme.go:28"

이는 kubectl rollout restart 명령어가 기본적으로 Argo Rollout CRD를 지원하지 않기 때문입니다. Argo Rollouts 공식 문서에 따르면, Pod를 재시작하기 위해서는 restartAt 필드를 수정해야 합니다.

중요한 점은 Argo Rollout 컨트롤러는 Pod를 단순히 삭제(Delete)하는 것이 아니라 축출(Evict) 한다는 점입니다.

Argo Rollout의 재시작 로직: 컨트롤러는 각 ReplicaSet을 순회하며 Pod의 생성 시간이 restartAt 시간보다 오래되었는지 확인합니다. restartAt 보다 오래된 Pod는 Evict(축출) 되며, 이를 통해 ReplicaSet이 새로운 Pod로 교체하도록 유도합니다.

 

문서에 나온 대로 restartAt 필드를 수정하여 재시작을 시도했습니다. 하지만 문제는 여기서 발생했습니다.

원인 분석 (Investigating the Cause)

Argo Rollouts 가이드를 자세히 살펴보니 다음과 같은 주의 사항이 있었습니다.

PDB 조건 우선순위: 너무 많은 Pod가 한 번에 재시작되는 것을 방지하기 위해 컨트롤러는 한 번에 maxUnavailable 수만큼의 Pod만 삭제하도록 제한합니다. 또한, Pod는 삭제(deleted)되는 것이 아니라 축출(evicted)되므로, 재시작 프로세스는 설정된 PodDisruptionBudgets(PDB)을 준수합니다.

이 동작 방식이 일반적인 Deployment의 재시작과 결정적인 차이점입니다.

멈춰있는 상태에서 PDB 상태를 확인해보니 에러가 명확히 보였습니다.

status:
  conditions:
  - lastTransitionTime: "2025-04-23T05:53:29Z"
    message: ""
    observedGeneration: 1
    reason: InsufficientPods
    status: "False"
    type: DisruptionAllowed
  currentHealthy: 1
  desiredHealthy: 1
  disruptionsAllowed: 0
  expectedPods: 1
  observedGeneration: 1

minAvailable: 1이고 현재 replicas: 1이므로, 허용된 중단(disruptionsAllowed)은 0입니다. Rollout 컨트롤러는 유일한 Pod를 Evict하려고 시도하지만, PDB가 이를 차단하고 있는 상황입니다.

Argo Rollout 소스 코드에서도 restartAt 필드를 패치하여 재시작을 처리하는 것을 확인할 수 있습니다: GitHub Reference

const (
    restartPatch = `{
    "spec": {
        "restartAt": "%s"
    }
}`
)

// RestartRollout restarts a rollout
func RestartRollout(rolloutIf clientset.RolloutInterface, name string, restartAt *time.Time) (*v1alpha1.Rollout, error) {
    ctx := context.TODO()
    if restartAt == nil {
        t := timeutil.Now().UTC()
        restartAt = &t
    }
    patch := fmt.Sprintf(restartPatch, restartAt.Format(time.RFC3339))
    return rolloutIf.Patch(ctx, name, types.MergePatchType, []byte(patch), metav1.PatchOptions{})
}

왜 일반 Deployment의 kubectl rollout restart는 작동할까?

Kubernetes의 PDB는 기본적으로 Voluntary Evictions을 방어하기 위해 설계되었습니다. (예: 노드 드레인, 노드 삭제 등) 하지만 kubectl rollout restart는 사용자의 명시적인 명령입니다. 이는 Voluntary Evictions로 간주되지 않습니다. 따라서 Deployment 컨트롤러는 PDB 제약 조건(Eviction API 제한)을 우회하여(보통 새 Pod를 먼저 띄우고 옛 Pod를 죽이는 방식 등으로) 재시작을 진행합니다.


ArgoCD는 왜 "Healthy"라고 표시했을까?

재시작이 멈춰있는 상황임에도 불구하고, ArgoCD UI에서는 상태가 Healthy로 표시되었습니다.

[이미지: ArgoCD에서 Rollout 상태가 Healthy로 표시된 화면]

이유는 크게 두 가지입니다.

1. Argo Rollout Health Check (Lua Script)

ArgoCD는 커스텀 리소스의 상태를 판단하기 위해 Lua 스크립트를 사용합니다.

health_status = {}
if obj.status ~= nil then
    if obj.status.phase == "Progressing" then
        health_status.status = "Progressing"
        health_status.message = "Rollout is progressing"
        return health_status
    end

    if obj.status.phase == "Degraded" then
        health_status.status = "Degraded"
        health_status.message = "Rollout is degraded"
        return health_status
    end

    if obj.status.phase == "Paused" then
        health_status.status = "Suspended"
        health_status.message = "Rollout is paused"
        return health_status
    end

    health_status.status = "Healthy"
    health_status.message = "Rollout is healthy"
    return health_status
end

이 로직은 "Progressing", "Degraded", "Paused" 상태가 아니면 기본적으로 Healthy를 반환합니다. restartAt 타임스탬프가 적용되었으나 실제로 Pod가 교체되지 않고 대기 중인 상태를 감지하는 로직이 없기 때문에 Healthy로 판단한 것입니다.

2. PDB Health Check

PDB는 Kubernetes 네이티브 리소스이며, ArgoCD는 표준 로직으로 이를 체크합니다.

func getPodDisruptionBudgetHealth(pdb *policyv1.PodDisruptionBudget) health.HealthStatus {
    if pdb == nil {
        return health.HealthStatusUnknown
    }
    if pdb.Status.CurrentHealthy >= *pdb.Spec.MinAvailable {
        return health.HealthStatusHealthy
    }
    return health.HealthStatusProgressing
}

결론 (Conclusion)

Kubernetes는 리소스의 상태 정보를 제공하지만, 그 상태를 Healthy라고 해석하는 것은 ArgoCD와 같은 도구의 몫입니다.

이번 케이스처럼 Argo Rollout 재시작 + Strict PDB가 결합된 엣지 케이스에서는, 실제로는 배포가 멈춰있음에도 불구하고 기본 헬스 체크 로직의 사각지대에 놓여 "정상"으로 오인될 수 있음을 확인했습니다.

PDB 설정 시, 특히 Replica 수가 적을 때는 Rollout의 재시작 메커니즘이 Eviction API를 사용한다는 점을 반드시 고려해야 합니다.