This is my life.

There are many like it, but this one is mine.

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 installbundle 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 の場合でも非効率的にはならないでしょう。
Makefileinstall タスクを追加して、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でもキャッシュが効きます。便利ですね。

f:id:duck8823:20181231052737p:plain

じゃあいつ Dockerfile を使うの?

今回は GitHub Actions で利用する Dockerfile を考えているうちに、Dockerfile を使わなくなりました。 Dockerfile は必要なコマンドなどを追加でインストールする場合に利用します。
例えば以下のような場合です。

FROM ruby:2.6

ADD apt-get install git

git コマンドが必要なため、ベースイメージに追加しています。

まとめ

案2 または 案4 が良さそうだと思っています。
推しは 案4 です。Dockerfileには実行に必要な依存のみを記述( DockerHubにイメージがあるならイメージを指定 )し、タスクはリポジトリルートのタスクランナーに記述すると便利です。