GitHub Actions上でDockerイメージをビルドしてECRにPushするサンプル(キャッシュ付き)

2021.12.20

広島の吉川です。

GitHub Actionsエコシステムにはdocker/build-push-actionというDockerイメージのビルド+プッシュ+キャッシュをよしなに行ってくれる便利なアクションがあります。

これを使ってECRにプッシュしたい、というのは結構多いシチュエーションだと思うのですが、意外にまだ情報が少ない気がしたため調査・検証してみました。

workflowの実行時間短縮のためキャッシュ設定も入れていきます。

TLDR

ECRへのプッシュ+actions/cacheでビルドをキャッシュするサンプルコードです。

# .github/workflows/build.yml

on: push

permissions:
  id-token: write
  contents: read

env:
  AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxxxxx:role/my-aws-role # GitHub Actions OIDC用IAMロール
  ECR_REGISTRY: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com # ECRレジストリURL
  ECR_REPOSITORY: my-ecr-repository # ECRリポジトリ名

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: docker/setup-buildx-action@v1

      - uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - uses: docker/login-action@v1
        with:
          registry: ${{ env.ECR_REGISTRY }}

      - uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

actinos/cacheを使わず、docker/build-push-actionのtype=ghaを使ったキャッシュの場合は以下になり、より記述はすっきりします。ただ、type=ghaの指定はExperimentalステータスである点に注意する必要があります。

# .github/workflows/build.yml

on: push

permissions:
  id-token: write
  contents: read

env:
  AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxxxxx:role/my-aws-role # GitHub Actions OIDC用IAMロール
  ECR_REGISTRY: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com # ECRレジストリURL
  ECR_REPOSITORY: my-ecr-repository # ECRリポジトリ名

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: docker/setup-buildx-action@v1

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - uses: docker/login-action@v1
        with:
          registry: ${{ env.ECR_REGISTRY }}

      - uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

事前準備

事前に

  • ECRの作成
  • GitHub Actions用のIAMロール作成

が必要です。本記事では割愛しています。

GitHub Actions用のIAMロール作成については下記をご覧ください。

GitHub Actions OIDCでconfigure-aws-credentialsでAssumeRoleする | DevelopersIO

docker/build-push-actionを使ってイメージをビルド+ECRにプッシュする

docker/build-push-actionを使ってビルドとプッシュを行います。

ECRのログインは

の2つのアクションが選択肢に上がりますが、今回はdocker/login-actionを使います。おそらくaws-actions/amazon-ecr-loginでもやりたいことは実現できるのですが、docker/build-push-actionと組み合わせるという観点でdocker/login-actionの方が情報やサンプルコードが多いと感じたためです。

特に、

をそれぞれ参考にできるのが大きく、これら一次情報のサンプルコードを合体するだけで「aws-actions/configure-aws-credentials+docker/login-actionでECRにログインし、docker/build-push-actionでビルドとプッシュを行う」が実現できました。

それが以下のworkflowファイルになります。

# .github/workflows/build.yml

on: push

permissions:
  id-token: write
  contents: read

env:
  AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxxxxx:role/my-aws-role # GitHub Actions OIDC用IAMロール
  ECR_REGISTRY: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com # ECRレジストリURL
  ECR_REPOSITORY: my-ecr-repository # ECRリポジトリ名

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: docker/setup-buildx-action@v1

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - uses: docker/login-action@v1
        with:
          registry: ${{ env.ECR_REGISTRY }}

      - uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}

- uses: docker/setup-buildx-action@v1 についてですが、Buildxというプラグインを導入することで既存のdockerコマンドの機能を拡張できるようです。

Docker Buildx | Docker Documentation

Docker Buildx is a CLI plugin that extends the docker command with the full support of the features provided by Moby BuildKit builder toolkit. It provides the same user experience as docker build with many new features like creating scoped builder instances and building against multiple nodes concurrently.

そして、docker/build-push-actionの説明に

GitHub Action to build and push Docker images with Buildx(強調は引用者による)

とあり、Buildxの使用が前提になっているように見えます。READMEのサンプルコードでもBuildx導入の記述があるため倣っています。

キャッシュの仕組みを加える

docker/build-push-actionでキャッシュする方法についてはリポジトリのcache.mdをチェックすると一通り分かるようになっています。

一次情報でここまでまとめてくれているのは助かりますね。

今回はこの中から3つの方法を試しました。

方法1 actions/cacheを使う

actions/cacheを使ってキャッシュするのはGitHubActionsでは王道な方法だと思います。シンプルで汎用性が高いActionなので、一度使い方を押さえればDocker以外のキャッシュにも横展開的に適用できるのが嬉しいです。

