LabHub
배우기 러닝패스 코스

GitLab CI/CD

設定でパイプラインの実行モデルを作る

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

.gitlab-ci.yml 한 장을 여덟 단계에 걸쳐 쌓아 올리면서 GitLab CI 의 실행 모델 — 스테이지 순서, needs 가 만드는 DAG, rules 가 가르는 생성 여부, artifactscache 의 차이 — 을 손으로 만든다. 마지막에는 그 모델을 계산하는 해석기를 직접 짠다.

왜 중요한가

이 파드에는 GitLab 서버도 GitLab Runner 도 없다. 그래서 이 실습은 파이프라인이 실제로 도는 흉내를 내지 않는다. 대신 러너가 없어도 정직하게 배울 수 있는 것만 다룬다 — 설정 언어와 그 실행 모델이다. 실무에서 파이프라인 때문에 막히는 순간은 대부분 명령을 몰라서가 아니라, 왜 이 잡이 안 만들어졌는지 왜 저 잡이 아직 기다리는지를 설명하지 못해서다. 그 답은 전부 이 파일의 구조에서 나온다. 채점도 파일을 눈으로 훑지 않고 YAML 파서로 읽어 구조를 확인한다 — 들여쓰기가 한 칸 어긋나 잡이 엉뚱한 키 아래 매달린 설정은 사람 눈에는 멀쩡해 보이고 실제로는 돌지 않기 때문이다.

단계

  1. /root/glci/.gitlab-ci.yml 을 만든다. 최상위 stagesbuild, test, deploy 를 이 순서로 두고, build-app 잡을 stage: build 로 두어 script 를 하나 이상 적는다.
  2. 잡을 더 붙여 세 스테이지를 모두 채운다. lintunit-teststage: test, deploy-stagingstage: deploy 다. 모든 잡에 script 가 있어야 하고, 모든 stage 값은 stages 목록 안에 있어야 한다.
  3. .python-base 라는 숨김 잡을 만들어 imagebefore_script 를 넣고, build-app·lint·unit-test 세 잡이 extends: .python-base 로 물려받게 한다. 물려받은 잡은 image 를 다시 적지 않는다. 최상위 default 에 기본 image 를 둔다.
  4. needs 를 붙여 DAG 를 만든다. lintneeds: [], unit-testneedsbuild-app, deploy-stagingneedsunit-test 를 적는다. build-app 에는 needs 를 두지 않는다.
  5. deploy-stagingrules 를 붙여 $CI_COMMIT_BRANCH == "main" 일 때 when: on_success 로 두고, 마지막 항목은 조건 없는 when: never 로 닫는다. deploy-prod 잡을 stage: deploy 로 새로 만들어 needsdeploy-staging 을 적고, 같은 브랜치 조건에서 when: manual · allow_failure: false 로 둔 뒤 역시 조건 없는 when: never 로 닫는다. onlyexcept 는 쓰지 않는다.
  6. build-appartifacts 를 붙여 pathsexpire_in 을 적는다. unit-testneeds 를 긴 형태로 바꿔 job: build-app · artifacts: true 로, deploy-stagingneedsartifacts: false 로 둔다.
  7. /root/glci/requirements.txt 를 내용 있는 파일로 만든다. build-appunit-testcache 를 붙이되 keyfilesrequirements.txt 를 가리키게 한다. build-apppolicy: pull-push, unit-testpolicy: pull 이다. cache.pathsartifacts.paths 와 겹치지 않게 한다.
  8. /root/glci/plan.py 를 만든다. 인자로 받은 설정 파일을 읽어 {"stages": [...], "waves": [[...], ...]} 를 표준출력에 JSON 으로 낸다. needs 가 있으면 그 목록만, 없으면 앞 스테이지의 모든 잡을 기다린다(기본 stagetest). 점으로 시작하는 키와 예약 키(stages·variables·default·include·workflow)는 잡이 아니다. 각 웨이브는 이름순으로 정렬한다. 아무 잡도 출발할 수 없으면 cycle 이라는 말을 출력하고 0 이 아닌 종료 코드로 끝낸다. 마지막으로 자기 설정에 대해 돌린 출력을 /root/glci/plan.json 에 저장한다.

참고

스테이지 순서와 첫 잡

/root/glci/.gitlab-ci.yml 을 만든다. 최상위 stagesbuild, test, deploy 를 이 순서로 두고, build-app 잡을 stage: build 로 두어 script 를 하나 이상 적는다.

/root/glci/.gitlab-ci.yml 을 만들고 최상위에 stages 목록을 둡니다. 이 목록의 순서가 곧 기본 실행 순서입니다. 그 아래에 build-app 이라는 최상위 키를 두고 stagescript 를 그 안에 들여씁니다. 채점은 grep 이 아니라 YAML 파서로 읽으니, 들여쓰기가 한 칸만 어긋나도 잡이 다른 키의 하위 항목이 되어 실패합니다.

세 스테이지를 모두 채우기

