APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

Actions Runner Controller Deep Dive!- コード解説 前編 -

こんにちは!ACS事業部の谷合です。 皆大好きGitHub Actionsにおける、GitHub社公式のSelf-hosted runnerであるActions Runner Controller(以降ARC)の紹介をシリーズでお送りしております。

前回前々回は以下の記事を書いておりました。

今回はARCのコードを解説していきたいと思います。
なお、解説は長くなるので今回を前編とします。

はじめに

私は全コードリーディング完了しておりますが、すべて解説するのは困難なため、以下の流れをコードから追ってみます。なお、コードはGo言語で書かれています。

  1. ARC is installed using the supplied Helm charts, and the controller manager pod is deployed in the specified namespace. A new AutoScalingRunnerSet resource is deployed via the supplied Helm charts or a customized manifest file. The AutoScalingRunnerSet controller calls GitHub's APIs to fetch the runner group ID that the runner scale set will belong to.
  2. The AutoScalingRunnerSet controller calls the APIs one more time to either fetch or create a runner scale set in the Actions Service before creating the Runner ScaleSet Listener resource.
  3. A Runner ScaleSet Listener pod is deployed by the AutoScaling Listener Controller. In this pod, the listener application connects to the Actions Service to authenticate and establish a long poll HTTPS connection. The listener stays idle until it receives a Job Available message from the Actions Service.
  4. When a workflow run is triggered from a repository, the Actions Service dispatches individual job runs to the runners or runner scalesets where the runs-on property matches the name of the runner scaleset or labels of self-hosted runners.
  5. When the Runner ScaleSet Listener receives the Job Available message, it checks whether it can scale up to the desired count. If it can, the Runner ScaleSet Listener acknowledges the message.
  6. The Runner ScaleSet Listener uses a Service Account and a Role bound to that account to make an HTTPS call through the Kubernetes APIs to patch the EphemeralRunner Set resource with the number of desired replicas count.
  7. The EphemeralRunner Set attempts to create new runners and the EphemeralRunner Controller requests a JIT configuration token to register these runners. The controller attempts to create runner pods. If the pod's status is failed, the controller retries up to 5 times. After 24 hours the Actions Service unassigns the job if no runner accepts it.
  8. Once the runner pod is created, the runner application in the pod uses the JIT configuration token to register itself with the Actions Service. It then establishes another HTTPS long poll connection to receive the job details it needs to execute.
  9. The Actions Service acknowledges the runner registration and dispatches the job run details.
  10. Throughout the job run execution, the runner continuously communicates the logs and job run status back to the Actions Service.
  11. When the runner completes its job successfully, the EphemeralRunner Controller checks with the Actions Service to see if runner can be deleted. If it can, the Ephemeral RunnerSet deletes the runner.

引用元:actions-runner-controller/docs/gha-runner-scale-set-controller at f1d7c52253b89f0beae60141f8465d9495cdc2cf · actions/actions-runner-controller · GitHub

突然ですが、以下の画像は何だと思いますか?
コード内の関数の呼び出し元呼び出し先、関数の要約をMiroボードで可視化したものになります。

すごく複雑ですよね。このコード内から上記流れを抜粋していきます。

この記事のこと

ARCのコード解説します。ただ、以下のルールの基、解説していきます。

  • 解説すること
    • ARCの起動からCI/CD Workflowの開始、終了までの動作
  • 解説しないこと
    • Proxy, TLSなどを使った接続設定
    • metricsの設定
  • コードリーディング時のcommit
    • f1d7c52253b89f0beae60141f8465d9495cdc2cf

ARCを構成するComponentの種類と役割

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を増減させる

Controller処理概要

ARCはControllerとして実装されています。
ControllerはReconcile Loopという機能を用い、CustomResource(今回はAutoscalingRunnerSet、AutoscalingListener、EphemeralRunnerSetなどのリソースを指します)定義を、管理対象リソースに強制させ、冪等性を保証しています。
ARCの冪等性は各CRの定義を、CRの子リソースに継承したり、各子リソースが適切なタイミングで作成、更新、削除されることを指します。
そのため、CustomResource(以降CR)デプロイを、契機に処理を開始します。こちらについては以下の記事で詳しく解説しておりますので参照ください。 techblog.ap-com.co.jp

コード解説

各Controllerの管理対象リソース監視コード

前述した通り、ControllerはReconcile Loopという機能で冪等性を保っています。
Controllerは監視対象リソースの作成、更新、削除イベントを監視して、いずれかのイベントが発生したらReconcile Loopを呼ぶといった動作をしています。
監視における対象は以下の通りです。なお、子リソースとは、CRをOwnerReferenceに設定しているリソースを指します。

  • 自分自身のCustomResource
  • 子リソース
  • それ以外

詳細な実装は、拙著の以下の記事も参照ください。 zenn.dev

それでは、各Controllerが何を監視しているかを見てみましょう。

AutoScalingRunnerSet

AutoScalingRunnerSet Controllerは自分自身と、子リソースであるEphemeralRunnerSetリソース、子リソースでないもののAutoScalingListenerリソースを監視しています。
なお、AutoScalingListenerリソースは親リソースがない唯一のCRです。なぜこのような仕様となっているかはどこにも書いてないですが、AutoScalingListenerリソースの役割であるGitHubとのロングポーリングを途絶えさせたくないなどの理由があるのかもしれません。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L746-L786

// SetupWithManager sets up the controller with the Manager.
func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
:
        return ctrl.NewControllerManagedBy(mgr).
                For(&v1alpha1.AutoscalingRunnerSet{}).
                Owns(&v1alpha1.EphemeralRunnerSet{}).
                Watches(&source.Kind{Type: &v1alpha1.AutoscalingListener{}}, handler.EnqueueRequestsFromMapFunc(
                        func(o client.Object) []reconcile.Request {
                                autoscalingListener := o.(*v1alpha1.AutoscalingListener)
                                return []reconcile.Request{
                                        {
                                                NamespacedName: types.NamespacedName{
                                                        Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
                                                        Name:      autoscalingListener.Spec.AutoscalingRunnerSetName,
                                                },
                                        },
                                }
                        },
                )).
                WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
                Complete(r)
}

EphemeralRunnerSet

EphemeralRunnerSet Controllerは自分自身と、子リソースであるEphemeralRunnerを監視しています。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/ephemeralrunnerset_controller.go#L547-L576

// SetupWithManager sets up the controller with the Manager.
func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
:
        return ctrl.NewControllerManagedBy(mgr).
                For(&v1alpha1.EphemeralRunnerSet{}).
                Owns(&v1alpha1.EphemeralRunner{}).
                WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
                Complete(r)
}

EphemeralRunner

EphemeralRunner Controllerは自分自身と、子リソースであるPodを監視しています。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/ephemeralrunner_controller.go#L789-L798

// SetupWithManager sets up the controller with the Manager.
func (r *EphemeralRunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
        // TODO(nikola-jokic): Add indexing and filtering fields on corev1.Pod{}
        return ctrl.NewControllerManagedBy(mgr).
                For(&v1alpha1.EphemeralRunner{}).
                Owns(&corev1.Pod{}).
                WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
                Named("ephemeral-runner-controller").
                Complete(r)
}

AutoScalingListener

AutoScalingRunnerSet Controllerは自分自身と、子リソースであるPodとServiceAccount、子リソースでないロールを監視しています。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L620-L677

