Docker Hub から GitHub Packages へ
Git tag やブランチをトリガーに Docker image を自動でビルドして Docker registry で公開・配布したいとなると、まず Docker Hub の利用が候補として考えられます。
あれだけの機能を無料で提供してくれている Docker Hub 開発チームには本当に感謝しているのですが、一点だけ不満があります。実行時間です。Docker Hub はビルドの開始が遅く、実行時間も結構かかりがちです。
これの解決策として外部サービスで Docker image をビルドして、そこから Registry に image を Push することが考えられます。外部サービスには色々な選択肢がありますが、今回は GitHub Actions を選びました。ビルド行程を GitHub Actions で行えば Docker Hub よりは速そう(速かった)です。おそらく CircleCI とかであればもっと速いでしょうが、ビルド行程の管理のしやすさも合わせて総合的に考えると GitHub Actions が便利だと思います。
この記事では GitHub Actions と GitHub Packages で Docker image をビルド・配布する方法について紹介します。(ついでに GitHub Actions から Docker Hub にも Push します。)
ちなみに GitHub Packages は旧名 GitHub Package Registry です。
GitHub Packages for Containers から GitHub Container Registry へ
最近になって GitHub Packages for Containers の後継となる GitHub Container Registry というサービスが登場しました。
GitHub Packages では docker pull
の実行前に docker login
が必要ですが GitHub Container Registry では Docker Hub と同様に docker pull
に関しては docker login
は必須ではありません。image の所属先も GitHub Packages for Containers と GitHub Container Registry では異なります。
比較項目 | GitHub Packages for Containers | GitHub Container Registry |
---|---|---|
docker pull |
docker login 必須 |
docker login 不要 |
URL | docker.pkg.github.com/owner/repository/image |
ghcr.io/owner/image |
GITHUB_TOKEN |
利用可能 | 利用不可 |
現時点では GitHub Packages for Containers では docker login
に secrets.GITHUB_TOKEN
を利用できるが GitHub Container Registry (GHCR) ではそうではないため Personal Access Token を発行して利用しなければならない点にも注意してください。これに関しては将来的に GHCR でも secrets.GITHUB_TOKEN
を login に利用できるよう改善されると嬉しいですね。
この記事で GitHub Actions workflow を紹介しているリポジトリ peaceiris/docker-mdbook では GitHub Container Registry に移行しました。この記事には古い workflow YAML file を残しておくので、最新の workflow を参考にしたい場合はリポジトリの方をみてください。
mdBook Docker image
rust-lang/mdBook の alpine base Docker image を以下のリポジトリで管理しており GitHub Actions で image をビルドして Docker Hub と GitHub Packages へ push しています。
以下のようなリリースフローとなっています。
- master branch へ push すると latest tag のイメージを公開
- Release を作成すると release tag のイメージを公開
- Pull Request ではビルドはするが push はしない
alpine:3.10
base のイメージと rust:alpine3.10
ベースのイメージ、この2種類を並列にビルドして配布しています。
リポジトリの概要
ファイル一覧
リポジトリのファイル一覧です。(記事に必要なものだけを抜粋)
.github/
└── workflows
└── push.yml
.hadolint.yaml
Dockerfile
Dockerfile
の中身です。
ARG BASE_IMAGE
FROM rust:slim-buster AS builder
ARG MDBOOK_VERSION
ENV MDBOOK_VERSION ${MDBOOK_VERSION:-0.3.5}
ENV ARC="x86_64-unknown-linux-musl"
RUN apt-get update && \
apt-get install --no-install-recommends -y \
musl-tools && \
rustup target add "${ARC}" && \
cargo install mdbook --version "${MDBOOK_VERSION}" --target "${ARC}"
FROM ${BASE_IMAGE}
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
COPY --from=builder /usr/local/cargo/bin/mdbook /usr/bin/mdbook
WORKDIR /book
ENTRYPOINT [ "/usr/bin/mdbook" ]
GitHub Actions workflow です。
name: Push Docker Image
on:
push:
branches:
- master
paths-ignore:
- '**.md'
release:
types: [published]
pull_request:
types: [opened, synchronize]
paths-ignore:
- '**.md'
env:
DOCKER_BASE_NAME: docker.pkg.github.com/${{ github.repository }}/mdbook
DOCKER_HUB_BASE_NAME: peaceiris/mdbook
jobs:
hadolint:
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- run: brew install hadolint
- run: hadolint ./Dockerfile
push:
runs-on: ubuntu-18.04
needs: hadolint
strategy:
matrix:
baseimage: ['alpine:3.10', 'rust:alpine3.10']
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Set env
run: |
if [ "${{ github.event_name }}" = 'release' ]; then
export TAG_NAME="${{ github.event.release.tag_name }}"
else
export TAG_NAME="latest"
fi
if [ "${{ startsWith( matrix.baseimage, 'rust:alpine') }}" = "true" ]; then
export TAG_NAME="${TAG_NAME}-rust"
fi
echo "PKG_TAG=${DOCKER_BASE_NAME}:${TAG_NAME}" >> ${GITHUB_ENV}
echo "HUB_TAG=${DOCKER_HUB_BASE_NAME}:${TAG_NAME}" >> ${GITHUB_ENV}
- name: Build ${{ matrix.baseimage }} base image
run: |
docker build . -t "${PKG_TAG}" --build-arg BASE_IMAGE="${{ matrix.baseimage }}"
docker tag "${PKG_TAG}" "${HUB_TAG}"
- name: Print mdBook version
run: |
docker run --rm ${PKG_TAG} --version
- name: Scan image
run: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v ${HOME}/.cache:/root/.cache aquasec/trivy:latest --exit-code 1 ${PKG_TAG}
- name: Login to Registries
if: github.event_name != 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
run: |
echo "${GITHUB_TOKEN}" | docker login docker.pkg.github.com -u peaceiris --password-stdin
echo "${DOCKER_HUB_TOKEN}" | docker login -u peaceiris --password-stdin
- name: Push to GitHub Packages
if: github.event_name != 'pull_request'
run: docker push "${PKG_TAG}"
- name: Push to Docker Hub
if: github.event_name != 'pull_request'
run: docker push "${HUB_TAG}"
GitHub Packages
# Pull alpine:3.10 base image
docker pull docker.pkg.github.com/peaceiris/docker-mdbook/mdbook:latest
docker pull docker.pkg.github.com/peaceiris/docker-mdbook/mdbook:v0.3.5
# Pull rust:alpine3.10 base image
docker pull docker.pkg.github.com/peaceiris/docker-mdbook/mdbook:latest-rust
docker pull docker.pkg.github.com/peaceiris/docker-mdbook/mdbook:v0.3.5-rust
Docker Hub
Autobuild 機能を OFF にしています。リポジトリをリンクしているだけです。
# Pull alpine:3.10 base image
docker pull peaceiris/mdbook:latest
docker pull peaceiris/mdbook:v0.3.5
# Pull rust:alpine3.10 base image
docker pull peaceiris/mdbook:latest-rust
docker pull peaceiris/mdbook:v0.3.5-rust
GitHub Actions Workflow の解説
トリガー
- master branch から
latest
tag - Release event から
vx.x.x
tag - Pull Request はビルドテスト用 (Push しない)
master へ push すると latest
tag のイメージが公開されます。v0.3.5
タグでリリースを作成すると v0.3.5
tag のイメージが公開されます。Pull Request ではビルドだけを行います。
on:
push:
branches:
- master
paths-ignore:
- '**.md'
release:
types: [published]
pull_request:
types: [opened, synchronize]
paths-ignore:
- '**.md'
workflow 全体で使う環境変数
Workflow 全体で使う環境変数を定義しており、各 Job で共有できます。
env:
DOCKER_BASE_NAME: docker.pkg.github.com/${{ github.repository }}/mdbook
DOCKER_HUB_BASE_NAME: peaceiris/mdbook
hadolint job
hadolint:
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- run: brew install hadolint
- run: hadolint ./Dockerfile
hadolint で Dockerfile
をチェックしています。hadolint の Docker image も公開されていますが .hadolint.yaml
を上手く読み込むことができなかったので macOS 仮想環境に homebrew でインストールした hadolint を使っています。
Push job
push:
runs-on: ubuntu-18.04
needs: hadolint
strategy:
matrix:
baseimage: ['alpine:3.10', 'rust:alpine3.10']
steps:
hadolint job が正常終了した場合に push job を並列に実行しています。
mdBook は mdbook test
というサブコマンドで Book 中に記述された Rust コードの実行テストができます。そのテストには Rust の実行環境が必要なので alpine:3.10
base のイメージだけでなく rust:alpine3.10
base のイメージもビルド・公開するようにしています。それぞれのイメージを順番にビルドするとビルド時間が2倍になってしまうので matrix で記述することにより並列実行しています。
- name: Set env
run: |
if [ "${{ github.event_name }}" = 'release' ]; then
export TAG_NAME="${{ github.event.release.tag_name }}"
else
export TAG_NAME="latest"
fi
if [ "${{ startsWith( matrix.baseimage, 'rust:alpine') }}" = "true" ]; then
export TAG_NAME="${TAG_NAME}-rust"
fi
echo "PKG_TAG=${DOCKER_BASE_NAME}:${TAG_NAME}" >> ${GITHUB_ENV}
echo "HUB_TAG=${DOCKER_HUB_BASE_NAME}:${TAG_NAME}" >> ${GITHUB_ENV}
イメージのタグを設定しています。 今日現在 (2020-10-23) set-env
で定義した環境変数は以降の step で利用できるようになります。set-env
を workflow で利用しているとログに warning が表示されます。将来の GitHub Actions runner において set-env
および add-path
の廃止が決定しているのでそれぞれ GITHUB_ENV
, GITHUB_PATH
を使うように workflow を更新する必要があります。
- echo "::set-env name=PKG_TAG::${DOCKER_BASE_NAME}:${TAG_NAME}"
+ echo "PKG_TAG=${DOCKER_BASE_NAME}:${TAG_NAME}" >> ${GITHUB_ENV}
cf. GitHub Actions: Deprecating set-env and add-path commands - GitHub Changelog
- name: Build ${{ matrix.baseimage }} base image
run: |
docker build . -t "${PKG_TAG}" --build-arg BASE_IMAGE="${{ matrix.baseimage }}"
docker tag "${PKG_TAG}" "${HUB_TAG}"
イメージのビルド step です。 matrix.baseimage
からベースイメージを取得しています。
- name: Print mdBook version
run: |
docker run --rm ${PKG_TAG} --version
なにか実行テストが可能であれば push する前にしておくと良いでしょう。ここでは単純に mdbook のバージョンを出力しています。
- name: Scan image
run: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v ${HOME}/.cache:/root/.cache aquasec/trivy:latest --exit-code 1 ${PKG_TAG}
aquasecurity/trivy で Docker image の脆弱性を検査しています。終了コードを設定すれば検出時に Job を停止させることができます。
- name: Login to Registries
if: github.event_name != 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
run: |
echo "${GITHUB_TOKEN}" | docker login docker.pkg.github.com -u peaceiris --password-stdin
echo "${DOCKER_HUB_TOKEN}" | docker login -u peaceiris --password-stdin
GitHub Packages と Docker Hub へログインします。GitHub Actions beta v2 のリリース当初は Packages への Push に Personal access token が必要でしたが、コミュニティの意見が反映され、今では自動生成される GITHUB_TOKEN
で Packages への Push ができるようになりました。
DOCKER_HUB_TOKEN
は Docker Hub で生成した Personal access token です。Secret 変数として事前に設定する必要があります。
Pull Request の時はスキップしたいので if: github.event_name != 'pull_request'
としています。
- name: Push to GitHub Packages
if: github.event_name != 'pull_request'
run: docker push "${PKG_TAG}"
- name: Push to Docker Hub
if: github.event_name != 'pull_request'
run: docker push "${HUB_TAG}"
最後に GitHub Packages と Docker Hub へイメージを Push しています。ひとつの step にまとめてもいいと思いますが、それぞれの実行時間を計測するためにあえて二つの step に分けています。(どちらも同じ実行時間でした。)
Pull Request の時はスキップしたいので if: github.event_name != 'pull_request'
としています。
定期的にイメージをアップデートする
alpine:3.10.x
の更新を反映するために定期的にイメージをアップデートする Workflow を別に作りました。合計12個の Job が並列に実行されて Job 1個の時間で完了します。
今は単純に version を列挙していますが何か上手いやり方があるかもしれません。
name: Update Docker Image
on:
schedule:
- cron: '11 11 */31 * *'
env:
DOCKER_BASE_NAME: docker.pkg.github.com/${{ github.repository }}/mdbook
DOCKER_HUB_BASE_NAME: peaceiris/mdbook
jobs:
update:
runs-on: ubuntu-18.04
strategy:
matrix:
baseimage: ['alpine:3.10', 'rust:alpine3.10']
version: ['0.3.0', '0.3.1', '0.3.2', '0.3.3', '0.3.4', '0.3.5']
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
ref: "v${{ matrix.version }}"
- name: Set env
run: |
export TAG_NAME="v${{ matrix.version }}"
if [ "${{ startsWith( matrix.baseimage, 'rust:alpine') }}" = "true" ]; then
export TAG_NAME="${TAG_NAME}-rust"
fi
echo "PKG_TAG=${DOCKER_BASE_NAME}:${TAG_NAME}" >> ${GITHUB_ENV}
echo "HUB_TAG=${DOCKER_HUB_BASE_NAME}:${TAG_NAME}" >> ${GITHUB_ENV}
- name: Build ${{ matrix.baseimage }} base image
run: |
docker build . -t "${PKG_TAG}" \
--build-arg BASE_IMAGE="${{ matrix.baseimage }}"
docker tag "${PKG_TAG}" "${HUB_TAG}"
- run: docker run --rm ${PKG_TAG} --version
- run: docker images
- name: Login to Registries
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
run: |
echo "${GITHUB_TOKEN}" | docker login docker.pkg.github.com -u peaceiris --password-stdin
echo "${DOCKER_HUB_TOKEN}" | docker login -u peaceiris --password-stdin
- name: Push to GitHub Packages
run: docker push "${PKG_TAG}"
- name: Push to Docker Hub
run: docker push "${HUB_TAG}"
まとめ
GitHub Packages と GitHub Actions の組み合わせは Docker Hub 以上に高速で柔軟性がありました。ビルド行程だけ GitHub Actions に任せてもいいですし、もう Docker Hub を使わずに GitHub Packages と GitHub Actions だけで完結させても問題ないでしょう。
以上です。ありがとうございました。