🧑‍🔧

Kubernetes カスタムコントローラー楽々メンテナンス

2023/01/22に公開

はじめに

Kubebuilder を利用すると、Kubernetes のカスタムコントローラーを開発するためののひな形を生成することができます。
Kubebuilder のひな形には、カスタムコントローラーのコンテナイメージビルドや、デプロイするためのマニフェストを生成するための仕組みが用意されています。
しかし、下記のような日々のメンテナンスやリリース作業のための仕組みは含まれていません。

  • 利用しているツールやライブラリの更新
  • Kubernetes のバージョンアップ対応
  • リリースノートの作成
  • コンテナイメージのビルド、コンテナレジストリへの登録
  • Helm Chart の作成とリリース

そこで本記事では、以下のツールを利用した自動化をおこない、日々のメンテナンスやリリース作業を楽にする方法について紹介したいと思います。

  • Renovate による依存関係の更新
  • GoReleaser によるコンテナイメージのリリース
  • GitHub によるリリースノートの自動生成
  • Helmify と Chart Releaser による Helm Chart のリリース

リリース&メンテナンス設定例

Renovate による依存関係の更新

Renovate は自動的に依存関係の更新をおこなってくれる非常に便利なツールです。

https://docs.renovatebot.com

Renovate を導入するだけで、go.mod のパッケージバージョンやマニフェストに含まれるコンテナイメージのバージョンなどをチェックし、バージョンアップのための PR を自動作成してくれます。
さらに、オートマージやパッケージごとの更新ルールなどを細かく設定することも可能です。

ただし、Kubebuilder で生成したプロジェクトでは Makefile の中にツールのバージョンが記述されているため、Renovate のデフォルト設定ではツールの更新をおこなうことができません。
そこで、各種ツールのバージョン管理には aqua の利用がおすすめです。
aqua を利用すると Renovate による自動更新が可能になります。詳しくは以下の記事を参考にしてください。

https://zenn.dev/zoetro/articles/eee98d772c2483

Kubernetes 更新のための Renovate 設定

Kubernetes は、4ヶ月に1回マイナーバージョンが更新され、直近3つのマイナーバージョンがサポートされています[1]

そのためカスタムコントローラーにおいても、直近3バージョンの Kubernetes をサポートするためにマトリックステストを実施します。
以下は GitHub Actions でのマトリックステストの設定例です。

.github/workflows/ci.yaml
name: CI
on:
  pull_request:
jobs:
  e2e:
    name: End-to-end Test
    runs-on: ubuntu-20.04
    strategy:
      matrix:
        k8s-version:
          - v1.23.12 # renovate: kindest/node
          - v1.24.6 # renovate: kindest/node
          - v1.25.3 # renovate: kindest/node
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v3
        with:
          go-version-file: go.mod
      - run: make test KUBERNETES_VERSION=${{ matrix.k8s-version }}

このとき Renovate で Kubernetes のマイナーバージョンが更新されてしまっては困ります。
そこで以下のようなルールを設定することで、Kubernetes のパッチバージョンの更新のみを Renovate でおこなうことが可能になります。

renovate.json5
{
  "packageRules": [
    {
      // マイナーバージョンとパッチバージョンの更新をまとめないようにする
      "description": "Separate minor and patch update for Kubernetes",
      "matchPackageNames": [
        "kindest/node"
      ],
      "separateMinorPatch": true
    },
    {
      // メジャーバージョンとマイナーバージョンの更新はおこなわない
      "description": "Disable major and minor update for Kubernetes",
      "enabled": false,
      "matchPackageNames": [
        "kindest/node"
      ],
      "matchUpdateTypes": [
        "major",
        "minor"
      ]
    }
  ],
  "regexManagers": [
    {
      // 指定した正規表現にマッチする文字列を kindest/node というコンテナイメージのバージョンで更新する
      "datasourceTemplate": "docker",
      "depNameTemplate": "kindest/node",
      "fileMatch": [
        "^\\.github\\/workflows\\/.+\\.ya?ml$"
      ],
      "matchStrings": [
        "- (?<currentValue>.+?) # renovate: kindest\\/node"
      ]
    }
  ]
}

