フラミナル

考え方や調べたことを書き殴ります。IT技術系記事多め

ArgoCDにおけるステータスの意味をコードを追って調べた

f:id:lirlia:20210917203350p:plain

結論

ArgoCDにおけるSync-Operationのステータスの意味は以下の通りです。

種類 状態
Healthy 正常
Progressing 正常ではないが、正常に近づいている状態(確実ではない)
Suspended 一時停止、実行待ち状態
Degraded リソースステータスが障害を示している場合、またはリソースが一定のタイムアウト内に正常な状態に到達できなかった場合
Missing クラスタにリソースが存在しない
Unknown ヘルスチェックに失敗したり状態が不明な場合

Resource Health - Argo CD - Declarative GitOps CD for Kubernetes をちゃんと読めばコードを読む必要などなかった・・・。

調査過程(コードリーディング)

func (sc *syncContext) getOperationPhase(hook *unstructured.Unstructured) (common.OperationPhase, string, error) {
    phase := common.OperationSucceeded
    message := fmt.Sprintf("%s created", hook.GetName())

    resHealth, err := health.GetResourceHealth(hook, sc.healthOverride)
    if err != nil {
        return "", "", err
    }
    if resHealth != nil {
        switch resHealth.Status {
        case health.HealthStatusUnknown, health.HealthStatusDegraded:
            phase = common.OperationFailed
            message = resHealth.Message
        case health.HealthStatusProgressing, health.HealthStatusSuspended:
            phase = common.OperationRunning
            message = resHealth.Message
        case health.HealthStatusHealthy:
            phase = common.OperationSucceeded
            message = resHealth.Message
        }
    }
    return phase, message, nil
}

gitops-engine/sync_context.go at 2c97a96cab1b9386728feed92e0fa31547ea2a98 · argoproj/gitops-engine · GitHub

次はここに飛んでいます。

func GetResourceHealth(obj *unstructured.Unstructured, healthOverride HealthOverride) (health *HealthStatus, err error) {

    // 削除待ちだけどここはヒットしないからスルーされるっぽい
    if obj.GetDeletionTimestamp() != nil {
        return &HealthStatus{
            Status:  HealthStatusProgressing,
            Message: "Pending deletion",
        }, nil
    }

    // resource.customizationsで設定したルールに従ってステータス検知
    if healthOverride != nil {
        health, err := healthOverride.GetResourceHealth(obj)
        if err != nil {
            // ステータスが拾えなかったらUNKNOWN
            health = &HealthStatus{
                Status:  HealthStatusUnknown,
                Message: err.Error(),
            }
            return health, err
        }
        if health != nil {
            return health, nil
        }
    }

    // 普通の時はこっちでステータス検知
    if healthCheck := GetHealthCheckFunc(obj.GroupVersionKind()); healthCheck != nil {
        if health, err = healthCheck(obj); err != nil {
            health = &HealthStatus{
                Status:  HealthStatusUnknown,
                Message: err.Error(),
            }
        }
    }
    return health, err

}

gitops-engine/health.go at 2c97a96cab1b9386728feed92e0fa31547ea2a98 · argoproj/gitops-engine · GitHub

普通の場合(ArgoCDによって用意されているやつ)

