Only one of three services changed, yet all were rebuilt
한국어 원문으로 표시합니다.
목표
parallel:matrix 로 조합만큼 잡을 늘리고 예외 조합과 특정 조합 기다리기를 다룬 뒤, 모노레포를 서비스별 자식 파이프라인과 저장소 구조에서 생성하는 동적 파이프라인으로 나눠 gitlab-ci-local 로 실행합니다.
왜 중요한가
설정이 커지는 방식은 대개 복붙입니다. 파이썬 버전이 하나 늘 때마다 잡을 복사하고, 서비스가 하나 늘 때마다 부모 파일에 블록을 붙이다 보면 한 파일에 모든 팀의 규칙이 쌓이고 모든 변경이 모든 서비스를 다시 빌드합니다. matrix 는 조합을 한 블록으로 줄이고, 자식 파이프라인은 서비스의 설정을 그 서비스의 디렉터리로 돌려보내며, changes 와 동적 생성은 바뀐 것만 돌게 합니다. 대신 잡 이름·산출물·변수가 어떻게 흘러가는지를 알아야 needs 와 규칙을 바르게 걸 수 있습니다.
단계
/root/glci-mono을 git 저장소로 만들고(.gitignore 에.gitlab-ci-local/).gitlab-ci.yml에 stages[build, package, trigger, generate, dynamic]와 잡build(stage build)를 두세요.parallel: matrix로PY는"3.11"·"3.12",OS는linux·alpine조합을 만들고, 스크립트는dist/py$PY-$OS.txt에py=<PY> os=<OS>를 쓰며 artifacts 로dist/를 올립니다. 커밋하고 실행하세요. 잡 이름이build: [3.11,linux]꼴로 넷 생겨야 합니다.- build 에 rules 를 더해
OS가alpine이고PY가"3.11"인 조합만when: never, 나머지는when: on_success로 두세요. 커밋하고 실행하면 잡이 셋만 돌아야 합니다. - 잡
package-linux(stage package)를 더해 needs 로 build 의PY: "3.12",OS: linux조합 하나만 가리키고(needs:parallel:matrix) 스크립트는ls dist로 하세요. 커밋하고 실행하면 package-linux 로그에py3.12-linux.txt하나만 보여야 합니다. /root/glci-mono/services/api/ci.yml에 잡api-unit(echo "api unit svc=$SVC")을 두고, 부모에 잡trigger-api(stage trigger)를 더해 변수SVC: api와trigger: include: services/api/ci.yml을 두세요. 커밋하고 실행하면 자식 파이프라인의 api-unit 로그가api unit svc=api여야 합니다.services/web/ci.yml에 잡web-unit(echo "web unit svc=$SVC")을 두고 부모에trigger-web(SVC web)을 더하세요. 두 trigger 잡에는 각각rules: changes:로services/api/**/*·services/web/**/*를 둡니다./root/glci-mono-origin.git에 bare 저장소를 만들어 origin 으로 등록하고, 커밋한 main 을 push 한 뒤git remote set-head origin main을 실행하세요. 채점기는 사본에서 web 만 고친 브랜치를 만들어 trigger-web 만 목록에 있는지 봅니다./root/glci-mono/scripts/generate.sh가services/아래 디렉터리마다 잡lint-<이름>(echo "lint <이름>")을 담은 YAML 을 표준 출력으로 내게 하세요. 부모에 잡generate(stage generate)가 그 출력을generated.yml로 저장해 artifacts 로 올리고, 잡run-generated(stage dynamic,needs: [generate])가trigger: include: - artifact: generated.yml, job: generate로 그것을 자식 파이프라인으로 돌리게 합니다. 커밋·push 하고 실행하면 lint-api·lint-web 이 돌아야 합니다./root/glci-mono/services/billing/에ci.yml대신README.md(내용 아무것이나)만 만들어 커밋·push 하세요. 부모.gitlab-ci.yml은 고치지 않습니다. 실행하면 동적 자식 파이프라인에lint-billing이 새로 생겨 돌아야 합니다. 채점기는 사본에 서비스 디렉터리를 하나 더 만들어서도 확인합니다.
참고
- 이 VM 에는 GitLab 서버와 러너가 없고, gitlab-ci-local 4.75.1 이 .gitlab-ci.yml 을 GitLab 과 같은 규칙으로 해석해 shell 로 잡을 실행합니다.
image:를 적으면 도커로 돌리려 하므로 쓰지 않습니다. 보호 변수·마스킹·CI_JOB_TOKEN·러너 태그·병합 요청 파이프라인 생성은 서버 기능이라 여기서 재현되지 않습니다. - 실행: 저장소 루트에서
gitlab-ci-local --shell-isolation --no-artifacts-to-source(잡마다 따로 된 작업 디렉터리, 산출물을 저장소에 되쓰지 않음), 잡 목록:gitlab-ci-local --list-csv-all, 해석된 설정:gitlab-ci-local --preview. gitlab-ci-local 은 git 이 추적하는 파일만 잡에 넘기므로 파일을 만들면git add하세요. 채점기는 저장소를 사본으로 떠 모든 파일을 커밋한 뒤 같은 도구로 다시 돌립니다. - gitlab-ci-local 의 차이(실측): 자식 파이프라인은 실험 기능이고 trigger:strategy 를 따르지 않아 자식이 실패해도 trigger 잡이 성공으로 끝납니다.
--variable은 자식에게 넘어가지 않고 trigger 잡의 variables 만 넘어갑니다. - 자식 파이프라인 잡의 로그도
.gitlab-ci-local/output/<잡이름>.log에 남습니다. - parallel:matrix(Job control) · Downstream pipelines · needs · Specify when jobs run with rules
파이썬 둘·OS 둘, 잡 넷을 한 블록으로
/root/glci-mono 을 git 저장소로 만들고(.gitignore 에 .gitlab-ci-local/) .gitlab-ci.yml 에 stages [build, package, trigger, generate, dynamic] 와 잡 build(stage build)를 두세요. parallel: matrix 로 PY 는 "3.11"·"3.12", OS 는 linux·alpine 조합을 만들고, 스크립트는 dist/py$PY-$OS.txt 에 py=<PY> os=<OS> 를 쓰며 artifacts 로 dist/ 를 올립니다. 커밋하고 실행하세요. 잡 이름이 build: [3.11,linux] 꼴로 넷 생겨야 합니다.
matrix 의 한 항목 안에 여러 변수를 적으면 모든 조합이 잡이 됩니다. 버전 값은 따옴표로 감싸야 3.10 이 3.1 이 되는 일이 없습니다. 목록은 gitlab-ci-local --list-csv-all 로 봅니다.
지원하지 않는 조합 하나만 뺀다
build 에 rules 를 더해 OS 가 alpine 이고 PY 가 "3.11" 인 조합만 when: never, 나머지는 when: on_success 로 두세요. 커밋하고 실행하면 잡이 셋만 돌아야 합니다.
matrix 변수는 rules:if 에서 보통 CI/CD 변수처럼 쓸 수 있습니다. 조합을 빼려고 matrix 를 두 항목으로 쪼개면 늘어날 때마다 목록을 다시 계산해야 하지만, 규칙으로 빼면 예외만 적으면 됩니다.
조합 하나의 산출물만 기다리고 받는다
잡 package-linux(stage package)를 더해 needs 로 build 의 PY: "3.12", OS: linux 조합 하나만 가리키고(needs:parallel:matrix) 스크립트는 ls dist 로 하세요. 커밋하고 실행하면 package-linux 로그에 py3.12-linux.txt 하나만 보여야 합니다.
matrix 로 생긴 잡을 needs 에 이름 그대로(build: [3.12,linux]) 적는 대신 parallel:matrix 로 변수 값을 적어 가리킵니다. 가리킨 조합의 산출물만 받습니다.
서비스 설정은 서비스 디렉터리에 둔다
/root/glci-mono/services/api/ci.yml 에 잡 api-unit(echo "api unit svc=$SVC")을 두고, 부모에 잡 trigger-api(stage trigger)를 더해 변수 SVC: api 와 trigger: include: services/api/ci.yml 을 두세요. 커밋하고 실행하면 자식 파이프라인의 api-unit 로그가 api unit svc=api 여야 합니다.
trigger 잡은 스크립트 대신 다른 파이프라인을 만듭니다. include 로 가리킨 파일이 자식 파이프라인 전체 설정이 되고, trigger 잡의 variables 는 자식에게 넘어갑니다. 서비스 팀이 자기 디렉터리의 파일만 고치면 되게 하는 구조입니다.
바뀐 서비스의 파이프라인만 켠다
services/web/ci.yml 에 잡 web-unit(echo "web unit svc=$SVC")을 두고 부모에 trigger-web(SVC web)을 더하세요. 두 trigger 잡에는 각각 rules: changes: 로 services/api/**/*·services/web/**/* 를 둡니다. /root/glci-mono-origin.git 에 bare 저장소를 만들어 origin 으로 등록하고, 커밋한 main 을 push 한 뒤 git remote set-head origin main 을 실행하세요. 채점기는 사본에서 web 만 고친 브랜치를 만들어 trigger-web 만 목록에 있는지 봅니다.
모노레포에서 모든 서비스를 매번 빌드하면 파이프라인 시간이 서비스 수만큼 늘어납니다. changes 는 원격 기본 브랜치와 비교한 변경을 보므로(gitlab-ci-local 기준) 원격이 있어야 합니다.
저장소 구조를 읽어 자식 파이프라인을 만든다
/root/glci-mono/scripts/generate.sh 가 services/ 아래 디렉터리마다 잡 lint-<이름>(echo "lint <이름>")을 담은 YAML 을 표준 출력으로 내게 하세요. 부모에 잡 generate(stage generate)가 그 출력을 generated.yml 로 저장해 artifacts 로 올리고, 잡 run-generated(stage dynamic, needs: [generate])가 trigger: include: - artifact: generated.yml, job: generate 로 그것을 자식 파이프라인으로 돌리게 합니다. 커밋·push 하고 실행하면 lint-api·lint-web 이 돌아야 합니다.
자식 파이프라인의 설정을 잡이 실행 중에 만들 수 있습니다. 서비스가 늘 때마다 부모 설정을 고치는 대신 규칙(디렉터리 = 잡)을 스크립트에 둡니다. 생성된 설정 안의 include 에는 CI/CD 변수를 쓸 수 없다는 제약이 공식 문서에 있습니다.
서비스를 하나 더해도 부모 설정은 그대로
/root/glci-mono/services/billing/ 에 ci.yml 대신 README.md(내용 아무것이나)만 만들어 커밋·push 하세요. 부모 .gitlab-ci.yml 은 고치지 않습니다. 실행하면 동적 자식 파이프라인에 lint-billing 이 새로 생겨 돌아야 합니다. 채점기는 사본에 서비스 디렉터리를 하나 더 만들어서도 확인합니다.
동적 파이프라인의 규칙이 저장소 구조에 있으므로, 디렉터리가 생기는 것만으로 잡이 생깁니다. 반대로 services 아래에 서비스가 아닌 디렉터리를 두면 그것도 잡이 된다는 뜻이라 규칙을 문서로 남겨 두어야 합니다.