4 minute read

이전 글에서는 Application Controller가 Application 이벤트를 informer로 받아 CUD 연산을 큐에 넣고, ...QueueItem() 계열 함수가 이를 비동기로 처리하는 흐름까지 살펴봤습니다. App이 새로 추가되면 informer의 AddFunc가 호출되고, 생성된 object의 key가 다른 고루틴으로 전달됩니다. 이 고루틴이 실제로 실행하는 함수가 processAppRefreshQueueItem입니다. 이번 글에서는 이 함수 내부를 따라가 보겠습니다.


processAppRefreshQueueItem 내부

processAppRefreshQueueItem은 CUD 연산이 발생한 app을 비동기로 처리하는 핵심 함수입니다. 내부 동작은 많지만, app 생성 흐름만 기준으로 보면 대략 다음 순서로 정리할 수 있습니다.

// https://github.com/argoproj/argo-cd/blob/a70b2293a06be06db/controller/appcontroller.go#L1541
func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext bool) {
	
	// ...
	
	// ✅ queue에서 다음 app key를 꺼냄
	appKey, shutdown := ctrl.appRefreshQueue.Get()
	
	// ...
	
	// ✅ 1. app 상태를 refresh해야 하는지 판단
	needRefresh, refreshType, comparisonLevel := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout, ctrl.statusHardRefreshTimeout)
	
	// ... 
	
	// ✅ 2. project 조회 및 app spec 검증
	project, hasErrors := ctrl.refreshAppConditions(app)
	
	// ...
	
	// ✅ 3. app 상태 비교
	compareResult, err := ctrl.appStateManager.CompareAppState(app, project, revisions, sources,
		refreshType == appv1.RefreshTypeHard,
		comparisonLevel == CompareWithLatestForceResolve, localManifests, hasMultipleSources, false)
		
	// ...
	
	// ✅ 4. app 리소스 트리 생성 및 summary 갱신
	tree, err := ctrl.setAppManagedResources(app, compareResult)
	
	// ...
	
	// ✅ 5. sync 가능 여부에 따라 auto-sync 수행
	canSync, _ := project.Spec.SyncWindows.Matches(app).CanSync(false)
	if canSync {
		syncErrCond, opMS := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources, compareResult.revisionUpdated)
		// ...
	}
	
	// ...
	// app 상태 갱신 및 마무리
}

가장 먼저 queue에서 원소를 가져온 뒤, 이후 흐름은 크게 다섯 단계로 나눌 수 있습니다.

  1. app 상태를 refresh해야 하는지 판단합니다. 이때 일반 refresh인지 hard refresh인지, 그리고 어느 수준까지 비교할지도 함께 결정합니다.
  2. project를 조회하고 app spec 및 접근 조건을 검증합니다. 대상 클러스터와 저장소 설정이 프로젝트 정책에 맞는지도 여기서 확인합니다.
  3. app 상태 비교: 가장 중요한 부분으로 원격 저장소의 상태와 k8s로 관리되는 app의 리소스를 비교하는 과정을 수행합니다.
  4. app 리소스 트리를 만들고 app summary를 갱신합니다.
  5. sync 가능 여부에 따라 auto-sync를 수행합니다.

processAppRefreshQueueItem에서 가장 중요한 부분은 3번째 단계인 app 상태 비교입니다. Argo CD의 핵심적인 GitOps 비교 로직은 ctrl.appStateManager.CompareAppState에서 수행됩니다. 이제 ctrl.appStateManager.CompareAppState 내부를 보겠습니다.

// https://github.com/argoproj/argo-cd/blob/a70b2293a06be06db/controller/state.go#L429
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool, rollback bool) (*comparisonResult, error) {

	// ...

	
	if len(localManifests) == 0 {

		// ...
		
		// ✅ 1. 원격 저장소에서 대상 object 조회
		targetObjs, manifestInfos, revisionUpdated, err = m.GetRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, rollback)
		
		// ...
	
	}

	// ...
	
	// ✅ 2. 중복 오브젝트 제거
	targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Namespace, targetObjs, infoProvider)
	
	// ...

	// ✅ 3. 대상 클러스터에 배포된 object 조회
	liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
	
	// ...
	
	// ✅ 4. 재조정 대상 오브젝트 정렬
	reconciliation := sync.Reconcile(targetObjs, liveObjByKey, app.Spec.Destination.Namespace, infoProvider)
	
	// ...
	
	// ✅ 5. diff 설정을 구성하고 비교 결과 계산
	diffResults, err := argodiff.StateDiffs(reconciliation.Live, reconciliation.Target, diffConfig)
	
	// ...
	
	// ✅ 6. 리소스를 순회하며 managedResources, resourceSummaries 생성
	managedResources := make([]managedResource, len(reconciliation.Target))
	resourceSummaries := make([]v1alpha1.ResourceStatus, len(reconciliation.Target))
	for i, targetObj := range reconciliation.Target {
		
		// ...
	
		if liveObj != nil {
			resourceVersion = liveObj.GetResourceVersion()
		}
		managedResources[i] = managedResource{
			Name:            resState.Name,
			Namespace:       resState.Namespace,
			Group:           resState.Group,
			Kind:            resState.Kind,
			Version:         resState.Version,
			Live:            liveObj,
			Target:          targetObj,
			Diff:            diffResult,
			Hook:            resState.Hook,
			ResourceVersion: resourceVersion,
		}
		resourceSummaries[i] = resState
	}
	
	// ...

	compRes := comparisonResult{
		syncStatus:           &syncStatus,
		healthStatus:         healthStatus,
		resources:            resourceSummaries,
		managedResources:     managedResources,
		reconciliationResult: reconciliation,
		diffConfig:           diffConfig,
		diffResultList:       diffResults,
		hasPostDeleteHooks:   hasPostDeleteHooks,
		revisionUpdated:      revisionUpdated,
	}
	
	// ...
	
	return &compRes, nil
}

