blog.anqou.net
rss
author
tags

複数の Docker イメージを一つの Nix Flake で管理して CI でビルドする

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 の imageNameimageTag 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 ライフを。