Kubernetes クラスタを管理していると、複数の Docker イメージをまとめて管理したくなることはよくあります。anqou.net ではこの目的で ket-containers というレポジトリを運用しています。いままでは複数の Dockerfile を単に配置して使っていたのですが、Nix の dockerTools.buildLayeredImage
を使うと Nix で Docker イメージのビルドができるので、これを Nix Flake を通して使うことで、一つの Nix Flake で複数の Docker イメージを管理できます。ということで、実際に ket-containers で行ってみました。この記事では flake.nix
の中身の説明と、それをビルドする GitHub Actions の workflow を説明します。
まず flake.nix
は以下のような感じにしておきます。ポイントは packagex.x86_64-linux
配下に Docker イメージの derivation を並べる点です。CI でイメージをビルドする際には、ここに書かれている package を一覧して使います:
{
# ...
outputs = {
self,
nixpkgs,
}: let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
packages.x86_64-linux = {
# postgres-backup.nix には dockerTools.buildLayeredImage を
# 呼び出すコードが書かれている。
postgres-backup = pkgs.callPackage ./postgres-backup.nix {};
# 管理したい Docker イメージの derivation をここに好きなだけ
# 並べる。
};
# ...
};
}
続いて GHA の workflow を書きます。基本的には nix build
を実行して Docker イメージを作ってから docker/build-push-action
を使って push するだけですが、各 Docker イメージを並列にビルドできるように、job を二段階に分けています。まず前段の job で nix flake show
を打って全 Docker イメージの名前を取得し、これを後段の job の matrix
に指定して並列化します。
また、Docker イメージのビルドを行う前に docker manifest inspect
を打って、すでに同名のイメージが存在するかを確認します。もし存在する場合は上書きを避けるため以降の処理を全てスキップしています。なお docker manifest inspect
に渡すイメージの名前を得るためには nix build
を実行する前に Docker イメージの名前が必要になります。これは nix eval
で derivation の imageName
と imageTag
attribute を取ってくることで実現できます。
# ...
jobs:
# ビルドする Docker イメージの derivation の名前を
# flake.nix から取ってくる job
get-packages:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.get-packages.outputs.value }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: get Nix packages
id: get-packages
run: |
# nix flake show --json で flake.nix の中身が取れるので
# それを jq で適当に加工する。
cd nix
list=$(nix flake show --json | jq -c '.packages."x86_64-linux" | keys')
echo "value=${list}" >> $GITHUB_OUTPUT
# 各 Docker イメージをビルドする。
build-and-push-image:
needs: get-packages
runs-on: ubuntu-latest
# get-packages job で取ってきた各 Docker イメージの名前を指定し
# 並列に Docker イメージのビルドが走るようにする。
strategy:
fail-fast: false
matrix:
package: ${{ fromJSON(needs.get-packages.outputs.packages) }}
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
# nix eval を叩いてイメージの名前とタグを取得する。
- name: Extract metadata
id: metadata
run: |
cd nix
IMAGE_NAME=$(nix eval --raw .#${{ matrix.package }}.imageName)
IMAGE_TAG=$(nix eval --raw .#${{ matrix.package }}.imageTag)
echo "tag=${IMAGE_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 作ろうとしているイメージがすでに push されているかを確認する。
# 存在する場合は上書きを避ける。
- name: Check if the image already exists
id: existence-check
run: |
if docker manifest inspect ${{ steps.metadata.outputs.tag }}; then
echo "b=0" >> "$GITHUB_OUTPUT"
else
echo "b=1" >> "$GITHUB_OUTPUT"
fi
# Docker image をビルドして push する。
- if: ${{ steps.existence-check.outputs.b == '1' }}
name: Build and push a Docker image
run: |
cd nix
nix build .#${{ matrix.package }}
docker load -i result
docker push ${{ steps.metadata.outputs.tag }}
以上のような workflow を用意したうえで flake.nix
に Docker イメージを追加すると CI 上で Docker イメージがビルドされて ghcr.io に push されます。また Docker イメージの derivation を修正した場合は version
の値を更新すると、まだ存在しないイメージであることが CI で検知され、新しい Docker イメージがリリースされるはずです。逆に、何か新しいコミットを追加して CI が動いた場合であっても version
の値が更新されなければ Docker イメージのリリースは走らない仕組みになっているので、Docker イメージが意図せず上書きされることを防げます。
#まとめ
Nix Flake で Docker イメージをまとめて良い Kubernetes ライフを。