CI/CD 개발 가이드라인

CI/CD에 특정한 개발 가이드는 여기에서 나열됩니다:

CI/CD YAML 참조 문서 가이드를 참조하여 CI/CD YAML 구문 참조 페이지를 업데이트하는 방법을 배워보세요.

CI/CD 사용 예시

우리는 ci-sample-projects 그룹을 유지하고 있으며, 이곳에는 GitLab CI/CD의 다양한 사용 사례를 보여주는 .gitlab-ci.yml의 예가 포함된 프로젝트들이 있습니다. 이 프로젝트들은 다양한 시나리오에서 사용할 수 있는 특정 구문도 다룹니다.

CI 아키텍처 개요

아래는 CI 아키텍처의 간단한 다이어그램입니다. 주요 구성 요소에 집중하기 위해 몇 가지 세부 사항은 생략되었습니다.

CI 소프트웨어 아키텍처

왼쪽에는 사용자가 생성하거나 자동화된 프로세스로 여러 이벤트에 따라 파이프라인을 트리거할 수 있는 이벤트가 있습니다:

이러한 이벤트 중 아무 것이나 트리거되면 CreatePipelineService가 호출되어 이벤트 데이터와 이를 트리거한 사용자 정보를 입력으로 받아 파이프라인 생성을 시도합니다.

CreatePipelineServiceYAML Processor(https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/yaml_processor.rb) 컴포넌트에 크게 의존하며, 이는 YAML 블롭을 입력으로 받아 파이프라인의 추상 데이터 구조(스테이지 및 모든 작업 포함)를 반환합니다. 이 컴포넌트는 YAML을 처리하는 동안 구조를 검증하고 구문 또는 의미적 오류를 반환합니다. YAML Processor 컴포넌트에서 파이프라인 구조를 정의하는 데 사용 가능한 모든 키워드를 정의합니다.

CreatePipelineServiceYAML Processor가 반환하는 추상 데이터 구조를 수신하고, 이를 지속 모델(파이프라인, 스테이지 및 작업 등)로 변환합니다. 이후 파이프라인 처리가 준비됩니다. 파이프라인 처리는 실행 순서(스테이지 또는 needs)에 따라 작업을 실행하는 것을 의미하며, 다음 중 하나가 발생할 때까지 진행됩니다:

  • 모든 예상 작업이 실행되었습니다.

  • 실패가 파이프라인 실행을 중단합니다.

파이프라인을 처리하는 컴포넌트는 ProcessPipelineService로, 이는 모든 파이프라인의 작업을 완료 상태로 이동하는 역할을 합니다. 파이프라인이 생성되면 모든 작업이 처음에 created 상태에 있습니다. 이 서비스는 파이프라인 구조를 기반으로 created 상태의 어떤 작업이 처리될 수 있는지를 살펴봅니다. 그런 다음 이 작업들을 pending 상태로 이동시키고, 이는 이제 러너에 의해 선택될 수 있음을 의미합니다. 작업이 실행된 후 성공적으로 완료되거나 실패할 수 있습니다. 파이프라인 내 작업의 각 상태 전환은 이 서비스를 다시 호출하며, 이는 완료를 향해 전환될 다음 작업을 찾습니다. 이 과정을 수행하는 동안 ProcessPipelineService는 작업, 스테이지 및 전체 파이프라인의 상태를 업데이트합니다.

다이어그램의 오른쪽에는 GitLab 인스턴스에 연결된 러너 목록이 있습니다. 이는 공유 러너, 그룹 러너 또는 프로젝트 러너일 수 있습니다. 러너와 Rails 서버 간의 통신은 API 엔드포인트로 그룹화된 러너 API 게이트웨이를 통해 이루어집니다.

우리는 러너를 등록, 삭제 및 검증할 수 있으며, 이는 데이터베이스에 대한 읽기/쓰기 쿼리를 초래합니다. 러너가 연결된 후, 다음 실행할 작업을 요청합니다. 이는 RegisterJobService를 호출하여 다음 작업을 선택하고 러너에 할당하는 과정을 거칩니다. 이 시점에서 작업은 running 상태로 전환되며, 상태 변화로 인해 다시 ProcessPipelineService를 트리거합니다. 더 많은 세부 정보는 작업 스케줄링을 참조하세요.

