CI/CD 개발 가이드라인

CI/CD에 특화된 개발 가이드는 여기에 나열되어 있습니다.

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

CI/CD 사용 예시

우리는 다양한 GitLab CI/CD 사용 사례에 대한 .gitlab-ci.yml의 예시를 보여주는 프로젝트가 포함된 ci-sample-projects 그룹을 유지 보수합니다. 또한 각각의 사용 사례에 대해 사용될 수 있는 특정 구문을 다룹니다.

CI 아키텍처 개요

다음은 CI 아키텍처의 간소화된 다이어그램입니다. 주요 컴포넌트에 중점을 두기 위해 일부 세부 사항은 생략되었습니다.

CI 소프트웨어 아키텍처

다이어그램의 왼쪽에는 사용자 또는 자동화에 의해 발생하는 여러 이벤트에 기반하여 파이프라인을 트리거하는 이벤트가 있습니다:

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

CreatePipelineService는 주로 입력으로 YAML 블롭을 사용하고 파이프라인의 추상 데이터 구조(단계 및 모든 잡 포함)를 반환하는 YAML Processor 컴포넌트에 의존합니다. 이 컴포넌트는 YAML의 구조를 확인하고 처리하면서 구문 또는 의미론적 오류를 반환합니다. YAML Processor 컴포넌트는 파이프라인을 구성하는 데 사용할 수 있는 모든 키워드를 정의하는 곳입니다.

CreatePipelineServiceYAML Processor에 의해 반환된 추상 데이터 구조를 받아들이고, 이를 지속 모델(pipeline, 단계 및 작업과 같은)로 변환합니다. 그런 다음 파이프라인은 처리할 준비가 됩니다. 파이프라인을 처리하는 것은 실행 순서(단계 또는 DAG)대로 작업을 실행하여 다음 중 하나가 발생할 때까지 계속됩니다.

  • 예상한 작업이 모두 실행됨.
  • 실패로 인해 파이프라인 실행이 중단됨.

파이프라인을 처리하는 컴포넌트는 ProcessPipelineService이며, 파이프라인의 모든 작업을 완료 상태로 이동하는 데 사용됩니다. 파이프라인이 생성되면 모든 작업은 초반에 created 상태에 있습니다. 해당 서비스는 다음 단계의 모든 작업이 파이프라인 구조를 기반으로 처리 가능한지를 확인합니다. 그런 다음이 작업을 pending 상태로 이동시켜 작업이 이제 러너(runner)에 의해 가져갈 수 있음을 의미합니다. 작업이 실행되고 성공적으로 완료되거나 실패합니다. 파이프라인 내 작업의 각 상태 전환은 이 서비스를 다시 트리거하며, 완료 방향으로 전환될 다음 작업을 찾습니다. 이 과정에서 ProcessPipelineService는 작업, 단계 및 전체 파이프라인의 상태를 업데이트합니다.

이 다이어그램의 오른쪽에는 GitLab 인스턴스에 연결된 러너(runner) 디렉터리이 있습니다. 이러한 러너(runner)는 공유 러너(runner), 그룹 러너(runner) 또는 프로젝트 러너(runner)일 수 있습니다. 러너(runner)와 Rails 서버 간의 통신은 Runner API Gateway로 그룹화된 API 엔드포인트 집합을 통해 발생합니다.

러너(runner)를 등록, 삭제 및 확인할 수 있으며, 이로 인해 데이터베이스에 읽기/쓰기 쿼리가 발생합니다. 러너(runner)가 연결된 후에는 다음 작업을 실행할 것을 계속 요청합니다. 이것은 RegisterJobService가 호출되어 다음 작업을 선택하고 러너에 할당합니다. 이 시점에서 작업은 running 상태로 전환되며, 상태 변경으로 인해 다시 ProcessPipelineService가 트리거됩니다. 더 자세한 내용은 작업 스케줄링을 참조하세요.

작업 실행 중에 러너(runner)는 로그와 저장해야 하는 가능한 결과물을 서버로 보내며, 작업은 이전 작업에 대한 결과물에 의존할 수 있습니다. 이 경우 러너는 전용 API 엔드포인트를 사용하여 이를 다운로드합니다.

결과물은 객체 리포지터리에 저장되고 메타데이터는 데이터베이스에 유지됩니다. 결과물의 중요한 예는 Merge Request에서 파싱되고 렌더링되는 보고서(예: JUnit, SAST, DAST)입니다.

작업 상태 전환은 모두 자동으로 이루어지지는 않습니다. 사용자는 매뉴얼 작업을 실행, 파이프라인을 취소하거나 특정 실패한 작업 또는 전체 파이프라인을 다시 시도할 수 있습니다. 작업의 상태 변화를 일으키는 모든 것은 파이프라인 전체의 상태를 추적하는 데 책임이 있는 ProcessPipelineService라고 할 수 있습니다.

