技術本部 サービスリライアビリティグループ(SRG)の松田です。
SRG(Service Reliability Group)は、主に弊社メディアサービスのインフラ周りを横断的にサポートしており、既存サービスの改善や新規立ち上げ、OSS貢献などを行っているグループです。

弊社では一部のプロダクトでGitHub Enterprise Server(以下GHES)を利用しています。GHESでGithub Actionsを使いたい場合、GitHub-hosted runnersは利用できないため、self-hosted runnersを選択することになります。私が関わっているプロダクトでは、既存のCIからGitHub Actionsに移行する予定があり、開発者がカジュアルに試せる環境を用意することにしました。

公式の導入方法のとおりにself-hosted runnersを導入すると以下の課題があります。

  • 同一ホスト上で毎回実行されるため、ファイル操作などを伴う処理の場合に冪等性が無い
  • 事前にホスト上に設定する必要があるため、ジョブキューに応じてオートスケールは出来ない

これらの課題を解決しつつself-hosted runnersを実行できるツールとしてactions-runner-controllerがあります。

今回はこちらの導入方法、メリット・デメリットを書いていきます。

導入方法

Kubernetes上で動作するため事前にClusterを用意します。
導入はactions-runner-controllerのREADMEの通りですが、GHES独自の部分がいくつかあります。

CRDの追加

actions-runner-controller内で利用するcert-managerとactions-runner-controllerを追加します。

$ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.4/cert-manager.yaml
$ kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml

GHESのURLを設定

利用しているGHESのURLをenvに設定します。

kubectl set env deploy controller-manager -c manager GITHUB_ENTERPRISE_URL="${GHES_URL}" -n actions-runner-system

Personal Access Tokenの登録

GHESとの認証方法はGitHub AppとPersonal Access Tokenの2通りあります。

今回はPersonal Access Tokenで認証します。Organization単位で利用できるようにするためadmin:orgscopeを持つPersonal Access Tokenを作成して登録します。

$ kubectl create secret generic controller-manager \
-n actions-runner-system \
--from-literal=github_token=${GITHUB_PERSONAL_ACCESS_TOKEN}

実行対象リポジトリの取得

今回はOrganization内の複数リポジトリで使用できるように設定します。
self-hosted runnersをジョブ数に応じてオートスケールさせるためには、HorizontalRunnerAutoscalerに対象リポジトリを登録する必要があります。

Organization内の全Repositoryは以下のコマンドで取得できます。

$ curl -v -H "Authorization: token ${TOKEN}" https://${GHES_URL}/api/v3/orgs/${ORG}/repos | jq -c -r '.[].name'

RunnerDeploymentとHorizontalRunnerAutoscalerの登録

self-hosted runnersを実行するRunnerDeploymentを登録します。
実行ログを保存するためにsidecarでaws-cliコンテナを立てて、Podが終了するときにS3へログを送信するようにしています。
aws_access_key_idとaws_secret_access_keyは事前にsecretに登録してください。
HorizontalRunnerAutoscalerを正しく動作させるために、先程取得したRepository名を設定してください。

これをapplyするとself-hosted runnersが利用できるようになります。

apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: ${ORG}-runnerdeploy
spec:
  replicas: 1
  template:
    spec:
      labels:
        - test-runner
      initContainers:
        - name: chmod
          image: alpine
          command: ["chmod", "777", "/runner/_diag"]
          volumeMounts:
          - name: runner-log
            mountPath: /runner/_diag
        organization: ${ORG}
        image: summerwind/actions-runner:v2.274.1
        dockerdWithinRunnerContainer: false
        env:
        - name: GITHUB_ENTERPRISE_URL
          value: "https://${GHES_URL}/"
        volumeMounts:
        - name: runner-log
          mountPath: /runner/_diag
        volumes:
        - name: runner-log
          emptyDir:
        sidecarContainers:
          - name: aws-cli
            image: amazon/aws-cli:2.1.1
            command: ["/bin/bash", "-c"]
            args: ["trap \"aws s3 cp /var/log/runner-log/Worker* s3://BUCKET/self-hosted-runner/`date +%Y/%m/%d/` --region REGION\" TERM INT; sleep infinity & wait"]
          env:
          - name: AWS_ACCESS_KEY_ID
            valueFrom:
              secretKeyRef:
                name: aws-access
                key: aws_access_key_id
          - name: AWS_SECRET_ACCESS_KEY
            valueFrom:
              secretKeyRef:
                name: aws-access
                key: aws_secret_access_key
            volumeMounts:
            - name: runner-log
              mountPath: /var/log/runner-log
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
  name: ${ORG}-runnerdeploy-autoscaler
