🫠

GitHub Actions で Artifacts を使ってdockerのbuildとpushのステップを分離させたら色々とハマった

2025/02/10に公開

こんにちは、ツクリンクでSREエンジニアをやってるida.です。

弊社ではプレビュー環境と呼んでる一時的な検証環境をGitHub Actionsを使って構築して運用しているのですが、インフラ構築の裏でDockerイメージのbuildをしたら早くなるんじゃね?と思い対応しました。
その過程で少しハマったので同じような人がいたら参考になればと思い記事を書いてます。

やろうとしたこと

元々は以下のようなフローで構築しておりました。

で、今回はdeploy_infrastructureの裏でbuildをしたら早くなりそうだったので以下のように変更することにしました。


今回の変更を実装するにあたり検討した記録はこちらにあるので興味があれば見てください🙇
やることとしては大きく以下を対応していきました。

  • buildとpushを別々のジョブに分離する
  • ジョブが分離されるのでArtifactsを用いてジョブ間でデータを連携する
  • 上記2つに伴いdocker関連のエクスポート処理やインポート処理を実装

Artifactsは以下を参照ください。
https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/storing-and-sharing-data-from-a-workflow

最終的なリソース

最終的なリソースだけ見たいという方もいると思うので、先にリソースを貼っておきます。
後述の問題や細かな対応は全て対応しているものになります。
※※※関連する部分だけに限定してます※※※

build_image:
  runs-on: ubuntu-22.04
  defaults:
    run:
      working-directory: ./foo
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker Image
        run: |
          docker build -t test:latest
          docker save -o build-image.tar test:latest
      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-image.tar
          path: ./foo/build-image.tar
          retention-days: 1

push_image:
  needs: [build_container]
  runs-on: ubuntu-22.04
  defaults:
    run:
      working-directory: ./deploy/preview
  permissions:
    id-token: write
    contents: read
    actions: write
  steps:
    - uses: actions/checkout@v4
    - name: Download Artifacts
      uses: actions/download-artifact@v4
      with:
        name: build-image.tar
        path: ./foo
    - name: Load Docker Image
      run: docker load -i build-image.tar
    - name: push
      env:
        REPO: xxxxxxxxxx
      run: |
        docker tag test:latest $(eval echo $REPO):latest
        docker push $(eval echo $REPO):latest
    - name: Delete Artifacts
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        ARTIFACTS_ID=$(gh api repos/${{ github.repository }}/actions/artifacts --jq '.artifacts[] | select(.name=="pre-build-image.tar") | .id')
        if [ -n "$ARTIFACTS_ID" ]; then
          gh api repos/${{ github.repository }}/actions/artifacts/$ARTIFACTS_ID -X DELETE
        else
          echo "No existing Artifacts found."
        fi

問題 1: Artifacts のアクションは working-directory が無視される

Artifactsを利用する際はdownload-artifactupload-artifactを利用すると思うのですが、これらはなんとworking-directoryを設定していても無視してルートから対象ファイルを探します。
大体どのアクションもworking-directoryを重んじてくれるので思いこみで気づくのが遅くなりました。。
解消方法としてはpathでworking-directlyと同じパス(もしくはファイルの格納先)を指定することでちゃんとファイルを見つけてくれました。

jobs:
  build_image:
    runs-on: ubuntu-22.04
    defaults:
      run:
        working-directory: ./foo

~~~~

      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-image.tar
          path: ./foo/pre-build-image.tar

download-artifactも同じくpathで出力したいパスを指定したら大丈夫です!

- name: Download Artifacts
  uses: actions/download-artifact@v4
  with:
    name: build-image.tar
    path: ./foo

問題 2: Docker を tar 形式で保存して Load するとエラーが発生

これは私の不注意ではあるのですが、docker buildでのoutputをdocker形式もしくはdocker saveで出力したらよかったものをtar形式で出力してました。
ただ、そのtar形式で出力したイメージをdocker load -iで読み込むと以下エラーが発生します。

open /var/lib/docker/tmp/docker-import-xxxxxxxxx/app/json: no such file or directory

いや、これわからなくないですかー???
ファイルがないって言ってますよね。。ファイルはあるのになんで見えてないんだろうとハマってました。そんな絶望の中ネットをうろうろしてたら以下のサイトを見つけて抜け出せました。
かなり助かりました🙇🙇🙇

https://stackoverflow.com/questions/54499389/cant-load-docker-from-saved-tar-image

実際にはtar形式ではなくdocker形式またはsaveで出力しましょうねということですね。

エラーになる書き方
- name: Build
  run: docker build -t test:latest --output type=tar,dest=build-image.tar
成功する書き方
- name: Build
  run: docker build -t test:latest --output type=docker,dest=build-image.tar

または

- name: Build
  run: |
    docker build -t test:latest
    docker save -o build-image.tar test:latest

インポートするときは普通にdocker load -iでインポートできます。

- name: Load Docker Image
  run: docker load -i build-image.tar

その他の細かい対応について

  1. pushするジョブでイメージをダウンロードした後にArtifactのイメージを削除
    残しておいてもそんなに問題なさそうでしたが、ストレージの無料枠が消費されるのがちょっと気になってダウンロードとプッシュが完了したらArtifactから削除するステップを実装しました。
    これでストレージコストはほとんど消費しないようになりました。

    - name: Delete Artifact
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/artifacts --jq '.artifacts[] | select(.name=="build-image.tar") | .id')
        if [ -n "$ARTIFACT_ID" ]; then
          gh api repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID -X DELETE
        else
          echo "No existing artifact found."
        fi
    

    ちなみにこの対応にはactions: writeが必要なので忘れず付与してください。

  2. Artifactへのアップロード時に保存期間を最低の1日に設定
    1.の対応をしてるので不要っちゃ不要なんですが、もし途中でワークフローが止まったりするとデフォルトの90日間ストレージに残ってストレージ容量を消費してしまいます。
    そのため最低の1日を保持期間に設定してArtifactにアップロードします。

    - name: Upload Artifact
      uses: actions/upload-artifact@v4
      with:
        name: build-image.tar
        path: ./foo/build-image.tar
        retention-days: 1
    

さいごに

サクッとできるかなと思ってやってみましたが思ったよりハマったなと思いました。
あと、肝心な起動時間は5~7分短縮できてました🎉

これでエンジニアの生産性も5~7分/回は向上できたのでより早くユーザに価値を提供していけるようになるのかなと思ってます。

Discussion