작업이 실행되는 동안 러너는 서버에 로그와 저장해야 할 모든 가능한 아티팩트를 보냅니다. 또한 작업은 실행을 위해 이전 작업의 아티팩트에 의존할 수 있습니다. 이 경우 러너는 전용 API 엔드포인트를 사용하여 이를 다운로드합니다.

아티팩트는 객체 스토리지에 저장되며, 메타데이터는 데이터베이스에 보관됩니다. 아티팩트의 중요한 예로는 보고서(JUnit, SAST 및 DAST와 같은)가 있으며, 이는 병합 요청에서 파싱되고 렌더링됩니다.

작업 상태 전환은 모두 자동화되어 있지는 않습니다. 사용자는 수동 작업을 실행하거나, 파이프라인을 취소하거나, 특정 실패 작업 또는 전체 파이프라인을 다시 시도할 수 있습니다. 작업의 상태를 변경하는 모든 것은 ProcessPipelineService를 트리거하며, 이는 전체 파이프라인의 상태를 추적하는 역할을 합니다.

특수한 유형의 작업은 브리지 작업으로, 이는 pending 상태로 전환할 때 서버 측에서 실행됩니다. 이 작업은 다단계 프로젝트 또는 자식 파이프라인과 같은 하위 파이프라인을 생성하는 역할을 합니다. 하위 파이프라인이 트리거될 때마다 CreatePipelineService에서 워크플로 루프가 다시 시작됩니다.

아키텍처에 대한 워크스루를 CI 백엔드 아키텍처 워크스루에서 시청할 수 있습니다.

작업 스케줄링

파이프라인이 생성되면 모든 작업이 한 번에 모든 단계에 대해 생성되며, 초기 상태는 created입니다. 이를 통해 파이프라인의 전체 내용을 시각화할 수 있습니다.

created 상태의 작업은 아직 러너에 의해 보이지 않습니다. 작업을 러너에 할당할 수 있도록 하려면, 작업은 먼저 pending 상태로 전이되어야 하며, 이는 다음의 경우에 발생할 수 있습니다:

  1. 작업이 파이프라인의 아주 첫 번째 단계에서 생성되었을 때.
  2. 작업이 수동 시작을 필요로 하며 트리거되었을 때.
  3. 이전 단계의 모든 작업이 성공적으로 완료되었을 때. 이 경우, 우리는 다음 단계의 모든 작업을 pending으로 전이합니다.
  4. 작업이 needs:를 사용하여 의존성을 지정하고 모든 의존 작업이 완료되었을 때.
  5. 작업이 Ci::PipelineCreation::DropNotRunnableBuildsService에 의해 실행할 수 없는 상태로 드롭되지 않았을 때.

러너가 연결되면, 서버에 지속적으로 폴링하여 다음 pending 작업을 실행하도록 요청합니다.

참고:

러너가 GitLab과 상호작용할 때 사용하는 API 엔드포인트는 lib/api/ci/runner.rb에서 정의되어 있습니다.

서버가 요청을 수신하면 Ci::RegisterJobService 알고리즘을 기반으로 pending 작업을 선택한 다음, 작업을 러너에 할당하고 보냅니다.

현재 단계의 모든 작업이 완료되면, 서버는 다음 단계의 모든 작업의 상태를 pending으로 변경하여 “잠금을 해제”합니다. 이제 러너가 새로운 작업을 요청할 때 스케줄링 알고리즘에 의해 선택될 수 있으며, 모든 단계가 완료될 때까지 계속됩니다.

러너와 GitLab 서버 간의 통신

러너가 등록 토큰을 사용하여 등록되면, 서버는 실행할 수 있는 작업의 유형을 알고 있습니다. 이는 다음에 따라 달라집니다:

  • 등록된 러너의 유형:
    • 공유 러너
    • 그룹 러너
    • 프로젝트 러너
  • 연관된 태그.