func GetHealthCheckFunc(gvk schema.GroupVersionKind) func(obj *unstructured.Unstructured) (*HealthStatus, error) {
    switch gvk.Group {
    case "apps":
        switch gvk.Kind {
        case kube.DeploymentKind:
            return getDeploymentHealth
        case kube.StatefulSetKind:
            return getStatefulSetHealth
        case kube.ReplicaSetKind:
            return getReplicaSetHealth
        case kube.DaemonSetKind:
            return getDaemonSetHealth
        }
    case "extensions":
        switch gvk.Kind {
        case kube.DeploymentKind:
            return getDeploymentHealth
        case kube.IngressKind:
            return getIngressHealth
        case kube.ReplicaSetKind:
            return getReplicaSetHealth
        case kube.DaemonSetKind:
            return getDaemonSetHealth
        }
    case "argoproj.io":
        switch gvk.Kind {
        case "Workflow":
            return getArgoWorkflowHealth
        }
    case "apiregistration.k8s.io":
        switch gvk.Kind {
        case kube.APIServiceKind:
            return getAPIServiceHealth
        }
    case "networking.k8s.io":
        switch gvk.Kind {
        case kube.IngressKind:
            return getIngressHealth
        }
    case "":
        switch gvk.Kind {
        case kube.ServiceKind:
            return getServiceHealth
        case kube.PersistentVolumeClaimKind:
            return getPVCHealth
        case kube.PodKind:
            return getPodHealth
        }
    case "batch":
        switch gvk.Kind {
        case kube.JobKind:
            return getJobHealth
        }
    case "autoscaling":
        switch gvk.Kind {
        case kube.HorizontalPodAutoscalerKind:
            return getHPAHealth
        }
    }
    return nil
}

gitops-engine/health.go at 2c97a96cab1b9386728feed92e0fa31547ea2a98 · argoproj/gitops-engine · GitHub

ここでは参考までにDeploymentを見ていく。

func getDeploymentHealth(obj *unstructured.Unstructured) (*HealthStatus, error) {
    gvk := obj.GroupVersionKind()
    switch gvk {

    case appsv1.SchemeGroupVersion.WithKind(kube.DeploymentKind):
        var deployment appsv1.Deployment
        err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &deployment)
        if err != nil {
            return nil, fmt.Errorf("failed to convert unstructured Deployment to typed: %v", err)
        }
        return getAppsv1DeploymentHealth(&deployment)
    case appsv1beta1.SchemeGroupVersion.WithKind(kube.DeploymentKind):
        var deployment appsv1beta1.Deployment
        err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &deployment)
        if err != nil {
            return nil, fmt.Errorf("failed to convert unstructured Deployment to typed: %v", err)
        }
        return getAppsv1beta1DeploymentHealth(&deployment)
    case extv1beta1.SchemeGroupVersion.WithKind(kube.DeploymentKind):
        var deployment extv1beta1.Deployment
        err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &deployment)
        if err != nil {
            return nil, fmt.Errorf("failed to convert unstructured Deployment to typed: %v", err)
        }
        return getExtv1beta1DeploymentHealth(&deployment)
    default:
        return nil, fmt.Errorf("unsupported Deployment GVK: %s", gvk)
    }
}

APIバージョンによって分けられている。とりあえずv1をみる

func getAppsv1DeploymentHealth(deployment *appsv1.Deployment) (*HealthStatus, error) {

    // pauseならHealthStatusSuspended
    if deployment.Spec.Paused {
        return &HealthStatus{
            Status:  HealthStatusSuspended,
            Message: "Deployment is paused",
        }, nil
    }

    // rolloutの結果を拾っている
    // Borrowed at kubernetes/kubectl/rollout_status.go https://github.com/kubernetes/kubernetes/blob/5232ad4a00ec93942d0b2c6359ee6cd1201b46bc/pkg/kubectl/rollout_status.go#L80
    if deployment.Generation <= deployment.Status.ObservedGeneration {
        cond := getAppsv1DeploymentCondition(deployment.Status, appsv1.DeploymentProgressing)
        if cond != nil && cond.Reason == "ProgressDeadlineExceeded" {
            return &HealthStatus{
                Status:  HealthStatusDegraded,
                Message: fmt.Sprintf("Deployment %q exceeded its progress deadline", deployment.Name),
            }, nil
        } else if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas {
            return &HealthStatus{
                Status:  HealthStatusProgressing,
                Message: fmt.Sprintf("Waiting for rollout to finish: %d out of %d new replicas have been updated...", deployment.Status.UpdatedReplicas, *deployment.Spec.Replicas),
            }, nil
        } else if deployment.Status.Replicas > deployment.Status.UpdatedReplicas {
            return &HealthStatus{
                Status:  HealthStatusProgressing,
                Message: fmt.Sprintf("Waiting for rollout to finish: %d old replicas are pending termination...", deployment.Status.Replicas-deployment.Status.UpdatedReplicas),
            }, nil
        } else if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas {
            return &HealthStatus{
                Status:  HealthStatusProgressing,
                Message: fmt.Sprintf("Waiting for rollout to finish: %d of %d updated replicas are available...", deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas),
            }, nil
        }
    } else {
        return &HealthStatus{
            Status:  HealthStatusProgressing,
            Message: "Waiting for rollout to finish: observed deployment generation less then desired generation",
        }, nil
    }

    return &HealthStatus{
        Status: HealthStatusHealthy,
    }, nil
}

