ディップ Advent Calendar 2023 の17日目の記事です!
はじめに
皆さん、Github Actions 使ってますか?
workflowのyamlファイルを書くだけで、PRマージなどのGithubイベントをトリガーに様々なジョブを実行してくれるので便利ですよね。
筆者も業務において、静的解析やテストの実行・stage環境へのリリース実行など、がっつり活用しています💪
このGithub Actionsですが、Github Enterprise上で使うためには セルフホストランナー
と呼ばれる、Actionsの実行環境を自前で作成する必要があります。
筆者の所属するチームでは、このセルフホストランナーをAWSのEC2マシン
を使って用意していたのですが、運用面で若干不満があり...
思い切ってこのセルフホストランナーを、オートスケール
させて使い捨て
にするよう設定してみたところ、中々使い心地が良かったので記事にしようと思います🙌
この記事ではオートスケーリングな使い捨てランナーを作成する簡単な手順と、その際いくつか注意すべきポイントなどを紹介していきます。
目次
導入背景
導入当時筆者の所属するチームでは主に3つのGithubのリポジトリを運用しており、
各リポジトリのworkflow ジョブを、1台の EC2 セルフホストランナー
で処理するという(貧乏くさい)体制をとっていました。
この体制ではworkflowの実行自体に問題は無いのですが、1,2ヶ月くらいするとランナーの作業ディレクトリにキャッシュやログなどが貯まるのか、EC2インスタンスのディスク使用量がいっぱいになり動かなくなっていました💦
さらにランナーはworkflowのジョブを1つ1つ順番にしか実行できません。そのため3つもリポジトリを連携していると、高頻度でジョブの実行待ちが発生してしまい、開発体験的によろしくありません。
動かなくなる度に作業ディレクトリやインスタンスを作り直したりする作業が地味にめんどくさく...
かといってランナーのEC2インスタンスを増やしたりインスタンスタイプを上げるのも、運用的・金銭的コストがなぁ...と思い悩んでおりました。
そこで、実行するジョブの数だけ必要なセルフホストランナーをその場で立ち上げ、ジョブを処理し終わった後にランナーを削除するような仕組みがあれば良さそうだと考え、
オートスケーリングな使い捨てランナー
を作るに至ります🙌
オートスケーリングランナーの構築
オートスケーリングなセルフホストランナーを構築するためにGithub公式から※当時推奨されていた方法は、以下の2つです。
-
actions-runner-controller/actions-runner-controller - セルフホストランナー用
Kubernetes コントローラー
。 -
philips-labs/terraform-aws-github-runner - セルフホストランナー用の
Terraform モジュール
。
構築当時チームではkubernetesを使っていなかったので、運用負荷を考えると難しそうなのかなと考えてphilips-labs/terraform-aws-github-runner
の仕組みを使うことにしました。
※ 現在Github公式から推奨されている方法は、actions-runner-controller/actions-runner-controller
のみになったようです。
とはいえphilips-labs/terraform-aws-github-runner
の開発も日々活発に行われているので、terraformに慣れている方などはこちらを使うようにしても、個人的には問題ないと思います👌
philips-labs/terraform-aws-github-runner
について
philips-labs/terraform-aws-github-runner
はオートスケーリングランナーを作る上で必要になる、AWSリソースを作成するためのterraformモジュールです。
手順を進めていくと、以下のようなリソースがお使いのAWS環境上に作成されます。
(画像は https://github.com/philips-labs/terraform-aws-github-runner より引用)
仕組み
詳しい方なら上記の画像を見てなんとなくイメージがつくかもしれませんが、簡単に書くと以下のような仕組みでランナーが立ち上がることになります。
-
Github App
がworkflowの実行を検知して、webhook経由でAPI Gateway
にリクエストを送信 -
API Gateway
はリクエスト元検証用のLambda
に、リクエスト元がGithub App
であるか検証を依頼 - 検証できた場合
Lambda
はsqs
にメッセージを送信 -
sqs
を監視していたランナー立ち上げ用のLambda
が、事前に作成されたテンプレート
を元に※EC2スポットインスタンス
を立ち上げる - スケジュールに基づいて
ランナー削除用のLambda
が、インスタンスを終了させる。
※ EC2スポットインスタンスは、AWS側の空き資源を利用するインスタンスです。空き具合によって終了させられる可能性がある分、通常より安価でインスタンスを利用することができます。
手順
基本はphilips-labs/terraform-aws-github-runner
の手順に沿って行えば問題ありません。
大まかに手順を整理すると以下のようになります。
- Github Appをセットアップ
- Lambdaの設定ファイルをダウンロード
- 必要があればEC2のAMIを作成
- terraformの各種パラメータを設定
- Github Appに作成したAWSリソースの情報をセット
1. Github Appをセットアップ
まずはActionsを使用したいリポジトリやOrgnizationにGithub App
を作成していきます。
こちらはリポジトリやOrgnizationの権限さえあれば、手順に則っていくと詰まることなく作成できるかと思います。
(こちらの記事で詳しくまとめていただいていたので、ぜひ参考にしてみてください)
2. Lambdaの設定ファイルをダウンロード
philips-labs/terraform-aws-github-runner
が提供しているLambdaの設定ファイルをダウンロードします。
こちらのページから各zipファイルをダウンロードするか、こちらのterraformを実行してダウンロードできます。
ダウンロードしてきたファイルたちは、後述するメインのterraformの実行内容にて使用します。
3. 必要があればEC2のAMIを作成
EC2スポットインスタンス
で使用するAMIを作成します。
philips-labs/terraform-aws-github-runner
はデフォルトでAmazon Linux2
のAMIを使ってインスタンスを立ち上げてくれます。
このAMIをカスタマイズしたい場合は、こちらの内容を参考に、packerを使用してAMIを作成することができます。
筆者はすでに使っていたランナーの環境がubuntu-focal-20.04
を使用していたので、そのカスタムAMIを作成しました。
以下はそのAMI作成のpackerの記述になります。ほぼテンプレそのままです。長いのでそのままのところは一部割愛。
packer {
required_plugins {
amazon = {
version = ">= 0.0.2"
source = "github.com/hashicorp/amazon"
}
}
}
variable "runner_version" {
description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided."
default = null
}
variable "region" {
description = "The region to build the image in"
type = string
default = "****" // AWSのリージョン
}
...
variable "instance_type" {
description = "The instance type Packer will use for the builder"
type = string
default = "c7g.large" // AMIのビルドに使用するインスタンスイメージ
}
variable "root_volume_size_gb" {
type = number
default = 8
}
...
locals {
runner_version = coalesce(var.runner_version, trimprefix(jsondecode(data.http.github_runner_release_json.body).tag_name, "v"))
}
source "amazon-ebs" "githubrunner" {
ami_name = "github-runner-ubuntu-focal-arm64-${formatdate("YYYYMMDDhhmm", timestamp())}" // 作成されるAMIの名前
instance_type = var.instance_type
region = var.region
security_group_id = var.security_group_id
subnet_id = var.subnet_id
associate_public_ip_address = var.associate_public_ip_address
source_ami_filter {
filters = {
name = "*ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["******"]
}
ssh_username = "ubuntu"
...
}
build {
name = "githubactions-runner"
sources = [
"source.amazon-ebs.githubrunner"
]
provisioner "shell" {
environment_vars = [
"DEBIAN_FRONTEND=noninteractive"
]
// AMIビルド時に実行したいコマンド達を書く。dockerなど、workflowで使用したいツールやパッケージなどはここで追加しておく。
inline = concat([
"sudo cloud-init status --wait",
"sudo apt-get -y update",
...
"sudo ./aws/install",
], var.custom_shell_commands)
}
provisioner "file" {
content = templatefile("../install-runner.sh", {
...
}
.
├── images
│ ├── install-runner.ps1
│ ├── install-runner.sh
│ ├── start-runner.ps1
│ ├── start-runner.sh
│ └── ubuntu-focal
│ └── github_agent.ubuntu.pkr.hcl
ここまでできたら、vpcやsubnetのid、access_keyやsecret_keyを環境変数において、packerのコマンドを実行していきます。
packer init .
packer validate .
packer build github_agent.ubuntu.pkr.hcl
無事AMIが作成されていれば完了です👌
4. terraformの各種パラメータを設定
philips-labs/terraform-aws-github-runnerのREADME
を参考に、terraformの内容を記述していきます!
最終的に次のようなファイル内容になりました。
terraform {
// S3にtfstateを置いておきたい場合に必要な設定。
backend "s3" {
bucket = "バケット名"
region = "リージョン名"
key = "キー名"
}
}
locals {
version = "使用するphilips-labsのバージョン"
environment = "環境名"
aws_region = "リージョン名"
}
variable "github_app_key_base64" {}
variable "github_app_id" {}
variable "vpc_id" {}
variable "subnet_ids" {}
variable "access_key" {}
variable "secret_key" {}
provider "aws" {
region = "リージョン名"
access_key = var.access_key
secret_key = var.secret_key
}
# ランダムIDの生成
resource "random_id" "random" {
byte_length = 20
}
module "runners" {
source = "philips-labs/github-runner/aws"
version = "使用するphilips-labsのバージョン"
# VPCを指定
aws_region = local.aws_region
vpc_id = var.vpc_id
subnet_ids = var.subnet_ids
prefix = local.environment
tags = {
Project = "プロジェクト名"
}
# GithubAppの設定
github_app = {
key_base64 = var.github_app_key_base64
id = var.github_app_id
webhook_secret = random_id.random.hex
}
# 先にダウンロードしておいたLambdaのパスを指定
webhook_lambda_zip = "lambdas-download/webhook.zip"
runner_binaries_syncer_lambda_zip = "lambdas-download/runner-binaries-syncer.zip"
runners_lambda_zip = "lambdas-download/runners.zip"
# organizationの設定
enable_organization_runners = true
runner_group_name = "設定したいランナーグループ名"
# ランナーユーザー設定
runner_run_as = "ubuntu"
# ubuntuのprebuilt AMI・ユーザーデータを設定
enable_userdata = false
ami_owners = ["AMIのowner_id"]
# packerで作成したAMI名
ami_filter = {
name = ["github-runner-ubuntu-focal-arm64-2023*"]
}
block_device_mappings = [{
# ubuntu rootデバイスの設定
device_name = "/dev/sda1"
delete_on_termination = true
volume_type = "gp3"
volume_size = 30
encrypted = true
iops = null
throughput = null
kms_key_id = null
snapshot_id = null
}]
# ssmでのec2アクセスを有効化
enable_ssm_on_runners = true
# 生成するランナーの最大数
runners_maximum_count = 20
instance_types = ["t4g.large"]
# ランナーのラベル指定
runner_extra_labels = "ephemeral"
# Runnerの立ち上げ待ち時間
runner_boot_time_in_minutes = 5
# lambdaなどのcloudwatchに出力する情報量。必要があれば"debug"にする
log_level = "info"
# ghes設定
ghes_url = "https://github..."
ghes_ssl_verify = true
# ランナーの使い捨て設定
enable_ephemeral_runners = true
# 最低起動時間
minimum_running_time_in_minutes = 5
# webhookの遅延をなしに
delay_webhook_event = 0
# 余分なランナーを作成しない
enable_job_queued_check = true
# イベント順を無視
enable_fifo_build_queue = false
# scaling downスケジュールを上書き
scale_down_schedule_expression = "cron(* * * * ? *)"
# ランナーのpool設定
pool_runner_owner = "ランナーのowner名"
pool_config = [{
size = 1
schedule_expression = "cron(* * * * ? *)"
}]
# pool時間を60秒に
pool_lambda_timeout = 60
}
これらの設定項目はあくまで一例です。
philips-labs/terraform-aws-github-runner
には他にも様々な設定項目があるので、より便利な使い方をしたい方はREADMEをご覧ください🙌
また、main.tfで使用するtfvarsのファイルも作成します。
ここで、先ほど作成したGithub Appのid
とbase64エンコードさせたGithub Appの秘密鍵
を忘れずに設定します。
github_app_key_base64 = "base64エンコードさせたgithub appの秘密鍵"
github_app_id = github appのid
...
最終的に以下のようなファイル構成になりました!
.
├── images
│ ├── install-runner.ps1
│ ├── install-runner.sh
│ ├── start-runner.ps1
│ ├── start-runner.sh
│ └── ubuntu-focal
│ └── github_agent.ubuntu.pkr.hcl
├── lambdas-download
│ ├── runner-binaries-syncer.zip
│ ├── runners.zip
│ └── webhook.zip
├── main.tf
├── modules
│ ├──...
├── outputs.tf
├── package-lock.json
└── terraform.tfvars
terraformの実行
この状態でterraformを実行していきます!
terraform init
terraform apply
無事にAWSのリソースが作成されれば完了です✅
5. Github Appに作成したAWSリソースの情報をセット
最後に作成したAWSリソースの情報をGithub Appに設定していきます。
こちらもphilips-labs/terraform-aws-github-runnerのReadMe
の手順に則っていくと詰まることなく作成できるかと思いますので、詳細は割愛させていただきます👍
(詳しくはまたまたこちらの記事が参考になるかと思います。)
以上で全ての手順が完了となります!
これで今まで通りの使い方で、オートスケーリングな使い捨てランナー
を使うことができるようになっているはずです👏
実装のポイント
いくつかこの仕組みを作る上で詰まったポイントがありましたので、皆さんが同じ轍を踏まないように共有させていただきます🤧
1. Lambdaたちのdebug
一番最初に全ての手順を完了させた後、ウッキウキでActionsのworkflowを走らせてみたところランナーが立ち上がらず...
なんでだろうと思ってCloudWatch
からLambda
のログを見に行ったのですが、具体的な内容は全然書かれていない...
この原因調査に結構な時間を持って行かれてしまいました😇
回り道して発見したのが以下のログレベルの記述
です。
# lambdaなどのcloudwatchに出力する情報量。必要があれば"debug"にする
log_level = "info"
先ほどのコードでも記述があったので皆さんはもう大丈夫かと思うのですが、
筆者が実装した当時、色々参考にさせていただいたサイトにはこちら書いている方があまりおらず、発見に時間がかかってしまいました(公式ドキュメントはしっかり読まないとですね...)
こちらの記述を"debug"
にすることでLambda
が詳細なログを吐いてくれるようになるので、開発中詰まったらこちらを変更するようにしましょう。
2. ghesの設定
上で書いた問題の原因です。
GithubEnterpriseを使用している場合、そのEnterpriseのドメインをterraformの記述に追加する必要がありました。
GHEを使用する方は忘れずに書くようにしましょう👍
# ghes設定
ghes_url = "https://github..."
3. EC2スポットインスタンスのサイズ
ようやくインスタンスが立ち上がるようになったと思いきや、今度はworkflowのjobが実行されない(または中々実行が進まない)問題が発生しました🤔
インスタンス自体はきちんと作成されているので、Lambdaのログにも異常はありません。
筆者はその謎を解き明かすべく、インスタンスの奥地へと向かいました。
するとどうやら、インスタンスの立ち上げ自体に時間がかかっているようでした。
見かけ上は立ち上がっていても、実際に使える状態にまではなっていません。(諸々のツールのダウンロードなんかに時間がかかるよう...?)
こちらはインスタンスタイプを性能の高いものに変更することで解消しました。
"t4g.small"
などではパワーが足りないことがあり、実用する上では"t4g.medium"
、"t4g.large"
などが安定するなといった所感でした。
もちろんコストは上がってしまいますが、それでもスポットインスタンス
なので筆者チームの使い方の場合通常インスタンス
を常駐させておくよりは安く付きました。
workflowのjobが中々実行されない場合は、インスタンスサイズを少し大きくしてあげると改善するかもしれません👍
4. EC2のセキュリティグループ
これは詰まったポイントではないのですが、役に立ちそうな知見なので共有しておきます。
Github Actionsのセルフホストランナーですが、実は通常 アウトバウンド通信しか必要としません
。
(最初に知った時はかなり意外でした)
そのためEC2ランナーのセキュリティグループでは特段理由がない限り、インバウンド通信を拒否することをおすすめします🙌
万が一変なところからアクセスされるんじゃないかと心配するくらいなら、いっそ全部拒否してしまった方が気が楽です。(使い捨てインスタンスなのでそもそも可能性は低いですが。)
ちなみに何かの理由があってインスタンスに直接アクセスしたい場合は、ssmで接続できるようにterraformから設定ができるので、そちらを使うのが良いかと思います👍
おわりに
長くなってしまいましたが、オートスケーリングな使い捨てランナーを作成する簡単な手順と、いくつか注意すべきポイントについて書かせていただきました。
この仕組みを導入した結果、workflowジョブの待ち時間は無くなり、ランナーのメンテナンスもほとんど不要になりました。
ただしAMIは定期的にビルドしたいと考えているため、今後はその部分の自動化にも取り組んでいきたいと考えています👍
筆者と同じようにセルフホストランナー周りで不満がある方たちは、ぜひこの仕組みを導入してみてはいかがでしょうか!
この記事が何かの役に立つことを祈っています!
それでは皆さま、良いGithub Actionsライフを✅
参考