함수 동작을 요약하면 다음과 같습니다.

  • 원격 저장소에 있는 오브젝트 목록을 가져옵니다.
  • 중복 리소스를 정리하고 현재 배포 상태를 조회합니다.
  • 재조정 대상을 계산합니다.
  • diff 설정을 구성하고 실제 비교를 수행합니다.
  • 리소스를 순회하면서 상태 정보를 정리합니다.

이 동작은 테스트 코드로도 확인할 수 있습니다.

// https://github.com/argoproj/argo-cd/blob/a70b2293a06be06db/controller/state_test.go#L482
// TestAppRevisions tests that revisions are properly propagated for a single source app
func TestAppRevisionsSingleSource(t *testing.T) {
	obj1 := NewPod()
	obj1.SetNamespace(test.FakeDestNamespace)
	data := fakeData{
		// ✅ 원격 저장소에는 Pod 하나가 존재함
		manifestResponse: &apiclient.ManifestResponse{
			Manifests: []string{toJSON(t, obj1)},
			Namespace: test.FakeDestNamespace,
			Server:    test.FakeClusterURL,
			Revision:  "abc123",
		},
		// ✅ 대상 클러스터에는 아직 배포된 리소스가 없음
		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
	}
	ctrl := newFakeController(&data, nil)

	app := newFakeApp()
	revisions := make([]string, 0)
	revisions = append(revisions, "")
	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, app.Spec.GetSources(), false, false, nil, app.Spec.HasMultipleSources(), false)
	// ✅ 기대 결과
	require.NoError(t, err)
	assert.NotNil(t, compRes)
	assert.NotNil(t, compRes.syncStatus)
	assert.NotEmpty(t, compRes.syncStatus.Revision)
	assert.Empty(t, compRes.syncStatus.Revisions)
}

위 테스트를 따라가 보면 흐름은 다음과 같습니다.

  1. CompareAppState의 ✅ 1번에서 targetObjs에 Pod 하나가 들어갑니다. 동시에 manifestResponse.Manifests에는 해당 manifest가 문자열로 담깁니다.
  2. 중복 오브젝트는 건너뜁니다.
  3. ✅ 3번에서 liveObjByKey로 현재 배포된 오브젝트를 조회하지만, 이 테스트에서는 아무것도 반환되지 않습니다.
  4. ✅ 4번에서 reconciliation이 만들어집니다. 여기에는 LiveTarget 배열이 들어 있는데, Live는 현재 클러스터 상태이고 Target은 Git에서 의도한 상태입니다. 이 테스트에서는 Live는 비어 있고 Target에만 Pod가 들어갑니다.
  5. ✅ 5번에서 reconciliation.Livereconciliation.Target을 기준으로 diff를 계산합니다. 즉, 아직 배포되지 않은 리소스라는 차이가 여기서 드러납니다.

테스트 코드는 VS Code에서 다음처럼 왼쪽의 초록색 버튼으로 쉽게 디버깅할 수 있습니다.

image.png

좀 더 복잡한 테스트를 보고 싶다면 TestCompareAppStateDuplicatedNamespacedResources를 함께 보면 좋겠습니다.

이렇게 k8s에 app 오브젝트가 생성되면 informer가 이벤트를 받고, queue를 거쳐 CompareAppState가 실제 오브젝트 상태를 파악하게 됩니다. 결국 이 함수 안에서 원격 저장소 상태와 클러스터 상태를 비교해 원하는 상태를 계산합니다. 다만 아직 남아 있는 주제가 두 가지 있습니다. 하나는 repository server와의 상호작용이고, 다른 하나는 실제 k8s 리소스 생성 과정입니다. 다음 글에서는 repository server와 통신하는 흐름을 이어서 살펴보겠습니다.

Leave a comment