🤖

Renovate で Goバージョンを自動更新する

に公開

最近、セキュリティインシデントのニュースを目にすることが増え、自分の業務においても身が引き締まる思いです。
例えば所属組織の Go で構築したシステムにおいては、Go バージョンを適切に上げることが脆弱性対策になります。

Go メジャーバージョンのリリースサイクルは約6ヶ月で、2つ先のバージョンがリリースされるまでの約1年間がサポート期間になります(Release Policy)。
サポート期間中にセキュリティの問題が発生したら更新版がリリースされ、それを取り込むことでセキュリティパッチを当てることができます。

これを踏まえて、次のようなバージョンアップ戦略を取ると安全性を高めることができます。

  • 使用している Go バージョンのサポートが切れる前に、メジャーバージョンを上げる
  • 使用している Go バージョンのマイナーバージョンがリリースされたら早いうちに取り込む

新しい Go バージョンをタイムリーに更新するには、Renovate を使うと容易に実現できます。

本記事では、

  • Renovate を使って Docker の golang イメージを更新する方法
  • go.mod の go ディレクティブを更新する方法

を紹介します。

前提

Renovate の基本的な使い方についてはこちらの記事をご参考ください。
https://zenn.dev/koga_atsu/articles/bd30838223271e

事前準備

Renovate をインストールしたリポジトリで、次のようなファイルを用意します。

ディレクトリ構成
.
├── renovate.json
├── Dockerfile
├── go.mod
└── main.go


renovate.json
// Renovate インストール時点の状態
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ]
}
Dockerfile
FROM golang:1.23.3

WORKDIR /go/src

COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o /go/main main.go
ENTRYPOINT ["/go/main"]
go.mod
module renovate-golang-tutorial

go 1.23.3

require (
	cloud.google.com/go/firestore v1.17.0
	cloud.google.com/go/storage v1.49.0
	go.opentelemetry.io/otel v1.33.0
	go.opentelemetry.io/otel/sdk v1.33.0
)


main.go
package main

import (
	"cloud.google.com/go/firestore"
	"cloud.google.com/go/storage"
	"context"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	// 依存ライブラリを使用していることを示すためのコードです。特に意味はありません。
	firestore.NewClientWithDatabase(context.Background(), "", "")
	storage.NewClient(context.Background(), storage.WithJSONReads())
	trace.NewTracerProvider()
	otel.Tracer("example.com/basic")
}

go mod tidyを実行して go.sum を生成します。
使用する Renovate のバージョンは 39.107.0 です。

golang イメージの更新方法

Dockerfile 内のイメージタグを更新するには、dockerfile マネージャーを使用します。

これはリポジトリ内で FileMatching の正規表現 に一致する Dockerfile を探してきて、FROM句などに指定しているイメージのタグを更新します。

初期設定の renovate.json でも dockerfile マネージャーは有効になっていますが、ここでは明示的に有効化するために enabledManagers に指定します。

renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  // 追加
  "enabledManagers": [
    "dockerfile"
  ]
}

上記の設定によって Dockerfile 内の golang イメージタグを最新化するPRが作成されました。

go ディレクティブの更新方法

続いて、go.mod の go ディレクティブを更新するには、enabledManagers に gomod を追加します。
但し、これだけだとマイナーバージョンの更新が行われないため、rangeStrategy に bump を指定します。

renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  "rangeStrategy": "bump",  // 追加
  "enabledManagers": [
    "dockerfile",
    "gomod"  // 追加
  ]
}

そうすると、go ディレクティブを最新のバージョンに更新するPRが作成されました。

現在の状態で Dependency Dashboard を見てみます。

上から1つ目と2つ目はこれまで説明したPRで、go バージョンアップのPRとして1つにまとめると分かりやすくなりそうです。次に、これらをまとめる方法を説明します。

上から3つ目から5つ目はライブラリ更新PRです。
これは gomod マネージャーが go ディレクティブの更新だけでなく、go モジュールの更新も担っているためです。
これらのPRをまとめる方法はこちらの記事で説明しているので、興味がある方は見てみてください。

go バージョンアップ関連PRを集約する

