設定でパイプラインの実行モデルを作る
한국어 원문으로 표시합니다.
목표
.gitlab-ci.yml 한 장을 여덟 단계에 걸쳐 쌓아 올리면서 GitLab CI 의 실행 모델 — 스테이지 순서, needs 가 만드는 DAG, rules 가 가르는 생성 여부, artifacts 와 cache 의 차이 — 을 손으로 만든다. 마지막에는 그 모델을 계산하는 해석기를 직접 짠다.
왜 중요한가
이 파드에는 GitLab 서버도 GitLab Runner 도 없다. 그래서 이 실습은 파이프라인이 실제로 도는 흉내를 내지 않는다. 대신 러너가 없어도 정직하게 배울 수 있는 것만 다룬다 — 설정 언어와 그 실행 모델이다. 실무에서 파이프라인 때문에 막히는 순간은 대부분 명령을 몰라서가 아니라, 왜 이 잡이 안 만들어졌는지 왜 저 잡이 아직 기다리는지를 설명하지 못해서다. 그 답은 전부 이 파일의 구조에서 나온다. 채점도 파일을 눈으로 훑지 않고 YAML 파서로 읽어 구조를 확인한다 — 들여쓰기가 한 칸 어긋나 잡이 엉뚱한 키 아래 매달린 설정은 사람 눈에는 멀쩡해 보이고 실제로는 돌지 않기 때문이다.
단계
/root/glci/.gitlab-ci.yml을 만든다. 최상위stages에build,test,deploy를 이 순서로 두고,build-app잡을stage: build로 두어script를 하나 이상 적는다.- 잡을 더 붙여 세 스테이지를 모두 채운다.
lint와unit-test는stage: test,deploy-staging은stage: deploy다. 모든 잡에script가 있어야 하고, 모든stage값은stages목록 안에 있어야 한다. .python-base라는 숨김 잡을 만들어image와before_script를 넣고,build-app·lint·unit-test세 잡이extends: .python-base로 물려받게 한다. 물려받은 잡은image를 다시 적지 않는다. 최상위default에 기본image를 둔다.needs를 붙여 DAG 를 만든다.lint는needs: [],unit-test는needs에build-app,deploy-staging은needs에unit-test를 적는다.build-app에는needs를 두지 않는다.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는 쓰지 않는다.build-app에artifacts를 붙여paths와expire_in을 적는다.unit-test의needs를 긴 형태로 바꿔job: build-app·artifacts: true로,deploy-staging의needs는artifacts: false로 둔다./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와 겹치지 않게 한다./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 을 깨뜨리기.
스테이지 순서와 첫 잡
/root/glci/.gitlab-ci.yml 을 만든다. 최상위 stages 에 build, test, deploy 를 이 순서로 두고, build-app 잡을 stage: build 로 두어 script 를 하나 이상 적는다.
/root/glci/.gitlab-ci.yml 을 만들고 최상위에 stages 목록을 둡니다. 이 목록의 순서가 곧 기본 실행 순서입니다. 그 아래에 build-app 이라는 최상위 키를 두고 stage 와 script 를 그 안에 들여씁니다. 채점은 grep 이 아니라 YAML 파서로 읽으니, 들여쓰기가 한 칸만 어긋나도 잡이 다른 키의 하위 항목이 되어 실패합니다.
세 스테이지를 모두 채우기
잡을 더 붙여 세 스테이지를 모두 채운다. lint 와 unit-test 는 stage: test, deploy-staging 은 stage: deploy 다. 모든 잡에 script 가 있어야 하고, 모든 stage 값은 stages 목록 안에 있어야 한다.
lint 와 unit-test 를 test 스테이지에, deploy-staging 을 deploy 스테이지에 추가합니다. 같은 스테이지에 잡이 둘이면 그 둘은 서로 병렬로 돕니다. stages 목록에 없는 이름을 stage 로 쓰면 GitLab 은 그 설정을 통째로 거부하니 철자를 확인하세요. 모든 잡에는 script 가 있어야 합니다.
숨김 잡과 extends 로 중복 걷어내기
.python-base 라는 숨김 잡을 만들어 image 와 before_script 를 넣고, build-app·lint·unit-test 세 잡이 extends: .python-base 로 물려받게 한다. 물려받은 잡은 image 를 다시 적지 않는다. 최상위 default 에 기본 image 를 둔다.
이름이 점으로 시작하는 키는 실행되지 않는 조각입니다. .python-base 에 image 와 before_script 를 넣고, build-app·lint·unit-test 세 잡이 extends 로 물려받게 하세요. 물려받았으면 잡에서 image 를 다시 적지 않습니다. 조각을 쓰지 않는 잡을 위해 최상위 default 에 기본 이미지를 둡니다.
needs 로 스테이지 벽 넘기
needs 를 붙여 DAG 를 만든다. lint 는 needs: [], unit-test 는 needs 에 build-app, deploy-staging 은 needs 에 unit-test 를 적는다. build-app 에는 needs 를 두지 않는다.
unit-test 는 needs 로 build-app 만 기다리게 하고, deploy-staging 은 unit-test 만 기다리게 합니다. lint 에는 needs: [] 를 줍니다 — 키가 없는 것과 빈 목록은 정반대의 뜻이라, 빈 목록이어야 앞 스테이지를 하나도 기다리지 않고 맨 앞에서 출발합니다. build-app 에는 needs 를 두지 않습니다.
rules 로 브랜치 조건과 수동 승인 걸기
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 는 쓰지 않는다.
deploy-staging 은 $CI_COMMIT_BRANCH == "main" 일 때 when: on_success, deploy-prod 는 같은 조건에서 when: manual 로 둡니다. 두 잡 모두 마지막 항목은 조건 없는 when: never 여야 합니다 — rules 는 처음 걸리는 하나만 적용하므로 조건 없는 항목을 가운데 두면 그 아래가 전부 죽습니다. only/except 는 쓰지 않습니다. deploy-prod 는 deploy 스테이지이고 deploy-staging 을 기다립니다.
artifacts 와 받을 사람 정하기
build-app 에 artifacts 를 붙여 paths 와 expire_in 을 적는다. unit-test 의 needs 를 긴 형태로 바꿔 job: build-app · artifacts: true 로, deploy-staging 의 needs 는 artifacts: false 로 둔다.
build-app 에 artifacts 를 붙여 paths 로 넘길 디렉터리를 적고 expire_in 도 함께 적습니다. 그리고 needs 를 긴 형태(- job: 이름 / artifacts: true|false)로 바꿔, 산출물을 실제로 쓰는 unit-test 만 true 로 받고 순서만 기다리는 deploy-staging 은 false 로 끕니다. 안 쓰는 잡까지 내려받으면 파이프라인이 조용히 느려집니다.
cache 키를 잠금 파일 해시로 잡기
/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 와 겹치지 않게 한다.
먼저 /root/glci/requirements.txt 를 만듭니다 — 캐시 키가 해시할 실제 파일이 있어야 합니다. 그다음 build-app 과 unit-test 에 cache 를 붙이되 key 는 고정 문자열이 아니라 files 형태로 그 파일을 가리키게 하세요. 캐시를 만드는 build-app 은 policy: pull-push, 읽기만 하는 unit-test 는 policy: pull 입니다. cache.paths 는 artifacts.paths 와 겹치면 안 됩니다.
파이프라인 해석기로 실행 순서 계산하기
/root/glci/plan.py 를 만든다. 인자로 받은 설정 파일을 읽어 {"stages": [...], "waves": [[...], ...]} 를 표준출력에 JSON 으로 낸다. needs 가 있으면 그 목록만, 없으면 앞 스테이지의 모든 잡을 기다린다(기본 stage 는 test). 점으로 시작하는 키와 예약 키(stages·variables·default·include·workflow)는 잡이 아니다. 각 웨이브는 이름순으로 정렬한다. 아무 잡도 출발할 수 없으면 cycle 이라는 말을 출력하고 0 이 아닌 종료 코드로 끝낸다. 마지막으로 자기 설정에 대해 돌린 출력을 /root/glci/plan.json 에 저장한다.
/root/glci/plan.py <설정파일> 로 부르면 표준출력에 {"stages": [...], "waves": [[...], ...]} 를 내야 합니다. 웨이브 계산 규칙은 셋입니다 — needs 가 있으면 그 목록만, 없으면 앞 스테이지의 모든 잡을 기다리고(기본 stage 는 test), 점으로 시작하는 키와 예약 키는 잡이 아닙니다. 각 웨이브는 이름순으로 정렬합니다. 아무도 출발하지 못하는 상태가 되면 순환이므로 cycle 을 출력하고 0 이 아닌 코드로 끝내세요. 진단은 표준에러로 보내야 표준출력이 JSON 으로 남습니다. 마지막에 자기 설정에 대해 돌린 결과를 /root/glci/plan.json 으로 저장합니다.