こんにちは!ACS事業部の谷合です。 皆大好きGitHub Actionsにおける、GitHub社公式のSelf-hosted runnerであるActions Runner Controller(以降ARC)の紹介をシリーズでお送りしております。
前回前々回は以下の記事を書いておりました。
- Actions Runner Controller Deep Dive!- アーキテクチャ編 - - APC 技術ブログ
- Actions Runner Controller Deep Dive!- 動作解説編 - - APC 技術ブログ
今回はARCのコードを解説していきたいと思います。
なお、解説は長くなるので今回を前編とします。
はじめに
私は全コードリーディング完了しておりますが、すべて解説するのは困難なため、以下の流れをコードから追ってみます。なお、コードはGo言語で書かれています。
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- The Actions Service acknowledges the runner registration and dispatches the job run details.
- Throughout the job run execution, the runner continuously communicates the logs and job run status back to the Actions Service.
- 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.
突然ですが、以下の画像は何だと思いますか?
コード内の関数の呼び出し元呼び出し先、関数の要約を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とのロングポーリングを途絶えさせたくないなどの理由があるのかもしれません。
// 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を監視しています。
// 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を監視しています。
// 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、子リソースでないロールを監視しています。
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が作成できるわけです。
- AutoScalingRunnerSet CustomResourceのデプロイ
- AutoScalingRunnerSet ControllerによってEphemeralRunnerSetとAutoScalingListener CustomResourceが順番にデプロイ
- AutoScalingListener ControllerによってAutoScalingListener Podが作成され、GitHubとのロングポーリングが開始されます。
- CI/CD jobが開始されると、AutoScalingListener Podに通知され、EphemeralRunnerSetの.Spec.Replicasを0からターゲットの数まで増やす
- EphemeralRunnerSet Controllerによって.Spec.Replicasの数だけEphemeralRunner CustomResourceが作成される。
- EphemeralRunner Controllerによって、EphemeralRunner Podが作成され、Self-hosted runnerとして動作する
- CI/CD job終了後は自動でGitHub上のRunnerと、EphemeralRunner Podが削除される
ARCの処理の流れ解説
AutoScalingRunnerSet、EphemeralRunnerSet、AutoScalingLilstenerリソースの作成
以下の動作を見ていきましょう。
- 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.
- 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が作成されます。
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を生成します。
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でデプロイする際に渡す必須の情報です。
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を取得します。
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を叩き、作成します。
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の取得を行います。
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して、作成します。
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関数が呼ばれます。
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関数で作成といった流れとなります。
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の定義を作成します。
複数工程に分かれているので、細かく見ていきたいと思います。
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リソースが再作成されます。
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は以下で定義されています。
ラベルやアノテーション一覧
// 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" )
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ラベルに値を入れます。
if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil { return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err) }
applyGitHubURLLabels関数の実装は面白いのですが、解説の粒度が細かくなるので今回はスコープ外とします。
興味ある方は以下の折りたたみをご確認ください。
applyGitHubURLLabels関数
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の実装
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リソースを作成します。
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にします。
// 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関数で作成といった流れとなります。
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関数と同様に、複数工程に分かれているので、細かく見ていきたいと思います。
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 を変数に入れます。
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ラベルに値を入れます。
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リソースを作成します。
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処理を終了します。
// 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関数で作成します。
// 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が存在していなかった場合でも、後続処理を継続できるわけです。
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の定義は特に重要なので、以降で解説します。
// 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関数でリソースを作成といった流れとなります。
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します。
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で行います。
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を作成します。
今回は説明は割愛します。
// 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の作成します。
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関数でリソースを作成といった流れとなります。
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定義を作成します。
複数工程に分かれているので、細かく見ていきたいと思います。
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経由で取得した値です。
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かで環境変数を分けています。
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
としてコマンド実行されています。
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
# 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追記: 第四回コード解説 後半は以下を参照ください。
ACS事業部のご紹介
私達ACS事業部はAzure・AKSなどのクラウドネイティブ技術を活用した内製化やGitHub Enterpriseの導入のご支援をしております。
www.ap-com.co.jp
www.ap-com.co.jp
また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。
www.ap-com.co.jp