CI/CD 개발 가이드라인
CI/CD에 특화된 개발 가이드는 여기에 나열되어 있습니다:
- 새 CI/CD 템플릿을 생성하는 경우 GitLab CI/CD 템플릿 개발 가이드를 읽어보세요.
- 새 키워드를 추가하거나 CI 스키마를 변경하는 경우 다음 가이드를 참조하세요:
CI/CD YAML 참조 문서 가이드를 참조하여 CI/CD YAML 구문 참조 페이지의 업데이트 방법을 학습하세요.
CI/CD 사용 예시
다양한 GitLab CI/CD 사용 사례에 대한 .gitlab-ci.yml
의 예시를 보여주는 프로젝트를 포함한 ci-sample-projects
그룹을 유지 관리하고 있습니다. 또한 다양한 시나리오에 사용될 수 있는 특정 구문을 다룹니다.
CI 아키텍처 개요
다음은 CI 아키텍처의 단순화된 다이어그램입니다. 주요 구성 요소에 중점을 두기 위해 일부 세부 사항은 생략되었습니다.
왼쪽에는 사용자 또는 자동화에 의해 일어나는 다양한 이벤트를 기반으로 파이프라인을 트리거할 수 있는 이벤트가 있습니다:
-
git push
는 파이프라인을 트리거하는 가장 흔한 이벤트입니다. - Web API.
- 사용자가 UI에서 “파이프라인 실행” 버튼을 선택하는 경우.
- 병합 요청이 생성 또는 업데이트될 때.
- MR이 병합 트레인에 추가될 때.
- 예약된 파이프라인이 실행될 때.
- 프로젝트가 상위 프로젝트에 구독되었을 때.
- Auto DevOps가 활성화되었을 때.
- GitHub 통합이 외부 풀 요청과 함께 사용될 때(../../ci/ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests).
- 상위 파이프라인에 브릿지 작업이 포함된 경우 이는 하위 파이프라인을 트리거하게 됩니다.
이러한 이벤트 중 하나를 트리거하면 입력 이벤트 데이터와 해당 이벤트를 트리거하는 사용자를 입력으로 받아들이고 파이프라인을 만들려고 시도하는 CreatePipelineService
가 호출됩니다.
CreatePipelineService
는 주요 파이프라인(단계 및 모든 작업 포함)의 추상 데이터 구조를 입력으로 받아들이고 반환하는 책임을 지고 있는 YAML Processor
구성요소에 많이 의존합니다. 이 구성요소는 입력으로 YAML blob를 받아들여 파이프라인의 추상 데이터 구조(단계 및 모든 작업 포함)를 반환하고 해당 구조를 처리하는 동안 YAML의 구조를 유효성 검사하며 구문 또는 의미론적 오류를 반환합니다. YAML Processor
구성요소는 파이프라인을 구성하는 데 사용할 수 있는 모든 키워드를 정의하는 곳입니다.
CreatePipelineService
는 YAML Processor
에서 반환된 추상 데이터 구조를 수신하고 이를 지속되는 모델(파이프라인, 단계, 작업 등)으로 변환합니다. 그러고 나면 파이프라인이 처리 준비가 됩니다. 파이프라인을 처리하는 것은 실행 순서(단계 또는 needs
별)로 작업을 실행(또는 needs
)하는 것을 의미하며 다음 중 하나가 발생할 때까지 계속됩니다:
- 예상 작업이 모두 실행될 때까지.
- 실패로 인해 파이프라인 실행이 중단될 때.
파이프라인을 처리하는 구성요소는 ProcessPipelineService
이며, 파이프라인의 모든 작업을 완료된 상태로 이동하는 책임을 지고 있습니다. 파이프라인이 생성되면 해당 작업은 초기에 created
상태입니다. 해당 서비스는 파이프라인 구조에 따라 처리 가능한 created
단계의 작업들을 살펴봅니다. 그런 다음 해당 작업들을 지금은 러너에 의해 수행될 수 있게 하기 위해 pending
상태로 이동시킵니다. 작업이 실행된 후 성공적으로 완료되거나 실패할 수 있습니다. 파이프라인 내 작업의 각 상태 전환은 다시 이 서비스를 트리거하게 하며, 다음 작업이 완료 상태로 전환되기 위해 확인됩니다. 이러한 과정에서 ProcessPipelineService
는 작업, 단계 및 전체 파이프라인의 상태를 업데이트합니다.
다이어그램의 오른쪽에는 GitLab 인스턴스에 연결된 러너 목록이 있습니다. 이는 공유 러너, 그룹 러너 또는 프로젝트 러너일 수 있습니다. 러너와 레일 서버 간의 통신은 러너 API 게이트웨이
로 그룹화된 API 엔드포인트 집합을 통해 수행됩니다.
러너를 등록, 삭제 및 확인할 수 있으며, 이는 데이터베이스에 읽기/쓰기 쿼리를 발생시킵니다. 러너가 연결된 후에는 다음 실행해야할 작업을 요청합니다. 이는 RegisterJobService
를 호출하게 되며, 해당 서비스는 다음 작업을 선택하고 해당 작업을 러너에 할당합니다. 이 시점에서 해당 작업은 running
상태로 전환되며, 상태 변경으로 인해 다시 ProcessPipelineService
가 트리거됩니다. 자세한 내용은 작업 스케줄링을 참조하세요.
작업이 실행되는 동안 러너는 로그와 저장해야 하는 가능한 파일 등을 서버로 보내고, 작업이 이전 작업의 결과물에 의존하는 경우 해당 결과물을 전용 API 엔드포인트를 사용하여 다운로드합니다.
결과물은 객체 저장소에 저장되며, 메타데이터는 데이터베이스에 유지됩니다. 결과물의 중요한 예시는 병합 요청에서 파싱 및 렌더링되는 보고서(JUnit, SAST 및 DAST 등)입니다.
작업 상태 전환은 모두 자동화되는 것은 아닙니다. 사용자는 수동 작업, 파이프라인을 취소, 특정 실패한 작업 또는 전체 파이프라인을 다시 시도하는 등 작업이 상태를 변경하는 경우 ProcessPipelineService
를 트리거합니다. 해당 서비스는 전체 파이프라인의 상태를 추적하는 책임을 지고 있습니다.
특별한 종류의 작업은 서버 측에서 실행되는 브릿지 작업으로, pending
상태로 전환될 때 실행됩니다. 이 작업은 다운스트림 파이프라인(다중 프로젝트 또는 자식 파이프라인과 같은)을 생성하는 책임을 지고 있습니다. 다운스트림 파이프라인이 트리거될 때마다 워크플로 루프는 다시 CreatePipelineService
에서 시작됩니다.
CI 백엔드 아키텍처 워크스루를 시청할 수 있습니다.
작업 스케줄링
Pipeline이 생성되면 모든 작업이 모든 단계에 대해 한꺼번에 created
상태로 생성됩니다. 이로써 파이프라인의 전체 내용을 시각화하는 것이 가능해집니다.
created
상태의 작업은 아직 runner에 의해 볼 수 없습니다. 작업을 runner에 할당할 수 있게 하려면, 작업은 먼저 pending
상태로 전환되어야 하는데, 이는 다음과 같은 경우에 발생할 수 있습니다.
- 작업이 파이프라인의 맨 처음 단계에 생성될 때.
- 작업이 수동으로 시작되어 트리거된 경우.
- 이전 단계의 모든 작업이 성공적으로 완료된 경우. 이 경우, 우리는 다음 단계의 모든 작업을
pending
로 전환합니다. - 작업이
needs:
를 사용하여 의존성을 지정하고 모든 종속 작업이 완료된 경우. - 작업이 러너에 의해 실행 가능하지 않은 상태로 인해
Ci::PipelineCreation::DropNotRunnableBuildsService
에 의해 dropped되지 않은 경우.
러너가 연결되면, 서버에 지속적으로 polling하여 다음 pending
작업을 실행할 것을 요청합니다.
참고:
러너가 GitLab과 상호작용하는 데 사용하는 API 엔드포인트는 lib/api/ci/runner.rb
에서 정의됩니다.
서버가 요청을 받은 후, Ci::RegisterJobService
알고리즘에 따라 pending
작업을 선택하고 러너에게 할당하고 보냅니다.
러너와 GitLab 서버 간 통신
러너가 등록 토큰을 사용하여 등록되면, 서버는 실행할 수 있는 작업 유형을 알게 됩니다. 이는 다음에 따라 달라집니다:
- 등록된 러너의 유형:
- 공유 러너
- 그룹 러너
- 프로젝트 러너
- 관련된 태그.
러너는 POST /api/v4/jobs/request
로 실행할 작업을 요청함으로써 통신을 시작합니다. 몇 초마다 polling이 발생하지만, 작업 대기열이 변경되지 않았을 경우, HTTP 헤더를 통하여 캐싱을 활용하여 서버 측 작업 부하를 줄입니다.
이 API 엔드포인트는 Ci::RegisterJobService
를 실행하며, 다음을 수행합니다:
-
pending
작업 풀에서 실행할 다음 작업을 선택합니다. - 해당 작업을 러너에 할당합니다.
- API 응답을 통해 러너에 제시합니다.
Ci::RegisterJobService
이 서비스에서 사용하는 쿼리 중 상위 수준 쿼리가 3개 있으며, 러너가 등록된 수준에 따라 선택됩니다:
- 공유 러너(instance-wide)용 작업 선택
- 더 적은 실행 작업을 가진 프로젝트를 우선하여 공정한 스케줄링 알고리즘을 활용함
- 그룹 러너용 작업 선택
- 프로젝트 러너용 작업 선택
이 작업 목록은 작업과 러너의 태그 간 일치에 따라 더 필터링됩니다.
참고: 작업에 태그가 포함된 경우, 러너는 해당 태그를 모두 만족하지 않는 경우 작업을 선택하지 않습니다. 러너는 작업에 정의된 태그보다 더 많은 태그를 가질 수 있지만, 반대는 아닙니다.
마지막으로, 러너가 태그가 있는 작업만 선택해야 하는 경우, 태그가 없는 모든 작업이 필터링됩니다.
이 시점에서 남은 pending
작업을 반복하며, 러너가 추가 정책에 따라 “선택”할 수 있는 첫 번째 작업을 할당하려고 시도합니다. 예를 들어, protected
로 표시된 러너의 경우, 작업은 보호된 브랜치(예: 프로덕션 배포)에 대해 실행해야 합니다.
풀에 있는 러너 수를 증가시키면, 서로 다른 러너에 동일한 작업을 할당하는 경우 충돌 가능성이 증가합니다. 이를 방지하기 위해 동일한 작업을 서로 다른 러너에 할당하는 충돌 오류를 우아하게 처리하고 목록에서 다음 작업을 할당합니다.
막힌 빌드 삭제
빌드를 “막힘”으로 표시하고 삭제하는 두 가지 방법이 있습니다.
- 빌드가 생성될 때,
Ci::PipelineCreation::DropNotRunnableBuildsService
는 작업이 실행 불가능하게 하는 미리 알려진 조건을 확인합니다:- 빌드를 실행할 만큼의 CI/CD Minutes가 충분하지 않은 경우, 빌드는 즉시
ci_quota_exceeded
로 삭제됩니다. -
앞으로, 빌드에 필요한 계획이 프로젝트에 없을 경우
allowed_plans
를 통해 사용 가능한 러너에 대한 계획이 지정된 경우, 빌드는 즉시no_matching_runner
로 삭제됩니다.
- 빌드를 실행할 만큼의 CI/CD Minutes가 충분하지 않은 경우, 빌드는 즉시
- 실행 가능한 러너가 없는 경우, 빌드는 1시간 후에
Ci::StuckBuilds::DropPendingService
에 의해 삭제됩니다.- 러너가 24시간 안에 작업을 선택하지 않으면 해당 시간을 지나고나서 처리 대기열에서 자동으로 제거됩니다.
- 만약
stuck
상태의 대기 중인 작업이 있을 경우, 해당 상태에서 실행할 수 있는 러너가 없는 경우, 1시간 후에 대기열에서 제거됩니다. - 두 가지 경우 모두 작업의 상태가
failed
로 변경되고 적절한 실패 이유로 표시됩니다.
이 차이의 이유
컴퓨팅 시간 할당량 메커니즘은 대부분 시간에 대해 일정한 결정이기 때문에, 작업이 생성될 때 미리 처리됩니다. 프로젝트가 한도를 초과하는 경우, 그 다음 달이 시작될 때까지 해당 작업이 계속 적용됩니다. 물론, 프로젝트 소유자는 추가 시간을 구입할 수 있지만, 이는 프로젝트가 수동으로 취해야 하는 조치입니다.
allowed_plans을 위한 동일한 메커니즘은 곧 사용될 것입니다. 프로젝트가 필요한 계획에 없고 작업이 해당 러너를 대상으로 하면, 프로젝트 소유자가 구성을 변경하거나 네임스페이스를 필요한 계획으로 업그레이드할 때까지 해당 작업은 지속적으로 실패할 것입니다.
이 두 가지 메커니즘은 SaaS에 특화되어 있으면서 매우 컴퓨트가 많이 필요한 경우입니다. 작업을 대기 중이기 전에 조기에 확인하고 실패하는 것은 여기에서 매우 타당합니다.
대기 중이거나 작업을 조기에 포기하는 다른 경우를 왜 처리하지 않는 이유는 무엇인가요? 어떤 경우에는 러너가 작업을 수행하는 데 시간이 오래 걸려서 작업이 대기 중인 경우도 있습니다. 이는 GitLab 수준에서 알 수 없는 일입니다. GitLab 큐의 크기에 따라 러너의 구성 및 용량에 따라 작업이 즉시 실행될 수도 있고 대기해야 할 수도 있습니다.
다른 이유도 있을 수 있습니다.
- 러너 유지 관리를 수행하고 있고 잠시 작동하지 않는 경우,
- 구성을 업데이트하고 실수로 태깅 및/또는 보호된 플래그를 엉망으로 만든 경우 (또는 당사의 SaaS 인스턴스 러너의 경우; 잘못된 비용 요소 또는
allowed_plans
구성을 할당한 경우).
위의 모든 것은 일시적으로 발생할 수 있는 문제이며 대부분 예상하지 못하고 조기에 감지하고 수정할 것으로 기대되는 문제입니다. 이러한 조건 중 하나가 발생하는 경우 즉시 작업을 삭제하고 싶지는 않습니다. 러너가 가용 용량에 도달했거나 일시적인 사용 불가능/구성 오류 때문에 작업을 즉시 삭제하는 것은 사용자에게 매우 해로울 것입니다.
GitLab CI/CD에서 “Job”의 정의
GitLab CI 컨텍스트에서의 “Job”은 지속적 통합, 전달 및 배포를 진행하는 작업을 의미합니다. 일반적으로 파이프라인에는 여러 스테이지가 포함되며, 스테이지에는 여러 개의 작업이 포함됩니다.
액티브 레코드 모델링에서는 Job이 CommitStatus
클래스로 정의됩니다.
또한, 다음과 같은 유형의 작업이 있습니다:
-
Ci::Build
… 실행할 작업을 지원하는 작업입니다. -
Ci::Bridge
… 하류 파이프라인을 트리거하는 작업입니다. -
GenericCommitStatus
… 외부 CI/CD 시스템에서 실행되는 작업으로, 예를 들어 젠킨스가 있습니다.
코드베이스에서 “Job” 용어를 사용할 경우, 독자들은 해당 클래스/객체가 위의 어떤 유형인지를 가정하게 됩니다.
특히 Ci::Build
클래스를 명시적으로 참조한다면, “job”으로 클래스/객체를 명명해서 혼란을 초래할 수 있습니다. 문서에서는 “빌드” 대신 일반적으로 “Job”을 사용해야 합니다.
코드베이스에는 몇 가지 일관성이 없는 부분이 있어 개선이 필요합니다.
예를 들어, CommitStatus
는 Ci::Job
이 되어야 하며, Ci::JobArtifact
은 Ci::BuildArtifact
가 되어야 합니다.
상세한 개선 계획은 이 이슈에서 확인할 수 있습니다.
계산 할당량
- “CI/CD minutes”가 “계산 할당량” 및 “계산 분”으로 GitLab 16.1에서 변경되었습니다.
이 다이어그램은 계산 할당량 기능과 그 구성 요소가 작동하는 방식을 보여줍니다.
이 기능에 대한 자세한 걸음마를 아래 동영상에서 확인하세요.