GitHub ActionsでAmazon ECR でのリモートキャッシュを利用してみた

はじめに

こんにちは。インフラ担当の平田です。

とある案件でGitHub Actionsを利用しているのですが、PHPのDockerイメージをビルドする際に時間がかかってしまうという課題がありました。レイヤーキャッシュが利用されていないことが要因だったため、Amazon ECRでのリモートキャッシュを利用して、ビルド時間を短縮する方法に取り組みました。 課題もありますが、紹介させていただきます。

構成

GitHub Actionsの構成

GitHub Actionsの流れは次のとおりです。

Github Actionsで実行すること

  1. ソースコードを対象ブランチにpushまたはマージする
  2. PHP及びNGINXのDockerfileからコンテナイメージをビルドする
  3. テスト及び脆弱性チェックをする
  4. ビルドしたイメージをAWSのECRにプッシュする
  5. ECSタスク定義を更新する
  6. ECS Serviceのタスク定義を最新バージョンに変更する
  7. 最新イメージを使用してコンテナをデプロイする

今回の記事は項番2の内容となります。

GitHub Actionsワークフロー

ビルド時のワークフローは下記です。 今回はこの記事に関連したPHPのイメージビルド部分のみ抜粋しています。

- name: Build PHP image
  id: build-image-php
  run: |
    docker build 
       --platform=linux/arm64 \
       -t php-image:latest \
       -f ./docker/php-fpm/Dockerfile . \
       --load

Dockerfile

PHPのイメージビルド時に利用するDockerfileを一部抜粋します。

FROM php:8.3.4-fpm AS base

RUN apt-get update && \
    apt-get install -y \
      libzip-dev \
      libicu-dev \
      libonig-dev \
      libpq-dev && \
    docker-php-ext-install \
      intl \
      pdo_pgsql \
      zip \
      bcmath && \
    pecl install redis && \
    docker-php-ext-enable redis
・・・続く

課題

GitHub ActionsでPHPのDockerfileからコンテナイメージをビルドする部分でビルドに25分もかかってしまっていました。

調べてみると、 docker-php-ext-installの部分だけで20分かかっていることがわかりました。 特にintlで14分。。。

FROM php:8.3.4-fpm AS base

RUN apt-get update && \
    apt-get install -y \
      ・・・
    docker-php-ext-install \
      intl \
      pdo_pgsql \
      zip \
      bcmath && \
    ・・・

時間短縮のためレイヤーキャッシュを利用できる方法を調べました。

BuildkitによるECRレイヤーキャッシュ

GitHub Actionsを実行する上でレイヤーキャッシュを持たせる方法はいくつかありましたが、AWSブログで紹介されている下記記事の方法を実践しました。

aws.amazon.com

BuildKit が Amazon ECR のように OCI 仕様を実装するレジストリでビルドキャッシュを保存および取得できることを意味します。このアップデートにより、ビルドおよびプッシュされたイメージとは別に、キャッシュイメージを Amazon ECR リポジトリにプッシュできるようになります。このキャッシュイメージは、後のビルドで参照でき、ノート PC からのプッシュか、 GitLab や GitHub Actions などのプラットフォーム上の本番 CI/CD ビルドからのプッシュかに関わらず、プッシュ時間を大幅に短縮することができます。

やってみた

まず、キャッシュ用のリポジトリを別に作成したかったので、ECRレジストリ内にキャッシュ用の新規リポジトリを作成しました。

GitHub ActionsでBuildKitを有効にするためには、docker/setup-buildx-actionを組み込む必要があります。

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2
  with:
    install: true

Dockerはバージョン20.10.7以降はデフォルトでBuldkitが有効になっているため、docker buldx buildコマンドではなく、docker buildコマンドでキャッシュ可能です。

Amazon ECR にキャッシュを入力し、その後のビルドで使用するようにするには、ビルドコマンドに –cache-to および –cache-from オプションを追加します。

--cache-fromでどこからキャッシュを引っ張ってくるのかを指定します。今回は新規で作成したキャッシュ用リポジトリを指定しています。タグはbuildcache-phpです。 もしキャッシュが存在しない場合やレイヤーに変更がある場合は、一からイメージが作成されます。

