내부 Executor 인터페이스
이 문서는 코드 내부에 대한 문서이므로, 사용자에게 노출되는 구성, 동작 또는 기능에 대한 문서보다 오래되기 쉽습니다. GitLab Runner는 작업이 실행되는 방식을 정의하기 위해 executors라는 개념을 사용합니다.
이 문서는 코드 내부에 대한 문서이므로, 사용자에게 노출되는 구성, 동작 또는 기능에 대한 문서보다 오래되기 쉽습니다. 이 페이지는 작성일 기준으로 정확합니다: 2022-01-26.
인터페이스#
GitLab Runner는 작업이 실행되는 방식을 정의하기 위해 executors라는 개념을 사용합니다.
GitLab CI/CD 작업 실행의 현재 철학은 _모든 것이 셸 스크립트_이지만, 이 스크립트는 다양한 방식으로 실행될 수 있습니다:
- GitLab Runner가 동작하는 호스트의 셸에서 직접 실행,
- SSH를 통해 액세스 가능한 외부 호스트의 셸에서 실행,
- VirtualBox 또는 Parallels로 관리되는 가상 머신의 셸에서 실행,
- Docker로 관리되는 컨테이너의 셸에서 실행,
및 몇 가지 다른 방식이 있습니다. 또한 _Custom Executor_도 있는데, 이는 사용자가 자체적인 작업 실행 방식을 구현하기 위해 매우 단순한 외부 인터페이스와 상호 작용할 수 있게 합니다.
이러한 _executors_는 모두 GitLab Runner 프로세스에 의해 내부적으로 오케스트레이션됩니다. 이를 위해 Runner는 executor가 작동하기 위해 구현해야 하는 일련의 Go 인터페이스를 사용합니다.
executor의 수명 주기와 작업 실행을 관리하는 두 가지 주요 인터페이스(common 패키지의 일부)는 다음과 같습니다:
ExecutorExecutorProvider
type Executor interface {
// Shell returns data about the shell and scripts this executor is bound to.
Shell() *ShellScriptInfo
// Prepare prepares the environment for build execution. e.g. connects to SSH, creates containers.
Prepare(options ExecutorPrepareOptions) error
// Run executes a command on the prepared environment.
Run(cmd ExecutorCommand) error
// Finish marks the build execution as finished.
Finish(err error)
// Cleanup cleans any resources left by build execution.
Cleanup()
// GetCurrentStage returns current stage of build execution.
GetCurrentStage() ExecutorStage
// SetCurrentStage sets the current stage of build execution.
SetCurrentStage(stage ExecutorStage)
}
type ExecutorProvider interface {
// CanCreate returns whether the executor provider has the necessary data to create an executor.
CanCreate() bool
// Create creates a new executor. No resource allocation happens.
Create() Executor
// Acquire acquires the necessary resources for the executor to run, e.g. finds a virtual machine.
Acquire(config *RunnerConfig) (ExecutorData, error)
// Release releases any resources locked by Acquire.
Release(config *RunnerConfig, data ExecutorData)
// GetFeatures returns metadata about the features the executor supports, e.g. variables, services, shell.
GetFeatures(features *FeaturesInfo) error
// GetConfigInfo extracts metadata about the config the executor is using, e.g. GPUs.
GetConfigInfo(input *RunnerConfig, output *ConfigInfo)
// GetDefaultShell returns the name of the default shell for the executor.
GetDefaultShell() string
}
기존의 모든 executor는 executors.AbstractExecutor 구조체(이 문서에서는 이후 AbstractExecutor라고 부름)도
확장합니다. 이는 소규모의 공통 기능 세트를 구현합니다. 새 코드가 인터페이스를 구현하는 한 AbstractExecutor 사용을
강제하는 코드 보호 장치는 없지만(작동함), 새로운 executor가 이를 확장할 것으로 예상됩니다.
이는 executor 간에 일부 기능의 일관된 동작을 보장하기 위함입니다.
편의를 위해 ExecutorProvider 인터페이스를 구현하고 대부분의 경우에 적합한
executors.DefaultExecutorProvider도 있습니다. 그러나 각 executor는 독립적으로 자체 _provider_를
구현할 수 있습니다(실제로 현재는 Docker Machine executor만 그렇게 하고 있습니다).
중요한 점은, Executor와 ExecutorProvider 모두 인터페이스이기 때문에 구현에서 다양한 구조체를
"스택"으로 쌓을 수 있습니다. 이 가능성의 사용은 예제 중 하나에서 보여줍니다.
Executor 인터페이스#
Executor 인터페이스는 작업 실행 관리를 담당합니다.
설명된 메서드는 작업 환경 준비(Prepare()), 작업 스크립트 실행(Run() 및 Finish();
참고: 후속 작업 단계는 별도의 Run() 호출로 실행됨) 및 작업 환경 정리(Cleanup())를 관리합니다.
또한 내부 Prometheus 메트릭 내보내기에 대한 통합을 제공하여 현재 executor 사용 단계에 대한 정보로
일부 관련 메트릭을 레이블링합니다(GetCurrentStage(), SetCurrentStage()).
Shell() 메서드는 현재 한 곳에서만 사용되며, 앞서 언급한 AbstractExecutor 구조체에서 완전히 구현됩니다.
기존 구현과 다양한 executor의 시간에 따른 발전을 고려하면, 이 메서드는 executor 인터페이스에서 제거되고
AbstractExecutor 사용을 강제하는 방식으로 다르게 처리되어야 할 것 같습니다.
인터페이스 사용은 매우 단순화하면 다음과 같습니다:
- executor 인스턴스가 제공되고 수신된 작업에 할당됩니다.
Shell()이 호출되어 셸 구성을 가져옵니다. 이는 작업에서 실행될 모든 스크립트를 준비하는 데 사용됩니다.Prepare()가 호출되어 작업 환경을 준비합니다(예: Kubernetes Pod 생성, Docker 컨테이너 세트 또는 VirtualBox VM). 이는 특정 executor 구현이 자체 준비를 처리하는 곳이기도 합니다.AbstractExecutor사용을 통해 모든 executor는 예를 들어 작업 추적 객체와 같은 일부 공통 기능에도 액세스할 수 있습니다.Run()이 여러 번 호출되며, 각각 executor와 함께 실행할 작업 실행 단계의 스크립트 세부 정보를 포함합니다.Finish()는 모든 작업 스테이지 실행이 완료된 후 작업이 완료로 표시될 때 호출됩니다. 일부 executor는 이 순간을 활용할 수 있습니다. 대부분은AbstractExecutor에 위임합니다.Cleanup()이 호출되어 작업 환경을 정리합니다. 이는Prepare()의 반대입니다.
또한 SetCurrentStage()는 executor 내부에서 호출됩니다(대부분은 AbstractExecutor에 위임하지만)
이 executor 인스턴스 내에서 시스템이 현재 어느 _executor 사용 단계_에 있는지 표시합니다.
그리고 GetCurrentStage()는 메트릭 수집기에 의해 임의의 순간에 외부에서 호출됩니다.
이 값은 다양한 작업에 대한 정보를 요약하고 일부 메트릭을 레이블링하는 데 사용됩니다.
ExecutorProvider 인터페이스#
ExecutorProvider 인터페이스는 executor 자체의 준비를 담당합니다. Executor 개념 주변에
추상화를 구축합니다. 이 추상화를 통해, 사용자가 config.toml의 executor 설정으로 구성하는 것은
실제로 executor provider입니다. 그런 다음 러너가 실행하는 모든 작업에 대해 새로운 독립적인 executor
인스턴스가 준비됩니다. _executor_의 유지 관리는 ExecutorProvider가 담당합니다.
설명된 메서드는 executor 인스턴스 생성(CanCreate(), Create()), 잠재적 작업을 위한 provider 리소스 예약
(Acquire(), Release())을 관리합니다. 또한 작업을 요청할 때 GitLab에 보고해야 하는 일부 정보 수집에 대한
지원도 있습니다(GetFeatures(), GetConfigInfo()). 마지막으로 제공된 executor에서 사용해야 하는 셸에 대한
정보를 제공하는 메서드도 있습니다(GetDefaultShell()).
인터페이스 사용은 매우 단순화하면 다음과 같습니다:
CanCreate(),GetFeatures()및GetDefaultShell()은 provider가 일반적으로 작동할 수 있는지 검증하기 위해 provider 등록 시 실행됩니다.- 특정
[[runners]]워커에 대한 새 작업 요청 전에,Acquire()가 호출되어 작업을 위한 provider 리소스의 예약을 확인하고 수행합니다. 이 곳에서 provider는 자체 용량을 제어하고 일부 사전 할당된 리소스에 대한 정보를 반환할 수 있습니다. GetFeatures()는 Runner가 지원하는 기능에 대한 정보를 다양한 API 요청으로 GitLab에 다시 보낼 수 있도록 여러 번 호출됩니다. 호출 중 하나는 작업에 대한 초기 요청을 준비할 때 이루어집니다.GetConfigInfo()도 마찬가지로 작업에 대한 초기 요청을 준비할 때 한 번만 호출됩니다. 이를 통해 사용된 구성에 대한 일부 정보를 GitLab에 보낼 수 있습니다.GetDefaultShell()도 마찬가지로 작업에 대한 초기 요청을 준비할 때 한 번만 호출됩니다. 이를 통해 사용된 셸에 대한 정보를 GitLab에 보낼 수 있습니다.- 작업이 수신되면 준비가 시작되고 어느 순간
Create()가 호출되어 새 executor 인스턴스를 생성합니다. - 작업 실행이 완전히 완료되면
Release()가 호출됩니다. 이 곳에서 provider는 이전에 작업을 위해 예약된 리소스 해제를 처리할 수 있습니다.
GitLab에 보고할 수 있는 기능 목록은 common/network.go의 FeaturesInfo 구조체에서 찾을 수 있습니다.
DefaultExecutorProvider#
DefaultExecutorProvider는 현재 ExecutorProvider 인터페이스의 두 가지 기존 구현 중 하나이며
대부분의 executor에서 사용됩니다. 어떻게 구성되어 있는지 설명하겠습니다.
type DefaultExecutorProvider struct {
Creator func() common.Executor
FeaturesUpdater func(features *common.FeaturesInfo)
ConfigUpdater func(input *common.RunnerConfig, output *common.ConfigInfo)
DefaultShellName string
}
Creator는 가장 중요한 부분입니다. 주어진 Executor 인터페이스 구현의 새 인스턴스를 반환하는 함수입니다.
각 executor에 의해 구현됩니다. 구현이 필요합니다.
Creator가 비어있으면 인터페이스의 CanCreate() 메서드가 실패합니다. provider의 Create() 호출은
Creator 함수로 프록시됩니다.
FeaturesUpdater와 ConfigUpdater는 기능 및 구성 정보를 요청할 수 있게 해주는 함수입니다. 모든 executor는
지원되는 기능이나 구성 세부 정보에 대한 정보를 노출하기 위해 이러한 함수를 사용합니다. FeaturesUpdater는
선택적이며 모든 executor는 목록에서 지원되는 기능을 보고해야 합니다. ConfigUpdater는 선택적이며 생략할 수
있습니다. DefaultShellName은 모든 executor에서 설정해야 합니다.
provider의 GetFeatures(), GetConfigInfo(), GetDefaultShell() 호출은 정의된 업데이터와 셸 이름을
사용하여 호출자에게 필요한 데이터를 노출합니다.
Acquire()와 Release()는 NOOP입니다. DefaultExecutorProvider는 리소스 관리 개념을 사용하지 않으며
모든 호출에 대해 executor의 새 인스턴스를 단순히 생성합니다.
사용 예시#
Shell#
_Shell executor_는 GitLab Runner가 제공하는 가장 단순한 executor입니다. GitLab Runner 자체가 실행되는 호스트에서 직접 생성된 단순한 셸 프로세스에서 작업 스크립트를 실행합니다. 여기에는 가상화도, 컨테이너도, 네트워크 통신도 없습니다.
ExecutorProvider#
Shell executor는 DefaultExecutorProvider를 사용합니다. 매우 제한된 수의 기능 사용을 보고합니다
(모든 경우에 두 가지, 플랫폼이 windows가 아닌 경우 두 가지 추가). 구성 세부 정보를 노출하지 않습니다.
셸은 Runner가 운영되는 플랫폼의 기본값에 따라 다릅니다. 로그인 셸로 구성됩니다.
Executor#
Prepare()에는 특별한 사항이 없습니다. Shell executor는 Runner 프로세스가 존재하는 시스템에서 모든 것을
직접 실행하므로, 빌드 및 캐시 경로가 사용 가능한지만 확인합니다. 그 후 AbstractExecutor 준비 단계로 위임합니다.
Run()은 제공된 스크립트 세부 정보를 사용하여 os/exec.Cmd 호출을 구성합니다. Shell executor는
해당 호출로 시작된 스크립트 실행 셸 프로세스와 작업 추적 객체 사이에 STDIN/STDOUT/STDERR가 적절히 전달되도록
합니다. 또한 명령의 종료 코드를 감지하고 인터페이스에서 예상하는 대로 보고합니다.
Finish()와 Cleanup()의 사용자 정의 구현이 없습니다. executor는 AbstractExecutor의 공통 단계로 위임합니다.
Docker#
_Docker executor_는 아마도 GitLab Runner executor 중 가장 강력하고 성숙한 것입니다. .gitlab-ci.yml에서
사용 가능한 대부분의 기능을 지원합니다. 모든 작업을 다른 작업과 격리된 환경에서 실행할 수 있습니다. 그러나 모든 작업은
하나의 호스트에서 실행되며 러너의 용량은 해당 호스트의 사용 가능한 리소스에 의해 제한됩니다.
Docker executor는 SSH 변형이라는 특별한 변형이 있습니다. 이 문서를 이해하기 쉽게 만들기 위해(executor 설명은 executor 인터페이스 작동 방식을 이해하는 데 도움이 되는 예시일 뿐이므로) Docker executor의 "일반" 변형만 설명하겠습니다.
windows 변형의 executor도 있습니다. 이 변형의 세부 정보도 이 설명에 포함하지 않겠습니다.
Docker executor에서 작업은 Docker 컨테이너에서 실행됩니다. 각 작업은 작업 디렉토리와 함께 하나 이상의 볼륨을
공유하는 연결된 컨테이너 세트를 가집니다. 주 컨테이너는 사용자가 지정한 이미지에서 생성됩니다. Runner가 스크립트를
실행할 셸을 노출해야 합니다. 추가로, Runner는 Runner가 제공하는 helper 이미지에서 predefined 컨테이너를
생성합니다. 이 컨테이너는 Git 및 Git LFS 소스 업데이트, 캐시 작업, 아티팩트 작업과 같은 공통 작업을 처리하는
스크립트를 실행하는 데 사용됩니다.
작업 구성에 따라 Runner는 정의된 services를 위한 추가 컨테이너를 생성할 수 있습니다. 이는 네트워킹을 통해
주 컨테이너에 연결되어 작업 스크립트가 그들이 노출하는 네트워크 사용 가능 서비스를 활용할 수 있습니다.
ExecutorProvider#
Docker executor도 DefaultExecutorProvider를 사용합니다. 몇 가지 더 많은 executor 관련 기능 사용을 보고하며,
추가로 일부 구성 세부 정보를 보고합니다.
셸은 하드코딩되어 있으며 플랫폼에 따라 다릅니다. Docker executor의 가장 인기 있는 linux 변형의 경우,
비로그인 셸로 구성됩니다.
Executor#
Prepare()는 이 executor에서 많이 활용됩니다. 해당 단계에서 Runner는 다양한 내부 도구(볼륨 관리자 또는
네트워크 관리자 등)를 준비하고 컨테이너가 작업 실행에 다음으로 사용할 기본 구성을 설정합니다. 또한 작업에 정의된
모든 이미지를 가져오는 단계이기도 합니다. 볼륨 생성, 장치 바인딩 및 서비스 컨테이너도 해당 단계에서 발생합니다.
Prepare()가 완료되면 환경이 predefined/작업 단계 실행 컨테이너 생성, 전체 스택에 연결 및 그 안에서 스크립트를
실행할 준비가 완전히 되어야 합니다.
Run()은 predefined 또는 작업 단계 컨테이너를 생성하고 연결하여 컨테이너의 주 프로세스로 실행되어야 하는
셸에서 스크립트를 실행합니다. 컨테이너의 STDOUT 및 STDERR를 작업 추적 객체로 프록시합니다. 또한 Docker Engine API를
사용하여 스크립트 실행 종료 코드를 감지합니다.
Finish()는 여기에 사용자 정의 동작이 없으며 단순히 AbstractExecutor로 위임합니다.
Cleanup()은 Prepare()의 반대이므로 컨테이너, 볼륨(영구적으로 구성되지 않은 것), 작업별 네트워크(사용된 경우)와
같은 정의된 모든 리소스를 제거합니다.
Docker Machine (오토스케일링 기능)#
_Docker Machine executor_는 실제로 일반 Docker executor 위에 구축된 오토스케일링 provider입니다.
인터페이스 개념을 활용하여 Docker executor를 자체적으로 캡슐화합니다. Docker Machine executor의 책임은 주로
ExecutorProvider 인터페이스에 집중되어 있습니다. 이를 통해 Docker Engine이 실행되는 VM 풀을 관리합니다.
관리는 os/exec.Cmd 호출을 실행하여 Docker Machine 도구를 사용하여 수행됩니다.
VM 관리는 "온디맨드" 또는 "백그라운드에서 오토스케일" 모드로 수행될 수 있습니다. 선택한 모드는 사용자가 제공한
구성에 따라 다릅니다. 첫 번째 모드에서는 작업 한도에 도달할 때까지 수신된 각 작업에 대해 VM이 생성됩니다.
두 번째 모드에서는 작업을 기다리는 구성 가능한 Idle VM 세트를 유지합니다. 최소 하나의 Idle VM이 있을 때만
작업이 요청됩니다. 수신된 작업에 하나의 Idle VM이 사용되면 다른 VM이 생성되어 대체됩니다. VM이 풀로 반환될 때
(그렇게 구성된 경우) Idle 수가 정의된 한도를 초과하면 provider는 일부를 제거하기 시작합니다. 원하는 수의 Idle VM과
원하는 총 관리 VM 수를 유지하려는 이 루프는 백그라운드에서 항상 작동합니다.
Docker Machine executor는 현재 단일 VM에서 한 번에 하나의 작업만 실행할 수 있도록 구현되어 있습니다.
작업 실행을 위해 Docker Machine executor는 Docker executor와 Docker Engine API 액세스 자격 증명을 구성할 수 있다는
사실을 사용합니다. 이를 통해 Docker Machine provider는 VM을 관리하고, 작업에 대한 VM을 선택하고, Docker executor를
인스턴스화하여 VM의 자격 증명 및 API 엔드포인트를 사용하도록 자동으로 구성합니다. 이를 통해 작업은 일반 Docker executor와
동일하게 실행되며(.gitlab-ci.yml 구문에서 사용 가능한 모든 다양한 기능 지원), 각 작업마다 독립적인 외부 호스트에서
실행됩니다.
ExecutorProvider#
Docker Machine executor는 ExecutorProvider 인터페이스의 자체 구현을 가져옵니다!
그러나 내부적으로 Docker executor를 사용하므로 Docker executor provider(자체적으로 DefaultExecutorProvider의
특정 구성임)도 인스턴스화하고 일부 호출을 직접 프록시하거나 자체 목적으로 내부에서 호출합니다.
CanCreate()는 Docker executor로 직접 프록시됩니다. GetFeatures(), GetConfigInfo() 및
GetDefaultShell()도 마찬가지입니다.
Create()는 매우 단순하여 machineExecutor(Executor 인터페이스 구현)를 자신에 대한 액세스와 함께
반환합니다. 이를 통해 Prepare()나 Cleanup()과 같은 단계가 오토스케일된 VM을 유지 관리하는 데 사용할 수 있습니다
(이에 대한 자세한 내용은 아래에서 설명합니다).
이 provider는 Executor Provider 인터페이스의 Acquire() 및 Release() 메서드를 최종적으로 사용하는 것이기도 합니다.
Acquire() 동작은 구성된 모드에 따라 다릅니다.
"온디맨드" 모드에서는 이전 기계 정리 호출 중 하나를 트리거하는 장소로 사용됩니다. 실제 획득은 수행하지 않으며
IdleCount가 0으로 설정되어 있으므로 기계가 작업 컨텍스트에서 온디맨드로 생성됩니다라고 로그에 기록합니다
(이는 사용자에게 표시되지 않으며 Runner 프로세스 로그에서 확인 가능합니다). 이를 통해 provider는 작업 컨텍스트에서
VM 생성을 시도합니다. 오토스케일링 구성의 정의된 한도 초과, 잘못된 오토스케일링 구성, 클라우드 provider 오류,
Docker Engine 가용성 문제와 같이 실패를 야기하는 것이 있으면 작업 실패로 이어집니다.
"백그라운드에서 오토스케일" 모드에서는 사용 가능한 Idle VM이 있는지 확인합니다. 있으면 예약하고 Runner가
새 작업 요청을 보낼 수 있도록 합니다. 작업이 수신되면 획득한 VM에 대한 정보를 얻습니다. 사용 가능한 Idle VM이
없으면 Acquire() 호출이 실패하여 Runner가 작업 요청을 보내지 못하게 합니다(Runner 로그에서
빌드를 처리할 수 있는 여유 기계가 없습니다 경고로 기록됩니다).
Release()는 두 모드에서 동일하게 작동합니다. 작업에 사용된 VM이 제거에 적합한지 확인하고 해당하는 경우
제거를 트리거합니다. 다른 경우에는 내부 오토스케일링 조정 메커니즘에 VM이 해제되어 Idle 풀로 돌아왔음을
신호하여 다시 사용할 수 있도록 합니다.
Executor#
Executor 인터페이스 구현은 Docker Machine executor에 특정한 코드와 Docker executor 캡슐화의 혼합이기도 합니다.
Docker Machine executor는 VM을 유지 관리, 선택, 사용하고 전용 Docker executor 인스턴스를 구성하는 데 필요한
모든 작업을 주입한 다음, 이 executor에 나머지를 처리하도록 의존합니다.
Shell() 호출은 Docker executor로 위임되고, Docker executor 자체는 (모든 executor와 마찬가지로)
AbstractExecutor로 위임합니다.
Prepare()는 사용할 VM을 준비합니다. 구성된 모드에 따라 사전 할당된 VM을 사용하거나 온디맨드로 생성하는 것을
의미할 수 있습니다. "온디맨드" 모드에서는 VM 생성으로 인한 잠재적 실패가 작업을 실패시킬 수 있는 곳입니다.
VM 세부 정보를 갖고 Docker Engine에 액세스하는 호스트와 자격 증명을 가리키도록 Docker executor 구성을 업데이트하고
Docker executor provider를 인스턴스화합니다. 마지막으로, 이전 예시에서 설명한 대로 모든 작업 환경 준비를 처리하기 위해
Docker Executor의 Prepare()를 호출합니다.
Run()과 Finish()는 특별한 동작이 없습니다. 단순히 내부 Docker executor로 호출을 프록시합니다.
GetCurrentStage()와 SetCurrentStage()도 Docker executor로의 프록시이며, Docker executor 자체는
AbstractExecutor 구현으로 위임합니다.
마지막으로, Cleanup() 호출은 두 가지를 수행합니다. 먼저 이전 예시에서 설명한 대로 VM의 작업 환경을 정리하기 위해
Docker executor의 Cleanup() 메서드를 내부적으로 호출합니다. 그런 다음 provider의 Release()를 호출하여
작업이 완료되었고 VM이 해제될 수 있음을 신호합니다.
