GitHub Actionsをなるべく安く使う
はじめに
GitHub Actionsは便利だが使いすぎると普通にお金がかかる。
特にrenovateなどで頻繁にPRを出すようにしていると、その分だけ頻繁にactionsが回って料金がかかる。
月に数万ほどは気にせずにお金をジャブジャブ投下できる会社なら別なのだが、なるべくお金を節約できたほうが普通は嬉しい。
この記事ではactionsをなるべく料金を安く効果的に使用するために行っているプラクティスをまとめる。
GitHub Actionsのself hosted runnerを使う
actionsにはrunnerを自前で用意するための仕組みが備わっていて、これをself hosted runnerと言う。
導入自体はそんなに難しくないので、以下の記事などを参考にすると良い。
ただし、ナイーブにself hosted runnerを使用する方法だと以下のような問題点がある。
- 起動するPCのCPUアーキテクチャによってはactionsの移行がスムーズに行かなかったりする
- self hostedをMac OS使って起動するときとか
- runnerの台数をスケールさせづらい
ので弊社ではdocker containerを使用してself hosted runnerを立てている。
docker containerを使用したself hosted runner
最初は自前でdocker imageをコツコツ作っていたのだが、いい感じのimageがあったのでそれを今は使用している。
自前で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
設定など詳しくは以下を参照。
containerでrunnerを走らせるときの問題点
containerはそれはそれで問題はあって、例えばgpu依存のライブラリなどではエラーが出たりするので対処が必要。
またchromeなども入っていないので、もしVRTなどをしたいときには自分で入れる必要がある。
とは言ってもただのlinterやapiを叩くだけの様なactionsはすんなり走るので、ほとんどのjobはすんなり移行ができる。
self hosted runnerのキャッシュ
runnerを自前で用意すると、actionsが用意しているキャッシュをそのまま使うと異常に遅い。
キャッシュをネットワーク越しにおいているので、そのダウンロードに時間が掛かる。
そのままだとキャッシュのステップだけで普通に10分以上かかったりするので、localにキャッシュをする方式にしている。
これを使えばネットワーク依存のキャッシュ速度に悩むことはなくなる。
注意する必要があるのはsetup/nodeなどのactionsのcache機構を使わないように設定を変えなければならない。
ただ今度はrunnerを立てているマシンのキャッシュ肥大化が起こるが、それは今後の課題。
不要なactionsを走らせない
これは王道だと思うが、コードの変更箇所を見て走らせる必要がないジョブは走らせないようにする。
やり方としてpathsなどでのフィルタリングを思いつくが、これはrequiredなjobのstatusがpendingのままになり、PRのマージをブロックするという問題がある。
解決策としては、jobレベルのifを使用すれば良い。
ただし今度はコードの差分をどの様に検知するかという問題があるが、それには以下のactionを使用する。
これを再利用可能ワークフローにしておけば、様々な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はすんなり移行できなかったりするが、それはぼちぼち解決していけば良い。