InfoGrab Docs

튜토리얼: 러너 어드미션 컨트롤러 구축

요약

이 튜토리얼은 CI/CD 작업 실행에 대한 사용자 정의 정책을 적용하는 러너 어드미션 컨트롤러를 구축하는 방법을 안내합니다. 이 튜토리얼의 코드 예제는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공하는 runner-controller-example 저장소에서 가져온 것입니다.

이 튜토리얼은 CI/CD 작업 실행에 대한 사용자 정의 정책을 적용하는 러너 어드미션 컨트롤러를 구축하는 방법을 안내합니다. 작업 라우터에 연결하고 이미지 허용 목록 정책을 구현하는 Go로 컨트롤러를 만들 것입니다.

이 튜토리얼의 코드 예제는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공하는 runner-controller-example 저장소에서 가져온 것입니다.

이 튜토리얼을 마치면 다음과 같은 작동하는 어드미션 컨트롤러를 갖게 됩니다:

  • gRPC를 사용하여 작업 라우터에 연결
  • GitLab에 자체 등록
  • 작업 어드미션 요청 수신
  • 사용자 정의 정책에 따라 작업 평가
  • 어드미션 결정 반환

러너 어드미션 컨트롤러를 구축하려면:

  1. GitLab에서 러너 컨트롤러 만들기
  2. 러너 컨트롤러 범위 지정
  3. 러너 컨트롤러 토큰 만들기
  4. Go 프로젝트 설정
  5. protobuf 정의에서 클라이언트 코드 생성
  6. 인증 구현
  7. 에이전트 등록 구현
  8. 어드미션 루프 구현
  9. 어드미션 정책 구현
  10. 드라이 런 상태로 테스트
  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에서 러너 컨트롤러 만들기#

러너 컨트롤러 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 인스턴스에서 도달 가능해야 한다는 것입니다.

다음 단계#

튜토리얼: 러너 어드미션 컨트롤러 구축

원문 보기
요약

이 튜토리얼은 CI/CD 작업 실행에 대한 사용자 정의 정책을 적용하는 러너 어드미션 컨트롤러를 구축하는 방법을 안내합니다. 이 튜토리얼의 코드 예제는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공하는 runner-controller-example 저장소에서 가져온 것입니다.

이 튜토리얼은 CI/CD 작업 실행에 대한 사용자 정의 정책을 적용하는 러너 어드미션 컨트롤러를 구축하는 방법을 안내합니다. 작업 라우터에 연결하고 이미지 허용 목록 정책을 구현하는 Go로 컨트롤러를 만들 것입니다.

이 튜토리얼의 코드 예제는 시작점으로 사용할 수 있는 완전한 참조 구현을 제공하는 runner-controller-example 저장소에서 가져온 것입니다.

이 튜토리얼을 마치면 다음과 같은 작동하는 어드미션 컨트롤러를 갖게 됩니다:

  • gRPC를 사용하여 작업 라우터에 연결
  • GitLab에 자체 등록
  • 작업 어드미션 요청 수신
  • 사용자 정의 정책에 따라 작업 평가
  • 어드미션 결정 반환

러너 어드미션 컨트롤러를 구축하려면:

  1. GitLab에서 러너 컨트롤러 만들기
  2. 러너 컨트롤러 범위 지정
  3. 러너 컨트롤러 토큰 만들기
  4. Go 프로젝트 설정
  5. protobuf 정의에서 클라이언트 코드 생성
  6. 인증 구현
  7. 에이전트 등록 구현
  8. 어드미션 루프 구현
  9. 어드미션 정책 구현
  10. 드라이 런 상태로 테스트
  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에서 러너 컨트롤러 만들기#

러너 컨트롤러 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 인스턴스에서 도달 가능해야 한다는 것입니다.

다음 단계#