📦

自前のDev Containerを自動事前ビルドする

2024/09/22に公開

Dev ContainerのイメージをGitHub Actionsで事前ビルド(プレビルド)する環境を構築したので手順をまとめておきます。

成果物はこちらです(自分で使う用なのであくまでも参考としてですが)。

https://github.com/publictheta/devcontainer-image-clang

はじめに

Dev Containerを使うと、ローカルのDockerコンテナーにVS Codeから接続したり、GitHub Codespacesにアクセスすることで、同じコンテナー環境で開発を行うことができます。

しかし、既存のイメージそのままではなく、必要なパッケージのインストールなどのカスタマイズを行っている場合、仕組み上その分Dockerイメージのビルドを行う必要があるので、初回の立ち上げにしばしば時間がかかります。

GitHub Codespaces自体にもプレビルドする機能はありますが、ローカルのDockerでも使いたいというような場合は、それを使うことができません。

Dockerイメージとして単に事前にビルドしておくだけでももちろんいいですが、できれば細かい設定も共有できれば便利ですし、FeaturesなどのDev Containerの機能を使っている場合は、それも含めてビルドしておきたいところです。

そこで今回は、devcontainers/ciを使って.devcontainerをまるごとDockerイメージとしてGitHub Actionsで自動ビルドする環境を構築しました。

これを使うことで、リポジトリに次のような.devcontainer/devcontainer.jsonを追加するだけで、拡張機能などの設定も含めた状態でDev Containerでの開発を始めることができます。

{
    "image": "ghcr.io/example/my-devcontainer-image:latest"
}

もちろんこれにさらに設定を追加してカスタマイズすることも可能です。

.devcontainerの用意

まず、ビルドするDev Containerを定義する.devcontainerディレクトリを用意します。

今回は、ビルド用のリポジトリを別に用意してsrcディレクトリの下に.devcontainerを置きました。

次のようなDockerfileを元に同じファイルシステムのFeaturesを参照するような構成にしましたが、既存のイメージやFeaturesを元にしたものや、Docker Composeを使ったものも可能です。

src/.devcontainer/devcontainer.json:

{
    "build": {
        "dockerfile": "Dockerfile"
    },
    "features": {
        "./features/feature1": {},
        "./features/feature2": {},
        // ...
    },
    "customizations": {
        "vscode": {
            "extensions": [
                // ...
            ]
        }
    },
}

srcに置く必要は必ずしもありませんが、そのままプロジェクトのルートに置くと、そのプロジェクトの開発に使うDev Containerとして認識されてしまうので、別の場所にしています。

リポジトリも、Gitのタグを使うためビルド用のリポジトリを用意しましたが、場合によっては/tools/.devcontainerにビルド用のソースを置いて、/.devcontainerからはそれをビルドしたイメージを参照するという運用も可能かもしれません。

GitHub Actionsの設定

あとはGitHub Actionsの設定を行うだけです。

devcontainers/cliを使ってコマンドを書いてもいいですが、今回はそれをCI化したdevcontainers/ci(CLIとCIで紛らわしいですが)を使います。

docker/build-push-actionでDockerイメージをビルドしたことのある場合は、代わりにこれを使う以外流れはほぼ同じです。

今回は、バージョンのついたタグをプッシュ場合、および手動で実行した場合(workflow_dispatch)に、ビルドが走るようにしました。

.github/workflows/build.yml:

name: Build

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:

jobs:
  build:
    # https://github.com/devcontainers/ci/issues/191
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha
      - id: format
        # https://github.com/devcontainers/ci/issues/235
        run: |
          prefix="ghcr.io/${{ github.repository }}:"

          tags=$(echo "${{ steps.meta.outputs.tags }}" | sed -e "s#${prefix}##g")
          tags=$(echo "${tags}" | tr "\n" ",")
          tags=$(echo "${tags}" | sed "s/,*$//")

          echo "tags=${tags}" >> $GITHUB_OUTPUT
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: devcontainers/ci@v0.3
        with:
          subFolder: src
          imageName: ghcr.io/${{ github.repository }}
          imageTag: ${{ steps.format.outputs.tags }}
          platform: linux/amd64,linux/arm64
          push: always
          cacheFrom: |
            ghcr.io/${{ github.repository }}
            ghcr.io/${{ github.repository }}:main

runs-onpermissions

runs-on: ubuntu-24.04
permissions:
  contents: read
  packages: write

まずruns-onでビルドに使うランナーを指定します。

今回は個人のプランでも使える最もコストのかからないUbuntuのランナーを使います。

