InfoGrab DocsInfoGrab Docs

튜토리얼: 러너 어드미션 컨트롤러 빌드하기

요약

이 튜토리얼에서는 CI/CD job 실행에 대한 커스텀 정책을 적용하는 러너 어드미션 컨트롤러를 빌드하는 과정을 안내합니다. 이 튜토리얼의 코드 예시는 runner-controller-example 리포지터리를 기반으로 작성되었으며, 이 리포지터리는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공합니다.

이 튜토리얼에서는 CI/CD job 실행에 대한 커스텀 정책을 적용하는 러너 어드미션 컨트롤러를 빌드하는 과정을 안내합니다. job 라우터에 연결하고 이미지 허용 목록 정책을 구현하는 컨트롤러를 Go로 작성합니다.

이 튜토리얼의 코드 예시는 runner-controller-example 리포지터리를 기반으로 작성되었으며, 이 리포지터리는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공합니다.

이 튜토리얼을 마치면 다음을 수행하는 동작하는 어드미션 컨트롤러를 갖추게 됩니다.

  • gRPC를 사용해 job 라우터에 연결합니다
  • GitLab에 자신을 등록합니다
  • job 어드미션 요청을 수신합니다
  • 커스텀 정책에 따라 job을 평가합니다
  • 어드미션 결정을 반환합니다

러너 어드미션 컨트롤러를 빌드하려면 다음을 수행합니다.

  1. GitLab에서 러너 컨트롤러 생성하기
  2. 러너 컨트롤러 범위 지정하기
  3. 러너 컨트롤러 토큰 생성하기
  4. Go 프로젝트 설정하기
  5. protobuf 정의에서 클라이언트 코드 생성하기
  6. 인증 구현하기
  7. 에이전트 등록 구현하기
  8. 어드미션 루프 구현하기
  9. 어드미션 정책 구현하기
  10. dry run 상태로 테스트하기
  11. 프로덕션에서 활성화하기

시작하기 전에#

다음을 준비합니다.

  • Ultimate 티어의 GitLab Self-Managed 또는 GitLab Dedicated
  • GitLab 인스턴스에 대한 관리자 액세스 권한
  • GitLab API와 상호작용하기 위한 다음 중 하나:
    • glab auth login으로 인증된 GitLab CLI (glab) 1.85.0 이상
    • curl 또는 다른 HTTP 클라이언트
  • Go 1.21 이상 설치
  • Protobuf 코드 생성을 위한 buf CLI 설치
  • GitLab 인스턴스에서 활성화된 다음 기능 플래그:
    • job_router
    • job_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 인스턴스에 도달할 수 있어야 한다는 것입니다.

다음 단계#

튜토리얼: 러너 어드미션 컨트롤러 빌드하기

GitLab v19.5
원문 보기

요약

이 튜토리얼에서는 CI/CD job 실행에 대한 커스텀 정책을 적용하는 러너 어드미션 컨트롤러를 빌드하는 과정을 안내합니다. 이 튜토리얼의 코드 예시는 runner-controller-example 리포지터리를 기반으로 작성되었으며, 이 리포지터리는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공합니다.

이 튜토리얼에서는 CI/CD job 실행에 대한 커스텀 정책을 적용하는 러너 어드미션 컨트롤러를 빌드하는 과정을 안내합니다. job 라우터에 연결하고 이미지 허용 목록 정책을 구현하는 컨트롤러를 Go로 작성합니다.

이 튜토리얼의 코드 예시는 runner-controller-example 리포지터리를 기반으로 작성되었으며, 이 리포지터리는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공합니다.

이 튜토리얼을 마치면 다음을 수행하는 동작하는 어드미션 컨트롤러를 갖추게 됩니다.

  • gRPC를 사용해 job 라우터에 연결합니다
  • GitLab에 자신을 등록합니다
  • job 어드미션 요청을 수신합니다
  • 커스텀 정책에 따라 job을 평가합니다
  • 어드미션 결정을 반환합니다

러너 어드미션 컨트롤러를 빌드하려면 다음을 수행합니다.

  1. GitLab에서 러너 컨트롤러 생성하기
  2. 러너 컨트롤러 범위 지정하기
  3. 러너 컨트롤러 토큰 생성하기
  4. Go 프로젝트 설정하기
  5. protobuf 정의에서 클라이언트 코드 생성하기
  6. 인증 구현하기
  7. 에이전트 등록 구현하기
  8. 어드미션 루프 구현하기
  9. 어드미션 정책 구현하기
  10. dry run 상태로 테스트하기
  11. 프로덕션에서 활성화하기

시작하기 전에#

다음을 준비합니다.

  • Ultimate 티어의 GitLab Self-Managed 또는 GitLab Dedicated
  • GitLab 인스턴스에 대한 관리자 액세스 권한
  • GitLab API와 상호작용하기 위한 다음 중 하나:
    • glab auth login으로 인증된 GitLab CLI (glab) 1.85.0 이상
    • curl 또는 다른 HTTP 클라이언트
  • Go 1.21 이상 설치
  • Protobuf 코드 생성을 위한 buf CLI 설치
  • GitLab 인스턴스에서 활성화된 다음 기능 플래그:
    • job_router
    • job_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 인스턴스에 도달할 수 있어야 한다는 것입니다.

다음 단계#