잡을 더 붙여 세 스테이지를 모두 채운다. lintunit-teststage: test, deploy-stagingstage: deploy 다. 모든 잡에 script 가 있어야 하고, 모든 stage 값은 stages 목록 안에 있어야 한다.

lintunit-testtest 스테이지에, deploy-stagingdeploy 스테이지에 추가합니다. 같은 스테이지에 잡이 둘이면 그 둘은 서로 병렬로 돕니다. stages 목록에 없는 이름을 stage 로 쓰면 GitLab 은 그 설정을 통째로 거부하니 철자를 확인하세요. 모든 잡에는 script 가 있어야 합니다.

숨김 잡과 extends 로 중복 걷어내기

.python-base 라는 숨김 잡을 만들어 imagebefore_script 를 넣고, build-app·lint·unit-test 세 잡이 extends: .python-base 로 물려받게 한다. 물려받은 잡은 image 를 다시 적지 않는다. 최상위 default 에 기본 image 를 둔다.

이름이 점으로 시작하는 키는 실행되지 않는 조각입니다. .python-baseimagebefore_script 를 넣고, build-app·lint·unit-test 세 잡이 extends 로 물려받게 하세요. 물려받았으면 잡에서 image 를 다시 적지 않습니다. 조각을 쓰지 않는 잡을 위해 최상위 default 에 기본 이미지를 둡니다.

needs 로 스테이지 벽 넘기

needs 를 붙여 DAG 를 만든다. lintneeds: [], unit-testneedsbuild-app, deploy-stagingneedsunit-test 를 적는다. build-app 에는 needs 를 두지 않는다.

unit-testneedsbuild-app 만 기다리게 하고, deploy-stagingunit-test 만 기다리게 합니다. lint 에는 needs: [] 를 줍니다 — 키가 없는 것과 빈 목록은 정반대의 뜻이라, 빈 목록이어야 앞 스테이지를 하나도 기다리지 않고 맨 앞에서 출발합니다. build-app 에는 needs 를 두지 않습니다.

rules 로 브랜치 조건과 수동 승인 걸기

deploy-stagingrules 를 붙여 $CI_COMMIT_BRANCH == "main" 일 때 when: on_success 로 두고, 마지막 항목은 조건 없는 when: never 로 닫는다. deploy-prod 잡을 stage: deploy 로 새로 만들어 needsdeploy-staging 을 적고, 같은 브랜치 조건에서 when: manual · allow_failure: false 로 둔 뒤 역시 조건 없는 when: never 로 닫는다. onlyexcept 는 쓰지 않는다.

deploy-staging$CI_COMMIT_BRANCH == "main" 일 때 when: on_success, deploy-prod 는 같은 조건에서 when: manual 로 둡니다. 두 잡 모두 마지막 항목은 조건 없는 when: never 여야 합니다 — rules 는 처음 걸리는 하나만 적용하므로 조건 없는 항목을 가운데 두면 그 아래가 전부 죽습니다. only/except 는 쓰지 않습니다. deploy-proddeploy 스테이지이고 deploy-staging 을 기다립니다.

artifacts 와 받을 사람 정하기

build-appartifacts 를 붙여 pathsexpire_in 을 적는다. unit-testneeds 를 긴 형태로 바꿔 job: build-app · artifacts: true 로, deploy-stagingneedsartifacts: false 로 둔다.

build-appartifacts 를 붙여 paths 로 넘길 디렉터리를 적고 expire_in 도 함께 적습니다. 그리고 needs 를 긴 형태(- job: 이름 / artifacts: true|false)로 바꿔, 산출물을 실제로 쓰는 unit-testtrue 로 받고 순서만 기다리는 deploy-stagingfalse 로 끕니다. 안 쓰는 잡까지 내려받으면 파이프라인이 조용히 느려집니다.

cache 키를 잠금 파일 해시로 잡기

/root/glci/requirements.txt 를 내용 있는 파일로 만든다. build-appunit-testcache 를 붙이되 keyfilesrequirements.txt 를 가리키게 한다. build-apppolicy: pull-push, unit-testpolicy: pull 이다. cache.pathsartifacts.paths 와 겹치지 않게 한다.

먼저 /root/glci/requirements.txt 를 만듭니다 — 캐시 키가 해시할 실제 파일이 있어야 합니다. 그다음 build-appunit-testcache 를 붙이되 key 는 고정 문자열이 아니라 files 형태로 그 파일을 가리키게 하세요. 캐시를 만드는 build-apppolicy: pull-push, 읽기만 하는 unit-testpolicy: pull 입니다. cache.pathsartifacts.paths 와 겹치면 안 됩니다.

파이프라인 해석기로 실행 순서 계산하기

/root/glci/plan.py 를 만든다. 인자로 받은 설정 파일을 읽어 {"stages": [...], "waves": [[...], ...]} 를 표준출력에 JSON 으로 낸다. needs 가 있으면 그 목록만, 없으면 앞 스테이지의 모든 잡을 기다린다(기본 stagetest). 점으로 시작하는 키와 예약 키(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 으로 저장합니다.