
結論
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
}
次はここに飛んでいます。
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
}
普通の場合(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
}
ここでは参考までに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"
)