러너는 POST /api/v4/jobs/request로 실행할 작업을 요청하여 통신을 시작합니다. 폴링이 몇 초마다 발생하지만, 작업 대기열이 변경되지 않으면 서버 측 작업 부하를 줄이기 위해 HTTP 헤더를 통한 캐싱을 활용합니다.

이 API 엔드포인트는 Ci::RegisterJobService를 실행합니다. 이는:

  1. pending 작업 풀에서 다음 실행 작업을 선택합니다.
  2. 작업을 러너에 할당합니다.
  3. API 응답을 통해 러너에 작업을 제공합니다.

Ci::RegisterJobService

이 서비스는 대부분의 작업을 수집하는 데 사용하는 3개의 최상위 쿼리가 있으며, 이는 러너가 등록된 수준에 따라 선택됩니다:

  • 공유 러너(인스턴스 전체)에 대한 작업 선택
    • 실행 중인 빌드가 적은 프로젝트를 우선하는 공정한 스케줄링 알고리즘을 활용합니다.
  • 그룹 러너에 대한 작업 선택
  • 프로젝트 러너에 대한 작업 선택

이 작업 목록은 작업과 러너 태그 간의 일치를 기준으로 추가적으로 필터링됩니다.

참고:

작업에 태그가 포함된 경우, 러너는 모든 태그와 일치하지 않는 작업을 선택하지 않습니다. 러너는 작업에 정의된 태그보다 더 많은 태그를 가질 수 있지만 그 반대는 불가능합니다.

마지막으로, 러너가 태그가 있는 작업만 선택할 수 있는 경우, 모든 태그가 없는 작업은 필터링됩니다.

이 시점에서 나머지 pending 작업을 반복 실행하며, 추가 정책에 따라 러너가 “선택할 수 있는” 첫 번째 작업을 할당하려고 시도합니다. 예를 들어, protected로 표시된 러너는 protected 브랜치(예: 프로덕션 배포)에 대해 실행되는 작업만 선택할 수 있습니다.

pool에 있는 러너 수를 늘리면 상충의 가능성도 증가합니다. 이는 동일한 작업이 다른 러너에 할당될 경우 발생할 수 있습니다. 이를 방지하기 위해 우리는 충돌 오류를 우아하게 처리하고 리스트에서 다음 작업을 할당합니다.

정체된 빌드를 드롭하기

“정체된” 빌드로 표시하고 드롭하는 방법은 두 가지가 있습니다.

  1. 빌드가 생성될 때, Ci::PipelineCreation::DropNotRunnableBuildsService는 작업을 실행할 수 없게 만드는 사전 알려진 조건을 확인합니다:
    • 빌드를 실행하기에 충분한 CI/CD Minutes가 없으면, 빌드는 ci_quota_exceeded로 즉시 드롭됩니다.
    • 미래에, 프로젝트가 빌드에 필요한 런너의 allowed_plans에 있는 플랜에 없으면, 빌드는 no_matching_runner로 즉시 드롭됩니다.
  2. 빌드를 수집할 수 있는 런너가 없는 경우, Ci::StuckBuilds::DropPendingService에 의해 1시간 후에 드롭됩니다.
    • 작업이 런너에 의해 24시간 내에 수집되지 않으면, 그 시간 후에 처리 대기열에서 자동으로 제거됩니다.
    • 대기 중인 작업이 정체되어 있고, 이를 처리할 수 있는 런너가 없는 경우, 1시간 후에 대기열에서 제거됩니다.
    • 두 경우 모두 작업의 상태는 적절한 실패 이유와 함께 failed로 변경됩니다.

이 차이의 이유

컴퓨트 분 단위 쿼터 메커니즘은 작업이 생성될 때 조기에 처리됩니다, 왜냐하면 이는 대부분의 경우 상수적인 결정이기 때문입니다.

프로젝트가 한도를 초과하면, 그 이후의 모든 일치는 다음 달이 시작될 때까지 그것에 적용됩니다.

물론, 프로젝트 소유자는 추가 분을 구매할 수 있지만, 이는 프로젝트가 취해야 하는 수동적인 작업입니다.