func (r *AutoscalingListenerReconciler) SetupWithManager(mgr ctrl.Manager) error {
:
    labelBasedWatchFunc := func(obj client.Object) []reconcile.Request {
        var requests []reconcile.Request
        labels := obj.GetLabels()
        namespace, ok := labels["auto-scaling-listener-namespace"]
        if !ok {
            return nil
        }

        name, ok := labels["auto-scaling-listener-name"]
        if !ok {
            return nil
        }
        requests = append(requests,
            reconcile.Request{
                NamespacedName: types.NamespacedName{
                    Name:      name,
                    Namespace: namespace,
                },
            },
        )
        return requests
    }

    return ctrl.NewControllerManagedBy(mgr).
        For(&v1alpha1.AutoscalingListener{}).
        Owns(&corev1.Pod{}).
        Owns(&corev1.ServiceAccount{}).
        Watches(&source.Kind{Type: &rbacv1.Role{}}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
        Watches(&source.Kind{Type: &rbacv1.RoleBinding{}}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
        WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
        Complete(r)
}

各Controllerが監視を行うことで、以下のようにAutoScalingRunnerSet リソースのデプロイ契機で、EphemeralRunnerSetとAutoScalingListenerリソースをデプロイ、EphemeralRunnerSetの.Spec.Replicas変更契機でEphemeralRunnerリソースとPodが作成できるわけです。

  1. AutoScalingRunnerSet CustomResourceのデプロイ
  2. AutoScalingRunnerSet ControllerによってEphemeralRunnerSetとAutoScalingListener CustomResourceが順番にデプロイ
  3. AutoScalingListener ControllerによってAutoScalingListener Podが作成され、GitHubとのロングポーリングが開始されます。
  4. CI/CD jobが開始されると、AutoScalingListener Podに通知され、EphemeralRunnerSetの.Spec.Replicasを0からターゲットの数まで増やす
  5. EphemeralRunnerSet Controllerによって.Spec.Replicasの数だけEphemeralRunner CustomResourceが作成される。
  6. EphemeralRunner Controllerによって、EphemeralRunner Podが作成され、Self-hosted runnerとして動作する
  7. CI/CD job終了後は自動でGitHub上のRunnerと、EphemeralRunner Podが削除される

ARCの処理の流れ解説

AutoScalingRunnerSet、EphemeralRunnerSet、AutoScalingLilstenerリソースの作成

以下の動作を見ていきましょう。

  1. ARC is installed using the supplied Helm charts, and the controller manager pod is deployed in the specified namespace. A new AutoScalingRunnerSet resource is deployed via the supplied Helm charts or a customized manifest file. The AutoScalingRunnerSet controller calls GitHub's APIs to fetch the runner group ID that the runner scale set will belong to.
  2. The AutoScalingRunnerSet controller calls the APIs one more time to either fetch or create a runner scale set in the Actions Service before creating the Runner ScaleSet Listener resource.

前述した通り、各コントローラでは自分自身または子リソース、それ以外を監視しています。
まず、AutoScalingRunnerSetリソースがデプロイされると、自分自身を監視しているAutoScalingRunnerSet ControllerのReconcile Loopが呼ばれ、以下のコードでGitHub上にRunner Scale Setが作成されます。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L188-L199

        scaleSetIdRaw, ok := autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey]
        if !ok {
                // Need to create a new runner scale set on Actions service
                log.Info("Runner scale set id annotation does not exist. Creating a new runner scale set.")
                return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
        }


        if id, err := strconv.Atoi(scaleSetIdRaw); err != nil || id <= 0 {
                log.Info("Runner scale set id annotation is not an id, or is <= 0. Creating a new runner scale set.")
                // something modified the scaleSetId. Try to create one
                return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
        }

この時、r.createRunnerScaleSet関数により、GitHub上にRunner Scale Setの作成と、AutoScalingRunnerSetリソースの更新がされます。
r.createRunnerScaleSet関数を見てみましょう。
まず、actionsClientFor関数を呼び出して、GitHubとやり取りをするためのclientを生成します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L412-L413

        logger.Info("Creating a new runner scale set")
        actionsClient, err := r.actionsClientFor(ctx, autoscalingRunnerSet)

actionsClientFor関数では、PATまたはGitHub App、およびRunner Scale Setを登録するURL(リポジトリ、Oraganization、Enterprise)を使い、clientを生成します。
なお、これらは、AutoScalingRunnerSetリソースをhelmでデプロイする際に渡す必須の情報です。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/charts/gha-runner-scale-set/templates/autoscalingrunnerset.yaml#L30-L32

spec:
  githubConfigUrl: {{ required ".Values.githubConfigUrl is required" (trimSuffix "/" .Values.githubConfigUrl) }}
  githubConfigSecret: {{ include "gha-runner-scale-set.githubsecret" . }}

その後、actionsClient.GetRunnerGroupByNameでGitHub APIを叩き、Runner Groupを取得し、Runner Group IDを取得します。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L422-L431

        runnerGroupId := 1
        if len(autoscalingRunnerSet.Spec.RunnerGroup) > 0 {
                runnerGroup, err := actionsClient.GetRunnerGroupByName(ctx, autoscalingRunnerSet.Spec.RunnerGroup)
                if err != nil {
                        logger.Error(err, "Failed to get runner group by name", "runnerGroup", autoscalingRunnerSet.Spec.RunnerGroup)
                        return ctrl.Result{}, err
                }


                runnerGroupId = int(runnerGroup.ID)
        }

Runner Group IDを取得できたら、Runner Group IDとautoscalingRunnerSet.Spec.RunnerScaleSetNameを引数にactionsClient.GetRunnerScaleSet関数を呼び出し、GitHubのRunner Scale Setを取得します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L433-L485

        runnerScaleSet, err := actionsClient.GetRunnerScaleSet(ctx, runnerGroupId, autoscalingRunnerSet.Spec.RunnerScaleSetName)
        if err != nil {
                logger.Error(err, "Failed to get runner scale set from Actions service",
                        "runnerGroupId",
                        strconv.Itoa(runnerGroupId),
                        "runnerScaleSetName",
                        autoscalingRunnerSet.Spec.RunnerScaleSetName)
                return ctrl.Result{}, err
        }

もしRunner Scale Setが存在しない場合は、actionsClient.CreateRunnerScaleSet関数でGitHub APIを叩き、作成します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L443-L464

        if runnerScaleSet == nil {
                runnerScaleSet, err = actionsClient.CreateRunnerScaleSet(
                        ctx,
                        &actions.RunnerScaleSet{
                                Name:          autoscalingRunnerSet.Spec.RunnerScaleSetName,
                                RunnerGroupId: runnerGroupId,
                                Labels: []actions.Label{
                                        {
                                                Name: autoscalingRunnerSet.Spec.RunnerScaleSetName,
                                                Type: "System",
                                        },
                                },
                                RunnerSetting: actions.RunnerSetting{
                                        Ephemeral:     true,
                                        DisableUpdate: true,
                                },
                        })
                if err != nil {
                        logger.Error(err, "Failed to create a new runner scale set on Actions service")
                        return ctrl.Result{}, err
                }
        }

最後に、patch関数でAutoScalingRunnerSetリソースの.metadata.annotationsにRunner Scale Setの名前とID、Runner Group名を入れます。

        logger.Info("Adding runner scale set ID, name and runner group name as an annotation and url labels")
        if err = patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
                obj.Annotations[runnerScaleSetNameAnnotationKey] = runnerScaleSet.Name
                obj.Annotations[runnerScaleSetIdAnnotationKey] = strconv.Itoa(runnerScaleSet.Id)
                obj.Annotations[AnnotationKeyGitHubRunnerGroupName] = runnerScaleSet.RunnerGroupName
                if err := applyGitHubURLLabels(obj.Spec.GitHubConfigUrl, obj.Labels); err != nil { // should never happen
                        logger.Error(err, "Failed to apply GitHub URL labels")
                }
        }); err != nil {
                logger.Error(err, "Failed to add runner scale set ID, name and runner group name as an annotation")
                return ctrl.Result{}, err
        }

以下にRunner Scale Setの取得、作成を行う関数を解説します。
折りたたんでおりますので、興味あればご確認ください。

Runner Scale Setの取得、作成

