Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Actions Runner Controller Deep Dive

Avatar for Junya Taniai Junya Taniai
January 26, 2024
720

Actions Runner Controller Deep Dive

Avatar for Junya Taniai

Junya Taniai

January 26, 2024
Tweet

Transcript

  1. ⾕合 純也(たにあい じゅんや) 福岡在住 • 所属:株式会社エーピーコミュニケーションズ Azure Container Solution事業部 Cloud

    Integrationチーム • 業務:GitHub Enterprise、 Microsoft Azureのコンテナ製品導入支援 • 趣味:Kubernetes Operator実装やコードリーディング、筋トレ jnytnai0613 Jnytnai0530 2
  2. はじめに Actions Runner Controller(以降ARC)の最新の機能であるAutoscaling Runner Scale Setsモードを、 紹介します。 なお、Autoscaling Runner

    Scale Sets登場以前のモードはレガシーモードとされていますので、 スコープ外とします。 actions/actions-runner-controller 今回は以下のブログを、登壇用としてまとめています。 Actions Runner Controller Deep Dive!- アーキテクチャ編 – Actions Runner Controller Deep Dive!- 動作解説編 - Actions Runner Controller Deep Dive!- コード解説 前編 - Actions Runner Controller Deep Dive!- コード解説 後編 - 4
  3. Agenda 1. アーキテクチャ紹介 2. 動作紹介 - 基本動作 - Runnerの最大/最小設定 -

    ARCでの、Workflow内Containerの使用 - Kubernetesモード 3. コードから見るARC - 事前知識 - GitHubとのロングポーリング - Runnerのスケール 5
  4. アーキテクチャ紹介 Controller • AutoScalingRunnerSet Controller EphemeralRunnerSetおよびAutoScalingListerner CustomResourceの作成および更新、削除 • EphemeralRunnerSet Controller

    EphemeralRunner CustomResourceの作成および更新、削除 • EphemeralRunner Controller EphemeralRunner Podの作成および削除 • AutoScalingListerner Controller AutoScalingListerner Podの作成および削除 Pod • Controller Pod 上記Controllerが動くmanager • EphemeralRunner Pod Self-hosted runnerとして動く • AutoScalingListerner Pod GitHubとのロングポーリングを確立し、GitHub Actions時にMessageを受信し、EphemeralRunnerSet の.Spec.Replicasを増減させる 6
  5. Agenda 1. アーキテクチャ紹介 2. 動作紹介 - 基本動作 - Runnerの最大/最小設定 -

    ARCでの、Workflow内Containerの使用 - Kubernetesモード 3. コードから見るARC - 事前知識 - GitHubとのロングポーリング - Runnerのスケール 9
  6. 動作紹介 - 基本動作 - $ INSTALLATION_NAME="arc-runner-set" $ NAMESPACE="arc-runners" $ GITHUB_CONFIG_URL="<リポジトリURL>"

    $ GITHUB_PAT="pre-defined-secret" $ helm install "${INSTALLATION_NAME}" ¥ --namespace "${NAMESPACE}" ¥ --set githubConfigUrl="${GITHUB_CONFIG_URL}" ¥ --set githubConfigSecret="${GITHUB_PAT}" ¥ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set HelmでのAutoScalingRunnerSetリソースのデプロイコマンドは以下の通り # AutoScalingRunnerSet $ kubectl -n arc-runners get autoscalingrunnersets.actions.github.com NAME MINIMUM RUNNERS MAXIMUM RUNNERS CURRENT RUNNERS STATE PENDING RUNNERS RUNNING RUNNERS FINISHED RUNNERS DELETING RUNNERS arc-runner-set 0 0 0 # AutoScalingListner $ kubectl -n arc-systems get autoscalinglisteners.actions.github.com NAME GITHUB CONFIGURE URL AUTOSCALINGRUNNERSET NAMESPACE AUTOSCALINGRUNNERSET NAME arc-runner-set-754b578d-listener https://github.com/apc-jnytnai0613/arc-test arc-runners arc-runner-set # AutoScalingListener Pod $ kubectl -n arc-systems get po arc-runner-set-754b578d-listener NAME READY STATUS RESTARTS AGE arc-runner-set-754b578d-listener 1/1 Running 0 8m3s # EphemeralRunnerSet $ kubectl -n arc-runners get ephemeralrunnersets.actions.github.com NAME DESIREDREPLICAS CURRENTREPLICAS PENDING RUNNERS RUNNING RUNNERS FINISHED RUNNERS DELETING RUNNERS arc-runner-set-tcttw 0 0 0 各種リソースがデプロイされたことが確認できる 10
  7. 動作紹介 - 基本動作 - name: Test workflow on: workflow_dispatch: jobs:

    test1: runs-on: arc-runner-set steps: - name: Hello world run: echo "Hello" test2: runs-on: arc-runner-set steps: - name: Hello world run: echo "World" 以下のWorkflowを実行してみる 12
  8. 動作紹介 - 基本動作 - EphemeralRunnerSetを確認すると、Worklflowのジョブ数に応じて、EphemeralRunnerのスケールアウトからスケールインまで 実行されていることが確認できる $ kubectl -n arc-runners

    get ephemeralrunnersets.actions.github.com -w NAME DESIREDREPLICAS CURRENTREPLICAS PENDING RUNNERS RUNNING RUNNERS FINISHED RUNNERS DELETING RUNNERS arc-runner-set-tcttw 0 0 0 arc-runner-set-tcttw 2 0 0 0 arc-runner-set-tcttw 2 2 2 0 arc-runner-set-tcttw 2 2 1 1 arc-runner-set-tcttw 2 1 0 1 arc-runner-set-tcttw 2 1 1 1 arc-runner-set-tcttw 2 2 1 1 arc-runner-set-tcttw 2 2 1 1 arc-runner-set-tcttw 2 2 0 2 arc-runner-set-tcttw 2 0 2 arc-runner-set-tcttw 1 0 1 arc-runner-set-tcttw 0 0 0 スケールアウト スケールイン 13 スケールイン
  9. Agenda 1. アーキテクチャ紹介 2. 動作紹介 - 基本動作 - Runnerの最大/最小設定 -

    ARCでの、Workflow内Containerの使用 - Kubernetesモード 3. コードから見るARC - 事前知識 - GitHubとのロングポーリング - Runnerのスケール 15
  10. $ INSTALLATION_NAME="arc-runner-set" $ NAMESPACE="arc-runners" $ GITHUB_CONFIG_URL="https://github.com/apc-jnytnai0613/arc-test" $ GITHUB_PAT="pre-defined-secret" $ helm

    upgrade "${INSTALLATION_NAME}" ¥ --namespace "${NAMESPACE}" ¥ --set githubConfigUrl="${GITHUB_CONFIG_URL}" ¥ --set githubConfigSecret="${GITHUB_PAT}" ¥ --set maxRunners=5 ¥ --set minRunners=2 ¥ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set HelmのオプションにmaxRunnersやminRunnersを設定することでRunnerに最大・最小設定を行うことが可能となる $ kubectl -n arc-runners get ephemeralrunnersets.actions.github.com -w NAME DESIREDREPLICAS CURRENTREPLICAS PENDING RUNNERS RUNNING RUNNERS FINISHED RUNNERS DELETING RUNNERS arc-runner-set-tcttw 2 2 0 2 EphemeralRunnerSetを確認すると、minRunnerで指定した数だけ、Runnerが起動しているのが見える。 動作紹介 - Runnerの最大/最小設定 - 16
  11. 動作紹介 - Runnerの最大/最小設定 - $ kubectl -n arc-runners get ephemeralrunnersets.actions.github.com

    -w NAME DESIREDREPLICAS CURRENTREPLICAS PENDING RUNNERS RUNNING RUNNERS FINISHED RUNNERS DELETING RUNNERS arc-runner-set-tcttw 2 2 0 2 arc-runner-set-tcttw 5 2 0 2 arc-runner-set-tcttw 5 5 3 2 arc-runner-set-tcttw 5 5 2 3 arc-runner-set-tcttw 5 5 1 4 arc-runner-set-tcttw 5 5 0 5 arc-runner-set-tcttw 5 4 0 4 arc-runner-set-tcttw 5 5 1 4 arc-runner-set-tcttw 5 4 1 3 arc-runner-set-tcttw 5 5 2 3 arc-runner-set-tcttw 5 5 1 4 arc-runner-set-tcttw 5 5 0 5 arc-runner-set-tcttw 4 5 0 5 arc-runner-set-tcttw 4 4 0 4 arc-runner-set-tcttw 4 3 0 3 arc-runner-set-tcttw 4 3 1 3 arc-runner-set-tcttw 4 4 1 3 : arc-runner-set-tcttw 2 1 1 0 arc-runner-set-tcttw 2 2 2 0 arc-runner-set-tcttw 2 2 1 1 arc-runner-set-tcttw 2 2 0 2 スケールアウト スケールイン Job数5のWorklflowを実行すると、EphemeralRunnerSetのreplicasが2→5に変化し、Workflow終了後は2に収束している ことが確認できます 17
  12. Agenda 1. アーキテクチャ紹介 2. 動作紹介 - 基本動作 - Runnerの最大/最小設定 -

    ARCでの、Workflow内Containerの使用 - Kubernetesモード 3. コードから見るARC - 事前知識 - GitHubとのロングポーリング - Runnerのスケール 18
  13. • Self-hosted runnerでは、runner内にdockerをあらかじめインストールしておく必要があります。 • Runnerコンテナの場合にWorkflowでコンテナを動かすには、docker.sockに接続してDocker Daemonと通信を行う必要があ りますが、ある工夫をしないと通信ができません。 ARCの場合は以下のエラーが発生しました。 ARCでの、Workflow内Containerの使用 Checking

    docker version /usr/bin/docker version --format '{{.Server.APIVersion}}’ Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? Error: Exit code 1 returned from process: file name '/usr/bin/docker', arguments 'version --format '{{.Server.APIVersion}}''. この問題を解決するに当たり、Workflowでコンテナを動かす場合は、以下の方法でdocker.sockと通信を行えます。 • ホスト上のdocker.sockをコンテナRunnerにマウントするDooD(Docker outside of Docker) • コンテナRunner内にさらにDockerを起動させるDinD(Docker in Docker) • Runner Container Hooks(actions/runner-container-hooks) の利用(ARCのRunner用コンテナイメージに組み込み 済) 参照:コンテナのセルフホストランナーの中でコンテナを使えるようにするrunner-container-hooks 19
  14. 動作紹介 - Kubernetesモード - Kubernetesモードは、helm用のvalues.yamlに以下の設定を記載します。 KubernetesモードではPVを/home/runner/_workディレクトリにマウントして、 Runner Podと、Workflowでコンテナを動かす用のJob Pod間で情報 を連携します。この時、ディレクトリに対して権限がない旨のエラーが発生するため、

    ドキュメントに従って、.spec.securityContext.fsGroupに コンテナのGIDを設定するか、以下のようにinitContainersを追加します。 $ cat values.yaml containerMode: type: "kubernetes" kubernetesModeWorkVolumeClaim: accessModes: ["ReadWriteOnce"] storageClassName: "managed" resources: requests: storage: 1Gi template: spec: initContainers: - name: kube-init image: ghcr.io/actions/actions-runner:latest command: ["sudo", "chown", "-R", "1001:1001", "/home/runner/_work"] volumeMounts: - name: work mountPath: /home/runner/_work containers: - name: runner image: ghcr.io/actions/actions-runner:latest command: ["/home/runner/run.sh"] env: - name: ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER value: "false" Kubernetesモード指定 PVC設定 Runner PodとJob Podの情報連携用ディレクトリ権限付与 20
  15. 動作紹介 - Kubernetesモード - $ INSTALLATION_NAME="arc-runner-set" $ NAMESPACE="arc-runners" $ GITHUB_CONFIG_URL="https://github.com/apc-jnytnai0613/arc-test"

    $ GITHUB_PAT="pre-defined-secret" $ helm upgrade "${INSTALLATION_NAME}" ¥ --namespace "${NAMESPACE}" ¥ --set githubConfigUrl="${GITHUB_CONFIG_URL}" ¥ --set githubConfigSecret="${GITHUB_PAT}" ¥ -f values.yaml ¥ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set インストール後、Workflowを実行することで、以下のように、PVCとPVが自動で作成されます。 $ k -n arc-runners get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE arc-runner-set-8xxhb-runner-67dzd-work Bound pvc-4bb5fdd8-4137-4014-b42a-b55177a552bb 1Gi RWO managed 20m arc-runner-set-8xxhb-runner-h8fh7-work Bound pvc-6fe85296-323f-4441-b9c5-606454703619 1Gi RWO managed 44h $ k -n arc-runners get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS pvc-4bb5fdd8-4137-4014-b42a-b55177a552bb 1Gi RWO Delete Bound arc-runners/arc-runner-set-8xxhb-runner-67dzd-work managed pvc-6fe85296-323f-4441-b9c5-606454703619 1Gi RWO Delete Bound arc-runners/arc-runner-set-8xxhb-runner-h8fh7-work managed 21
  16. 動作紹介 - Kubernetesモード - function createPod(jobContainer, services, registry, extension) {

    var _a; return __awaiter(this, void 0, void 0, function () { var containers, appPod, instanceLabel, _b, claimName, secret, secretReference, body; var _c; return __generator(this, function (_d) { switch (_d.label) { : case 1: _b.nodeName = _d.sent(); claimName = (0, constants_1.getVolumeClaimName)(); appPod.spec.volumes = [ { name: 'work', persistentVolumeClaim: { claimName: claimName } } ]; if (!registry) return [3 /*break*/, 3]; return [4 /*yield*/, createDockerSecret(registry)]; : } }); }); } Workflow内でコンテナを実際に動かすのは、Runner Podではなく、Job Podです。 Job PodはARCが作成するのではなく、Runnerコンテナに組み込み済みのRunner Container Hooksが行います。 Runner Container HooksでJob Podを作成するコードは以下の通りで、values.yamlで指定したPVCを使用し、Volumeをマ ウントします。この時、workという名前のVolumeが使用され、ユーザは指定できませんでした。 22
  17. 動作紹介 - Kubernetesモード - ADR 0096: Hook extensions https://github.com/actions/runner-container-hooks/blob/main/docs/adrs/0096-hook-extensions.md 前述した通り、Runner

    Container Hookでは定義がガチガチに決まっており、ユーザが自由にVolumeの指定などができないよう になっていました。 そこで、最新のRunner Container Hook(0.4.0)から拡張が提供されました!! 拡張したいJob Pod定義をConfigMapリソース化して、Runner PodにACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE環 境変数として、読み込ませるだけで、拡張が可能になります。 これで、Volumeだけでなく、ServiceAccountなどのカスタムできるぞ! 23
  18. Agenda 1. アーキテクチャ紹介 2. 動作紹介 - 基本動作 - Runnerの最大/最小設定 -

    ARCでの、Workflow内Containerの使用 - Kubernetesモード 3. コードから見るARC - 事前知識 - GitHubとのロングポーリング - Runnerのスケール 24
  19. コードから見るARC - 事前知識 - 28 通常、なんらかの要因で本来あるべき振る舞いに乱れが生じた際、リトライ処理が必要となる。 ControllerはReconcile Loopという機能で、リトライ処理を自動化し、処理の冪等性を保証している。 つまり、あるべき姿が常に保持可能である。 あるべき姿

    Controller 管理対象Target (K8s 標準Resource, 外部Resource等) 比較 1. CRで管理対象Targetのあるべき姿を定義し、Deployする 2. Controllerが管理対象Targetあるべき姿を監視する 3. Controllerがあるべき姿と現在の姿を比較 4. 差異があれば、あるべき姿に戻す Runnerが削除されたら、自動デプロイ
  20. コードから見るARC - 事前知識 - 29 func (r * K8sNoviceOperatorReconciler) SetupWithManager(mgr

    ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). // 自分自身 For(&k8snoviceoperatorv1.K8sNoviceOperator{}). Complete(r) } • Reconcile Loopが呼び出されるタイミング • 10時間毎(デフォルト) • Manager.SyncPeriodで指定した時間毎 • 自分自身 or 子リソース or それ以外の監視対象のイベント発生時 自分自身以外の監視方法 Custom Controller外のリソースのイベントを監視する https://zenn.dev/ap_com/articles/5b64ab1ccd60a2
  21. コードから見るARC - 事前知識 - 1. 2個のジョブが定義されたGitHub Actions Workflowが実行される 2. ListenerはGitHubとロングポーリングを張っている。GitHubからジョブ実行のメッセージ受信

    3. ListenerはEphemeralRunnerSetの.Spec.ReplicasをWorkflowジョブの数だけスケールする 4. EphemeralRunnerSetの変更を検知したEphemeralRunnerSet Controllerは.Spec.Replicasの数だけ、 EphemeralRunnerを作成 5. EphemeralRunnerの作成を検知したEphemeralRunner Controllerは、 EphemeralRunnerの数だけRunner Podを作成 6. Runner Podは自分自身をGitHub Actionsに登録 Listenerの動作 31
  22. コードから見るARC - 事前知識 - Controller • AutoScalingRunnerSet Controller EphemeralRunnerSetおよびAutoScalingListerner CustomResourceの作成および更新、削除

    • EphemeralRunnerSet Controller EphemeralRunner CustomResourceの作成および更新、削除 • EphemeralRunner Controller EphemeralRunner Podの作成および削除 • AutoScalingListerner Controller AutoScalingListerner Podの作成および削除 Pod • Controller Pod 上記Controllerが動くmanager • EphemeralRunner Pod Self-hosted runnerとして動く • AutoScalingListerner Pod GitHubとのロングポーリングを確立し、GitHub Actions時にMessageを受信し、EphemeralRunnerSet の.Spec.Replicasを増減させる 今回の登場人物 今回の登場人物 32
  23. Agenda 1. アーキテクチャ紹介 2. 動作紹介 - 基本動作 - Runnerの最大/最小設定 -

    ARCでの、Workflow内Containerの使用 - Dindモード - Kubernetesモード 3. コードから見るARC - 事前知識 - GitHubとのロングポーリング - Runnerのスケール 33
  24. コードから見るARC - GitHubとのロングポーリング - 1. 2個のジョブが定義されたGitHub Actions Workflowが実行される 2. ListenerはGitHubとロングポーリングを張っている。GitHubからジョブ実行のメッセージ受信

    3. ListenerはEphemeralRunnerSetの.Spec.ReplicasをWorkflowジョブの数だけスケールする 4. EphemeralRunnerSetの変更を検知したEphemeralRunnerSet Controllerは.Spec.Replicasの数だけ、 EphemeralRunnerを作成 5. EphemeralRunnerの作成を検知したEphemeralRunner Controllerは、 EphemeralRunnerの数だけRunner Podを作成 6. Runner Podは自分自身をGitHub Actionsに登録 34 Listenerの動作
  25. コードから見るARC - GitHubとのロングポーリング - func (s *Service) Start() error {

    for { s.logger.Info("waiting for message...") select { case <-s.ctx.Done(): s.logger.Info("service is stopped.") return nil default: err := s.rsClient.GetRunnerScaleSetMessage(s.ctx, s.processMessage) if err != nil { return fmt.Errorf("could not get and process message. %w", err) } } } } <<ロングポーリングの実体>> 無限ループでGitHubからメッセージを待っている。 context.Contextがキャンセルされたら、ループを抜ける。そうでなければ、メッセージ受信を待ち続ける。 メッセージ受信は、 s.rsClient.GetRunnerScaleSetMessage関数で、メッセージの処理は、 s.processMessage関数で行われる。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/cmd/githubrunnerscalesetlistener/autoScalerService.go#L83-L97 35
  26. コードから見るARC - GitHubとのロングポーリング - func (m *AutoScalerClient) GetRunnerScaleSetMessage(ctx context.Context, handler

    func(msg *actions.RunnerScaleSetMessage) error) error { for { message, err := m.client.GetMessage(ctx, m.lastMessageId) if err != nil { return fmt.Errorf("get message failed from refreshing client. %w", err) } if message == nil { continue } err = handler(message) if err != nil { return fmt.Errorf("handle message failed. %w", err) } m.lastMessageId = message.MessageId return m.deleteMessage(ctx, message.MessageId) } } メッセージ受信は、 GetRunnerScaleSetMessage関数で行われる。この時、 受信したメッセージは、引数にあるhandler関数( s.processMessage関数)で処理され、EphemeralRunnerSetのスケールに 利用される。 https://github.com/actions/actions-runner- controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/cmd/githubrunnerscalesetlistener/autoScalerMessageListener.go#L132-L162 36
  27. コードから見るARC - GitHubとのロングポーリング - func (c *Client) GetMessage(ctx context.Context, messageQueueUrl,

    messageQueueAccessToken string, lastMessageId int64) (*RunnerScaleSetMessage, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err } resp, err := c.Do(req) if err != nil { return nil, err } if resp.StatusCode == http.StatusAccepted { defer resp.Body.Close() return nil, nil } var message *RunnerScaleSetMessage err = json.NewDecoder(resp.Body).Decode(&message) if err != nil { return nil, err } return message, nil } メッセージ受信は、GetMessage関数で行われる。 受信は、GETメソッドでRequestを作成して、Do関数でResponseを得る。得たjsonのResponseをGoの構造体にDecodeして メッセージに変換するといった流れとなる。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/github/actions/client.go#L457-L510 37
  28. Agenda 1. アーキテクチャ紹介 2. 動作紹介 - 基本動作 - Runnerの最大/最小設定 -

    ARCでの、Workflow内Containerの使用 - Dindモード - Kubernetesモード 3. コードから見るARC - 事前知識 - GitHubとのロングポーリング - Runnerのスケール 38
  29. コードから見るARC - Runnerのスケール - 1. 2個のジョブが定義されたGitHub Actions Workflowが実行される 2. ListenerはGitHubとロングポーリングを張っている。GitHubからジョブ実行のメッセージ受信

    3. ListenerはEphemeralRunnerSetの.Spec.ReplicasをWorkflowジョブの数だけスケールする 4. EphemeralRunnerSetの変更を検知したEphemeralRunnerSet Controllerは.Spec.Replicasの数だけ、 EphemeralRunnerを作成 5. EphemeralRunnerの作成を検知したEphemeralRunner Controllerは、 EphemeralRunnerの数だけRunner Podを作成 6. Runner Podは自分自身をGitHub Actionsに登録 39 Listenerの動作
  30. コードから見るARC - Runnerのスケール - func (s *Service) processMessage(message *actions.RunnerScaleSetMessage) error

    { var batchedMessages []json.RawMessage if err := json.NewDecoder(strings.NewReader(message.Body)).Decode(&batchedMessages); err != nil { return fmt.Errorf("could not decode job messages. %w", err) } err := s.rsClient.AcquireJobsForRunnerScaleSet(s.ctx, availableJobs) if err != nil { return fmt.Errorf("could not acquire jobs. %w", err) } return s.scaleForAssignedJobCount(message.Statistics.TotalAssignedJobs) } ロングポーリングで受信したメッセージには、GitHubでAssignされたジョブの合計数が統計として、包含されている。 このジョブの合計数に応じて、 s.scaleForAssignedJobCount関数で、EphemeralRunnerSetをスケールさせる。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/cmd/githubrunnerscalesetlistener/autoScalerService.go#L99-L204 40
  31. コードから見るARC - Runnerのスケール - func (s *Service) scaleForAssignedJobCount(count int) error

    { targetRunnerCount := int(math.Max(math.Min(float64(s.settings.MaxRunners), float64(count)), float64(s.settings.MinRunners))) if targetRunnerCount != s.currentRunnerCount { err := s.kubeManager.ScaleEphemeralRunnerSet(s.ctx, s.settings.Namespace, s.settings.ResourceName, targetRunnerCount) if err != nil { return fmt.Errorf("could not scale ephemeral runner set (%s/%s). %w", s.settings.Namespace, s.settings.ResourceName, err) } s.currentRunnerCount = targetRunnerCount } return nil } scaleForAssignedJobCount関数では、まずスケールすべき数を求めます。 1. math.Min(float64(s.settings.MaxRunners), float64(count)) ActionsRunnerScaleSetデプロイ時に指定したMaxRunnersと、GitHub ActionsでAssignされた数の内、小さい方を選択する 2. math.Max(math.Min(float64(s.settings.MaxRunners), float64(count)), float64(s.settings.MinRunners)) 1で求めた値と、 ActionsRunnerScaleSetデプロイ時に指定したMinRunnersの内、大きい方を選択する この計算により、MinRunners以下にはスケールされないようになっている。 上記で計算した数を引数にs.kubeManager.ScaleEphemeralRunnerSet関数を呼び出して、EphemeralRunnerSetのスケールを 行う。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/cmd/githubrunnerscalesetlistener/autoScalerService.go#L206-L226 41
  32. コードから見るARC - Runnerのスケール - func (k *AutoScalerKubernetesManager) ScaleEphemeralRunnerSet(ctx context.Context, namespace,

    resourceName string, runnerCount int) error { original := &v1alpha1.EphemeralRunnerSet{Spec: v1alpha1.EphemeralRunnerSetSpec{Replicas: -1}} originalJson, err := json.Marshal(original) if err != nil { k.logger.Error(err, "could not marshal empty ephemeral runner set") } patch := &v1alpha1.EphemeralRunnerSet{Spec: v1alpha1.EphemeralRunnerSetSpec{Replicas: runnerCount}} patchJson, err := json.Marshal(patch) if err != nil { k.logger.Error(err, "could not marshal patch ephemeral runner set") } mergePatch, err := jsonpatch.CreateMergePatch(originalJson, patchJson) if err != nil { k.logger.Error(err, "could not create merge patch json for ephemeral runner set") } patchedEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{} err = k.RESTClient().Patch(types.MergePatchType).Prefix("apis", "actions.github.com", "v1alpha1").Namespace(namespace).Resource("EphemeralRunnerSets"). Name(resourceName).Body([]byte(mergePatch)).Do(ctx).Into(patchedEphemeralRunnerSet) if err != nil { return fmt.Errorf("could not patch ephemeral runner set , patch JSON: %s, error: %w", string(mergePatch), err) } return nil } スケールさせるべき数runnerCountを使ってパッチを作成して、RestClientでPatchを当て、EphemeralRunnerSetをスケール させる https://github.com/actions/actions-runner- controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/cmd/githubrunnerscalesetlistener/autoScalerKubernetesManager.go#L40-L83 42
  33. コードから見るARC - Runnerのスケール - 1. 2個のジョブが定義されたGitHub Actions Workflowが実行される 2. ListenerはGitHubとロングポーリングを張っている。GitHubからジョブ実行のメッセージ受信

    3. ListenerはEphemeralRunnerSetの.Spec.ReplicasをWorkflowジョブの数だけスケールする 4. EphemeralRunnerSetの変更を検知したEphemeralRunnerSet Controllerは.Spec.Replicasの数だけ、 EphemeralRunnerを作成 5. EphemeralRunnerの作成を検知したEphemeralRunner Controllerは、 EphemeralRunnerの数だけRunner Podを作成 6. Runner Podは自分自身をGitHub Actionsに登録 Listenerの動作 43
  34. コードから見るARC - Runnerのスケール - func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error

    { : return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.EphemeralRunnerSet{}). Owns(&v1alpha1.EphemeralRunner{}). WithEventFilter(predicate.ResourceVersionChangedPredicate{}). Complete(r) } EphemeralRunnerSet ControllerはForの部分で、自身のCustomResourceが変更されたのを検知する仕組みになっている。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/ephemeralrunnerset_controller.go#L547- L576 44
  35. コードから見るARC - Runnerのスケール - func (r *EphemeralRunnerSetReconciler) createEphemeralRunners(ctx context.Context, runnerSet

    *v1alpha1.EphemeralRunnerSet, count int, log logr.Logger) error { // Track multiple errors at once and return the bundle. errs := make([]error, 0) for i := 0; i < count; i++ { ephemeralRunner := r.resourceBuilder.newEphemeralRunner(runnerSet) if err := ctrl.SetControllerReference(runnerSet, ephemeralRunner, r.Scheme); err != nil { log.Error(err, "failed to set controller reference on ephemeral runner") errs = append(errs, err) continue } log.Info("Creating new ephemeral runner", "progress", i+1, "total", count) if err := r.Create(ctx, ephemeralRunner); err != nil { log.Error(err, "failed to make ephemeral runner") errs = append(errs, err) continue } } return multierr.Combine(errs...) } Listenerにより、EphemeralRunnerSetがスケールされたことを検知したEphemeralRunnerSet Controllerは、 createEphemeralRunners関数を呼び出し、スケールさせるべき数分、EphemeralRunnerを作成する。 この時、 r.resourceBuilder.newEphemeralRunner関数で、 EphemeralRunner定義を作成する。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/ephemeralrunnerset_controller.go#L342- L369 45
  36. コードから見るARC - Runnerのスケール - func (b *resourceBuilder) newEphemeralRunner(ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet) *v1alpha1.EphemeralRunner

    { return &v1alpha1.EphemeralRunner{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ GenerateName: ephemeralRunnerSet.Name + "-runner-", Namespace: ephemeralRunnerSet.Namespace, Labels: labels, Annotations: annotations, }, Spec: ephemeralRunnerSet.Spec.EphemeralRunnerSpec, } } EphemeralRunnerは、 SpecにEphemeralRunnerSet.Spec.EphemeralRunnerSpecを設定することで、 EphemeralRunnerSetの 定義を引き継ぐ。これらを更にRunner Podに引き継ぐことでPodがRunnerとして機能することになる。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L438-L466 46
  37. コードから見るARC - Runnerのスケール - { "githubConfigSecret": "pre-defined-secret", "githubConfigUrl": "https://github.com/apc-jnytnai0613/arc-test", "metadata":

    {}, "runnerScaleSetId": 17, "spec": { "containers": [ { "command": [ "/home/runner/run.sh" ], "image": "ghcr.io/actions/actions-runner:latest", "name": "runner", "resources": {} } ], "restartPolicy": "Never", "serviceAccountName": "arc-runner-set-gha-rs-no-permission" } } EphemeralRunnerSet.Spec.EphemeralRunnerSpecは以下のような定義となっている。 • GitHubの認証に使用するPATやGitHub App • リポジトリURL • Runner Pod用コンテナイメージ などが定義されている。 47
  38. コードから見るARC - Runnerのスケール - 1. 2個のジョブが定義されたGitHub Actions Workflowが実行される 2. ListenerはGitHubとロングポーリングを張っている。GitHubからジョブ実行のメッセージ受信

    3. ListenerはEphemeralRunnerSetの.Spec.ReplicasをWorkflowジョブの数だけスケールする 4. EphemeralRunnerSetの変更を検知したEphemeralRunnerSet Controllerは.Spec.Replicasの数だけ、 EphemeralRunnerを作成 5. EphemeralRunnerの作成を検知したEphemeralRunner Controllerは、 EphemeralRunnerの数だけRunner Podを作成 6. Runner Podは自分自身をGitHub Actionsに登録 Listenerの動作 48
  39. コードから見るARC - Runnerのスケール - func (r *EphemeralRunnerReconciler) SetupWithManager(mgr ctrl.Manager) error

    { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.EphemeralRunner{}). Owns(&corev1.Pod{}). WithEventFilter(predicate.ResourceVersionChangedPredicate{}). Named("ephemeral-runner-controller"). Complete(r) } EphemeralRunner ControllerはForの部分で、自身のCustomResourceが変更されたのを検知する仕組みになっている。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/ephemeralrunner_controller.go#L789- L798 49
  40. コードから見るARC - Runnerのスケール - Listenerの動作 50 EphemeralRunnerSet Controllerにより、EphemeralRunnerが作成されたことを検知したEphemeralRunner Controller は、Runner

    Podを作成する。Pod作成前に、GitHub上にJobごとにRunnerを登録する時用の、Just-In-Time(以降JIT) Configを作 成する。JIT ConfigはGenerateJitRunnerConfig関数により、scaleSetEndpointにPOSTメソッドを投げ、作成される。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/github/actions/client.go#L691-L719 func (c *Client) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *RunnerScaleSetJitRunnerSetting, scaleSetId int) (*RunnerScaleSetJitRunnerConfig, error) { path := fmt.Sprintf("/%s/%d/generatejitconfig", scaleSetEndpoint, scaleSetId) body, err := json.Marshal(jitRunnerSetting) if err != nil { return nil, err } req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, path, bytes.NewBuffer(body)) if err != nil { return nil, err } resp, err := c.Do(req) if err != nil { return nil, err } var runnerJitConfig *RunnerScaleSetJitRunnerConfig err = json.NewDecoder(resp.Body).Decode(&runnerJitConfig) if err != nil { return nil, err } return runnerJitConfig, nil }
  41. コードから見るARC - Runnerのスケール - Listenerの動作 51 JIT Configは以下のような形式となっており、RunnerScaleSetIdやRunnerScaleSetNameなどScaleSetに関する情報も含まれる。 JIT ConfigはK8sのSecretリソースとして作成され、2重にエンコードされています。

    { ".runner": { "AgentId": "322", "AgentName": "arc-runner-set-v7nzm-runner-fmfcz", "DisableUpdate": "True", "Ephemeral": "True", "PoolId": "1", "PoolName": null, "ServerUrl": "https://pipelines.actions.githubusercontent.com/abKlCigKduPqALsnnIvHC16pYwPgN97mNIjD0UdG00l4ne31uP/", "RunnerScaleSetId": "12", "RunnerScaleSetName": "arc-runner-set", "WorkFolder": "_work" }, ".credentials": {xxxxxxxxxxxxxxxxxxxxxxx}, ".credentials_rsaparams": {xxxxxxxxxxxxxxxxxxxxxxx} } 参考:新しいjust-in-time runnerでセルフホストランナーのオートスケールが劇的に楽になりそう
  42. コードから見るARC - Runnerのスケール - Listenerの動作 53 Runner Podは、createPod関数で作成される。 r.resourceBuilder.newEphemeralRunnerPod関数で定義を作成し、EphemeralRunnerリソースを親としてOwnerReferenceを設 定後、r.Create関数でPodを作成します。

    https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/ephemeralrunner_controller.go#L559- L630 func (r *EphemeralRunnerReconciler) createPod(ctx context.Context, runner *v1alpha1.EphemeralRunner, secret *corev1.Secret, log logr.Logger) (ctrl.Result, error) { newPod := r.resourceBuilder.newEphemeralRunnerPod(ctx, runner, secret, envs...) if err := ctrl.SetControllerReference(runner, newPod, r.Scheme); err != nil { log.Error(err, "Failed to set controller reference to a new pod") return ctrl.Result{}, err } log.Info("Created new pod spec for ephemeral runner") if err := r.Create(ctx, newPod); err != nil { log.Error(err, "Failed to create pod resource for ephemeral runner.") return ctrl.Result{}, err } return ctrl.Result{}, nil }
  43. コードから見るARC - Runnerのスケール - Listenerの動作 54 Runner Pod定義は、newEphemeralRunnerPod関数で作成される。 定義内には、JIT Configが環境変数として設定される。

    https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L468-L534 func (b *resourceBuilder) newEphemeralRunnerPod(ctx context.Context, runner *v1alpha1.EphemeralRunner, secret *corev1.Secret, envs ...corev1.EnvVar) *corev1.Pod { var newPod corev1.Pod newPod.Spec = runner.Spec.PodTemplateSpec.Spec newPod.Spec.Containers = make([]corev1.Container, 0, len(runner.Spec.PodTemplateSpec.Spec.Containers)) for _, c := range runner.Spec.PodTemplateSpec.Spec.Containers { if c.Name == EphemeralRunnerContainerName { c.Env = append( c.Env, corev1.EnvVar{ Name: EnvVarRunnerJITConfig, ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: secret.Name, }, Key: jitTokenKey, }, }, },) c.Env = append(c.Env, envs...) } newPod.Spec.Containers = append(newPod.Spec.Containers, c) } return &newPod }
  44. コードから見るARC - Runnerのスケール - 1. 2個のジョブが定義されたGitHub Actions Workflowが実行される 2. ListenerはGitHubとロングポーリングを張っている。GitHubからジョブ実行のメッセージ受信

    3. ListenerはEphemeralRunnerSetの.Spec.ReplicasをWorkflowジョブの数だけスケールする 4. EphemeralRunnerSetの変更を検知したEphemeralRunnerSet Controllerは.Spec.Replicasの数だけ、 EphemeralRunnerを作成 5. EphemeralRunnerの作成を検知したEphemeralRunner Controllerは、 EphemeralRunnerの数だけRunner Podを作成 6. Runner Podは自分自身をGitHub Actionsに登録 Listenerの動作 55
  45. コードから見るARC - Runnerのスケール - Listenerの動作 56 GitHubへのRunnerの登録は、ARCではなく、ghcr.io/actions/actions-runnerイメージから行う。 Pod起動後に、JIT Configを使用し、Runnerの登録を行う流れとなる。 "spec":

    { "containers": [ { "command": ["/home/runner/run.sh"] "env": [ { "name": "ACTIONS_RUNNER_INPUT_JITCONFIG", "valueFrom": { "secretKeyRef": { "key": "jitToken", "name": "arc-runner-set-qwchf-runner-kqdv9" } } }, { "name": "GITHUB_ACTIONS_RUNNER_EXTRA_USER_AGENT", "value": "actions-runner-controller/0.6.1" } ], "image": "ghcr.io/actions/actions-runner:latest", "imagePullPolicy": "Always", "name": "runner", :