GitHub Actions で利用するDockerfileについて考える
最近 GitHub Actions を色々と試しています。 GitHub Actions で使う Dockerfile や対象リポジトリのディレクトリ構成についてちょっと考えてみました。
DockerfileにおけるADD
GitHub Actions では、イベントの発生直後にDockerイメージのビルドを行うようです。
このとき、指定されたディレクトリの配下にあるファイルだけが ADD の対象になります。
以下のような構成のリポジトリーを想定してみましょう。
|-- hello-world (repository) | |__ .github | |__ main.workflow | |__ action | │__ Dockerfile |-- Gemfile
このとき action/Dockerfile
の内容を以下とします。
FROM ruby:2.6 ADD . . RUN bundle install --path vendor/bundle ENTRYPOINT ["bundle", "exec"] CMD ["rspec"]
リポジトリー直下の Gemfile
を使って、 Dockerイメージのビルド時に bundle install
を行う想定です。
この構成で Action を実行すると、以下のようにエラーとなってしまいます。
... Step 1/5 : FROM ruby:2.6 2.6: Pulling from library/ruby Digest: sha256:4ec9d4622afc4abe11e282a9ee77c2edfa979263ba03075365007bad9bd67171 Status: Downloaded newer image for ruby:2.6 ---> 91f360ea5325 Step 2/5 : ADD . . ---> 8ef90ba24305 Step 3/5 : RUN bundle install --path vendor/bundle ---> Running in a1abb0e2421a Could not locate Gemfile The command '/bin/sh -c bundle install --path vendor/bundle' returned a non-zero code: 10 ### FAILED with bundle 03:35:35Z (11.064s)
Gemfile
が存在しないと怒られてしまいました。
action ディレクトリー以下に Gemfile
が存在しないためです。
いくつか回避する方法があります。
案1: Gemfile を action ディレクトリーに配置する
|-- hello-world (repository) | |__ .github | |__ main.workflow | |__ action | │__ Dockerfile | │__ Gemfile
この場合、bundle install
は成功します。
しかし、普段の開発時に
bundle install --gemfile=action/Gemfile
のように Gemfile の場所を指定する必要が出てきます。 また、様々なツールなどがリポジトリー直下の Gemfile を想定して作られています。 それぞれのツールで Gemfileの場所 を設定する必要があるでしょう。
これを回避するように以下のように二重管理するのも個人的に好きではありません。
|-- hello-world (repository) | |__ .github | |__ main.workflow | |__ action | │__ Dockerfile | │__ Gemfile |-- Gemfile
案2: 複数のアクションに分ける
ワークフローで bundle install
と bundle exec rspec
を分けてしまう方法です。
この場合、 Dockerfile は以下のようになります。
FROM ruby:2.6
はい、そうですね。 Dockerfile を用意する必要はありません。
uses を docker://...
として記述することで、 DockerHub のイメージを利用することができます。
|-- hello-world (repository) | |__ .github | |__ main.workflow |-- Gemfile
ワークフローの定義は以下のようにします。
workflow "workflow" { on = "pull_request" resolves = [ "test", ] } action "install" { uses = "docker://ruby:2.6" runs = "bundle" args = ["install", "--path", "vendor/bundle"] } action "test" { uses = "docker://ruby:2.6" needs = ["install"] runs = "bundle exec" args = "rspec" }
2つのアクションでワークスペースは共有されており、 bundle exec rspec
の前に bundle install
を行うように定義することで実行することができます。
この方法で問題なさそうですが、アクションをいくつも定義することになり、ワークフローが複雑になるかもしれません。
案3: タスクランナーを action ディレクトリーに配置する
|-- hello-world (repository) | |__ .github | |__ main.workflow | |__ action | │__ Dockerfile | │__ Makefile |-- Gemfile
公式などでは entrypoint.sh
で紹介されていますが、ここではタスクランナーとして Makefile
をおいています。
Makefile
では以下のように2つのコマンドをまとめて定義しておきます。
test: bundle install --path vendor/bundle bundle exec rspec
ここで、 Dockerfile は以下のようになります。
FROM ruby:2.6 ADD Makefile /Makefile ENTRYPOINT ["make", "-f", "/Makefile"] CMD ["test"]
Makefile を ADD し、 コマンドでパスを指定しています。
アクション実行時に実際に作業するパスは /github/workspace
ですが、
/github/workspace
にファイルをADDしても実行時にはファイルが見当たりません(おそらくボリュームマウントしている?)。
よって、 /github/workspace
ではない任意のディレクトリにADDしたあとにコマンドでパスを指定する必要があります。
この方法の場合、ワークフローで何をしているか知りたい場合に action ディレクトリ以下の Makefile
の中を見る必要があります。
案4: タスクランナーをリポジトリルートに配置する
先程紹介した test タスク は、 CI だけでなく開発時にもよく使います。
タスクランナー( Makefile )をリポジトリのルートに配置しましょう。
この場合も DockerHub のイメージで十分です。
|-- hello-world (repository) | |__ .github | |__ main.workflow |-- Gemfile |-- Makefile
ワークフローは以下のようになります。
workflow "workflow" { on = "pull_request" resolves = [ "test", ] } action "test" { uses = "docker://ruby:2.6" runs = "make" args = "test" }
かなりスッキリしました。
こうしておくと、別のCIサービスを利用している場合も make test
だけでテストが実行できるようになります。
複数のタスクを実行する場合
bundle exec rspec
および bundle exec danger
など複数の bundle exec
を利用したい場面を想定します。
アクションをバラバラに定義して都度 bundle install
をするのは非効率的です。
この場合 ワークフローを以下のように定義することで、 install を一回にまとめることができます。
workflow "workflow" { on = "pull_request" resolves = [ "test", "danger", ] } action "install" { uses = "docker://ruby:2.6" runs = "bundle" args = ["install", "--path", "vendor/bundle"] } action "test" { uses = "docker://ruby:2.6" needs = ["install"] runs = "bundle exec" args = "rspec" } action "danger" { uses = "docker://ruby:2.6" needs = ["install"] secrets = ["GITHUB_TOKEN"] runs = "bundle exec" args = "danger" }
上記は 案2 の例です。
bundle install
ではキャッシュが効くので、案4 の場合でも非効率的にはならないでしょう。
Makefile
に install
タスクを追加して、test と danger のそれぞれのアクションを依存させます。
install: bundle install --path vendor/bundle test: bundle install --path vendor/bundle bundle exec rspec danger: bundle install --path vendor/bundle bundle exec danger
ワークフローは以下のようになります。
workflow "workflow" { on = "pull_request" resolves = [ "test", "danger", ] } action "install" { uses = "docker://ruby:2.6" runs = "make" args = "install" } action "test" { uses = "docker://ruby:2.6" needs = ["install"] runs = "make" args = "test" } action "danger" { uses = "docker://ruby:2.6" needs = ["install"] secrets = ["GITHUB_TOKEN"] runs = "make" args = "danger" }
この方法であれば、ローカル開発時は make test
のみで依存のインストール(アップデート)から行ってくれ、CIでもキャッシュが効きます。便利ですね。
じゃあいつ Dockerfile を使うの?
今回は GitHub Actions で利用する Dockerfile を考えているうちに、Dockerfile を使わなくなりました。
Dockerfile は必要なコマンドなどを追加でインストールする場合に利用します。
例えば以下のような場合です。
FROM ruby:2.6 ADD apt-get install git
git コマンドが必要なため、ベースイメージに追加しています。
まとめ
案2 または 案4 が良さそうだと思っています。
推しは 案4 です。Dockerfileには実行に必要な依存のみを記述( DockerHubにイメージがあるならイメージを指定 )し、タスクはリポジトリルートのタスクランナーに記述すると便利です。