allowed_plans에 대한 동일한 메커니즘이 사용될 것입니다.

프로젝트가 요구된 플랜에 없고 작업이 그러한 런너를 목표로 할 경우, 프로젝트 소유자가 구성을 변경하거나 네임스페이스를 요구된 플랜으로 업그레이드할 때까지 지속적으로 실패할 것입니다.

이 두 가지 메커니즘은 또한 매우 SaaS에 특화되어 있으며 동시에 SaaS의 규모를 고려할 때 상당히 계산 비용이 많이 듭니다.

작업이 대기 상태로 전환되기 전에 확인을 수행하고 조기에 실패하는 것이 여기에서 많은 의미를 갖습니다.

왜 다른 대기 및 드롭 작업의 경우를 조기에 처리하지 않을까요?

일부 경우에는, 작업이 대기 중인 이유는 런너가 작업을 수집하는 데 느리기 때문입니다.

이는 GitLab 수준에서 알 수 있는 것이 아닙니다.

런너의 구성 및 용량과 GitLab의 대기열 크기에 따라 작업이 즉시 수집될 수도 있고, 기다려야 할 수도 있습니다.

다른 이유도 있을 수 있습니다:

  • 런너 유지보수를 처리 중이며 일정 기간 사용할 수 없는 경우,
  • 구성을 업데이트하는 중이며 실수로 태깅 및/또는 보호 플래그를 잘못 설정했거나 (혹은 우리의 SaaS 인스턴스 런너의 경우; 잘못된 비용 요소 또는 allowed_plans 구성 할당).

이 모든 것은 일시적인 문제일 수 있으며 대체로 발생할 것으로 예상되지 않으며 조기에 감지되고 수정되기를 기대합니다.

우리는 이러한 조건 중 하나가 발생할 때 작업을 즉시 드롭하고 싶지 않습니다.

런너의 용량이 가득 차 있거나 일시적인 이용 불가/구성 실수로 인해 작업을 드롭하는 것은 사용자에게 매우 해로울 것입니다.

GitLab CI/CD에서 “작업”의 정의

GitLab CI 맥락에서 “작업”은 지속적인 통합, 배포 및 배급을 추진하는 작업을 의미합니다.

일반적으로 파이프라인은 여러 단계로 구성되며, 각 단계는 여러 작업을 포함합니다.

Active Record 모델링에서는 작업이 CommitStatus 클래스로 정의됩니다.

그 위에, 우리는 다음과 같은 유형의 작업을 가지고 있습니다:

  • Ci::Build … 러너에 의해 실행될 작업입니다.

  • Ci::Bridge … 하위 파이프라인을 트리거하는 작업입니다.

  • GenericCommitStatus … 예를 들어 Jenkins와 같은 외부 CI/CD 시스템에서 실행될 작업입니다.

코드베이스에서 “작업” 용어를 사용하면 독자는 해당 클래스/객체가 위의 어떤 유형인지 가정하게 됩니다.

Ci::Build 클래스에 특별히 언급할 경우, 객체/클래스를 “작업”이라고 이름짓지 않아야 합니다. 이는 혼란을 초래할 수 있습니다. 문서에서는 “작업”을 일반적으로 사용해야 하며, “빌드”는 사용하지 말아야 합니다.

우리 코드베이스에는 리팩토링이 필요한 몇 가지 불일치가 있습니다.

예를 들어, CommitStatusCi::Job이어야 하며, Ci::JobArtifactCi::BuildArtifact여야 합니다.

전체 리팩토링 계획은 이 문제를 참조하세요.

컴퓨트 쿼터

  • GitLab 16.1에서 “CI/CD 분”에서 “컴퓨트 쿼터” 및 “컴퓨트 분”으로 이름이 변경되었습니다.

이 다이어그램은 컴퓨트 쿼터 기능과 그 구성 요소가 어떻게 작동하는지를 보여줍니다.

컴퓨트 쿼터 아키텍처

아래 비디오에서 이 기능에 대한 자세한 설명을 시청하세요.

비디오 보기: CI/CD 분 - 아키텍처 개요.