브릿지 잡은 서버 측에서 실행되는 특수 유형의 작업으로, pending 상태로 전환될 때 실행됩니다. 이 작업은 다운스트림 파이프라인(다중 프로젝트 또는 하위 파이프라인과 같은)을 생성하는 것이 책임입니다. 다음 다운스트림 파이프라인이 트리거될 때마다 CreatePipelineService에서의 워크플로 루프가 다시 시작됩니다.

CI Backend Architectural Walkthrough에서 아키텍처 워크스루를 시청할 수 있습니다.

작업 스케줄링

파이프라인이 생성되면 모든 단계의 모든 작업이 한꺼번에 created 상태로 생성됩니다. 이로써 파이프라인의 전체 내용을 시각화할 수 있게 됩니다.

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

  1. 작업이 파이프라인의 맨 처음 단계에 생성된 경우.
  2. 작업이 매뉴얼으로 시작되어야 하고 트리거되었을 경우.
  3. 이전 단계의 모든 작업이 성공적으로 완료된 경우. 이 경우 다음 단계의 모든 작업을 pending 상태로 전환합니다.
  4. 작업이 needs:를 사용하여 DAG 의존성을 지정하고 모든 종속 작업이 완료된 경우.
  5. Ci::PipelineCreation::DropNotRunnableBuildsService에 의해 실행되지 않는 상태 때문에 버릴 작업되지 않았을 경우.

러너가 연결되면 서버가 지속적으로 폴링하여 다음 pending 작업을 실행할지를 확인합니다.

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

서버가 요청을 받으면 Ci::RegisterJobService 알고리즘에 따라 pending 작업을 선택하고 러너에게 할당하여 작업을 보냅니다.

현재 단계에 있는 모든 작업이 완료되면 서버는 다음 단계의 모든 작업을 pending로 변경하여 모든 단계가 완료될 때까지 새로운 작업을 러너가 선택할 수 있도록 해제합니다.

runner와 GitLab 서버 간의 통신

러너(runner)가 등록되면, 서버는 어떤 유형의 작업을 실행할 수 있는지 알게 됩니다. 이는 다음에 따라 달라집니다:

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

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

이 API 엔드포인트는 Ci::RegisterJobService를 실행하는데, 이는 다음과 같은 작업을 수행합니다:

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

Ci::RegisterJobService

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

  • 공유 러너용 작업 선택 (인스턴스 수준)
    • 실행 중인 빌드가 적은 프로젝트를 우선시하는 공정한 스케줄링 알고리즘 사용
  • 그룹 러너용 작업 선택
  • 프로젝트 러너용 작업 선택

그런 다음 이 작업 디렉터리은 작업 및 러너 태그 간의 일치를 통해 더 필터링됩니다.

note
작업에 태그가 포함된 경우 러너는 모든 태그와 일치하지 않을 경우 해당 작업을 선택하지 않습니다. 러너에는 작업에서 정의된 태그보다 많은 태그가 있을 수 있지만, 그 반대는 아닙니다.

마지막으로, 러너가 선택할 수 있는 작업이 태그가 있는 작업만이라면, 모든 태그가 없는 작업이 최종적으로 필터링됩니다.

이 시점에서 남은 pending 작업을 루프를 통해 진행하고 추가 정책을 기반으로 러너가 “선택할 수 있는” 첫 번째 작업을 시도합니다. 예를 들어, protected로 표시된 러너는 보호된 브랜치(예: 프로덕션 배포)에 대해 실행해야 하는 작업만 선택할 수 있습니다.

풀에 있는 러너 수를 늘릴수록 동일한 작업을 다른 러너에 할당하여 충돌이 발생할 가능성도 높아집니다. 이를 방지하기 위해 충돌 오류를 정상적으로 처리하고 디렉터리에서 다음 작업을 할당합니다.

막힌 빌드 삭제

“막힌” 빌드를 표시하고 삭제하는 두 가지 방법이 있습니다.

  1. 빌드가 만들어지면, Ci::PipelineCreation::DropNotRunnableBuildsService는 즉시 실행할 수 없는 작업에 대한 미리 알려진 조건을 확인합니다:
    • 빌드를 실행할만큼 충분한 CI/CD 분이 없는 경우, 빌드는 즉시 ci_quota_exceeded와 함께 삭제됩니다.
    • 미래에는, 프로젝트가 빌드에 사용 가능한 러너를 허용하는 allowed_plans를 통해 가용된 계획이 아니라면, 빌드는 즉시 no_matching_runner와 함께 삭제됩니다.
  2. 사용 가능한 러너가 빌드를 가져가지 않으면,Ci::StuckBuilds::DropPendingService에 의해 1시간 후에 삭제됩니다.
    • 24시간 이내에 러너가 작업을 가져가지 않으면 해당 작업은 해당 시간이 지난 후에 처리 대기열에서 자동으로 제거됩니다.
    • 러너가 처리할 수 없는 막힌 대기 중인 작업이라면, 1시간 후에 큐에서 제거됩니다.
    • 양쪽 경우 모두 작업의 상태가 적절한 실패 이유와 함께 failed로 변경됩니다.

