propose-translation-update.sh 실행을 위한 Zuul CI/CD 파이프라인 흐름 분석
지난 글을 통해 번역 자동화 파이프라인의 전체 흐름을 정리하면서, Zuul Executor에 대해 추가적으로 공부한 내용을 공유하고자 합니다.
OpenStack은 하나의 프로젝트가 아니라 수십 개의 컴포넌트와 서비스로 구성된 초대형 오픈소스 클라우드 인프라이다. 이 중에서 번역(i18n) 파이프라인은 모든 사용자 인터페이스와 문서화된 메시지에 대해 다국어 번역을 관리하는 시스템이다.
전체 번역 워크플로우는 단일 Job 수준에서 보면 단순해 보이지만 실제로는 수십 개의 프로젝트, 여러 개의 브랜치, 그리고 다양한 언어를 대상으로 한다. 따라서 병렬 처리 없이는 번역 자동화 파이프라인 전체가 비효율적이고 느려질 수밖에 없다.
이번 글에서는 OpenStack i18n 번역 자동화 파이프라인의 핵심 인프라인 Zuul Executor가 어떻게 Job을 병렬로 실행하는지, 그리고 어떻게 확장성과 안전성을 확보하는지에 대해 설명하고자 한다.
병렬 처리의 필요성
OpenStack i18n 번역 자동화 파이프라인은 단일 프로젝트나 저장소만 다루는 것이 아니다. 아래와 같은 복잡한 요구사항을 동시에 만족시켜야 한다.
- Horizon, Keystone, Neutron 등 수십 개의 다중 프로젝트
- master, stable/2023.2, stable/yoga 등의 릴리즈 브랜치
- 20개 이상의 다국어 지원
- Zanata/Weblate → Git 자동 propose 주기적 수행
예를 들어, Horizon 프로젝트만 봐도 master, stable/2023.2, stable/zed 등 여러 브랜치에 대해 별도의 번역 Job이 동작하며, 이 Job은 하루에도 수 차례 실행된다. 여기에 Zanata/Weblate에서 주기적으로 Push되는 번역 데이터까지 더해지면, 한 번에 수십 개의 Job이 생성된다.
이러한 작업들을 순차적으로 처리하려면 단일 Job 기준으로 수십 분이 소요될 수 있지만, 병렬적으로 실행하면 전체 처리 시간은 단일 Job 처리 시간에 거의 수렴하게 된다.
Zuul Executor의 병렬 처리 메커니즘
Zuul Executor 는 단일 프로세스지만 내부적으로 Job 단위의 워커(worker) 스레드를 생성하여 병렬 처리를 지원한다.
구조는 아래와 같다.
Zuul Executor
│
├── Worker 1 (Job A)
│ └── /builds/uuid-A/
│ └── Git, playbook, secrets...
│
├── Worker 2 (Job B)
│ └── /builds/uuid-B/
│
├── Worker 3 (Job C)
│ └── /builds/uuid-C/
│
└── ... up to max-job count
각 워커는 완전히 독립된 환경을 유지한다.
각 워커가 수행하는 공통 작업
- 전용 디렉토리 생성 (/var/lib/zuul/builds/{uuid}/)
- 프로젝트 Git checkout (pre-merged 상태 포함)
- Secret 복호화 및 환경변수 구성
- Ansible Playbook 실행
- 결과 로그 저장 및 report
실제 OpenStack i18n 번역 워크플로우에서 Job들이 어떻게 병렬적으로 실행되는지 예를 들어 살펴보자.
| Job ID | 대상 프로젝트 | 브랜치 | 노드 유형 | 실행 환경 |
|---|---|---|---|---|
| J001 | Horizon | master |
Ubuntu Bionic VM | 원격 SSH 노드 |
| J002 | Horizon | stable/zed |
Ubuntu Jammy VM | 원격 SSH 노드 |
| J003 | Keystone | master |
로컬 | Executor localhost |
| J004 | Neutron | master |
Ubuntu Focal | 원격 SSH 노드 |
위 4개의 Job이 Scheduler에 의해 동시에 Executor로 전달되면, Executor는 아래와 같은 작업을 수행한다.
Worker-1: Horizon master → zanata.ini 템플릿 생성, Git 준비, propose 실행Worker-2: Horizon stable/zed → 동일 절차로 별도 디렉토리에서 실행Worker-3: Keystone → Weblate 인증정보를 환경변수로 주입, 로컬에서 propose 수행Worker-4: Neutron → 원격 노드에 SSH 연결, shell script 실행
각 Job은 자원들을 완전히 분리된 채 사용하게 된다.
Git repository: 각기 다른 pre-merged ref 체크아웃Secret: Job 단위 메모리 내 복호화Playbook: 프로젝트별 custom 역할 실행로그 디렉토리: /logs/{project}/{uuid}에 분리하여 저장
병렬 실행을 위한 워크스페이스 분리
각 Job이 실행되는 디렉토리는 독립적으로 생성되며, 해당 워커 외에는 접근이 불가능하다.
/var/lib/zuul/builds/fd7c1e8c/
├── .git/
├── playbooks/
├── zuul-workspace.yaml
├── job-output.txt
├── .ansible/
└── .tox/
이 디렉토리는 다음과 같은 장점을 제공한다.
- Git 상태 격리 (브랜치, merge 상태 모두 분리)
- Secret 및 환경변수의 격리
- 캐시, 로그 등 출력물 분리
- 디버깅 시 해당 Job만 타겟 가능
리소스 제한과 튜닝
Zuul Executor 는 기본적으로 병렬 Job 수를 설정할 수 있다.
executor:
max-job: 6
이 수치를 올리면 더 많은 Job을 동시에 실행할 수 있지만 CPU, 메모리, I/O 성능에 따라 제한해야 한다. (OpenStack i18n 번역 서버에서는 일반적으로 4~6개 병렬 Job이 가장 안정적인 성능을 보인다.)
Job 간 충돌 방지 메커니즘
Zuul Executor는 Job 간 충돌을 철저하게 방지한다.
| 항목 | 격리 방식 |
|---|---|
| Git | 디렉토리별 별도 checkout |
| Secret | 메모리 상 복호화, 로그 마스킹 |
| 환경변수 | Ansible → environment 블록으로만 전달 |
| 로그 | Job별 디렉토리 저장 및 upload |
| 실행 | Playbook, shell 모두 별도 프로세스에서 수행 |
다수 Job이 동시에 돌아가는 상황에서 하나의 Job이 실패하거나 충돌을 일으켜도 다른 Job에 영향을 주지 않는다.
이렇게 Zuul Executor가 CI 파이프라인에서 생성되는 여러 Job들을 완전히 독립적으로 실행하고 관리하기 때문에 horizon, keystone, neutron, nova 등 수십개의 프로젝트에 대해 다국어 번역 제안을 동시 수행해도 몇 분 이내에 모든 propose가 완료된다.