GitHub Actionsをなるべく安く使う

 
0
このエントリーをはてなブックマークに追加
Kazuki Moriyama
Kazuki Moriyama (森山 和樹)

はじめに

GitHub Actionsは便利だが使いすぎると普通にお金がかかる。
特にrenovateなどで頻繁にPRを出すようにしていると、その分だけ頻繁にactionsが回って料金がかかる。
月に数万ほどは気にせずにお金をジャブジャブ投下できる会社なら別なのだが、なるべくお金を節約できたほうが普通は嬉しい。

この記事ではactionsをなるべく料金を安く効果的に使用するために行っているプラクティスをまとめる。

GitHub Actionsのself hosted runnerを使う

actionsにはrunnerを自前で用意するための仕組みが備わっていて、これをself hosted runnerと言う。

https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners

導入自体はそんなに難しくないので、以下の記事などを参考にすると良い。

https://qiita.com/h_tyokinuhata/items/7a9297f75d0513572f4a

ただし、ナイーブにself hosted runnerを使用する方法だと以下のような問題点がある。

  • 起動するPCのCPUアーキテクチャによってはactionsの移行がスムーズに行かなかったりする
    • self hostedをMac OS使って起動するときとか
  • runnerの台数をスケールさせづらい

ので弊社ではdocker containerを使用してself hosted runnerを立てている。

docker containerを使用したself hosted runner

最初は自前でdocker imageをコツコツ作っていたのだが、いい感じのimageがあったのでそれを今は使用している。

https://github.com/myoung34/docker-github-actions-runner

自前でimageを作ろうとすると、actionsのrunnerの後処理などの面倒なことを実装したり、なるべくgithub環境に近づけるために色々しないといけないのだが、そういうことを代わりにしてくれているので便利である。

あとは以下のようなcomposeファイルを定義して、upすればrunnerが10台立ち上がる。

version: '3.8'

services:
  runner:
    image: myoung34/github-runner:latest
    environment:
      ACCESS_TOKEN: ${PERSONAL_ACCESS_TOKEN}
      RUNNER_WORKDIR: /tmp/runner/work
      RUNNER_SCOPE: org
      ORG_NAME: <GITHUB_ORG_NAME>
      LABELS: self-hosted,Linux
    restart: always
    security_opt:
      # needed on SELinux systems to allow docker container to manage other docker containers
      - label:disable
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock'
      # note: a quirk of docker-in-docker is that this path
      # needs to be the same path on host and inside the container,
      # docker mgmt cmds run outside of docker but expect the paths from within
    deploy:
      replicas: 10

image

設定など詳しくは以下を参照。

https://github.com/myoung34/docker-github-actions-runner/wiki/Usage

containerでrunnerを走らせるときの問題点

containerはそれはそれで問題はあって、例えばgpu依存のライブラリなどではエラーが出たりするので対処が必要。

またchromeなども入っていないので、もしVRTなどをしたいときには自分で入れる必要がある。

https://github.com/myoung34/docker-github-actions-runner/issues/237

とは言ってもただのlinterやapiを叩くだけの様なactionsはすんなり走るので、ほとんどのjobはすんなり移行ができる。

self hosted runnerのキャッシュ

runnerを自前で用意すると、actionsが用意しているキャッシュをそのまま使うと異常に遅い。
キャッシュをネットワーク越しにおいているので、そのダウンロードに時間が掛かる。
そのままだとキャッシュのステップだけで普通に10分以上かかったりするので、localにキャッシュをする方式にしている。

https://github.com/corca-ai/local-cache

これを使えばネットワーク依存のキャッシュ速度に悩むことはなくなる。
注意する必要があるのはsetup/nodeなどのactionsのcache機構を使わないように設定を変えなければならない。

ただ今度はrunnerを立てているマシンのキャッシュ肥大化が起こるが、それは今後の課題。

不要なactionsを走らせない

これは王道だと思うが、コードの変更箇所を見て走らせる必要がないジョブは走らせないようにする。
やり方としてpathsなどでのフィルタリングを思いつくが、これはrequiredなjobのstatusがpendingのままになり、PRのマージをブロックするという問題がある。

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks

解決策としては、jobレベルのifを使用すれば良い。

https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution

ただし今度はコードの差分をどの様に検知するかという問題があるが、それには以下のactionを使用する。

https://github.com/marketplace/actions/changed-files

これを再利用可能ワークフローにしておけば、様々なjobからチェックの機構だけを呼び出して利用できる。

# detect-change.yml
# コードの差分を検知するための再利用可能ワークフロー
on:
  workflow_call:
    outputs:
      detect_change:
        value: ${{ jobs.check.outputs.detect_change }}
    inputs:
      directory_str:
        type: string
        required: false
        default: ""
      ignore_directory_str:
        type: string
        required: false
        default: ""

jobs:
  check:
    runs-on: self-hosted
    outputs:
      detect_change: ${{ steps.detect_change.outputs.any_modified }}

    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          fetch-depth: 0
      - name: Detect changed files
        id: detect_change
        uses: tj-actions/changed-files@fea790cb660e33aef4bdf07304e28fedd77dfa13 # v39
        with:
          # duplicateしたときfiles_ignoreが優先される
          files: |
            ${{ inputs.directory_str || '**' }}
            .github/workflows
          files_ignore: |
            ${{ inputs.ignore_directory_str }}
      - run: |
          echo ${{ steps.detect_change.outputs.all_modified_files }}
          echo ${{ steps.detect_change.outputs.any_modified }}
# lintなどの実際に走らせたいworkflow
name: codecheck

on:
  push:
    branches:
      - main
      - dev
  pull_request:

defaults:
  run:
    shell: bash

jobs:
  check:
    # 再利用可能ワークフローを使用
    uses: ./.github/workflows/detect-change.yml
    with:
      # 例えばterraformの変更は無視
      ignore_directory_str: terraform/**

  self-hosted-lint:
    # needsで依存させて変更があったときのみjobを走らせる
    needs:
      - check
    if: needs.check.outputs.detect_change == 'true'

    runs-on: self-hosted

    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
      # 実際に走らせたいjobのステップ群...

ポイントはコードの差分チェックjobもself hosted runnerで走らせること。
こうすればいくらチェック機構を入れても料金が増えることはなく、チェックによる無駄が省けて減るだけ。

最後に

以上の様にやれば7割方のactionsはself-hostedに移行できる。
パフォーマンス的にもrunnerを走らせる環境にもよるが、元のgithub環境のrunnerと比べて遜色なく動いている。
残り3割のactionsはすんなり移行できなかったりするが、それはぼちぼち解決していけば良い。

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。