正式なドキュメントは英語版であり、この日本語訳はAI支援翻訳により作成された参考用のものです。日本語訳の一部の内容は人間によるレビューがまだ行われていないため、翻訳のタイミングにより英語版との間に差異が生じることがあります。最新かつ正確な情報については、英語版をご参照ください。

チュートリアル: Runnerコントローラーの許可リストをビルドする

このチュートリアルでは、CI/CDジョブの実行に関するカスタムポリシーを適用する、Runnerコントローラーのビルドについて説明します。Goでコントローラーを作成し、ジョブルーターに接続して、イメージ許可リストポリシーを実装します。

このチュートリアルのコード例は、runner-controller-exampleリポジトリから転載したもので、出発点として使用できる完全な参照実装を提供します。

このチュートリアルを終えるまでに、次のことを行う、実用的なコントローラーが完成します:

  • gRPCを使用してジョブルーターに接続する
  • GitLabに自身を登録する
  • ジョブアドミッションリクエストを受信する
  • カスタムポリシーに対してジョブを検証する
  • アドミッションの決定を返す

Runnerコントローラーをビルドするには、次の手順を実行します:

  1. GitLabでRunnerコントローラーを作成する
  2. Runnerコントローラーのスコープを設定する
  3. Runnerコントローラートークンを作成する
  4. Goプロジェクトをセットアップする
  5. protobuf定義からクライアントコードを生成する
  6. 認証を実装する
  7. エージェント登録を実装する
  8. アドミッションループを実装する
  9. アドミッションポリシーを実装する
  10. ドライラン状態でテストする
  11. 本番環境で有効にする

はじめる前

以下を確認してください:

  • UltimateティアのGitLabセルフマネージドまたはGitLab Dedicated
  • GitLabインスタンスへの管理者アクセス
  • GitLab APIとやり取りするための次のいずれか:
    • GitLab CLI (glab) 1.85.0以降。glab auth loginで認証されています
    • 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コントローラーを作成する

RunnerコントローラーAPIを使用して、Runnerコントローラーを作成します。

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を保存します。

Runnerコントローラーのスコープを設定する

Runnerコントローラーは、アドミッションリクエストを受信するようにスコープを設定する必要があります。スコープがないと、有効にしてもコントローラーは非アクティブのままになります。

このチュートリアルでは、インスタンス内のすべてのRunnerにコントローラーのスコープを設定します:

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コントローラーAPIを使用して、特定のRunnerにコントローラーのスコープを設定することもできます。特定のRunnerに対してのみジョブを検証するようにコントローラーを設定する場合は、Runnerレベルのスコープ設定を使用します。

Runnerコントローラートークンを作成する

ジョブルーターで認証するために、Runnerコントローラーのトークンを作成します:

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エージェントリポジトリ内のProtobuf定義からgRPCクライアントコードを生成する必要があります。次の方法を含め、任意の方法を使用できます:

  • .protoファイルをフェッチして、protocを直接使用します。
  • bufを使用してコードを自動的にフェッチおよび生成します。

Protobuf定義の詳細については、Runnerコントローラーの仕様のクライアントコードの生成を参照してください。

このチュートリアルでは、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

これにより、gRPCクライアントコードがinternal/rpc/に作成されます。

認証を実装する

Runnerコントローラーは、gRPCメタデータヘッダーを使用してジョブルーターで認証します。仕様の詳細については、Runnerコントローラーの仕様の認証を参照してください。

必要なヘッダーを含む認証情報プロバイダーを作成します:

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分ごと)。仕様の詳細については、Runnerコントローラーの仕様の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
}

アドミッションループを実装する

アドミッションループは、ジョブルーターからジョブの詳細を受信し、決定を送信します。仕様の詳細については、Runnerコントローラーの仕様の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>"

これで、アドミッションの決定がジョブの実行に影響を与えるようになりました。

Runnerコントローラーのホスト

Runnerコントローラーのホストは、アドミッション制御の影響を受けるジョブのスケールと負荷に応じて異なりますGitLabインスタンスの。唯一の要件は、GitLabインスタンスがRunnerコントローラーから到達可能であることです。それは接続先であるためです。

次の手順