ただし、現在のubuntu-latestではビルド時に使用されるskopeoというツールのバージョンの問題でビルドが失敗することがあるので、ubuntu-24.04を指定するようにします(関連Issue: devcontainers/ci#191)。

permissionsには、リポジトリの内容を読むためのcontents: readと、ビルドの結果をGitHub Container Registry(ghcr.io)にプッシュするためのpackages: writeを指定しています。

actions/checkoutdocker/metadata-actionformat

steps:
  - uses: actions/checkout@v4
  - uses: docker/metadata-action@v5
    id: meta
    with:
      images: ghcr.io/${{ github.repository }}
      tags: |
      type=ref,event=branch
      type=semver,pattern={{version}}
      type=semver,pattern={{major}}.{{minor}}
      type=sha

次にactions/checkoutでリポジトリをチェックアウトし、docker/metadata-actionでリポジトリの情報からビルドされたイメージにつけるタグを取り出します。

  • type=ref,event=branch: ブランチ名(例: main
  • type=semver,pattern={{version}}: タグから取り出されたバージョン(例: 1.2.3
  • type=semver,pattern={{major}}.{{minor}}: タグから取り出されたメジャーバージョンとマイナーバージョン(例: 1.2
  • type=sha: コミットのハッシュ(例: sha-ad132f5

type=semverを指定しているので、latestタグも自動で追加されます。

詳しい情報はtagsのドキュメントにあります。

ただし、docker/metadata-actionが取り出すtagsは次のような形式になっており、docker/build-push-actionで使う場合はいいのですが、devcontainers/ciimageTagの形式とは異なるため、変換する必要があります(関連Issue: devcontainers/ci#235)。

ghcr.io/example/example-image:latest
ghcr.io/example/example-image:1.2.3
ghcr.io/example/example-image:1.2

次のformatステップでは、それをdevcontainers/ciに合わせて次のような形式に変換しています。

latest,1.2.3,1.2
  - id: format
    run: |
      prefix="ghcr.io/${{ github.repository }}:"
  
      tags=$(echo "${{ steps.meta.outputs.tags }}" | sed -e "s#${prefix}##g")
      tags=$(echo "${tags}" | tr "\n" ",")
      tags=$(echo "${tags}" | sed "s/,*$//")
  
      echo "tags=${tags}" >> $GITHUB_OUTPUT

docker/setup-qemu-actiondocker/setup-buildx-action

  - uses: docker/setup-qemu-action@v3
  - uses: docker/setup-buildx-action@v3

docker/setup-qemu-actiondocker/setup-buildx-actionは、マルチアーキテクチャーに対応したイメージをビルドするために必要です。

個人プランで使うGitHubホステッドランナーは、現時点でUbuntuをAMDのx86-64で動かす(Dadsv5-series使われている)ため、ARM向けのビルドはQEMUでエミュレートして行います。

ただし、これにはかなりの時間がかかるため(実は色々な要因でビルドに20分強かかってしまうのですがそのかなりの部分を占めます)、現在TeamやEnterpriseプランのみで使えるArmのランナーが使える場合は、Arm向けのビルドはそちらで行い、後でAMD向けにビルドしたものとマージするという方法が理想的でしょう(関連Issue: devcontainers/ci#268、コメントにArmのランナーを使った実例あり)。

ちなみに、QEMUを使う場合でもmatrixを使って並列化することも考えられますが、今回は省略しています。

docker/login-action

  - uses: docker/login-action@v3
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

ビルドを行う前に、DockerイメージをアップロードするGitHub Container Registryにdocker/login-actionを使ってログインします。

Docker Hubなどの他のレジストリを使ってもいいのですが、GitHub Container Registryは特に追加の設定が必要ないのが便利です。

devcontainers/ci

  - uses: devcontainers/ci@v0.3
    with:
      push: always
      subFolder: src
      imageName: ghcr.io/${{ github.repository }}
      imageTag: ${{ steps.format.outputs.tags }}
      platform: linux/amd64,linux/arm64
      cacheFrom: |
        ghcr.io/${{ github.repository }}
        ghcr.io/${{ github.repository }}:main

ビルドはdevcontainers/ciで行います。push: alwaysを指定することでアップロードもビルド後に自動で行われます。

今回はsrc.devcontainerを置いているので、subFolder: srcを指定しています。

cacheFromには、ビルド時にキャッシュとして使うイメージを指定することができます(しなくてもデフォルトでimageNameと同じものは指定されるはず)。

ただし、デフォルトではlatestタグのものが利用されるようになっているため、latestタグが生成されないケース(workflow_dispatchでブランチ名とハッシュのタグしか生成されない)に対応するため、:mainを追加で指定しています。

なお、今回は行っていませんが実は、devcontainers/ciは開発環境のイメージでテストなどを実行できるようにすることを目的としており、runCmdを指定することで、ビルド後に、あるいはビルド済みのイメージを使って、コマンドを実行することができます。

- uses: devcontainers/ci@v0.3
  with:
    cacheFrom: |
        ghcr.io/${{ github.repository }}
        ghcr.io/${{ github.repository }}:main
    push: never
    runCmd: make test

すでにデバッグ用のイメージを作ってテストを行っているという方もいるかもしれませんが、まとめて行えて便利かもしれません。

おわりに

以上の設定を行うだけで、.devcontainerに次のようなdevcontainer.jsonを置くだけで、ビルド済みのDev Containerですぐに開発を始めるようになります。

{
    "image": "ghcr.io/example/my-devcontainer-image:latest"
}

実際に作ったのは、次のClangを使ったC++を書く用のDev Containerですが、C++用の細かい設定もイメージ自体に埋め込むことができて便利です。

https://github.com/publictheta/devcontainer-image-clang

今後再利用もできるように細かくFeaturesに分けて構成したので、そのたびにapt-get updaterm -rf /var/lib/apt/lists/*を実行するせいでビルド時間がかかるようになってしまったのですが、これはトレードオフかなと思います。

あとはArmランナーが個人のプランでも使えるようになったあたりでビルドの並列化はしたいですね。

Dev Containerは起動に時間がかかるのがちょっと、という方はぜひ試してみてください。

参考

Dev Containerについては、VS Code、GitHub Codespacesそれぞれの紹介がまずスタートポイントになります。

https://code.visualstudio.com/docs/devcontainers/containers

https://docs.github.com/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers

事前ビルド(プレビルド)については、Dev Containerのドキュメントに記事があります。

https://containers.dev/guide/prebuild

GitHub Actionsを使った事前ビルド(プレビルド)については、devcontainers/ciのリポジトリにあるドキュメントが参考になります。

https://github.com/devcontainers/ci/blob/main/docs/github-action.md

Discussion