Runner Scale Setの取得は、actionsClient.GetRunnerScaleSet関数で行います。
以下のように、scaleSetEndpoint、runnerGroupId、runnerScaleSetNameを使用し、APIのパスを作成します。
この時、scaleSetEndpointは _apis/runtime/runnerscalesetsになります。そのパスをDo関数に渡してGetメソッドのRequestを送り、ResponseをDecodeすることでRunner Scale Setの取得を行います。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/github/actions/client.go#L288-L317

func (c *Client) GetRunnerScaleSet(ctx context.Context, runnerGroupId int, runnerScaleSetName string) (*RunnerScaleSet, error) {
        path := fmt.Sprintf("/%s?runnerGroupId=%d&name=%s", scaleSetEndpoint, runnerGroupId, runnerScaleSetName)
        req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
        if err != nil {
                return nil, err
        }


        resp, err := c.Do(req)
        if err != nil {
                return nil, err
        }


        if resp.StatusCode != http.StatusOK {
                return nil, ParseActionsErrorFromResponse(resp)
        }


        var runnerScaleSetList *runnerScaleSetsResponse
        err = json.NewDecoder(resp.Body).Decode(&runnerScaleSetList)
        if err != nil {
                return nil, err
        }
        if runnerScaleSetList.Count == 0 {
                return nil, nil
        }
        if runnerScaleSetList.Count > 1 {
                return nil, fmt.Errorf("multiple runner scale sets found with name %s", runnerScaleSetName)
        }


        return &runnerScaleSetList.RunnerScaleSets[0], nil
}

Runner Scale Setの作成は、scaleSetEndpointは _apis/runtime/runnerscalesets対してPostメソッドをRequestして、作成します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/github/actions/client.go#L380-L405

func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
        body, err := json.Marshal(runnerScaleSet)
        if err != nil {
                return nil, err
        }


        req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, scaleSetEndpoint, bytes.NewReader(body))
        if err != nil {
                return nil, err
        }


        resp, err := c.Do(req)
        if err != nil {
                return nil, err
        }


        if resp.StatusCode != http.StatusOK {
                return nil, ParseActionsErrorFromResponse(resp)
        }
        var createdRunnerScaleSet *RunnerScaleSet
        err = json.NewDecoder(resp.Body).Decode(&createdRunnerScaleSet)
        if err != nil {
                return nil, err
        }
        return createdRunnerScaleSet, nil
}

ここまでくると、EphemeralRunnerSetおよびAutoScalingListener リソースの作成の準備ができます。

EphemeralRunnerSetリソースはr.createEphemeralRunnerSet関数で作成されます。
まず、EphemeralRunnerSetリソースのリストを取得し、最新のEphemeralRunnerSetリソースがない場合はr.createEphemeralRunnerSet関数が呼ばれます。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L223-L233

        existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet)
        if err != nil {
                log.Error(err, "Failed to list existing ephemeral runner sets")
                return ctrl.Result{}, err
        }


        latestRunnerSet := existingRunnerSets.latest()
        if latestRunnerSet == nil {
                log.Info("Latest runner set does not exist. Creating a new runner set.")
                return r.createEphemeralRunnerSet(ctx, autoscalingRunnerSet, log)
        }

r.createEphemeralRunnerSet関数の定義は以下の通りです。
r.resourceBuilder.newEphemeralRunnerSet関数で定義を作成し、ctrl.SetControllerReference関数でOwnerReferenceにAutoScalingRunnerSetリソースを親として設定後に、r.Create関数で作成といった流れとなります。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L623-L643

func (r *AutoscalingRunnerSetReconciler) createEphemeralRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, log logr.Logger) (ctrl.Result, error) {
        desiredRunnerSet, err := r.resourceBuilder.newEphemeralRunnerSet(autoscalingRunnerSet)
        if err != nil {
                log.Error(err, "Could not create EphemeralRunnerSet")
                return ctrl.Result{}, err
        }


        if err := ctrl.SetControllerReference(autoscalingRunnerSet, desiredRunnerSet, r.Scheme); err != nil {
                log.Error(err, "Failed to set controller reference to a new EphemeralRunnerSet")
                return ctrl.Result{}, err
        }


        log.Info("Creating a new EphemeralRunnerSet resource")
        if err := r.Create(ctx, desiredRunnerSet); err != nil {
                log.Error(err, "Failed to create EphemeralRunnerSet resource")
                return ctrl.Result{}, err
        }


        log.Info("Created a new EphemeralRunnerSet resource", "name", desiredRunnerSet.Name)
        return ctrl.Result{}, nil
}

r.resourceBuilder.newEphemeralRunnerSet関数はEphemeralRunnerSetの定義を作成します。
複数工程に分かれているので、細かく見ていきたいと思います。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L390-L436

func (b *resourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*v1alpha1.EphemeralRunnerSet, error) {
        runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey])
        if err != nil {
                return nil, err
        }
        runnerSpecHash := autoscalingRunnerSet.RunnerSetSpecHash()


        labels := map[string]string{
                labelKeyRunnerSpecHash:          runnerSpecHash,
                LabelKeyKubernetesPartOf:        labelValueKubernetesPartOf,
                LabelKeyKubernetesComponent:     "runner-set",
                LabelKeyKubernetesVersion:       autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion],
                LabelKeyGitHubScaleSetName:      autoscalingRunnerSet.Name,
                LabelKeyGitHubScaleSetNamespace: autoscalingRunnerSet.Namespace,
        }


        if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
                return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
        }


        newAnnotations := map[string]string{
                AnnotationKeyGitHubRunnerGroupName: autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName],
        }


        newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
                TypeMeta: metav1.TypeMeta{},
                ObjectMeta: metav1.ObjectMeta{
                        GenerateName: autoscalingRunnerSet.ObjectMeta.Name + "-",
                        Namespace:    autoscalingRunnerSet.ObjectMeta.Namespace,
                        Labels:       labels,
                        Annotations:  newAnnotations,
                },
                Spec: v1alpha1.EphemeralRunnerSetSpec{
                        Replicas: 0,
                        EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
                                RunnerScaleSetId:   runnerScaleSetId,
                                GitHubConfigUrl:    autoscalingRunnerSet.Spec.GitHubConfigUrl,
                                GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
                                Proxy:              autoscalingRunnerSet.Spec.Proxy,
                                GitHubServerTLS:    autoscalingRunnerSet.Spec.GitHubServerTLS,
                                PodTemplateSpec:    autoscalingRunnerSet.Spec.Template,
                        },
                },
        }


        return newEphemeralRunnerSet, nil
}

autoscalingRunnerSet.RunnerSetSpecHash関数では、AutoScalingRunnerSetリソースの.specからHash値を求めます。
このHash値をlabelKeyRunnerSpecHashラベルに入れます。
labelKeyRunnerSpecHashラベルは、EphemeralRunnerSetリソースの再作成が必要かの判断に使われます。
AutoScalingRunnerSetリソースの.specが変更されたら再度Hash値が求められ、labelKeyRunnerSpecHashラベルと比較することでEphemeralRunnerSetリソースが再作成されます。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L395-L404

        runnerSpecHash := autoscalingRunnerSet.RunnerSetSpecHash()


        labels := map[string]string{
                labelKeyRunnerSpecHash:          runnerSpecHash,
                LabelKeyKubernetesPartOf:        labelValueKubernetesPartOf,
                LabelKeyKubernetesComponent:     "runner-set",
                LabelKeyKubernetesVersion:       autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion],
                LabelKeyGitHubScaleSetName:      autoscalingRunnerSet.Name,
                LabelKeyGitHubScaleSetNamespace: autoscalingRunnerSet.Namespace,
        }

余談ですが、各種ラベルやアノテーションのKeyは以下で定義されています。

ラベルやアノテーション一覧

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/constants.go#L25-L60