spec:
  scaleTargetRef:
    name: ${ORG}-runnerdeploy
  minReplicas: 1
  maxReplicas: 5
  scaleDownDelaySecondsAfterScaleOut: 300
  metrics:
  - type: TotalNumberOfQueuedAndInProgressWorkflowRuns
    repositoryNames:
    - ${REPOSITORY_A}
    - ${REPOSITORY_B}

メリット

ジョブ実行ごとにrunnerが作成されるため冪等性がある

公式の導入方法のとおりにするとジョブ実行後もrunnerタスクは残り続け、VM内で変更したファイルはそのままです。

このツールでは、実行するジョブごとにrunner Podが作成されるため冪等性があります。

ジョブ数に応じてオートスケールする

ジョブ数に応じてrunnerを増やして並列に実行できます。

Organization単位でRunnerを指定できる

RunnerDeployment一つでOrganization一つを設定できるので、複数Organizationの設定も簡単にできます。

しかし、Repositoryごとに必要なPod性能が大きく異なる場合もあるため、ある程度似通った性能ごとに作成した方がリソース効率がよくなります。

デメリット

Kubernetes Clusterを管理する必要がある

すでにCI/CD用のClusterがある場合はそこにdeployするだけですが、ない場合はself-hosted runnersのためにClusterを管理する必要があります。

NodeのコストもClusterの運用コストも小さくは無いため、利用規模が小さい場合は他の手段を採用したほうが結果的に楽ができるかもしれません。

今回は利用するOrganizationや実行ジョブ数が多く、冪等性とオートスケールが必須要件であったため採用しました。

Docker in Dockerになる

Dockerコンテナのactionを実行する場合は、sidecarで立ち上がるDockerコンテナ内で実行されます。(runner Pod内で実行することも可能です)

Docker in Dockerになるためprivileged: trueで実行する必要があり、実行される処理によっては危険です。

Repositoryの追加作業が必要になる

Repositoryが新規作成された際に、HorizontalRunnerAutoscalerの対象Repositoryとして追加する機能は現在ありません。

そのため、新規作成されるごとに追加してHorizontalRunnerAutoscalerをDeployしなおす必要があります。

Tips

  • ときどき、ジョブ実行後にrunner Podが削除されず手動で削除しないとキューが溜まり続けるときがあります。
  • pushするごとにジョブを実行するようにしているとリソースを使い切ってキューが溜まり続けてしまうため、お試しで構築している場合はPull Requestで実行するくらいにしておくと良いと思います。

おわりに

self-hosted runnersをKubernetes上で実行できるツール actions-runner-controllerについて紹介しました。

大規模にself-hosted runnersを活用する場合は非常に便利なツールですのでぜひ!

弊社では他にもself-hosted runnersを実行するツールとして、Private Cloud Groupでmyshoesをクローズドβで提供しています。紹介記事はこちら

利用者としてはGitHub-hosted runnersのような使用感になるため、運用コストを下げたいチームではこちらが採用されるでしょう。

私が担当しているプロダクトも今後myshoesに移行していく予定です。

2019年新卒入社。技術本部サービスリライアビリティグループ所属。AmebaBlogのインフラなどを担当しています。