튜토리얼: 러너 어드미션 컨트롤러 구축
이 튜토리얼은 CI/CD 작업 실행에 대한 사용자 정의 정책을 적용하는 러너 어드미션 컨트롤러를 구축하는 방법을 안내합니다. 이 튜토리얼의 코드 예제는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공하는 runner-controller-example 저장소에서 가져온 것입니다.
이 튜토리얼은 CI/CD 작업 실행에 대한 사용자 정의 정책을 적용하는 러너 어드미션 컨트롤러를 구축하는 방법을 안내합니다. 작업 라우터에 연결하고 이미지 허용 목록 정책을 구현하는 Go로 컨트롤러를 만들 것입니다.
이 튜토리얼의 코드 예제는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공하는 runner-controller-example 저장소에서 가져온 것입니다.
이 튜토리얼을 마치면 다음과 같은 작동하는 어드미션 컨트롤러를 갖게 됩니다:
- gRPC를 사용하여 작업 라우터에 연결
- GitLab에 자체 등록
- 작업 어드미션 요청 수신
- 사용자 정의 정책에 따라 작업 평가
- 어드미션 결정 반환
러너 어드미션 컨트롤러를 구축하려면:
- GitLab에서 러너 컨트롤러 만들기
- 러너 컨트롤러 범위 지정
- 러너 컨트롤러 토큰 만들기
- Go 프로젝트 설정
- protobuf 정의에서 클라이언트 코드 생성
- 인증 구현
- 에이전트 등록 구현
- 어드미션 루프 구현
- 어드미션 정책 구현
- 드라이 런 상태로 테스트
- 프로덕션에서 활성화
시작하기 전에#
다음이 있어야 합니다:
- 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에서 러너 컨트롤러 만들기#
러너 컨트롤러 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"
또는 러너 컨트롤러 API를 사용하여 특정 러너에 컨트롤러 범위를 지정할 수 있습니다. 컨트롤러가 특정 러너에 대한 작업만 평가하도록 하려면 러너 수준 범위 지정을 사용합니다.
러너 컨트롤러 토큰 만들기#
러너 컨트롤러가 작업 라우터에 인증하기 위한 토큰을 만듭니다:
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 정의에서 클라이언트 코드 생성#
Kubernetes용 GitLab Agent 저장소의 Protobuf 정의에서 gRPC 클라이언트 코드를 생성해야 합니다. 다음을 포함하여 선호하는 방법을 사용할 수 있습니다:
.proto파일을 수동으로 벤더링하고protoc를 직접 사용.buf를 사용하여 코드를 자동으로 가져오고 생성.
Protobuf 정의에 대한 자세한 내용은 러너 컨트롤러 사양의 클라이언트 코드 생성을 참조하세요.
이 튜토리얼은 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 메타데이터 헤더를 사용하여 작업 라우터에 인증합니다. 사양 세부 정보는 러너 컨트롤러 사양의 인증을 참조하세요.
필요한 헤더를 포함하는 자격 증명 공급자를 만듭니다:
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}),
)
에이전트 등록 구현#
존재 추적 및 모니터링을 위해 작업 라우터에 컨트롤러를 등록합니다. 존재를 유지하기 위해 주기적으로(권장: 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
}
어드미션 루프 구현#
어드미션 루프는 작업 라우터에서 작업 세부 정보를 수신하고 결정을 보냅니다. 사양 세부 정보는 러너 컨트롤러 사양의 RunnerControllerService 및 프로토콜 흐름을 참조하세요.
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 상태에 있으면 CI/CD 파이프라인을 트리거합니다.
컨트롤러 로그를 확인하여 어드미션 요청을 수신하는지 확인합니다.
작업 라우터는 결정을 기록하지만 드라이 런 상태의 컨트롤러에 대해서는 적용하지 않습니다.
이 작업을 통해 적용을 활성화하기 전에 동작을 검증하고 배포 위험을 줄일 수 있습니다.
프로덕션에서 활성화#
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>"
이제 어드미션 결정이 작업 실행에 영향을 미칩니다.
러너 컨트롤러 호스팅#
러너 컨트롤러 호스팅은 GitLab 인스턴스의 규모와 어드미션 컨트롤의 영향을 받는 작업의 부하에 따라 달라집니다. 유일한 요구 사항은 러너 컨트롤러가 연결되는 곳이 GitLab 인스턴스에서 도달 가능해야 한다는 것입니다.
다음 단계#
- 완전한 예제 구현을 검토합니다.
- 프로토콜 세부 정보는 러너 컨트롤러 사양을 읽습니다.
- 더 복잡한 정책에 Open Policy Agent (OPA) 사용을 탐색합니다.
