LabHub

CAPA — Argo 프로젝트 인증 어소시에이트 · Argo Workflows 와 Events · 이론

Job 으로 안 되는 것이 Workflows 를 만들었다

LabHub 에서 이어서 보기

한 줄 요약

Argo Workflows 는 컨테이너 여러 개를 순서와 의존성을 갖고 돌리는 엔진이다. 쿠버네티스 Job 으로 시작해서 왜 부족한지를 알아야 Workflow 의 필드들이 납득된다. Events 는 그 워크플로를 무엇이 시작시킬지를 담당하는 별도 프로젝트다.

왜 이게 필요했나

Job 은 "이 컨테이너를 성공할 때까지 돌려라" 를 표현한다. 여기까지는 완벽하다. 문제는 두 번째 작업이 생길 때다. 빌드가 끝나야 테스트가 돌고, 테스트가 끝나야 배포가 돌고, 배포가 실패하면 알림이 나가야 한다면 Job 두세 개를 어떻게 엮을 것인가. 방법은 세 가지뿐이다. 컨테이너 하나에 셸 스크립트로 전부 넣거나, 외부 오케스트레이터(젠킨스 등)가 Job 을 순서대로 만들게 하거나, 아니면 의존성을 표현할 수 있는 상위 개념을 도입하는 것이다.

첫째는 실패 지점을 잃는다. 스크립트 중간에서 죽으면 어디서 죽었는지 로그를 뒤져야 하고, 재시도는 항상 처음부터다. 둘째는 클러스터 밖에 상태를 두는 것이라 오케스트레이터가 죽으면 파이프라인이 미아가 된다. Workflows 는 셋째를 택했다. 작업 사이의 의존성과 데이터 전달을 쿠버네티스 오브젝트 안에 선언으로 넣은 것이다.

어떻게 동작하나

Workflow 의 spec 에는 templates 배열과 entrypoint 하나가 있다. entrypoint 는 어느 템플릿부터 시작할지 가리키는 이름이다. 템플릿에는 여러 종류가 있는데 실제로 컨테이너를 돌리는 container 템플릿, 다른 템플릿들을 순서대로 부르는 steps 템플릿, 의존성 그래프로 부르는 dag 템플릿, 사람의 승인을 기다리는 suspend 템플릿이 대표다.

steps 와 dag 의 차이는 표현력이다. steps 는 리스트의 리스트라 같은 층에 있는 것은 병렬, 다음 층은 그다음이라는 단순한 모델이다. dag 는 각 태스크가 dependencies 로 자기 선행 조건을 직접 적기 때문에 A 와 B 가 끝나야 C 가 돌고 B 만 끝나면 D 가 도는 식의 그래프를 그릴 수 있다. 파이프라인이 다섯 단계를 넘어가면 steps 로는 "이 단계가 왜 여기 있는지" 를 읽기 어려워져서 대개 dag 로 간다.

데이터는 두 갈래로 흐른다. parameters 는 문자열이고 artifacts 는 파일이다. 한 템플릿의 outputs.artifacts 에 경로를 적으면 그 경로를 아티팩트 저장소에 올리고, 다음 템플릿의 inputs.artifacts 가 그것을 내려받아 지정한 경로에 푼다. 이 중간 저장소가 필요하다는 점이 Job 과의 결정적 차이다. Job 은 파드끼리 파일을 넘길 방법이 없어서 PVC 를 공유하거나 스스로 어딘가에 올려야 한다.

재사용은 WorkflowTemplate 이 맡는다. 자주 쓰는 템플릿을 네임스페이스에 하나 올려 두고, 각 Workflow 의 태스크가 templateRef 로 이름과 템플릿을 가리킨다. 조직 표준 빌드 절차를 한 곳에서 고치면 모든 파이프라인에 반영되는 구조다. CronWorkflow 는 Workflow 스펙을 workflowSpec 아래에 통째로 품고 schedule 과 concurrencyPolicy 를 얹은 것이다. concurrencyPolicy 를 Forbid 로 두면 앞 실행이 아직 안 끝났을 때 새 실행을 건너뛴다.

Events 는 EventSource → Sensor → Trigger 세 오브젝트로 이뤄진다. EventSource 는 웹훅·S3·Kafka·캘린더 같은 바깥 세계를 구독해 내부 이벤트 버스로 흘려 보내고, Sensor 는 그 버스를 구독하다가 조건이 맞으면, Trigger 가 Workflow 나 다른 쿠버네티스 오브젝트를 만든다. 왜 별도 프로젝트인지는 이 목록만 봐도 답이 나온다. 이 어댑터들을 배포 컨트롤러 안에 넣기 시작하면 Argo CD 는 배포 도구가 아니라 통합 미들웨어가 된다.

현장에서 만나는 모습

저자 홈랩에서 이 구분이 실제로 문제가 된 사례가 KubeVirt 였다. 컴포넌트 상태는 전부 AllComponentsReady 였는데 VM 이 뜨지 않았고, virt-launcher 파드 명세를 뜯어보니 init 컨테이너가 실행할 바이너리를 담은 볼륨 마운트가 빠져 있었다. 이 클러스터에서만 세 번째 확인한 교훈이 "상태가 Ready 인 것과 실제로 동작하는 것은 다른 명제" 라는 것이다.

워크플로에서 정확히 같은 함정이 있다. dag 의 태스크가 Succeeded 로 끝났다는 것은 컨테이너가 0으로 종료했다는 뜻일 뿐, 다음 태스크가 기대하는 아티팩트를 실제로 남겼다는 뜻이 아니다. outputs.artifacts 에 적은 경로에 파일이 없으면 그 태스크는 성공했는데 다음 태스크가 입력을 못 찾아 실패한다. 그래서 아티팩트를 넘기는 태스크는 마지막에 "그 경로에 파일이 있는지" 를 스스로 확인하는 습관이 필요하다. 마찬가지로 GPU 워크로드를 워크플로에서 돌릴 때는 nvidia.com/gpu: 1 만 요청하면 32GB 카드가 필요한 학습이 8GB 노트북 GPU 에 얹힐 수 있다. 쿠버네티스 입장에서는 둘 다 GPU 한 개이기 때문이고, 그래서 노드에 체급 라벨을 붙이고 nodeSelector 로 고르게 만들어야 한다.

다음 실습에서 할 것

/root/capa-wf/ 에 Workflow(dag 의존성, 파라미터, 아티팩트), CronWorkflow, WorkflowTemplate 참조를 작성한 다음, 같은 일을 하는 쿠버네티스 Job 과 CronJob 을 실제 클러스터에 올려 둘을 나란히 놓고 비교한다. 마지막에는 파일과 클러스터 양쪽에서 값을 뽑아 요약 컨피그맵을 만든다.