// Labels applied to resources
const (
        // Kubernetes labels
        LabelKeyKubernetesPartOf    = "app.kubernetes.io/part-of"
        LabelKeyKubernetesComponent = "app.kubernetes.io/component"
        LabelKeyKubernetesVersion   = "app.kubernetes.io/version"


        // Github labels
        LabelKeyGitHubScaleSetName      = "actions.github.com/scale-set-name"
        LabelKeyGitHubScaleSetNamespace = "actions.github.com/scale-set-namespace"
        LabelKeyGitHubEnterprise        = "actions.github.com/enterprise"
        LabelKeyGitHubOrganization      = "actions.github.com/organization"
        LabelKeyGitHubRepository        = "actions.github.com/repository"
)


// Finalizer used to protect resources from deletion while AutoscalingRunnerSet is running
const AutoscalingRunnerSetCleanupFinalizerName = "actions.github.com/cleanup-protection"


const AnnotationKeyGitHubRunnerGroupName = "actions.github.com/runner-group-name"


// Labels applied to listener roles
const (
        labelKeyListenerName      = "auto-scaling-listener-name"
        labelKeyListenerNamespace = "auto-scaling-listener-namespace"
)


// Annotations applied for later cleanup of resources
const (
        AnnotationKeyManagerRoleBindingName           = "actions.github.com/cleanup-manager-role-binding"
        AnnotationKeyManagerRoleName                  = "actions.github.com/cleanup-manager-role-name"
        AnnotationKeyKubernetesModeRoleName           = "actions.github.com/cleanup-kubernetes-mode-role-name"
        AnnotationKeyKubernetesModeRoleBindingName    = "actions.github.com/cleanup-kubernetes-mode-role-binding-name"
        AnnotationKeyKubernetesModeServiceAccountName = "actions.github.com/cleanup-kubernetes-mode-service-account-name"
        AnnotationKeyGitHubSecretName                 = "actions.github.com/cleanup-github-secret-name"
        AnnotationKeyNoPermissionServiceAccountName   = "actions.github.com/cleanup-no-permission-service-account-name"
)

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L45-L50

const (
        // Label
        labelKeyRunnerSpecHash            = "runner-spec-hash"
        :
        // Annotation
        runnerScaleSetIdAnnotationKey     = "runner-scale-set-id"
        runnerScaleSetNameAnnotationKey   = "runner-scale-set-name"
)

次に、applyGitHubURLLabels関数でAutoScalingRunnerSetリソースの.Spec.GitHubConfigUrlがEnterpriseか、Organizationか、Repositoryかを判断して、LabelKeyGitHubEnterpriseまたはLabelKeyGitHubOrganizationまたはLabelKeyGitHubRepositoryラベルに値を入れます。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L406-L408

        if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
                return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
        }

applyGitHubURLLabels関数の実装は面白いのですが、解説の粒度が細かくなるので今回はスコープ外とします。
興味ある方は以下の折りたたみをご確認ください。

applyGitHubURLLabels関数

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L612-L629

func applyGitHubURLLabels(url string, labels map[string]string) error {
        githubConfig, err := actions.ParseGitHubConfigFromURL(url)
        if err != nil {
                return fmt.Errorf("failed to parse github config from url: %v", err)
        }

        if len(githubConfig.Enterprise) > 0 {
                labels[LabelKeyGitHubEnterprise] = trimLabelValue(githubConfig.Enterprise)
        }
        if len(githubConfig.Organization) > 0 {
                labels[LabelKeyGitHubOrganization] = trimLabelValue(githubConfig.Organization)
        }
        if len(githubConfig.Repository) > 0 {
                labels[LabelKeyGitHubRepository] = trimLabelValue(githubConfig.Repository)
        }

        return nil
}

actions.ParseGitHubConfigFromURLの実装

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/github/actions/config.go#L32-L73

func ParseGitHubConfigFromURL(in string) (*GitHubConfig, error) {
        u, err := url.Parse(strings.Trim(in, "/"))
        if err != nil {
                return nil, err
        }


        isHosted := isHostedGitHubURL(u)


        configURL := &GitHubConfig{
                ConfigURL: u,
                IsHosted:  isHosted,
        }


        invalidURLError := fmt.Errorf("%q: %w", u.String(), ErrInvalidGitHubConfigURL)


        pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")


        switch len(pathParts) {
        case 1: // Organization
                if pathParts[0] == "" {
                        return nil, invalidURLError
                }


                configURL.Scope = GitHubScopeOrganization
                configURL.Organization = pathParts[0]


        case 2: // Repository or enterprise
                if strings.ToLower(pathParts[0]) == "enterprises" {
                        configURL.Scope = GitHubScopeEnterprise
                        configURL.Enterprise = pathParts[1]
                        break
                }


                configURL.Scope = GitHubScopeRepository
                configURL.Organization = pathParts[0]
                configURL.Repository = pathParts[1]
        default:
                return nil, invalidURLError
        }


        return configURL, nil
}

最後に、EphemeralRunnerSetリソースのMetadataおよびSpecに値を入れて、returnします。
この定義を使って、r.createEphemeralRunnerSet関数内のr.Create関数でEphemeralRunnerSetリソースを作成します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L414-L435

        newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
                TypeMeta: metav1.TypeMeta{},
                ObjectMeta: metav1.ObjectMeta{
                        GenerateName: autoscalingRunnerSet.ObjectMeta.Name + "-",
                        Namespace:    autoscalingRunnerSet.ObjectMeta.Namespace,
                        Labels:       labels,
                        Annotations:  newAnnotations,
                },
                Spec: v1alpha1.EphemeralRunnerSetSpec{
                        Replicas: 0,
                        EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
                                RunnerScaleSetId:   runnerScaleSetId,
                                GitHubConfigUrl:    autoscalingRunnerSet.Spec.GitHubConfigUrl,
                                GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
                                Proxy:              autoscalingRunnerSet.Spec.Proxy,
                                GitHubServerTLS:    autoscalingRunnerSet.Spec.GitHubServerTLS,
                                PodTemplateSpec:    autoscalingRunnerSet.Spec.Template,
                        },
                },
        }


        return newEphemeralRunnerSet, nil

AutoScalingRunnerSet ControllerのReconcile Loopに戻ります。

以下のコードでAutoscalingListener リソースが作成済かを確認します。
r.Get関数でAutoscalingListener リソースをK8sから取得し、存在しなければlistenerFoundをfalseにします。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L240-L251

        // Make sure the AutoscalingListener is up and running in the controller namespace
        listener := new(v1alpha1.AutoscalingListener)
        listenerFound := true
        if err := r.Get(ctx, client.ObjectKey{Namespace: r.ControllerNamespace, Name: scaleSetListenerName(autoscalingRunnerSet)}, listener); err != nil {
                if !kerrors.IsNotFound(err) {
                        log.Error(err, "Failed to get AutoscalingListener resource")
                        return ctrl.Result{}, err
                }


                listenerFound = false
                log.Info("AutoscalingListener does not exist.")
        }

上記処理でAutoscalingListener リソースが見つからない場合、r.createAutoScalingListenerForRunnerSet関数で作成します。 https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L296-L304

        // Make sure the AutoscalingListener is up and running in the controller namespace
        if !listenerFound {
                if r.drainingJobs(&latestRunnerSet.Status) {
                        log.Info("Creating a new AutoscalingListener is waiting for the running and pending runners to finish. Waiting for the running and pending runners to finish:", "running", latestRunnerSet.Status.RunningEphemeralRunners, "pending", latestRunnerSet.Status.PendingEphemeralRunners)
                        return ctrl.Result{}, nil
                }
                log.Info("Creating a new AutoscalingListener for the runner set", "ephemeralRunnerSetName", latestRunnerSet.Name)
                return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log)
        }

r.createAutoScalingListenerForRunnerSet関数の定義は以下の通りです。
imagePullSecretsを取得し、r.resourceBuilder.newAutoScalingListener関数で定義を作成し、r.Create関数で作成といった流れとなります。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L645-L667

