こんにちは!
コアシステム開発Gでテックリードをやっている古賀です。
最近、セキュリティインシデントのニュースを目にすることが増え、自分の業務においても身が引き締まる思いです。
例えば弊社には Go で構築したシステムがありますが、Go バージョンを適切に上げることが脆弱性対策になります。
Go メジャーバージョンのリリースサイクルは約6ヶ月で、2つ先のバージョンがリリースされるまでの約1年間がサポート期間になります(Release Policy)。
サポート期間中にセキュリティの問題が発生したら更新版がリリースされ、それを取り込むことでセキュリティパッチを当てることができます。
これを踏まえて、次のようなバージョンアップ戦略を取ると安全性を高めることができます。
- 使用している Go バージョンのサポートが切れる前に、メジャーバージョンを上げる
- 使用している Go バージョンのマイナーバージョンがリリースされたら早いうちに取り込む
新しい Go バージョンをタイムリーに更新するには、Renovate を使うと容易に実現できます。
本記事では、Renovate を使って Docker の golang イメージを更新する方法と、go.mod の go ディレクティブを更新する方法を紹介します。
前提
Renovate の基本的な使い方についてはこちらの記事をご参考ください。
事前準備
Renovate をインストールしたリポジトリで、次のようなファイルを用意します。
. ├── renovate.json ├── Dockerfile ├── go.mod └── main.go
// Renovate インストール時点の状態 { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ] }
FROM golang:1.23.3 WORKDIR /go/src COPY . . RUN GOOS=linux GOARCH=amd64 go build -o /go/main main.go ENTRYPOINT ["/go/main"]
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 )
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
に指定します。
{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ], // 追加 "enabledManagers": [ "dockerfile" ] }
上記の設定によって Dockerfile 内の golang
イメージタグを最新化するPRが作成されました。
go ディレクティブの更新方法
続いて、go.mod の go ディレクティブを更新するには、enabledManagers
に gomod を追加します。
但し、これだけだとマイナーバージョンの更新が行われないため、rangeStrategy に bump を指定します。
{ "$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 にします。
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バージョンを出力します。
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 にします。
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バージョンを出力します。
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 という機能が担っています。別の記事でこの辺もより詳しく紹介できたらと思います。
さいごに
ホワイトプラスでは、ビジョンやバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など、「生活領域×テクノロジー」で事業を展開しています。
弊社に興味がある方は、オウンドメディア「ホワプラSTYLE」をご覧ください。オンラインでのカジュアル面談も可能ですので、ぜひお気軽にお問い合わせください。