続いて、Kubernetes のカスタムコントローラー実装に利用する k8s.io/client-gok8s.io/api などのパッケージの更新について考えます。
これらのパッケージは Kubernetes v1.x.y に対応して v0.x.y というバージョンがつけられています。

ところが k8s.io/client-go は、過去にメジャーバージョン1以上のものがリリースされていました。
そのため、Renovate は大きなメジャーバージョンのものを最新版だと勘違いして更新をおこなおうとします。
これを防ぐために、以下のような設定を追加しておくとよいでしょう。

renovate.json5
{
    {
      // client-go の v12.0 などを取ってこないように、メジャーバージョンアップを無効化する
      "description": "Disable major updates for k8s.io/client-go",
      "enabled": false,
      "matchDatasources": [
        "go"
      ],
      "matchPackageNames": [
        "k8s.io/client-go"
      ],
      "matchUpdateTypes": [
        "major"
      ]
    }
  ]
}

GoReleaser によるコンテナイメージのリリース

カスタムコントローラーをリリースする際には、コンテナイメージのビルドとコンテナレジストリへの登録が必要となります。
最近はマルチアーキテクチャに対応したコンテナイメージが要求されることも多いでしょう[2]

GoReleaser を利用すると、Go言語で書かれたプログラムに対して、複数のアーキテクチャに対応したバイナリやコンテナイメージを作成しリリースすることが可能になります。

https://goreleaser.com

ここでは詳細は解説しませんが、以下のような設定ファイルを用意することで、マルチアーキテクチャに対応したコンテナイメージがビルドされます。

.goreleaser.yml
project_name: website-operator
dist: bin/
release:
  # リリースページにカスタムコントローラーのバイナリをアップロードする必要はないのでスキップする
  skip_upload: true
builds:
  - id: website-operator
    env:
      - CGO_ENABLED=0
    main: ./cmd/website-operator
    binary: website-operator
    goos:
      - linux
    goarch:
      - amd64
      - arm64
    ldflags:
      # バージョン番号を埋め込んでビルドする
      - -X github.com/zoetrope/website-operator.Version={{.Version}}
dockers:
  - image_templates:
      - "ghcr.io/zoetrope/website-operator:{{ .Version }}-amd64"
    use: buildx
    dockerfile: ./Dockerfile
    ids:
      - website-operator
    extra_files:
      - LICENSE
    build_flag_templates:
      - "--platform=linux/amd64"
      - "--label=org.opencontainers.image.created={{.Date}}"
      - "--label=org.opencontainers.image.revision={{.FullCommit}}"
      - "--label=org.opencontainers.image.version={{.Version}}"
  - image_templates:
      - "ghcr.io/zoetrope/website-operator:{{ .Version }}-arm64"
    use: buildx
    dockerfile: ./Dockerfile
    ids:
      - website-operator
    extra_files:
      - LICENSE
    build_flag_templates:
      - "--platform=linux/arm64"
      - "--label=org.opencontainers.image.created={{.Date}}"
      - "--label=org.opencontainers.image.revision={{.FullCommit}}"
      - "--label=org.opencontainers.image.version={{.Version}}"
docker_manifests:
  - name_template: "ghcr.io/zoetrope/website-operator:{{ .Version }}"
    image_templates:
      - "ghcr.io/zoetrope/website-operator:{{ .Version }}-amd64"
      - "ghcr.io/zoetrope/website-operator:{{ .Version }}-arm64"
  - name_template: "ghcr.io/zoetrope/website-operator:{{ .Major }}.{{ .Minor }}"
    image_templates:
      - "ghcr.io/zoetrope/website-operator:{{ .Version }}-amd64"
      - "ghcr.io/zoetrope/website-operator:{{ .Version }}-arm64"

そして GitHub Actions で以下のようなワークフローを用意すると、タグを打つだけでマルチアーキテクチャ対応のコンテナイメージがレジストリに登録され、GitHub 上でリリースがおこなわれます。

.github/workflows/release.yaml
name: Release
on:
  push:
    tags:
      - 'v*'