func (r *AutoscalingRunnerSetReconciler) createAutoScalingListenerForRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, log logr.Logger) (ctrl.Result, error) {
        var imagePullSecrets []corev1.LocalObjectReference
        for _, imagePullSecret := range r.DefaultRunnerScaleSetListenerImagePullSecrets {
                imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{
                        Name: imagePullSecret,
                })
        }


        autoscalingListener, err := r.resourceBuilder.newAutoScalingListener(autoscalingRunnerSet, ephemeralRunnerSet, r.ControllerNamespace, r.DefaultRunnerScaleSetListenerImage, imagePullSecrets)
        if err != nil {
                log.Error(err, "Could not create AutoscalingListener spec")
                return ctrl.Result{}, err
        }


        log.Info("Creating a new AutoscalingListener resource", "name", autoscalingListener.Name, "namespace", autoscalingListener.Namespace)
        if err := r.Create(ctx, autoscalingListener); err != nil {
                log.Error(err, "Failed to create AutoscalingListener resource")
                return ctrl.Result{}, err
        }


        log.Info("Created a new AutoscalingListener resource", "name", autoscalingListener.Name, "namespace", autoscalingListener.Namespace)
        return ctrl.Result{}, nil
}

r.resourceBuilder.newAutoScalingListener関数はEphemeralRunnerSetリソースの定義を作成します。
r.resourceBuilder.newEphemeralRunnerSet関数と同様に、複数工程に分かれているので、細かく見ていきたいと思います。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L74-L126

func (b *resourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, namespace, image string, imagePullSecrets []corev1.LocalObjectReference) (*v1alpha1.AutoscalingListener, error) {
        runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey])
        if err != nil {
                return nil, err
        }


        effectiveMinRunners := 0
        effectiveMaxRunners := math.MaxInt32
        if autoscalingRunnerSet.Spec.MaxRunners != nil {
                effectiveMaxRunners = *autoscalingRunnerSet.Spec.MaxRunners
        }
        if autoscalingRunnerSet.Spec.MinRunners != nil {
                effectiveMinRunners = *autoscalingRunnerSet.Spec.MinRunners
        }


        labels := map[string]string{
                LabelKeyGitHubScaleSetNamespace: autoscalingRunnerSet.Namespace,
                LabelKeyGitHubScaleSetName:      autoscalingRunnerSet.Name,
                LabelKeyKubernetesPartOf:        labelValueKubernetesPartOf,
                LabelKeyKubernetesComponent:     "runner-scale-set-listener",
                LabelKeyKubernetesVersion:       autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion],
                labelKeyRunnerSpecHash:          autoscalingRunnerSet.ListenerSpecHash(),
        }


        if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
                return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
        }


        autoscalingListener := &v1alpha1.AutoscalingListener{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      scaleSetListenerName(autoscalingRunnerSet),
                        Namespace: namespace,
                        Labels:    labels,
                },
                Spec: v1alpha1.AutoscalingListenerSpec{
                        GitHubConfigUrl:               autoscalingRunnerSet.Spec.GitHubConfigUrl,
                        GitHubConfigSecret:            autoscalingRunnerSet.Spec.GitHubConfigSecret,
                        RunnerScaleSetId:              runnerScaleSetId,
                        AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace,
                        AutoscalingRunnerSetName:      autoscalingRunnerSet.Name,
                        EphemeralRunnerSetName:        ephemeralRunnerSet.Name,
                        MinRunners:                    effectiveMinRunners,
                        MaxRunners:                    effectiveMaxRunners,
                        Image:                         image,
                        ImagePullPolicy:               scaleSetListenerImagePullPolicy,
                        ImagePullSecrets:              imagePullSecrets,
                        Proxy:                         autoscalingRunnerSet.Spec.Proxy,
                        GitHubServerTLS:               autoscalingRunnerSet.Spec.GitHubServerTLS,
                },
        }


        return autoscalingListener, nil
}

まず、AutoScalingRunnerSetリソースをhelmでデプロイする際に指定した、MaxRunners とMinRunners を変数に入れます。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L80-L87

        effectiveMinRunners := 0
        effectiveMaxRunners := math.MaxInt32
        if autoscalingRunnerSet.Spec.MaxRunners != nil {
                effectiveMaxRunners = *autoscalingRunnerSet.Spec.MaxRunners
        }
        if autoscalingRunnerSet.Spec.MinRunners != nil {
                effectiveMinRunners = *autoscalingRunnerSet.Spec.MinRunners
        }

次に、EphemeralRunnerSetリソースと同様にapplyGitHubURLLabels関数でAutoScalingRunnerSetリソースの.Spec.GitHubConfigUrlがEnterpriseか、Organizationか、Repositoryかを判断して、LabelKeyGitHubEnterpriseまたはLabelKeyGitHubOrganizationまたはLabelKeyGitHubRepositoryラベルに値を入れます。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L98-L100

        if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
                return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
        }

最後に、AutoScalingListenerリソースのMetadataおよびSpecに値を入れて、returnします。
この定義を使って、r.createAutoScalingListenerForRunnerSet関数内のr.Create関数でAutoScalingListenerリソースを作成します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L102-L125

        autoscalingListener := &v1alpha1.AutoscalingListener{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      scaleSetListenerName(autoscalingRunnerSet),
                        Namespace: namespace,
                        Labels:    labels,
                },
                Spec: v1alpha1.AutoscalingListenerSpec{
                        GitHubConfigUrl:               autoscalingRunnerSet.Spec.GitHubConfigUrl,
                        GitHubConfigSecret:            autoscalingRunnerSet.Spec.GitHubConfigSecret,
                        RunnerScaleSetId:              runnerScaleSetId,
                        AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace,
                        AutoscalingRunnerSetName:      autoscalingRunnerSet.Name,
                        EphemeralRunnerSetName:        ephemeralRunnerSet.Name,
                        MinRunners:                    effectiveMinRunners,
                        MaxRunners:                    effectiveMaxRunners,
                        Image:                         image,
                        ImagePullPolicy:               scaleSetListenerImagePullPolicy,
                        ImagePullSecrets:              imagePullSecrets,
                        Proxy:                         autoscalingRunnerSet.Spec.Proxy,
                        GitHubServerTLS:               autoscalingRunnerSet.Spec.GitHubServerTLS,
                },
        }


        return autoscalingListener, nil

AutoScaleRunnerSetリソースのReconcile Loopに再度戻ります。

EphemeralRunnerSet、AutoScalingListenerの両リソースが作成できたら、AutoScaleRunnerSetリソースのStatusを更新してReconcile Loop処理を終了します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalingrunnerset_controller.go#L306-L317

        // Update the status of autoscaling runner set.
        if latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners {
                if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
                        obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas
                        obj.Status.PendingEphemeralRunners = latestRunnerSet.Status.PendingEphemeralRunners
                        obj.Status.RunningEphemeralRunners = latestRunnerSet.Status.RunningEphemeralRunners
                        obj.Status.FailedEphemeralRunners = latestRunnerSet.Status.FailedEphemeralRunners
                }); err != nil {
                        log.Error(err, "Failed to update autoscaling runner set status with current runner count")
                        return ctrl.Result{}, err
                }
        }

ここまでで、AutoScaleRunnerSet、EphemeralRunnerSet、AutoScalingListenerリソースのそれぞれのリソースが作成できました。

AutoScalingListener Podの作成

ここからは、以下動作を解説します。
なお、ここではAutoScalingListener Podの作成までを解説し、AutoScalingListener Podの動作については次回に解説します。

3.A Runner ScaleSet Listener pod is deployed by the AutoScaling Listener Controller. In this pod, the listener application connects to the Actions Service to authenticate and establish a long poll HTTPS connection. The listener stays idle until it receives a Job Available message from the Actions Service.

各Controllerの管理対象リソース監視コードの説明のセクションで述べたように、各Controllerは自分自身も監視しています。
そのため、Controllerに属する各リソースが作成または更新、削除された時点で、各ControllerのReconcile Loopが呼ばれます。

