自前のDev Containerを自動事前ビルドする
Dev ContainerのイメージをGitHub Actionsで事前ビルド(プレビルド)する環境を構築したので手順をまとめておきます。
成果物はこちらです(自分で使う用なのであくまでも参考としてですが)。
はじめに
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-on、permissions
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/checkout、docker/metadata-action、format
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/ciのimageTagの形式とは異なるため、変換する必要があります(関連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-action、docker/setup-buildx-action
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
docker/setup-qemu-actionとdocker/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++用の細かい設定もイメージ自体に埋め込むことができて便利です。
今後再利用もできるように細かくFeaturesに分けて構成したので、そのたびにapt-get updateとrm -rf /var/lib/apt/lists/*を実行するせいでビルド時間がかかるようになってしまったのですが、これはトレードオフかなと思います。
あとはArmランナーが個人のプランでも使えるようになったあたりでビルドの並列化はしたいですね。
Dev Containerは起動に時間がかかるのがちょっと、という方はぜひ試してみてください。
参考
Dev Containerについては、VS Code、GitHub Codespacesそれぞれの紹介がまずスタートポイントになります。
事前ビルド(プレビルド)については、Dev Containerのドキュメントに記事があります。
GitHub Actionsを使った事前ビルド(プレビルド)については、devcontainers/ciのリポジトリにあるドキュメントが参考になります。
Discussion