튜토리얼: 러너 어드미션 컨트롤러 빌드하기
GitLab v19.5요약
이 튜토리얼에서는 CI/CD job 실행에 대한 커스텀 정책을 적용하는 러너 어드미션 컨트롤러를 빌드하는 과정을 안내합니다. 이 튜토리얼의 코드 예시는 runner-controller-example 리포지터리를 기반으로 작성되었으며, 이 리포지터리는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공합니다.
이 튜토리얼에서는 CI/CD job 실행에 대한 커스텀 정책을 적용하는 러너 어드미션 컨트롤러를 빌드하는 과정을 안내합니다. job 라우터에 연결하고 이미지 허용 목록 정책을 구현하는 컨트롤러를 Go로 작성합니다.
이 튜토리얼의 코드 예시는 runner-controller-example 리포지터리를 기반으로 작성되었으며, 이 리포지터리는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공합니다.
이 튜토리얼을 마치면 다음을 수행하는 동작하는 어드미션 컨트롤러를 갖추게 됩니다.
- gRPC를 사용해 job 라우터에 연결합니다
- GitLab에 자신을 등록합니다
- job 어드미션 요청을 수신합니다
- 커스텀 정책에 따라 job을 평가합니다
- 어드미션 결정을 반환합니다
러너 어드미션 컨트롤러를 빌드하려면 다음을 수행합니다.
- GitLab에서 러너 컨트롤러 생성하기
- 러너 컨트롤러 범위 지정하기
- 러너 컨트롤러 토큰 생성하기
- Go 프로젝트 설정하기
- protobuf 정의에서 클라이언트 코드 생성하기
- 인증 구현하기
- 에이전트 등록 구현하기
- 어드미션 루프 구현하기
- 어드미션 정책 구현하기
- dry run 상태로 테스트하기
- 프로덕션에서 활성화하기
시작하기 전에#
다음을 준비합니다.
- Ultimate 티어의 GitLab Self-Managed 또는 GitLab Dedicated
- GitLab 인스턴스에 대한 관리자 액세스 권한
- GitLab API와 상호작용하기 위한 다음 중 하나:
glab auth login으로 인증된 GitLab CLI (glab) 1.85.0 이상curl또는 다른 HTTP 클라이언트
- Go 1.21 이상 설치
- Protobuf 코드 생성을 위한
bufCLI 설치 - GitLab 인스턴스에서 활성화된 다음 기능 플래그:
job_routerjob_router_admission_control
FF_USE_JOB_ROUTER환경 변수가true로 설정된 GitLab Runner 18.9 이상.
GitLab에서 러너 컨트롤러 생성하기#
러너 컨트롤러를 생성하려면 runner controllers API를 사용합니다.
강제 적용을 활성화하기 전에 컨트롤러 동작을 검증하려면 dry_run 상태로 시작합니다.
glab runner-controller create --description "Image allowlist controller" --state dry_run
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{"description": "Image allowlist controller", "state": "dry_run"}' \
--url "https://gitlab.example.com/api/v4/runner_controllers"
다음 단계를 위해 반환된 id를 저장합니다.
러너 컨트롤러 범위 지정하기#
러너 컨트롤러는 어드미션 요청을 수신하려면 범위가 지정되어 있어야 합니다. 범위가 없으면 활성화되어 있어도 컨트롤러가 비활성 상태로 남습니다.
이 튜토리얼에서는 컨트롤러 범위를 인스턴스의 모든 러너로 지정합니다.
glab runner-controller scope create <controller_id> --instance
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/runner_controllers/<controller_id>/scopes/instance"
또는 Runner controllers API를 사용해 컨트롤러 범위를 특정 러너로 지정할 수 있습니다. 컨트롤러가 특정 러너에 대해서만 job을 평가하도록 하려면 러너 수준 범위 지정을 사용합니다.
러너 컨트롤러 토큰 생성하기#
job 라우터로 인증할 러너 컨트롤러 토큰을 생성합니다.
glab runner-controller token create <controller_id> --description "Production token"
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{"description": "Production token"}' \
--url "https://gitlab.example.com/api/v4/runner_controllers/<controller_id>/tokens"
반환된 token 값을 안전하게 저장합니다. 토큰은 한 번만 표시됩니다.
Go 프로젝트 설정하기#
새 Go 프로젝트를 생성합니다.
mkdir runner-admission-controller
cd runner-admission-controller
go mod init example.com/runner-admission-controller
protobuf 정의에서 클라이언트 코드 생성하기#
GitLab Agent for Kubernetes 리포지터리에 있는 Protobuf 정의로부터 gRPC 클라이언트 코드를 생성해야 합니다. 원하는 방법을 사용할 수 있으며, 예를 들면 다음과 같습니다.
.proto파일을 수동으로 벤더링하고protoc을 직접 사용하는 방법.buf를 사용해 코드를 자동으로 가져오고 생성하는 방법.
Protobuf 정의에 대한 자세한 내용은 러너 컨트롤러 사양의 generating client code 를 참고합니다.
이 튜토리얼에서는 buf를 사용합니다. buf.gen.yaml을 생성합니다.
version: v2
managed:
enabled: true
disable:
- module: buf.build/bufbuild/protovalidate
override:
- file_option: go_package
value: internal/rpc
inputs:
- git_repo: https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent.git
branch: master
plugins:
- local: ["go", "run", "google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.10"]
out: .
- local: ["go", "run", "google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1"]
out: .
코드를 생성합니다.
buf generate
이 명령을 실행하면 internal/rpc/에 gRPC 클라이언트 코드가 생성됩니다.
인증 구현하기#
러너 컨트롤러는 gRPC 메타데이터 헤더를 사용해 job 라우터로 인증합니다. 사양에 대한 자세한 내용은 러너 컨트롤러 사양의 Authentication 을 참고합니다.
필요한 헤더를 포함하는 자격 증명 제공자를 생성합니다.
type tokenCredentials struct {
token string
}
func (t *tokenCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + t.token,
"gitlab-agent-type": "runnerc",
}, nil
}
func (t *tokenCredentials) RequireTransportSecurity() bool {
return true
}
다음 코드를 사용해 gRPC 연결을 생성합니다.
conn, err := grpc.NewClient(kasAddress,
grpc.WithTransportCredentials(credentials.NewTLS(nil)),
grpc.WithPerRPCCredentials(&tokenCredentials{token: agentToken}),
)
에이전트 등록 구현하기#
존재 여부 추적과 모니터링을 위해 컨트롤러를 job 라우터에 등록합니다. 존재 상태를 유지하려면 주기적으로(권장: 3분마다) 재등록합니다. 사양에 대한 자세한 내용은 러너 컨트롤러 사양의 AgentRegistrar 를 참고합니다.
func registerAgent(ctx context.Context, conn *grpc.ClientConn, instanceID int64) error {
client := rpc.NewAgentRegistrarClient(conn)
_, err := client.Register(ctx, &rpc.RegisterRequest{
Meta: &rpc.Meta{
Version: "1.0.0",
GitRef: "main",
Architecture: runtime.GOARCH,
},
InstanceId: instanceID,
})
return err
}
어드미션 루프 구현하기#
어드미션 루프는 job 라우터로부터 job 세부 정보를 수신하고 결정을 전송합니다. 사양에 대한 자세한 내용은 러너 컨트롤러 사양의 RunnerControllerService 와 Protocol Flow 를 참고합니다.
func handleAdmissionRequest(ctx context.Context, client rpc.RunnerControllerServiceClient) error {
admissionCtx, cancel := context.WithCancel(ctx)
defer cancel()
stream, err := client.AdmitJob(admissionCtx)
if err != nil {
return err
}
// Wait for admission request
req, err := stream.Recv()
if err != nil {
return err
}
// Evaluate the job (implement your policy here)
admitted, reason := evaluateJob(req)
// Send decision
var resp *rpc.AdmitJobResponse
if admitted {
resp = &rpc.AdmitJobResponse{
AdmissionResponse: &rpc.AdmitJobResponse_Admitted{Admitted: &rpc.Admitted{}},
}
} else {
resp = &rpc.AdmitJobResponse{
AdmissionResponse: &rpc.AdmitJobResponse_Rejected{
Rejected: &rpc.Rejected{Reason: reason},
},
}
}
if err := stream.Send(resp); err != nil {
return err
}
_ = stream.CloseSend()
var x any
err = stream.RecvMsg(x) // consume EOF
if err != io.EOF {
return err
}
return nil
}
어드미션 정책 구현하기#
커스텀 정책 로직을 구현합니다. 이 예시는 :latest 태그가 붙은 이미지를 거부합니다.
func evaluateJob(req *rpc.AdmitJobRequest) (admitted bool, reason string) {
imageName := req.GetImage().GetName()
// Reject :latest tags
if strings.HasSuffix(imageName, ":latest") {
return false, "images with :latest tag are not allowed"
}
// Check allowlist
allowed := []string{"alpine", "ubuntu", "golang", "ruby", "node", "python"}
for _, prefix := range allowed {
if strings.HasPrefix(imageName, prefix) {
return true, ""
}
}
return false, fmt.Sprintf("image %s is not in the approved list", imageName)
}
dry run 상태로 테스트하기#
컨트롤러가 실행 중이고 dry_run 상태일 때 CI/CD 파이프라인을 트리거합니다. 컨트롤러
로그를 확인해 어드미션 요청을 수신하는지 검증합니다. job 라우터는 dry run 상태인
컨트롤러에 대해 결정을 로그로 남기지만 강제 적용하지는 않습니다. 이 작업을 통해 강제
적용을 활성화하기 전에 동작을 검증하고 배포 위험을 줄일 수 있습니다.
프로덕션에서 활성화하기#
dry_run 상태에서 컨트롤러 동작을 검증한 후 enabled 상태로 업데이트합니다.
glab runner-controller update <controller_id> --state enabled
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{"state": "enabled"}' \
--url "https://gitlab.example.com/api/v4/runner_controllers/<controller_id>"
이제 어드미션 결정이 job 실행에 영향을 미칩니다.
러너 컨트롤러 호스팅#
러너 컨트롤러 호스팅 방식은 GitLab 인스턴스의 규모와 어드미션 제어의 영향을 받는 job 부하에 따라 자유롭게 선택할 수 있습니다. 유일한 요구 사항은 러너 컨트롤러가 연결하는 대상인 GitLab 인스턴스에 도달할 수 있어야 한다는 것입니다.
다음 단계#
- 전체 예시 구현을 확인합니다.
- 프로토콜 세부 정보는 runner controller specification 을 참고합니다.
- 더 복잡한 정책에는 Open Policy Agent (OPA) 사용을 검토합니다.