ここからは、AutoScalingListenerリソースのPod作成の流れを見ていきます。
EphemeralRunnerSetリソースはReconcile Loopの中でEphemeralRunnerリソースを作成しますが、AutoScalingListener PodがWorkflowジョブの数に応じてEphemeralRunnerSetリソースの.Spec.Replicasを増やしたことをトリガーにして作成されます。この処理についても次回で解説します。

まず、AutoscalingRunnerSetの.Spec.GitHubConfigSecretに指定されているSecretを取得し、そのミラーとなるAutoScalingListener用のSecretを、r.createSecretsForListener関数で作成します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L132-L152

        // Check if the GitHub config secret exists
        secret := new(corev1.Secret)
        if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Spec.GitHubConfigSecret}, secret); err != nil {
                log.Error(err, "Failed to find GitHub config secret.",
                        "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
                        "name", autoscalingListener.Spec.GitHubConfigSecret)
                return ctrl.Result{}, err
        }


        // Create a mirror secret in the same namespace as the AutoscalingListener
        mirrorSecret := new(corev1.Secret)
        if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerSecretMirrorName(autoscalingListener)}, mirrorSecret); err != nil {
                if !kerrors.IsNotFound(err) {
                        log.Error(err, "Unable to get listener secret mirror", "namespace", autoscalingListener.Namespace, "name", scaleSetListenerSecretMirrorName(autoscalingListener))
                        return ctrl.Result{}, err
                }


                // Create a mirror secret for the listener pod in the Controller namespace for listener pod to use
                log.Info("Creating a mirror listener secret for the listener pod")
                return r.createSecretsForListener(ctx, autoscalingListener, secret, log)
        }

r.createSecretsForListener関数では、Secret定義の作成、OwnerReferenceの設定と、r.Create関数での作成を行っているだけなので、深くは解説しません。
ただ、return処理が少し面白いので、この部分のみ解説します。return ctrl.Result{Requeue: true}, nilとあるように、ctrl.Result構造体のRequeueフィールドをtrueにしてreturnしています。これは、今まで出てきていなかった実装です。Requeueフィールドをtrueにすることで、Reconcile Loopを抜けた場合もキューに入れられ、即座にReconcile Loopが再実行されます。このため、上記コードでmirrorSecretが存在していなかった場合でも、後続処理を継続できるわけです。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L468-L483

func (r *AutoscalingListenerReconciler) createSecretsForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
        newListenerSecret := r.resourceBuilder.newScaleSetListenerSecretMirror(autoscalingListener, secret)


        if err := ctrl.SetControllerReference(autoscalingListener, newListenerSecret, r.Scheme); err != nil {
                return ctrl.Result{}, err
        }


        logger.Info("Creating listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
        if err := r.Create(ctx, newListenerSecret); err != nil {
                logger.Error(err, "Unable to create listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
                return ctrl.Result{}, err
        }


        logger.Info("Created listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
        return ctrl.Result{Requeue: true}, nil
}

ServiceAccount、Role、RoleBindingsを作成します。
ServiceAccountとRoleが、RoleBindingsに紐づけられます。
Roleの定義は特に重要なので、以降で解説します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L162-L210

        // Make sure the runner scale set listener service account is created for the listener pod in the controller namespace
        serviceAccount := new(corev1.ServiceAccount)
        if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerServiceAccountName(autoscalingListener)}, serviceAccount); err != nil {
                if !kerrors.IsNotFound(err) {
                        log.Error(err, "Unable to get listener service accounts", "namespace", autoscalingListener.Namespace, "name", scaleSetListenerServiceAccountName(autoscalingListener))
                        return ctrl.Result{}, err
                }


                // Create a service account for the listener pod in the controller namespace
                log.Info("Creating a service account for the listener pod")
                return r.createServiceAccountForListener(ctx, autoscalingListener, log)
        }

        // TODO: make sure the service account is up to date

        // Make sure the runner scale set listener role is created in the AutoscalingRunnerSet namespace
        listenerRole := new(rbacv1.Role)
        if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRole); err != nil {
                if !kerrors.IsNotFound(err) {
                        log.Error(err, "Unable to get listener role", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", scaleSetListenerRoleName(autoscalingListener))
                        return ctrl.Result{}, err
                }


                // Create a role for the listener pod in the AutoScalingRunnerSet namespace
                log.Info("Creating a role for the listener pod")
                return r.createRoleForListener(ctx, autoscalingListener, log)
        }
:
        // Make sure the runner scale set listener role binding is created
        listenerRoleBinding := new(rbacv1.RoleBinding)
        if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRoleBinding); err != nil {
                if !kerrors.IsNotFound(err) {
                        log.Error(err, "Unable to get listener role binding", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", scaleSetListenerRoleName(autoscalingListener))
                        return ctrl.Result{}, err
                }


                // Create a role binding for the listener pod in the AutoScalingRunnerSet namespace
                log.Info("Creating a role binding for the service account and role")
                return r.createRoleBindingForListener(ctx, autoscalingListener, listenerRole, serviceAccount, log)
        }

以下はRoleの定義の関数です。
コードは今までのリソース作成関数の作りと変わりません。
r.resourceBuilder.newScaleSetListenerRole関数で定義を作成し、r.Create関数でリソースを作成といった流れとなります。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L540-L551

func (r *AutoscalingListenerReconciler) createRoleForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) {
        newRole := r.resourceBuilder.newScaleSetListenerRole(autoscalingListener)


        logger.Info("Creating listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules)
        if err := r.Create(ctx, newRole); err != nil {
                logger.Error(err, "Unable to create listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules)
                return ctrl.Result{}, err
        }


        logger.Info("Created listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules)
        return ctrl.Result{Requeue: true}, nil
}

r.resourceBuilder.newScaleSetListenerRole関数ではロールの定義を作成します。 rulesForListenerRole関数で、ルールを作成して、ロール定義をreturnします。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L314-L333

func (b *resourceBuilder) newScaleSetListenerRole(autoscalingListener *v1alpha1.AutoscalingListener) *rbacv1.Role {
        rules := rulesForListenerRole([]string{autoscalingListener.Spec.EphemeralRunnerSetName})
        rulesHash := hash.ComputeTemplateHash(&rules)
        newRole := &rbacv1.Role{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      scaleSetListenerRoleName(autoscalingListener),
                        Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
                        Labels: map[string]string{
                                LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
                                LabelKeyGitHubScaleSetName:      autoscalingListener.Spec.AutoscalingRunnerSetName,
                                labelKeyListenerNamespace:       autoscalingListener.Namespace,
                                labelKeyListenerName:            autoscalingListener.Name,
                                "role-policy-rules-hash":        rulesHash,
                        },
                },
                Rules: rules,
        }


        return newRole
}

rulesForListenerRole関数がロール作成の上で、一番重要な箇所となります。
EphemeralRunnerSetリソース、およびEphemeralRunnerのStatusサブリソースに対するpatch操作を許可します。
AutoScalingListener Podは、EphemeralRunnerSetリソースの.Spec.ReplicasとEphemeralRunnerのStatusの変更をpatchで行います。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L596-L610

func rulesForListenerRole(resourceNames []string) []rbacv1.PolicyRule {
        return []rbacv1.PolicyRule{
                {
                        APIGroups:     []string{"actions.github.com"},
                        Resources:     []string{"ephemeralrunnersets"},
                        ResourceNames: resourceNames,
                        Verbs:         []string{"patch"},
                },
                {
                        APIGroups: []string{"actions.github.com"},
                        Resources: []string{"ephemeralrunners", "ephemeralrunners/status"},
                        Verbs:     []string{"patch"},
                },
        }
}

Proxy経由でのGitHubとの接続もサポートされているため、以下のr.createProxySecret関数でSecretを作成します。
今回は説明は割愛します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L212-L225

        // Create a secret containing proxy config if specified
        if autoscalingListener.Spec.Proxy != nil {
                proxySecret := new(corev1.Secret)
                if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: proxyListenerSecretName(autoscalingListener)}, proxySecret); err != nil {
                        if !kerrors.IsNotFound(err) {
                                log.Error(err, "Unable to get listener proxy secret", "namespace", autoscalingListener.Namespace, "name", proxyListenerSecretName(autoscalingListener))
                                return ctrl.Result{}, err
                        }

                        // Create a mirror secret for the listener pod in the Controller namespace for listener pod to use
                        log.Info("Creating a listener proxy secret for the listener pod")
                        return r.createProxySecret(ctx, autoscalingListener, log)
                }
        }