jobs:
  release:
    runs-on: ubuntu-20.04
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: docker/setup-qemu-action@v2
      - uses: docker/setup-buildx-action@v2
      - name: GHCR Login
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/setup-go@v3
        with:
          go-version-file: go.mod
      - name: Set previous release tag for GoReleaser
        run: |
          # Helm Chart の最新版ではなく、本体の最新版のタグを取得する
          export TAG=$(curl -s "https://api.github.com/repos/zoetrope/website-operator/releases/latest" | jq -r .tag_name)
          echo "GORELEASER_PREVIOUS_TAG=${TAG}" >> $GITHUB_ENV
      - name: GoReleaser
        uses: goreleaser/goreleaser-action@v4
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GitHub によるリリースノートの自動生成

リリースの際にはリリースノートを用意する必要がありますが、手書きするのは非常に面倒です。
GitHub では、前回のリリースと今回のリリースの差分から自動的にリリースノートを生成することができます[3]

https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes

この機能を利用するために、.goreleaser.yml に以下の設定を追加します。

.goreleaser.yml
changelog:
  # GitHub のリリースノート生成機能を利用する
  use: github-native

また、以下のファイルを用意することで、付与されたラベルに応じて PR をカテゴリー分けしたり、リリースノートに含めないようすることができます。

.github/release.yml
changelog:
  exclude:
    labels:
      - ignore-for-release
      - ci
      - documentation
      - refactoring
      - test
  categories:
    - title: Features
      labels:
        - enhancement
    - title: Bug Fixes
      labels:
        - bug
    - title: Deprecated
      labels:
        - deprecate
    - title: Removed
      labels:
        - remove
    - title: Security
      labels:
        - security
    - title: Dependencies
      labels:
        - dependencies
    - title: Others
      labels:
        - "*"

なお、Renovate に以下の設定を追加しておけば、Renovate が作成した PR にラベルをつけることができます。

renovate.json5
{
  "labels": [
    "dependencies"
  ]
}

上記の設定により、次のようなリリースノートが作成されます。

Helmify と Chart Releaser による Helm Chart のリリース

Kubebuilder 生成したプロジェクトには、Kustomize を利用してカスタムコントローラーのマニフェストを生成する仕組みが用意されています。
Kustomize は便利なツールではありますが、ユーザーがカスタムコントローラーをデプロイする際にこの仕組みを利用するのはやや不便ではあります。
そこで、Helm Chart を用意して、helm install コマンドで簡単にデプロイできるような仕組みを用意しましょう。

Helmify

Helmify を利用すると、既存のマニフェストから Helm Chart を生成してくれます。

https://github.com/arttor/helmify

Kubebuilder のプロジェクトにも対応しており、以下のコマンドで Helm Chart の生成がおこなえます。

$ kustomize build config/default | helmify -crd-dir charts/website-operator
$ tree ./charts/
./charts/
└── website-operator
    ├── Chart.yaml
    ├── crds
    │   └── website-crd.yaml
    ├── templates
    │   ├── _helpers.tpl
    │   ├── deployment.yaml
    │   ├── leader-election-rbac.yaml
    │   ├── manager-config.yaml
    │   ├── manager-rbac.yaml
    │   ├── ui-rbac.yaml
    │   └── ui.yaml
    └── values.yaml

生成された Helm Chart は必要に応じて書き換えるとよいでしょう。

Chart Releaser

Helm Chart をリリースするには、GitHub のリリースページにチャートファイルをアップロードし、gh-pages でインデックスファイルを公開する必要があります。
Chart Releaser を利用すると、簡単に Helm Chart のリリースをおこなうことができます。

https://github.com/helm/chart-releaser

さて、先ほど GoReleaser を使ってカスタムコントローラーをリリースする仕組みを用意しました。
このカスタムコントローラーのリリースにあわせて、自動的に Helm Chart もリリースすると便利ではないでしょうか。

そこで、前段のリリースジョブが成功したら、自動的に Helm Chart をリリースするジョブを用意します。

.github/workflows/release.yaml
name: Release
on:
  push:
    tags:
      - 'v*'
