自前の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