Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

GitHub Actions と GitHub Packages で Docker image を配布する

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 loginsecrets.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 を参考にしたい場合はリポジトリの方をみてください。

移行時の作業内容: Migrate from docker.pkg.github.com to ghcr.io by peaceiris · Pull Request #12 · peaceiris/docker-mdbook

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

hadolintDockerfile をチェックしています。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}

イメージのタグを設定しています。set-env で定義した環境変数は以降の step で利用できるようになります。 今日現在 (2020-10-23) 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 だけで完結させても問題ないでしょう。

以上です。ありがとうございました。

peaceiris
Hugo ʕ◔ϖ◔ʔ や GitHub Actions について書いています。こっち https://peaceiris.com/ に引っ越しましたので、今後 Qiita への新規投稿はしませんが記事のメンテはします。
https://peaceiris.com/
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away