GitLab CI/CD · 실행 모델 — 순서·조건·전달 · 실습
설정으로 파이프라인의 실행 모델 만들기
목표
.gitlab-ci.yml 한 장을 여덟 단계에 걸쳐 쌓아 올리면서 GitLab CI 의 실행 모델 — 스테이지 순서, needs 가 만드는 DAG, rules 가 가르는 생성 여부, artifacts 와 cache 의 차이 — 을 손으로 만든다. 마지막에는 그 모델을 계산하는 해석기를 직접 짠다.
왜 중요한가
이 파드에는 GitLab 서버도 GitLab Runner 도 없다. 그래서 이 실습은 파이프라인이 실제로 도는 흉내를 내지 않는다. 대신 러너가 없어도 정직하게 배울 수 있는 것만 다룬다 — 설정 언어와 그 실행 모델이다. 실무에서 파이프라인 때문에 막히는 순간은 대부분 명령을 몰라서가 아니라, 왜 이 잡이 안 만들어졌는지 왜 저 잡이 아직 기다리는지를 설명하지 못해서다. 그 답은 전부 이 파일의 구조에서 나온다. 채점도 파일을 눈으로 훑지 않고 YAML 파서로 읽어 구조를 확인한다 — 들여쓰기가 한 칸 어긋나 잡이 엉뚱한 키 아래 매달린 설정은 사람 눈에는 멀쩡해 보이고 실제로는 돌지 않기 때문이다.
단계
1. /root/glci/.gitlab-ci.yml 을 만든다. 최상위 stages 에 build, test, deploy 를 이 순서로 두고, build-app 잡을 stage: build 로 두어 script 를 하나 이상 적는다.
2. 잡을 더 붙여 세 스테이지를 모두 채운다. lint 와 unit-test 는 stage: test, deploy-staging 은 stage: deploy 다. 모든 잡에 script 가 있어야 하고, 모든 stage 값은 stages 목록 안에 있어야 한다.
3. .python-base 라는 숨김 잡을 만들어 image 와 before_script 를 넣고, build-app·lint·unit-test 세 잡이 extends: .python-base 로 물려받게 한다. 물려받은 잡은 image 를 다시 적지 않는다. 최상위 default 에 기본 image 를 둔다.
4. needs 를 붙여 DAG 를 만든다. lint 는 needs: [], unit-test 는 needs 에 build-app, deploy-staging 은 needs 에 unit-test 를 적는다. build-app 에는 needs 를 두지 않는다.
5. deploy-staging 에 rules 를 붙여 $CI_COMMIT_BRANCH == "main" 일 때 when: on_success 로 두고, 마지막 항목은 조건 없는 when: never 로 닫는다. deploy-prod 잡을 stage: deploy 로 새로 만들어 needs 에 deploy-staging 을 적고, 같은 브랜치 조건에서 when: manual · allow_failure: false 로 둔 뒤 역시 조건 없는 when: never 로 닫는다. only 와 except 는 쓰지 않는다.
6. build-app 에 artifacts 를 붙여 paths 와 expire_in 을 적는다. unit-test 의 needs 를 긴 형태로 바꿔 job: build-app · artifacts: true 로, deploy-staging 의 needs 는 artifacts: false 로 둔다.
7. /root/glci/requirements.txt 를 내용 있는 파일로 만든다. build-app 과 unit-test 에 cache 를 붙이되 key 는 files 로 requirements.txt 를 가리키게 한다. build-app 은 policy: pull-push, unit-test 는 policy: pull 이다. cache.paths 는 artifacts.paths 와 겹치지 않게 한다.
8. /root/glci/plan.py 를 만든다. 인자로 받은 설정 파일을 읽어 {"stages": [...], "waves": [[...], ...]} 를 표준출력에 JSON 으로 낸다. needs 가 있으면 그 목록만, 없으면 앞 스테이지의 모든 잡을 기다린다(기본 stage 는 test). 점으로 시작하는 키와 예약 키(stages·variables·default·include·workflow)는 잡이 아니다. 각 웨이브는 이름순으로 정렬한다. 아무 잡도 출발할 수 없으면 cycle 이라는 말을 출력하고 0 이 아닌 종료 코드로 끝낸다. 마지막으로 자기 설정에 대해 돌린 출력을 /root/glci/plan.json 에 저장한다.
참고
- 파서로 확인하는 습관을 들이면 사고가 절반으로 준다:
python3 -c "import yaml,sys;print(list(yaml.safe_load(open(sys.argv[1]))))" /root/glci/.gitlab-ci.yml로 최상위 키 목록을 찍어 보면 잡이 사라졌는지 바로 보인다. needs: []와needs키가 아예 없는 것은 정반대의 뜻이다. 전자는 아무것도 안 기다리고, 후자는 앞 스테이지 전부를 기다린다.rules는 처음 걸리는 항목 하나만 적용하고 멈춘다. 조건 없는when: never는 반드시 목록 맨 끝에 둔다.- 흔한 실수: 조각 이름에서 점을 빼먹어 실행되는 잡으로 만들기,
extends로 물려받고도image를 다시 적기, 캐시 경로와 산출물 경로를 같게 두기, 해석기의 진단 메시지를 표준출력으로 보내 JSON 을 깨뜨리기.
단계 8개
- 스테이지 순서와 첫 잡
- 세 스테이지를 모두 채우기
- 숨김 잡과 extends 로 중복 걷어내기
- needs 로 스테이지 벽 넘기
- rules 로 브랜치 조건과 수동 승인 걸기
- artifacts 와 받을 사람 정하기
- cache 키를 잠금 파일 해시로 잡기
- 파이프라인 해석기로 실행 순서 계산하기