Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
proposed | devops verify | - |
Runner 통합
비 목표
이 제안은 Step Runner 바이너리를 대상 환경으로 배포하거나 아래에서 설명하는 Step Runner gRPC 서비스를 시작하는 것은 다루지 않습니다. 이 제안의 나머지 부분은 Step Runner 바이너리가 대상 환경에 있고 gRPC 서비스가 실행되어 로컬 소켓에서 수신 대기 중이라고 가정합니다. 마찬가지로 이 제안은 Step Runner
서비스의 라이프사이클 및 서비스가 중지될 경우 재시작하는 방법과 업그레이드하는 방법 등을 다루지 않습니다.
관련 청사진은 배포 및 라이프사이클 관리를 참조하세요.
단계 서비스 gRPC 정의
Step Runner 서비스 gRPC 정의는 다음과 같습니다.
service StepRunner {
rpc Run(RunRequest) returns (RunResponse);
rpc FollowSteps(FollowStepsRequest) returns (stream FollowStepsResponse);
rpc FollowLogs(FollowLogsRequest) returns (stream FollowLogsResponse);
rpc Finish(FinishRequest) returns (FinishResponse);
rpc Status(StatusRequest) returns (StatusResponse);
}
message Variable {
string key = 1;
string value = 2;
bool file = 3;
bool masked = 4;
}
message Job {
repeated Variable variables = 1;
string job_id = 2;
string pipeline_id = 3;
string build_dir = 4;
repeated string token_prefixes = 5;
}
message Masking {
repeated string phrases = 1;
repeated string token_prefixes = 2;
}
message RunRequest {
string id = 1;
string work_dir = 2;
map<string,string> env = 3;
Masking masking = 4;
Job job = 5;
string steps = 6;
}
message RunResponse {
}
message FollowStepsRequest {
string id = 1;
}
message FollowStepsResponse {
StepResult result = 1;
}
message FollowLogsRequest {
string id = 1;
int32 offset = 2;
}
message FollowLogsResponse {
bytes data = 1;
}
message FinishRequest {
string id = 1;
}
message FinishResponse {
}
message Status {
string id = 1;
bool finished = 2;
int32 exit_code = 3;
google.protobuf.Timestamp start_time = 4;
google.protobuf.Timestamp end_time = 5;
}
message StatusRequest {
string id = 1;
}
message StatusResponse {
repeated Status jobs = 1;
}
Runner는 위의 gRPC 서비스를 통해 Step Runner와 상호작용합니다. 이 서비스는 실행 환경의 로컬 소켓에서 시작됩니다. Runner는 먼저 대상 환경에 executor별 프로토콜을 통해 연결한 다음 제공된 proxy
명령어를 사용하여 gRPC
서비스에 연결하고, Runner에서 Step Runner로 gRPC
요청을 투명하게 터널링합니다(자세한 내용은 프록시 명령어 참조). 이는 Nesting이 전용 Mac 인스턴스에서 gRPC 서비스를 제공하는 방식과 동일합니다. 이 서비스에는 Run
, FollowSteps
, FollowLogs
, Finish
, Status
라는 다섯 개의 RPC가 있습니다.
Run
은 단계를 초기에 전달하는 것입니다. FollowSteps
는 단계 결과 트레이스를 스트리밍 응답으로 요청합니다. FollowLogs
는 마찬가지로 실행 중인 단계의 프로세스에서 생성되는 출력(stdout
/stderr
) 및 Step Runner 자체에서 생성한 로그의 스트리밍 응답을 요청합니다. Finish
는 요청의 실행을 멈추고 가능한 한 빨리 리소스를 정리합니다. Status
는 지정된 작업의 상태를 나열하거나 지정된 작업이 없는 경우 Step Runner 서비스의 모든 활성 작업(완료되었지만 Finish
되지 않은 작업 포함)의 상태를 나열합니다. 예를 들어 Status를
러너는 크래시 후에 복구하는 데 사용할 수 있습니다.
Step Runner gRPC 서비스는 여러 Run
페이로드를 동시에 실행할 수 있습니다. 다시 말해, Run
을 호출할 때마다 새로운 고루틴이 시작되어 그 단계를 실행합니다. 동시에 여러 개의 Run
을 호출할 수 있습니다.
단계가 실행되는 동안 단계 결과 트레이스 및 서브 프로세스 로그를 GitLab Runner로 스트리밍할 수 있습니다. 이를 통해 Runner(또는 호출자)가 단계 수준에서 실행을 따를 수 있으며, 서브 프로세스 및 Step Runner 로그를 (FollowLogs
)로 스트리밍할 수 있습니다. 로그는 특정 형식으로 작성되며, 단계가 Runner로 스트리밍되기 전에 Step Runner에 의해 민감한 토큰은 마스킹될 것입니다.
Status
를 제외한 모든 API는 멱등성을 가지며, 동일한 매개변수로 동일한 API 호출을 여러 번 한 경우 동일한 결과를 반환해야 합니다. 예를 들어 Run
이 동일한 id
로 여러 번 호출된 경우 첫 번째 호출만 작업 요청을 처리를 시작하고, 후속 호출은 성공 상태를 반환하지만 그 외에는 아무것도 하지 않아야 합니다. 마찬가지로 동일한 id
로 Finish
를 여러 번 호출하면 첫 번째 호출에서 해당 작업을 완료하고 제거하며, 후속 호출은 아무 작업도 수행하지 않아야 합니다.
서비스는 클라이언트가 잘 행동하지 않을 것으로 가정하고, 임산을 종료하지 않거나 Finish
를 호출하지 않고 해당 Run
요청을 조기에 연결 해제하는 클라이언트를 처리할 수 있어야 합니다. 이를 위해 Step Runner
프로세스는 일정 간격으로 스캔하여 만료 또는 비정상적으로 실행되고 있는(중단되지 않은) 작업을 식별하고 정리할 수 있어야 합니다. 만료된 작업은 특정 시간 전에(그리고 Finish
되지 않았을 경우) 완료된 작업일 수 있습니다. 비정상적으로 실행되는 작업은 일정 기간 동안(아마도 아웃풋을 생성하지 않은) 실행되는 작업일 수 있습니다.
마지막으로 아래의 Runner 실행자들에 단계를 통합하기 쉽도록, 단계가 Run
/Follow*
/Finish
API를 실행하고 Follow*
호출이 연결성 문제로 연결이 끊어질 경우 Step Runner
서비스에 다시 연결하는 클라이언트 라이브러리를 제공하는 것이 권장됩니다.
RunRequest 매개변수
단계는 Runner로 전달되는 RunRequest.Steps
필드 내에서 step.go의 JSON 직렬화 버전으로 Step Runner에 전달됩니다. id
필드는 Step Runner
서비스에서 실행 중인 각 요청을 고유하게 식별합니다. RunRequest.Env
필드는 각 단계가 실행될 때 인젝션되어야 하는 환경 변수를 보유합니다.
옵션인 Job
매개변수에는 해당 CI 작업에서 선택한 매개변수가 포함됩니다. Job
에는 해당 CI 작업의 빌드 디렉터리가 포함되며, Job.BuildDir
는 RunRequest.WorkDir
로 복사되어 요청의 모든 단계에서 기존 작업 스크립트 동작을 보존하기 위해 해당 디렉터리에서 호출되어야 합니다. RunRequest
에는 또한 CI 작업의 환경 변수(즉, CI 구성에서 작업 및 전역 수준에서 정의된variables
)가 포함됩니다. Runner가 RunRequest
를 만들 때, 변수는 Job.Variables
에 포함되어야 하고, RunRequest.Env
는 비워둬야 합니다. 실행 요청이 처리될 때, 파일 유형의 변수는 파일에 작성되고, 변수는 확장되어 RunRequest.Env
로 복사됩니다. 또한, 요청의 나머지 부분에서 Job
필드는 삭제되어야 합니다. 변수는 실행 환경 내의 개체를 참조할 수 있기 때문에, 파일 유형의 변수 또한 전통적인 Runner 작업 실행과 동일한 경로에 쓰여져야 합니다. 마찬가지로, Job.Variables
에서 마스킹할 구절이 추출되어 Masking.Phrases
를 작성하고, Job.TokenPrefixes
는 Masking.TokenPrefixes
로 복사되어야 합니다.
단계를 실행하려는 Runner 이외의 클라이언트는 Job
필드를 생략할 수 있으며, 이 경우 Masking
및 Env
필드는 호출자에 의해 직접 채워져야 합니다.
로그 형식
FollowLogs
API에서 발신된 로그 라인은 다음과 같은 형식을 가져야 합니다.
<타임스탬프> <스트림> <표준출력/표준에러> <추가 플래그> <메시지>
이 형식은 이 Merge Request에서 Runner에 도입된 동일한 로그 형식입니다.
이 형식으로 메시지를 형식화하기 전에 민감한 변수나 토큰을 가려주는 작업은 Step Runner
가 담당할 것입니다. 가려진 라이브러리는 GitLab Runner
와 Step Runner
간에 공유되어야 합니다.
마스킹
Step Runner
는 민감한 변수나 토큰을 가릴 책임이 있을 것입니다. 이 작업은 위의 로그 형식으로 메시지를 형식화하기 전에 수행되어야 합니다. 변수를 가리기 위해 사용된 라이브러리는 GitLab Runner
와 Step Runner
간에 공유되어야 합니다. (관련 모듈들은 여기를 참조하세요).
프록시 명령
Step Runner
이진 파일에는 (일반적으로 텍스트 기반) stdin
/stdout
/stderr
기반 프로토콜에서 gRPC 서비스로 데이터를 프록시하는 명령이 포함될 것입니다. 이 명령은 gRPC 서비스가 실행 중인 호스트와 동일한 곳에서 실행되어야 하며, stdin
에서 입력을 읽은 후 그것을 로컬 소켓을 통해 gRPC 서비스로 전달하고, 동일한 소켓을 통해 gRPC 서비스에서 출력을 받은 후 이를 stdout
/stderr
를 통해 클라이언트에 전달할 것입니다. 이 명령을 통해 클라이언트(Runner와 같은)는 stdin
/stderr
/stdout
기반 프로토콜을 통해 gRPC
서비스로 투명하게 터널링할 수 있으며, Docker 이미지에서 Step Runner 서비스의 gRPC 포트를 노출시킬 필요가 없어지거나 VM에서 SSH 포트 포워딩을 설정할 필요가 없어져 Runner가 설립된 프로토콜(예: SSH와 docker exec
)을 통해 Step Runner
와 상호 작용할 수 있게 됩니다. stdout
은 Step Runner
서비스로부터의 응답을 쓰기 위해 예약되어야 하며, stderr
은 프록시
명령 자체에서 발생하는 오류를 위해 예약되어야 합니다.
실행자
다음은 각 Runner 실행자에서 GitLab Runner가 Step Runner에 연결하는 방법입니다:
Instance
Instance 실행자는 오늘과 동일하게 SSH를 통해 접근됩니다. 그러나 명령어를 입력하는 대신 프록시 명령을 호출하여 Step Runner 소켓에 연결합니다. 그러면 Runner가 직접 gRPC
호출을 하고, SSH
연결을 통해 gRPC
서비스로 투명하게 터널링할 수 있습니다. 이는 Runner가 전용 Mac 인스턴스에서 Nesting 서버를 호출하여 VM을 만드는 방식과 동일합니다.
이는 Step Runner가 작업 실행 환경에 존재하고 시작되어 있어야 한다는 요구사항이 있습니다.
Docker
Step Runner가 존재하고 gRPC 서비스가 실행 중이라는 동일한 요구사항이 Docker 실행자(및 docker-autoscaler
)에도 적용됩니다. 그러나 컨테이너 내부의 gRPC 서비스에 연결하려면 Runner가 컨테이너에 docker exec
를 하고, gRPC 서비스에 연결하기 위해 프록시 명령을 실행해야 합니다. 그리고 클라이언트는 docker exec
의 stdin
에 쓸 수 있으며, 이는 투명하게 gRPC 서비스로 프록시될 것이며, 그것의 stdout/stderr
에서는 gRPC 서비스로부터의 응답이 포함될 것입니다.
Kubernetes
Kubernetes 노드의 Kubelet은 실행 중인 Pod의 컨테이너에서 프로세스를 시작할 수 있는 exec API를 노출합니다. 이를 사용하여 호출자가 Pod 내에서 gRPC
호출을 할 수 있는 브릿지 프로세스를 exec create
할 것입니다. 이는 Docker 실행자와 유사한 방식으로 동작합니다.
보호된 Kubelet API에 접근하기 위해 우리는 Pod에 exec 하위 리소스를 제공하는 Kubernetes API를 사용해야 합니다. 호출자는 Pod의 URL에 /exec
를 추가하여 POST하고, 그 후에 양방향 바이트 스트리밍을 위해 SPDY 프로토콜까지 연결해야 합니다. 따라서 GitLab Runner는 Kubernetes API를 사용하여 Step Runner 서비스에 연결하고 작업 페이로드를 전달할 수 있습니다.
이는 kubectl exec
동작 방식과 동일합니다. 사실 대부분의 내부 요소(예: SPDY 협상)는 client-go
라이브러리로 제공됩니다. 따라서 Runner는 kubectl
을 외부에서 실행하는 대신에 필요한 라이브러리를 가져와서 Kubernetes API에 직접 호출할 수 있습니다.
과거의 Kubernetes Executor의 약점 중 하나는 단일 exec를 통해 전체 작업을 실행하는 것이었습니다. 이를 완화하기 위해 Runner는 기존의 쉘 프로세스에 “재연결”할 수 있는 attach 명령을 대신 사용합니다.
그러나 Step Runner의 경우 이는 필요하지 않습니다. 왜냐하면 exec는 장기 실행되는 gRPC 프로세스에 대한 브릿지를 설정하기 때문입니다. 연결이 끊기면, Runner는 다른 연결을 exec하여 follow
와 같은 RPC 호출을 계속할 것입니다.