どうやらdeployementの場合はrolloutの結果に従って判断をしていることがわかった。

独自に定義する場合(Applicationなど)

App-of-Appsパターンの場合Applicationの状態を管理したいが上で確認したコードを見るとargoproj.ioリソースではWorkflowというリソースしかヒットしないため正しくチェックされない。

    case "argoproj.io":
        switch gvk.Kind {
        case "Workflow":
            return getArgoWorkflowHealth
        }

そのため公式で書かれている通り以下のような設定を書く必要がある。

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
data:
  resource.customizations: |
    argoproj.io/Application:
      health.lua: |
        hs = {}
        hs.status = "Progressing"
        hs.message = ""
        if obj.status ~= nil then
          if obj.status.health ~= nil then
            hs.status = obj.status.health.status
            if obj.status.health.message ~= nil then
              hs.message = obj.status.health.message
            end
          end
        end
        return hs

このスクリプトは「hs map変数にobjの内容を突っ込んで返しているだけ」

じゃあobjはなんなのかというとウォッチしているk8sリソースのことで、そのリソースのstatus/health/statusを持ってきているだけ。

apiVersion: argoproj.io/v1alpha1
kind: Application
status:
  health:
    status: Degraded

よってスクリプトを用意するとk8sで管理されている値を使って状態管理ができるようになる。 じゃあこのApplicationというカスタムリソースはどういう時にDegradedやRunningになるか?

ちなみに成功している(同期が完了している)時はこういう状態でした。

Status:
  Health:
    Status: Healthy

そのため最初の条件分岐を見ると「OperationSucceeded」になる。

     case health.HealthStatusUnknown, health.HealthStatusDegraded:
            phase = common.OperationFailed
            message = resHealth.Message

        case health.HealthStatusProgressing, health.HealthStatusSuspended:
            phase = common.OperationRunning
            message = resHealth.Message

        case health.HealthStatusHealthy:
            phase = common.OperationSucceeded
            message = resHealth.Message

じゃあこのStatusにはどんな種類があるのか?をみるとgitops-engineの方に定義されている。

const (
    // Indicates that health assessment failed and actual health status is unknown
    HealthStatusUnknown HealthStatusCode = "Unknown"

    // Progressing health status means that resource is not healthy but still have a chance to reach healthy state
    HealthStatusProgressing HealthStatusCode = "Progressing"

    // Resource is 100% healthy
    HealthStatusHealthy HealthStatusCode = "Healthy"

    // Assigned to resources that are suspended or paused. The typical example is a
    // [suspended](https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/#suspend) CronJob.
    HealthStatusSuspended HealthStatusCode = "Suspended"

    // Degrade status is used if resource status indicates failure or resource could not reach healthy state
    // within some timeout.
    HealthStatusDegraded HealthStatusCode = "Degraded"

    // Indicates that resource is missing in the cluster.
    HealthStatusMissing HealthStatusCode = "Missing"
)

gitops-engine/health.go at 2c97a96cab1b9386728feed92e0fa31547ea2a98 · argoproj/gitops-engine · GitHub