이 차이의 이유

CI 분 할당량 메커니즘은 대부분의 경우 초기에 처리되는 작업이므로 작업이 생성될 때 미리 설정된 결정입니다. 프로젝트가 제한을 초과하면, 그 다음 달이 시작될 때까지 해당 제한에 맞는 모든 다음 작업이 적용될 것입니다. 물론 프로젝트 소유자는 추가 분을 구매할 수 있지만, 이는 프로젝트가 매뉴얼으로 수행해야 하는 매뉴얼 작업입니다.

이와 유사한 메커니즘이 allowed_plans에 대해 사용될 것입니다](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121761). 프로젝트가 필요한 계획에 없고 러너를 대상으로 하는 작업의 경우 프로젝트 소유자가 구성을 변경하거나 네임스페이스를 필요한 계획으로 업그레이드할 때까지 빌드는 계속해서 실패할 것입니다.

이 두 메커니즘은 매우 SaaS에 특화되어 있으며 동시에 SaaS의 규모를 고려할 때 상당히 계산적으로 소모적입니다. 작업이 대기 중일 때 여기서 조기에 확인하고 실패하는 것이 매우 합리적인 선택입니다.

왜 다른 경우에 대기 및 작업을 조기에 처리하지 않는 이유? 어떤 경우에는 작업이 대기 중인 것은 러너가 작업을 수행하는 데 시간이 오래 걸리는 경우 때문입니다. 이것은 GitLab 수준에서 알 수 없는 것입니다. 러너의 구성 및 용량 및 GitLab의 대기열 크기에 따라 작업은 즉시 수행될 수도, 대기해야 할 수도 있습니다.

또한 다른 이유가 있을 수 있습니다.

  • 러너 유지 보수를 처리하고 잠시 사용할 수 없는 경우,
  • 구성을 업데이트하고 실수로 태그 또는 보호된 플래그를 망가뜨린 경우(또는 SaaS 인스턴스 러너의 경우; 잘못된 비용 요소 또는 allowed_plans 구성을 할당한 경우).

모든 것은 일시적인 문제일 수 있으며 대부분 예상치 않게 발생하고 조기에 감지 및 수정될 것으로 예상됩니다. 이러한 조건 중 하나가 발생할 때 즉시 작업을 삭제하고 싶지는 않습니다. 러너(runner)가 수용 능력을 초과하는 경우 또는 일시적으로 사용할 수 없거나, 구성 오류가 있는 경우 사용자에게 매우 해로운 것입니다.

“Job”의 정의 GitLab CI/CD

GitLab CI 컨텍스트에서 “Job”은 지속적 통합, 전달, 배포를 진행하는 작업을 의미합니다. 일반적으로 파이프라인에는 여러 단계가 포함되며, 단계에는 여러 작업이 포함됩니다.

Active Record 모델링에서 작업은 CommitStatus 클래스로 정의됩니다. 또한, 다음과 같은 유형의 작업이 있습니다:

  • Ci::Build … 러너에 의해 실행되는 작업
  • Ci::Bridge … 하위 파이프라인을 트리거하는 작업
  • GenericCommitStatus … 외부 CI/CD 시스템에서 실행되는 작업(예: Jenkins)

코드베이스에서 “Job” 용어를 사용할 때, 독자들은 해당 클래스/객체가 위의 어떤 유형인지로 예상할 것입니다. 특별히 Ci::Build 클래스를 참조하는 경우 “job”으로 객체/클래스를 명명하지 않아야 하며, 이는 혼란을 야기할 수 있습니다. 설명서에서는 “Build” 대신 일반적으로 “Job”을 사용해야 합니다.

코드베이스에는 리팩토링이 필요한 일부 일관성이 있습니다. 예를 들어, CommitStatusCi::Job이어야 하며, Ci::JobArtifactCi::BuildArtifact여야 합니다. 전체적인 리팩토링 계획은 이 이슈를 참조하십시오.

Compute quota

  • GitLab 16.1부터 “CI/CD minutes”가 “compute quota” 및 “compute minutes”로 이름이 변경되었습니다(https://gitlab.com/groups/gitlab-com/-/epics/2150).

이 다이어그램은 Compute quota 기능과 해당 컴포넌트가 작동하는 방법을 보여줍니다.

compute quota architecture

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