いよいよAutoScalingListetner Podの作成を行います。
r.Get関数でPodを取得し、存在しなければ、Podの作成します。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L229-L244

        listenerPod := new(corev1.Pod)
        if err := r.Get(ctx, client.ObjectKey{Namespace: autoscalingListener.Namespace, Name: autoscalingListener.Name}, listenerPod); err != nil {
                if !kerrors.IsNotFound(err) {
                        log.Error(err, "Unable to get listener pod", "namespace", autoscalingListener.Namespace, "name", autoscalingListener.Name)
                        return ctrl.Result{}, err
                }


                if err := r.publishRunningListener(autoscalingListener, false); err != nil {
                        // If publish fails, URL is incorrect which means the listener pod would never be able to start
                        return ctrl.Result{}, nil
                }


                // Create a listener pod in the controller namespace
                log.Info("Creating a listener pod")
                return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, mirrorSecret, log)
        }

r.createListenerPod関数を見ていきます。 コードは一部省略しています。省略している部分はProxyやTLS接続設定の箇所となります。
今回は純粋なPodの作成のみを解説していきます。今回もr.resourceBuilder.newScaleSetListenerPod関数で定義を作成し、AutoScalingListenerリソースを親としてOwnerReferenceの設定、r.Create関数でリソースを作成といった流れとなります。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/autoscalinglistener_controller.go#L346-L425

func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
:
        newPod, err := r.resourceBuilder.newScaleSetListenerPod(autoscalingListener, serviceAccount, secret, metricsConfig, envs...)
        if err != nil {
                logger.Error(err, "Failed to build listener pod")
                return ctrl.Result{}, err
        }


        if err := ctrl.SetControllerReference(autoscalingListener, newPod, r.Scheme); err != nil {
                logger.Error(err, "Failed to set controller reference")
                return ctrl.Result{}, err
        }


        logger.Info("Creating listener pod", "namespace", newPod.Namespace, "name", newPod.Name)
        if err := r.Create(ctx, newPod); err != nil {
                logger.Error(err, "Unable to create listener pod", "namespace", newPod.Namespace, "name", newPod.Name)
                return ctrl.Result{}, err
        }


        logger.Info("Created listener pod", "namespace", newPod.Namespace, "name", newPod.Name)
        return ctrl.Result{}, nil
}

r.resourceBuilder.newScaleSetListenerPod関数でPod定義を作成します。
複数工程に分かれているので、細かく見ていきたいと思います。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L133-L299

func (b *resourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret, metricsConfig *listenerMetricsServerConfig, envs ...corev1.EnvVar) (*corev1.Pod, error) {
        listenerEnv := []corev1.EnvVar{
                {
                        Name:  "GITHUB_CONFIGURE_URL",
                        Value: autoscalingListener.Spec.GitHubConfigUrl,
                },
                {
                        Name:  "GITHUB_EPHEMERAL_RUNNER_SET_NAMESPACE",
                        Value: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
                },
                {
                        Name:  "GITHUB_EPHEMERAL_RUNNER_SET_NAME",
                        Value: autoscalingListener.Spec.EphemeralRunnerSetName,
                },
                {
                        Name:  "GITHUB_MAX_RUNNERS",
                        Value: strconv.Itoa(autoscalingListener.Spec.MaxRunners),
                },
                {
                        Name:  "GITHUB_MIN_RUNNERS",
                        Value: strconv.Itoa(autoscalingListener.Spec.MinRunners),
                },
                {
                        Name:  "GITHUB_RUNNER_SCALE_SET_ID",
                        Value: strconv.Itoa(autoscalingListener.Spec.RunnerScaleSetId),
                },
                {
                        Name:  "GITHUB_RUNNER_SCALE_SET_NAME",
                        Value: autoscalingListener.Spec.AutoscalingRunnerSetName,
                },
                {
                        Name:  "GITHUB_RUNNER_LOG_LEVEL",
                        Value: scaleSetListenerLogLevel,
                },
                {
                        Name:  "GITHUB_RUNNER_LOG_FORMAT",
                        Value: scaleSetListenerLogFormat,
                },
        }
        listenerEnv = append(listenerEnv, envs...)


        if _, ok := secret.Data["github_token"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_TOKEN",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_token",
                                },
                        },
                })
        }


        if _, ok := secret.Data["github_app_id"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_APP_ID",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_app_id",
                                },
                        },
                })
        }


        if _, ok := secret.Data["github_app_installation_id"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_APP_INSTALLATION_ID",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_app_installation_id",
                                },
                        },
                })
        }


        if _, ok := secret.Data["github_app_private_key"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_APP_PRIVATE_KEY",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_app_private_key",
                                },
                        },
                })
        }


        var ports []corev1.ContainerPort
        if metricsConfig != nil && len(metricsConfig.addr) != 0 {
                listenerEnv = append(
                        listenerEnv,
                        corev1.EnvVar{
                                Name:  "GITHUB_METRICS_ADDR",
                                Value: metricsConfig.addr,
                        },
                        corev1.EnvVar{
                                Name:  "GITHUB_METRICS_ENDPOINT",
                                Value: metricsConfig.endpoint,
                        },
                )


                _, portStr, err := net.SplitHostPort(metricsConfig.addr)
                if err != nil {
                        return nil, fmt.Errorf("failed to split host:port for metrics address: %v", err)
                }
                port, err := strconv.ParseInt(portStr, 10, 32)
                if err != nil {
                        return nil, fmt.Errorf("failed to convert port %q to int32: %v", portStr, err)
                }
                ports = append(
                        ports,
                        corev1.ContainerPort{
                                ContainerPort: int32(port),
                                Protocol:      corev1.ProtocolTCP,
                                Name:          "metrics",
                        },
                )
        }


        podSpec := corev1.PodSpec{
                ServiceAccountName: serviceAccount.Name,
                Containers: []corev1.Container{
                        {
                                Name:            autoscalingListenerContainerName,
                                Image:           autoscalingListener.Spec.Image,
                                Env:             listenerEnv,
                                ImagePullPolicy: scaleSetListenerImagePullPolicy,
                                Command: []string{
                                        "/github-runnerscaleset-listener",
                                },
                                Ports: ports,
                        },
                },
                ImagePullSecrets: autoscalingListener.Spec.ImagePullSecrets,
                RestartPolicy:    corev1.RestartPolicyNever,
        }


        labels := make(map[string]string, len(autoscalingListener.Labels))
        for key, val := range autoscalingListener.Labels {
                labels[key] = val
        }


        newRunnerScaleSetListenerPod := &corev1.Pod{
                TypeMeta: metav1.TypeMeta{
                        Kind:       "Pod",
                        APIVersion: "v1",
                },
                ObjectMeta: metav1.ObjectMeta{
                        Name:      autoscalingListener.Name,
                        Namespace: autoscalingListener.Namespace,
                        Labels:    labels,
                },
                Spec: podSpec,
        }


        if autoscalingListener.Spec.Template != nil {
                mergeListenerPodWithTemplate(newRunnerScaleSetListenerPod, autoscalingListener.Spec.Template)
        }


        return newRunnerScaleSetListenerPod, nil
}