後述のtype=ghaがExperimentalステータスという点も鑑みると、現状最も安牌な方法といえそうです。

# .github/workflows/build.yml(一部抜粋)

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: docker/setup-buildx-action@v1

+     - uses: actions/cache@v2
+       with:
+         path: /tmp/.buildx-cache
+         key: ${{ runner.os }}-buildx-${{ github.sha }}
+         restore-keys: |
+           ${{ runner.os }}-buildx-

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - uses: docker/login-action@v1
        with:
          registry: ${{ env.ECR_REGISTRY }}

      - uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
+         cache-from: type=local,src=/tmp/.buildx-cache
+         cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

+     - name: Move cache
+       run: |
+         rm -rf /tmp/.buildx-cache
+         mv /tmp/.buildx-cache-new /tmp/.buildx-cache

name: Move cache については、

⚠️ At the moment caches are copied over the existing cache so it keeps growing. The Move cache step is used as a temporary fix (see https://github.com/moby/buildkit/issues/1896).

とあるように、現状、キャッシュがどんどん増えてしまう問題があるため、このように都度クリアする処理が必要なようです。将来的にはこの処理を書かなくても済むように解決されると思われます。

方法2 type=ghaを使う

actinos/cacheを使わず、docker/build-push-actioのcache-from・cache-toにtype=ghaを指定する方法は2行追加するだけで実現できます。驚くほど簡潔に済むので一番採用したいですが、まだExperimentalである点に注意する必要があります。

build-push-action/cache.md at master · docker/build-push-action

🧪 This cache exporter is considered EXPERIMENTAL until further notice. Please provide feedback on BuildKit repository if you encounter any issues.

# .github/workflows/build.yml(一部抜粋)

      - uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
+         cache-from: type=gha
+         cache-to: type=gha,mode=max

方法3 type=registry+ECRを使う(失敗)

最後はRegistry Cacheを使う方法です。今回はイメージをECRにプッシュするのでキャッシュもECRで行いたいです。

ということで以下の記述を行いました。

# .github/workflows/build.yml(一部抜粋)

      - uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
+         cache-from: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:buildcache
+         cache-to: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:buildcache,mode=max

ところがGitHub Actionsを走らせるとエラーが発生しました。

Error: buildx failed with: error: failed to solve: error writing manifest blob: failed commit on ref "sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": unexpected status: 400 Bad Request

調べてみたところ、どうもBuildxのmanifestと呼ばれるファイル形式にAmazon ECR側が未対応のようでした。

これらのIssueで報告されており、機能リクエストも行われているようですが、まだ対応はされていないようです。

実際に試せてはいないですが、DockerHubやGitHub Container Registryであれば使えそうです。

キャッシュの効果を計測してみた

試しに以下のようなシンプルなPHPプロジェクトを作って実験してみました。

Dockerfile

ucan-lab/docker-laravelを参考にしつつ下記のDockerfileを作成しました。

FROM php:7.3

RUN apt-get update && \
    apt-get -y install git libicu-dev libonig-dev libzip-dev unzip locales && \
    apt-get clean
RUN docker-php-ext-install intl pdo_mysql zip bcmath

WORKDIR /app

COPY index.php index.php

今回は特に使いませんが、PHP拡張をインストールしています。キャッシュの効果を見れるようにビルド時間をやや長くしたいためです。

本当はCMDやENTRYPOINTも設定しないといけないですが、実際にサーバ起動するまではやらないので省略しています。

index.php

<?php

echo 'hello';

検証手順

まず、コードをgit pushして一度GitHub Actionsを走らせます。これによりキャッシュが作成されます。

その状態でアプリケーションコードのindex.phpに変更を入れてcommit+pushします。

<?php

- echo 'hello';
+ echo 'world';

変更したのはindex.phpのみなので、それより前のプロセスはキャッシュが使われます。

FROM php:7.3

RUN apt-get update && \
    apt-get -y install git libicu-dev libonig-dev libzip-dev unzip locales && \
    apt-get clean
RUN docker-php-ext-install intl pdo_mysql zip bcmath

WORKDIR /app

# ↑↑↑ここより上はキャッシュが使われる(再ビルドしない)↑↑↑

COPY index.php index.php

結果

条件 実行時間
キャッシュなし 1m55s
actions/cacheを使ったキャッシュ 1m13s
type=ghaを使ったキャッシュ 49s

狙い通り、キャッシュにより高速化がなされているようです。その中でも「type=ghaを使ったキャッシュ」が「actions/cacheを使ったキャッシュ」より高速なのが興味深いです。なぜ差が出ているのかの詳細までは追いきれていませんが、ますますtype=ghaが使いたくなり、Stableになるのが待たれます。

参考