golang イメージの更新PRと go ディレクティブの更新PRを集約するには、それぞれのPRを識別するためのルールを packageRules に記述し、同一の groupName を設定します。
groupName が同じものは同一グループになり、同一のPRになります。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  "rangeStrategy": "bump",
  "enabledManagers": [
    "dockerfile",
    "gomod"
  ],
  // 追加
  "packageRules": [
    {
      "matchManagers": [
        "gomod"
      ],
      // go ディレクティブの更新を担うデータソースを設定する
      "matchDatasources": [
        "golang-version"
      ],
      "groupName": "golang-version-updates"  // 同じグループ名を設定
    },
    {
      "matchManagers": [
        "dockerfile"
      ],
      "groupName": "golang-version-updates"  // 同じグループ名を設定
    }
  ]
}

上記の設定を行うと、golang イメージの更新PRと go ディレクティブの更新PRが集約されました。

go ディレクティブバージョンと golang イメージバージョンがズレたらどうなるか

新しい Go バージョンがリリースされ、Renovate によってgo ディレクティブが更新されるタイミング(1)と、golangイメージタグが更新されるタイミング(2)はズレる可能性があります。

現実的には Go バージョンがリリースされた後にそのバージョンの golang イメージがリリースされる流れになるはずなので、(1)よりも先に(2)が単独で行われることは無さそうです。

しかしここではケーススタディとして、(1)および(2)が単独で実行されるとどうなるかを見てみます。

ケース1:go ディレクティブ > golang イメージバージョン

go.mod の go ディレクティブを 1.23.4 にします。

go.mod
module renovate-golang-tutorial

go 1.23.4

require (
    cloud.google.com/go/firestore v1.17.0
    cloud.google.com/go/storage v1.49.0
    go.opentelemetry.io/otel v1.33.0
    go.opentelemetry.io/otel/sdk v1.33.0
)

Dockerfile の golang イメージを 1.23.3 にし、ENTRYPOINT でビルドしたGoバージョンを出力します。

Dockerfile
FROM golang:1.23.3

WORKDIR /go/src

COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o /go/main main.go
ENTRYPOINT ["/bin/sh", "-c", "go version -m /go/main"]

これでイメージをビルドすると、エラーが発生します。

$ docker build -t my-go-app .
 > [4/4] RUN GOOS=linux GOARCH=amd64 go build -o /go/main main.go:
#0 0.575 go: go.mod requires go >= 1.23.4 (running go 1.23.3; GOTOOLCHAIN=local)

go ディレクティブに指定したバージョン(1.23.4)は必要最低限のバージョンという位置付けにもかかわらず、ビルドバージョン(1.23.3)がそれを満たしていないためエラーになっています。

そのため、このケースではイメージのビルドが失敗してしまいます。

ケース2:go ディレクティブ < golang イメージバージョン

go.mod の go ディレクティブを 1.23.3 にします。

go.mod
module renovate-golang-tutorial

go 1.23.3

require (
    cloud.google.com/go/firestore v1.17.0
    cloud.google.com/go/storage v1.49.0
    go.opentelemetry.io/otel v1.33.0
    go.opentelemetry.io/otel/sdk v1.33.0
)

Dockerfile の golang イメージを 1.23.4 にし、ENTRYPOINT でビルドしたGoバージョンを出力します。

Dockerfile
FROM golang:1.23.4

WORKDIR /go/src

COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o /go/main main.go
ENTRYPOINT ["/bin/sh", "-c", "go version -m /go/main"]

イメージをビルドし docker run を実行すると、 Go のバージョン情報が 1.23.4 と出力されました。

$ docker build -t my-go-app . && docker run --rm my-go-app
/go/main: go1.23.4
        path    command-line-arguments
        〜以下省略〜

このケースでは go ディレクティブに指定したバージョン(1.23.3)よりも、ビルドに使用するバージョン(1.23.4)の方が大きいためビルドが通り、Go 1.23.4 としてアプリケーションを実行できます。

まとめ

Renovate を使って Go バージョンの更新を自動化する方法を見てきました。
go directive のバージョンとビルドするバージョンが相違した時の挙動は、Go 1.21 以降では Go Toolchains という機能が担っています。別の記事でこの辺もより詳しく紹介できたらと思います。

Discussion