--cache-toでどこにキャッシュを保存するのかを指定しています。こちらももちろん同様に新規で作成したキャッシュ用リポジトリを指定します。

mode=min, image-manifest=true, oci-mediatypes=true を使用することで、Dockerのビルドキャッシュを最適化しています。mode=min はキャッシュの保存を最小限に抑え、重複したキャッシュ生成を防ぎます。image-manifest=true と oci-mediatypes=true により、キャッシュがマルチプラットフォーム対応のOCI標準形式で保存され、互換性が向上します。これにより、キャッシュがいくつも生成される問題を解決しています。

- name: Build PHP image
  id: build-image-php
  run: |
    docker build
        --cache-from=${{ steps.login-ecr.outputs.registry }}/${{ secrets.DEV_ECR_REPOSITORY_CACHE }}:buildcache-php
        --cache-to=type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ secrets.DEV_ECR_REPOSITORY_CACHE }}:buildcache-php,mode=min,image-manifest=true,oci-mediatypes=true \
        --platform=linux/arm64 \
        -t php-image:latest \
        -f ./docker/php-fpm/Dockerfile . \
        --load

1回目のビルドではレイヤーキャッシュがキャッシュ用のリポジトリに存在しないため、新規でイメージが作成されます。1回目のビルド後にキャッシュ用リポジトリを確認するとキャッシュが保存されていることが確認できます。

ECRのキャッシュ

2回目以降のビルドからキャッシュ用リポジトリにあるレイヤーキャッシュを利用します。 実行結果を確認すると、キャッシュを利用することができているようです。

#7 importing cache manifest from ***.dkr.ecr.***.amazonaws.com/***:buildcache-php
#7 inferred cache manifest type: ***lication/vnd.oci.image.manifest.v1+json done
#7 DONE 1.9s

#12 [base 2/2] RUN apt-get update && apt-get install -y  libzip-dev libicu-dev libonig-dev ibpq-dev && docker-php-ext-install intl pdo_pgsql zip bcmath && pecl install redis && docker-php-ext-enable redis
#12 CACHED

レイヤーキャッシュを利用することで当初25分かかっていたビルドも10分ほどで完了できるようになりました。

キャッシュを利用することの課題

ビルド時間は短縮されたのですが、課題もあります。

Dockerfileに変更がない限り古いキャッシュを利用する問題

ECRに保存されたキャッシュレイヤーは、新しいビルドで古いキャッシュが使用され続ける可能性があります。 Dockerfileに変更が加えられた場合は、一からビルドが行われますが、基本的にはレイヤーキャッシュが使用されることになるため、使用されている古いパッケージがいずれ脆弱性のあるバージョンになってしまう可能性があることに注意が必要です。

料金の問題

ECRにキャッシュを保存すると、ECRのストレージ使用量に応じてコストが発生します。 特に、多くのレイヤーをキャッシュとして保存し続ける場合、ストレージコストが増大する可能性があります。 イメージのサイズを小さくしたり、ライフサイクルポリシーを設定したりすることを検討した方が良いかと思います。 また、キャッシュを利用する際のデータ転送量も課金対象のため、ビルドの頻度によっては注意が必要です。

おわりに

また、下記のように開発者がcacheを利用するかどうか選択できるフラグを設定し、trueの場合のみECRのキャッシュを利用するように記述するのも良いかと思います。

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    env:
      USE_CACHE: true # キャッシュを使用するかのフラグ
- name: Build PHP image
  id: build-image-php
  run: |
     if [ "${{ env.USE_CACHE }}" = "true" ]; then
       CACHE_FROM="--cache-from=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_CACHE }}:buildcache-php"
     else
       CACHE_FROM=""
     fi
     docker build \
         $CACHE_FROM \
         --cache-to=type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_CACHE }}:buildcache-php,mode=min,image-manifest=true,oci-mediatypes=true \
         --platform=linux/arm64 \
         -t php-image:latest \
         -f ./docker/php-fpm/Dockerfile . \
         --load

USE_CACHEのフラグを設定することで、 trueの場合はキャッシュを利用したビルドができ、falseの場合は一からイメージをビルドしてくれます。 本来であれば毎回新しく一からビルドしていくことが望ましいですが、連続したデプロイ時などは利用できそうです。