jobs:
  release:
    # 省略
  chart-release:
    runs-on: ubuntu-20.04
    needs: release
    # 前段のreleaseジョブが成功した場合にのみ実行する
    if: contains(needs.release.result, 'success')
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Set chart version
        run: |
          helm repo add website-operator https://zoetrope.github.io/website-operator
          helm repo update

          # gitのタグからバージョン番号を抜き出す
          tag_version=${GITHUB_REF##*/v}

          # 現在リリースされているHelm Chartのバージョンを取得する
          chart_version=$(helm search repo website-operator -o json | jq -r 'sort_by(.version) | .[-1].version')
          chart_patch_version=${chart_version##*.}
          
          # 今回リリースするHelm Chartのバージョンを計算する。
          # 通常はパッチバージョンに+1する
          # ただしメジャーやマイナーバージョンが変化している場合は、パッチバージョンは0にする。
          new_patch_version=$(($chart_patch_version+1))
          local_version=$(cat charts/website-operator/Chart.yaml | yq .version | sed "s/0-chart-patch-version-placeholder/$chart_patch_version/g")
          [ "$local_version" != "$chart_version" ] && new_patch_version=0

          # プレースホルダーのバージョンを今回のリリースバージョンに置き換える
          sed --in-place "s/app-version-placeholder/$tag_version/g" charts/website-operator/Chart.yaml
          sed --in-place "s/0-chart-patch-version-placeholder/$new_patch_version/g" charts/website-operator/Chart.yaml
          sed --in-place "s/app-version-placeholder/$tag_version/g" charts/website-operator/values.yaml
      - name: Create release notes
        run: |
          # Helm Chart のリリースノートには、本体のリリースへのリンクを追加する
          tag_version=${GITHUB_REF##*/}
          cat <<EOF > ./charts/website-operator/RELEASE.md
          Helm chart for Website Operator [$tag_version](https://github.com/zoetrope/website-operator/releases/tag/$tag_version)

          EOF
      - name: Configure Git
        run: |
          git config user.name "$GITHUB_ACTOR"
          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
      - name: Install Helm
        uses: azure/setup-helm@v3
      - name: Run chart-releaser
        uses: helm/chart-releaser-action@v1.5.0
        with:
          config: cr.yaml
        env:
          CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

なお、バージョン番号を自動的に書き換えられるように、Chart.yamlversionappVersion をプレースホルダーにしておきます。

Chart.yaml
apiVersion: v2
name: website-operator
description: Helm chart for Website Operator
home: https://github.com/zoetrope/website-operator
type: application
version: 0.2.0-chart-patch-version-placeholder
appVersion: app-version-placeholder

同様に values.yamltag もプレースホルダーにしておきます。

values.yaml
controller:
  image:
    repository: ghcr.io/zoetrope/website-operator
    tag: app-version-placeholder
  replicas: 1
ui:
  image:
    repository: ghcr.io/zoetrope/website-operator-ui
    tag: app-version-placeholder
  replicas: 1

Chart Releaser 用の設定ファイルは以下のようになります。

cr.yaml
owner: zoetrope
git-repo: website-operator
release-name-template: "{{ .Name }}-chart-{{ .Version }}"
# Helm Chartのリリースがlatestにならないようにしておく
make-release-latest: false
# リリースノートのファイルはGitHub Actionsで生成する
release-notes-file: RELEASE.md

以上で、カスタムコントローラー本体のリリース後に自動的に Helm Chart がリリースされ、helm install コマンドでカスタムコントローラーをデプロイできるようになりました。

まとめ

今回紹介した仕組みを導入することにより、依存関係は Renovate で自動更新できるようになり、Git のタグを打つだけでコンテナイメージと Helm Chart をリリースすることが可能となりました。

これらの仕組みは下記のリポジトリで利用しているので、興味のある方はぜひ参考にしてみてください。

https://github.com/zoetrope/website-operator

脚注
  1. https://kubernetes.io/releases/ ↩︎

  2. Kubebuilder v3.7.0 で Multiple architecture support が追加されました。 ↩︎

  3. GoReleaser にもリリースノート生成機能はありますが、GitHub のほうが使いやすくておすすめです。 ↩︎

Discussion