まずは、Podのコンテナで利用する環境変数を定義します。
環境変数の値は、AutoScalingListeterリソースの.Specの値などです。
なお、これらの値はAutoScalingRunnerSetリソースの.Specの値から継承されてきたものや、AutoScalingRunnerSet Controllerが付与したものなどとなります。
例えば、autoscalingListener.Spec.GitHubConfigUrlやautoscalingListener.Spec.MaxRunners、autoscalingListener.Spec.MinRunnersはAutoScalingRunnerSetリソースをhelmでデプロイする際のオプションの値です。autoscalingListener.Spec.RunnerScaleSetIdはAutoScalingRunnerSet ControllerがGitHubのAPI経由で取得した値です。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L134-L172

        listenerEnv := []corev1.EnvVar{
                {
                        Name:  "GITHUB_CONFIGURE_URL",
                        Value: autoscalingListener.Spec.GitHubConfigUrl,
                },
                {
                        Name:  "GITHUB_EPHEMERAL_RUNNER_SET_NAMESPACE",
                        Value: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
                },
                {
                        Name:  "GITHUB_EPHEMERAL_RUNNER_SET_NAME",
                        Value: autoscalingListener.Spec.EphemeralRunnerSetName,
                },
                {
                        Name:  "GITHUB_MAX_RUNNERS",
                        Value: strconv.Itoa(autoscalingListener.Spec.MaxRunners),
                },
                {
                        Name:  "GITHUB_MIN_RUNNERS",
                        Value: strconv.Itoa(autoscalingListener.Spec.MinRunners),
                },
                {
                        Name:  "GITHUB_RUNNER_SCALE_SET_ID",
                        Value: strconv.Itoa(autoscalingListener.Spec.RunnerScaleSetId),
                },
                {
                        Name:  "GITHUB_RUNNER_SCALE_SET_NAME",
                        Value: autoscalingListener.Spec.AutoscalingRunnerSetName,
                },
                {
                        Name:  "GITHUB_RUNNER_LOG_LEVEL",
                        Value: scaleSetListenerLogLevel,
                },
                {
                        Name:  "GITHUB_RUNNER_LOG_FORMAT",
                        Value: scaleSetListenerLogFormat,
                },
    }

次に、GitHubへの接続に使用するSecretのDataを環境変数に入れます。
この時、PAT(github_token)か、GitHub Appかで環境変数を分けています。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L174-L228

        if _, ok := secret.Data["github_token"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_TOKEN",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_token",
                                },
                        },
                })
        }

        if _, ok := secret.Data["github_app_id"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_APP_ID",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_app_id",
                                },
                        },
                })
        }

        if _, ok := secret.Data["github_app_installation_id"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_APP_INSTALLATION_ID",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_app_installation_id",
                                },
                        },
                })
        }

        if _, ok := secret.Data["github_app_private_key"]; ok {
                listenerEnv = append(listenerEnv, corev1.EnvVar{
                        Name: "GITHUB_APP_PRIVATE_KEY",
                        ValueFrom: &corev1.EnvVarSource{
                                SecretKeyRef: &corev1.SecretKeySelector{
                                        LocalObjectReference: corev1.LocalObjectReference{
                                                Name: secret.Name,
                                        },
                                        Key: "github_app_private_key",
                                },
                        },
                })
        }

最後に、Podの定義をreturnします。
なお、AutoScalingListener Podは、cmd/githubrunnerscalesetlistenerのコードを使ってGitHubとのやり取りおよび、EphemeralRunnerSetの操作を行います。そのためimageは、RCのController Podと同じimageであり、このコードを含む ghcr.io/actions/gha-runner-scale-set-controller:タグ を使用します。
このコードは.Spec.Commandで /github-runnerscaleset-listener としてコマンド実行されています。

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/controllers/actions.github.com/resourcebuilder.go#L262-L298

        podSpec := corev1.PodSpec{
                ServiceAccountName: serviceAccount.Name,
                Containers: []corev1.Container{
                        {
                                Name:            autoscalingListenerContainerName,
                                Image:           autoscalingListener.Spec.Image,
                                Env:             listenerEnv,
                                ImagePullPolicy: scaleSetListenerImagePullPolicy,
                                Command: []string{
                                        "/github-runnerscaleset-listener",
                                },
                                Ports: ports,
                        },
                },
                ImagePullSecrets: autoscalingListener.Spec.ImagePullSecrets,
                RestartPolicy:    corev1.RestartPolicyNever,
        }


        labels := make(map[string]string, len(autoscalingListener.Labels))
        for key, val := range autoscalingListener.Labels {
                labels[key] = val
        }


        newRunnerScaleSetListenerPod := &corev1.Pod{
                TypeMeta: metav1.TypeMeta{
                        Kind:       "Pod",
                        APIVersion: "v1",
                },
                ObjectMeta: metav1.ObjectMeta{
                        Name:      autoscalingListener.Name,
                        Namespace: autoscalingListener.Namespace,
                        Labels:    labels,
                },
                Spec: podSpec,
        }


        if autoscalingListener.Spec.Template != nil {
                mergeListenerPodWithTemplate(newRunnerScaleSetListenerPod, autoscalingListener.Spec.Template)
        }


        return newRunnerScaleSetListenerPod, nil

補足ですが、cmd/githubrunnerscalesetlistenerは、以下のDockerfileでgo buildされ、/github-runnerscaleset-listenerとしてdistrolessなimage内にCOPYされています。興味のある方は以下の折りたたみをご確認ください。

Dockerfile

https://github.com/actions/actions-runner-controller/blob/f1d7c52253b89f0beae60141f8465d9495cdc2cf/Dockerfile#L35-L55

# Build
RUN --mount=target=. \
  --mount=type=cache,mode=0777,target=${GOCACHE} \
  export GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} && \
  go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}' -X 'github.com/actions/actions-runner-controller/build.CommitSHA=${COMMIT_SHA}'" -o /out/manager main.go && \
  go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}' -X 'github.com/actions/actions-runner-controller/build.CommitSHA=${COMMIT_SHA}'" -o /out/github-runnerscaleset-listener ./cmd/githubrunnerscalesetlistener && \
  go build -trimpath -ldflags="-s -w" -o /out/github-webhook-server ./cmd/githubwebhookserver && \
  go build -trimpath -ldflags="-s -w" -o /out/actions-metrics-server ./cmd/actionsmetricsserver && \
  go build -trimpath -ldflags="-s -w" -o /out/sleep ./cmd/sleep

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot

WORKDIR /

COPY --from=builder /out/manager .
COPY --from=builder /out/github-webhook-server .
COPY --from=builder /out/actions-metrics-server .
COPY --from=builder /out/github-runnerscaleset-listener .
COPY --from=builder /out/sleep .

以上が完了すると、AutoScalingListener Podが作成されます。

ここまでで、AutoScalingRunnerSet、EphemaralRunnerSet、AutoScalingListerリソース、AutoScalingLister Podが作成されました。
これで、次回解説するAutoScalingLister PodがGitHubとのロングポーリングを確立することで、Workflow実行準備が完了します。

さいごに

結構長くなりましたが、まだまだ続きますよ!!
次回は、AutoScalingLister Podでのロングポーリングなどの動作、Workflow実行をトリガーとしたEphemaralRunnerリソースとEphemaralRunner Podの作成、Workflow終了時の動作をコードで追っていきます。
次回も楽しみにしていただけると嬉しいです!!

2023/09/30追記: 第四回コード解説 後半は以下を参照ください。

techblog.ap-com.co.jp

ACS事業部のご紹介

私達ACS事業部はAzure・AKSなどのクラウドネイティブ技術を活用した内製化やGitHub Enterpriseの導入のご支援をしております。
www.ap-com.co.jp www.ap-com.co.jp また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。 www.ap-com.co.jp

本記事の投稿者: 谷合純也
AKS/ACA、GitHubをメインにインフラ系のご支援を担当しています。
junya0530さんの記事一覧 | Zenn