ぽよメモ

レガシーシステム考古学専攻

go.modについての陥りやすい誤解

はじめに

 これはあくあたん工房アドベントカレンダー 2021 11日目の記事です。
 ポエムを書いていたら気分が暗くなったので、消して自分の過去のメモを記事にすることにしました。そんな解釈するやつおらへんやろwwと是非笑って読んでください。

 2023-09-19追記:Go 1.21からいくつか挙動に変更が入ったので加筆しています。これまでの内容には触らずそのままに、1.21以降における挙動を追記しているのでご注意ください。

go.modにおけるGoのバージョン指定

注意:この節ではGo 1.21未満における挙動について解説します。1.21以降では異なる挙動になります

go.modには go 1.17 のように、Goのバージョンを指定するディレクティブがあります。

  • 陥りやすい誤解:指定したバージョンでのみビルドする。
  • 実際の挙動:指定したバージョンまでの言語機能のみが利用できる。

https://golang.org/ref/mod#go-mod-file-go

the go directive still affects use of new language features

go 1.15 と書いた場合にでもGo 1.17でビルドすることは出来ます。ただしGo 1.17や1.16で入った新しい言語機能を使うことは出来ません。上のページにも下記の様な例が掲載されています。

For example, if a module has the directive go 1.12, its packages may not use numeric literals like 1_000_000, which were introduced in Go 1.13.

依存先のgoディレクティブの方が古いバージョンを指す場合

以下のGoを使ってビルドします。

❯ go version
go version go1.17.5 darwin/arm64

こんな感じでファイルを用意しました。

.
├── A
│   ├── go.mod
│   └── hello.go
├── B
│   ├── go.mod
│   └── hello.go
├── go.mod
└── main.go

ルート直下のgo.modでA・Bをそれぞれrequire && replaceしています

module main-module

go 1.17

require (
    A v0.0.0
    B v0.0.0
)

replace A => ./A
replace B => ./B

各モジュールA・BはHelloという関数を持っており、自身のモジュール名をしゃべります(↓はモジュールAの場合)。

package A

import "fmt"

func Hello() {
    fmt.Println("This is module A.")
}

ルート直下のmain.goでそれらの関数を呼びます。

package main

import (
    "A"
    "B"
)

func main() {
    A.Hello()
    B.Hello()
}

ただし、go.modで指定するgoディレクティブをそれぞれ以下の様に設定します。

  • モジュールA:go 1.17
  • モジュールB:go 1.12
  • ルート直下のgo.mod:go 1.17
--- A/go.mod 2021-12-11 09:35:59.000000000 +0900
+++ B/go.mod  2021-12-11 09:36:05.000000000 +0900
@@ -1,3 +1,3 @@
-module A
+module B

-go 1.17
+go 1.12

この状態でgo run main.goすると、正しくビルドされ実行できます。

❯ go run ./main.go
This is module A.
This is module B.

また、go 1.17を指定しているモジュールAやmainモジュールでは、Go 1.17までの言語機能を全て使うことが出来ます。ただしmodule B内でGo 1.12よりも新しい言語機能を使うことは出来ません。ここではリファレンスに掲載されていた例にならい、1_000_000 というリテラルを使ってみることにします。

package B

import "fmt"

func Hello() {
    fmt.Println("This is module B.")
    // Go 1.13以上でしか使えないリテラル
    fmt.Println(1_000_000)
}
❯ go run ./main.go
# B
B/hello.go:8:17: underscores in numeric literals requires go1.13 or later (-lang was set to go1.12; check go.mod)

依存先のgoディレクティブの方が新しいバージョンを指す場合

例えば以下の様な場合です。

  • モジュールA:go 1.17
  • モジュールB:go 1.12
  • ルート直下のgo.mod:go 1.12

普通にビルド・実行ができます。

❯ go run ./main.go
This is module A.
This is module B.

ここで、モジュールAでGo 1.12では使えない機能を使ってみます。

package A

import "fmt"

func Hello() {
    fmt.Println("This is module A.")
    fmt.Println(1_000_000)
}

これもまたビルド・実行することができます。

❯ go run ./main.go
This is module A.
1000000
This is module B.

ただし、main.goの中でGo 1.12よりも新しい機能を直接使うことは出来ません。

package main

import (
    "A"
    "B"
    "fmt"
)

func main() {
    A.Hello()
    B.Hello()
    // これを足す
    fmt.Println(1_000_000)
}
❯ go run ./main.go
# command-line-arguments
./main.go:13:17: underscores in numeric literals requires go1.13 or later (-lang was set to go1.12; check go.mod)

goのバージョンよりgoディレクティブが先行する場合

ここまでは全てGo 1.17.5でビルドしてきましたが、試しにGo 1.16くらいでビルドしてみます。

❯ go version
go version go1.16.12 darwin/arm64
  • モジュールA:go 1.17
  • モジュールB:go 1.12
  • ルート直下のgo.mod:go 1.17

を指定していてかつGo 1.17以降でのみ使えるような機能を含まない場合。

❯ go run ./main.go
This is module A.
This is module B.

このときGo 1.17でしか使えないような機能を使うとビルドに失敗します。例えばGo1.17でmathモジュールに追加された MaxInt を参照するとエラーになります。

❯ go run ./main.go
# command-line-arguments
./main.go:13:17: undefined: math.MaxInt
note: module requires Go 1.17

goディレクティブまとめ

  • あるモジュール内ではgoディレクティブで指定したバージョンまでの言語機能が使える。
  • 自身より先行するgoディレクティブを指定するモジュールを使える(1.21未満の場合)
    • ただし最初の制約により、自身の中では自身の指定したバージョンまでの機能しか使えない。
  • 自身より小さいバージョンを指定するモジュールも使える。
    • ただし最初の制約により、そのモジュールの中ではそのモジュールの指定したバージョンまでの機能しか使えない。
  • goバージョンよりgoディレクティブが先行していても、そのgoバージョンまでで使用できる機能だけで構成されていればビルドできる。

実際にはもっと細かい違いなどがあるはずなので、これ以上は公式のリファレンスを読みましょう。

https://go.dev/ref/mod#go-mod-file-go

1.21以降のgo.modにおけるGoのバージョン指定

Go 1.21のリリースノートで言及されていました。挙動が以下の様に変更されています。

  • 陥りやすい誤解:指定したバージョンでのみビルドする。
  • 実際の挙動:指定したバージョン以降でのみビルド可能

Go 1.21 Release Notes - The Go Programming Language

Go 1.21 now reads the go line in a go.work or go.mod file as a strict minimum requirement: go 1.21.0 means that the workspace or module cannot be used with Go 1.20 or with Go 1.21rc1.

これはGo 1.21以降のみに適用されるため、例えば go 1.21 と書かれた(ただしgo 1.21の新機能を含まない)goモジュールをgo 1.20.xでビルドすることは可能です。

require時のバージョンの指定

require ディレクティブでバージョンを指定できます。go get モジュールパス@バージョン などとすると指定のバージョンに固定できます。

  • 陥りやすい誤解:このモジュールはそのバージョン以外は許容しない。
  • 実際の挙動:そのバージョン以上、次のメジャーバージョン未満を許容する。

例えば require A v1.0.0 とすると、このAの取り得るバージョンの範囲はv1.0.0からv2.0.0未満となります。ただし勝手に変わるわけではなく、他の依存先が他のAのバージョンに依存しているわけでないならv1.0.0が利用されます。詳しくは次節で説明します。

この挙動は同じメジャーバージョンの間で完全な後方互換性が保たれることが前提となっています。自身でモジュールを作る時は、この点に十分配慮する必要があります。

Minimal version selection

Goがモジュールのバージョンを選択する際のアルゴリズムです。

  • 陥りやすい誤解:そのモジュールの一番小さいバージョンを使う*1
  • 実際の挙動:依存を全部考慮したときに要件を満たす最小バージョンを選ぶ

以下に公式の仕様があります。

https://golang.org/ref/mod#minimal-version-selection

つまり、あるモジュールAについてモジュールBには require A v1.1.0、モジュールCには require A v1.3.0 という制約があったとき、BとCを同時に利用しAのバージョンを明示しない場合はv1.3.0が利用されます。また、BとCを同時に利用しかつrequire A v1.1.0 などとより低いバージョンを明示しても、v1.3.0が利用されます。

バージョンを完全に固定したい場合は replace を利用することになります。例えばv1.1.0 に強制的に固定したい場合は replace A => A v1.1.0 とします。これは利用するモジュール全てに影響し、強制的にv1.1.0を使うことになります*2

v2などメジャーバージョンが変わる場合はインポートするモジュールパスが異なる、という制約があるため、同時にrequireに指定して使うことが出来るはずです*3

モジュールのバージョン

  • 陥りやすい誤解:セマンティックバージョニングに従うタグでしか指定できない
  • 実際の挙動:特定のコミットハッシュを指すことができる

go.modの仕様には疑似バージョン(Pseudo version)というものが記載されています。
https://go.dev/ref/mod#pseudo-versions

これは特定のコミットハッシュからセマンティックバージョンに対応したバージョン文字列に変換するための仕様です。例えば v0.0.0-20191109021931-daa7c04131f5 のようなものです。つまり

<既にあるバージョン+1>-<commitの日時>-<12桁のコミットハッシュ>

というバージョンが構成されます。既にあるバージョン+1とは、vX.Y.Z が既にあり指しているコミットハッシュがそれよりも新しい場合、vX.Y.Z+1 になるという意味です。ここでバージョンがまだ一つも付けられていない場合に v0.0.0 が採用されます。

golang.org/x/* などにはこのバージョニングを採用しているものが複数有ります*4。例えば golang/x/netリポジトリ(ミラーですが)を見るとタグが一つも付いていないことがわかります。

github.com

これを利用すると、vendoringはしたもののglideやdepに触れることなく今に至る古のGoプロダクトを、破壊的なことをせずにgo modulesに対応させることができます*5。ただしvendoringしたときのコミットないしバージョンがわかっていることが前提になります。

replaceの波及先

  • 陥りやすい誤解:replaceディレクティブは常に有効
  • 実際の挙動:replaceディレクティブは、main moduleに書かれたもののみが有効。

あるモジュールA内のmainパッケージをビルドするとします。ここで、モジュールAは別のモジュールBに依存しているとします。このとき、モジュールBのgo.modに記述されたreplaceは無視されます。

https://go.dev/ref/mod#go-mod-file-replace

replace directives only apply in the main module’s go.mod file and are ignored in other modules. See Minimal version selection for details.

依存先が別のパスにreplaceしている場合

依存先でのバージョンのreplaceは単に無視されてビルドされますが、モジュールパスを書き換えてしまっている場合にどうなるかやってみます。

.
├── A
│   ├── go.mod
│   └── hello.go
├── B
│   ├── go.mod
│   └── hello.go
├── go.mod
└── main.go

モジュールAはモジュールBに依存し、main.goはモジュールAに依存しています。ここで、AからBを使うためにreplaceを使ってローカルパスを指すようにします。

module A

go 1.17

require B v0.0.0

replace B => ../B

そしてルート直下のgo.modでこのAをrequireします。

module main-module

go 1.17

require A v0.0.0

replace A => ./A

A・Bは自身のモジュール名をしゃべる関数Helloを持っていますが、AのHello内でBのHelloも呼ぶことにします。

package A

import (
    "B"
    "fmt"
)

func Hello() {
    fmt.Println("This is module A")
    B.Hello()
}

このAのHello関数をmain.goで呼びます。

package main

import "A"

func main() {
    A.Hello()
}

このビルドは失敗し、非常にわかりにくいエラーが出ます。

❯ go run ./main.go
A/main.go:4:5: missing go.sum entry for module providing package B (imported by A); to add:
    go get A@v0.0.0

replaceできていないので、go buildではBを通常のモジュールとして認識し、チェックサムを確認しようとします(たぶん)。しかし、go.sumにBについての記述がないので、ちゃんとgo getできてないんじゃない?というエラーを吐いているという事だと思います(たぶん)。

ここでルート直下のgo.modでBに対するreplaceを書いてやると正しくビルドすることができます。

module main-module

go 1.17

require A v0.0.0

replace A => ./A
replace B => ./B
❯ go run ./main.go
This is module A
This is module B

go.sum

  • 陥りやすい誤解:package-lock.jsonのようなロックファイル
  • 実際の挙動:モジュールのあるバージョンのハッシュ値を記録したもの

https://golang.org/ref/mod#go-sum-files

これはロックファイルではありません。モジュールのハッシュを計算し、記憶しているだけです。 これにより、同じバージョン・同じモジュール名で改ざんされたモジュールがあったとき、改ざんされたことを検知できるようになっています(なので、リポジトリに含めておいた方が良い)。

まとめ

公式リファレンスを読みましょう。

golang.org

以上です。アドベントカレンダーの他の記事も、是非お楽しみください。


*1:だってminimumをselectするって書いてあるじゃん!

*2:ただし後述するように、これはmainモジュールのgo.modに書かれたreplaceのみが反映されるため、依存先のreplaceは機能しません

*3:たぶん

*4:タグが付いているものもありますgithub.com

*5:みんなもうgo modules使ってるからこんなことはしなくて大丈夫だよね!!

おうちKubernetes feat. cybozu-go/neco-apps

はじめに

 これはCybozu Advent Calendar 2021 7日目の記事です。是非他の記事も読んでみてください。

 Kubernetesの名を聞くようになって久しく、皆様も業務・プライベート問わず日々YAMLを書かれていることでしょう。自分専用のプライベートクラスタが欲しいと思われている方もきっとたくさん居られるはず!今日は自分がプライベートで遊んでいるKubernetesクラスタを紹介したいと思います。

 注意  この記事では私費で機材を購入したりプライベートでKubernetesについて学んだりしていますが、完全に筆者の趣味でありたまたま実益を兼ねているだけです。会社としてそれらの行為を指示及び推奨するものではありません。

なぜおうちKubernetes

 Docker DesktopのKubernetes、minikube、kindなどで手軽にクラスタを建てることができ、簡単な開発環境であれば非常に容易に作成することができるのもKubernetesの魅力です。
 しかし、これらで作成したクラスタクラウドプロバイダの提供するマネージドサービスと比較していくつか足りない機能があります。

 などなど、ちゃんと独立したクラスタで自由にやりたくなります。ではなぜクラウドプロバイダのマネージドKubernetesではなく、わざわざオンプレミスなのか?

……。

家にKubernetesクラスタがあるとなんかかっこいい

ではやっていきましょう!*1

ハードウェアの選定

 情報収集していると、Raspberry Piを使ってクラスタを組んでおられる方が多いようです。

ryusa.hatenablog.com

blog.chatagiriii.com

developers.cyberagent.co.jp

 RasPiの小さなクラスタも非常にかわいらしく魅力的なのですが、世は大半導体不足時代、Raspberry Piの値段も高止まりしていますし在庫も全然ありません。

www.switch-science.com

PoE Hatを買ったりPoEスイッチを買ったりmicroSDは不安だからSSDで……とかやっているとコスト的にも中古でx86のPCを購入するのと大差無いことに気付いたため、NUCを中古で4台購入しました。

f:id:pudding_info:20211130225725j:plain
NUC7i5DNKE 4台

小さくてかわいいですね。2コア4スレッド、メモリ最大32GB搭載可能なモデルです。SSDはM.2 NVMeなものを一つ搭載できるのみ、NICも一つしかないのが不満ではありますが、まぁ実用上困ることはないでしょう。

f:id:pudding_info:20211130215929j:plain
NUCに備わっている端子

vPro対応プロセッサーを搭載しており、Intel AMT*2によってリモート電源管理可能なProモデルです*3

 NUC本体が4台で7万円、8GBメモリを4枚・500GB SSDを4枚で4万円、合計11万円程度で揃いました。

クラスタのブートストラップ

 cybozuKubernetesクラスタはsabakan、そしてCKEを用いて管理されています。

github.com

github.com

これらを使うことも検討しましたが、Intel AMTをサポートしているMAAS*4がいいかんじだったので、MAAS + kubesprayクラスタを構築することにしました。MAASは家に元々あったサーバにインストールしています。

 最初にIntel AMTの設定をしておけば、後は自動でUbuntuをインストール・セットアップすることができるようになりました。各NUCにディスプレイやキーボードを繋いで何度も試行錯誤したりする必要が無く便利です。ただしMAASがDHCPサーバなどを提供するため、家の中で他の端末が使うものとは別のVLANを切ってそこで運用しています。

f:id:pudding_info:20211130230929p:plain
MAASのダッシュボード

cybozu-go/neco-apps

 ここまでで素のKubernetesクラスタはたちましたが、このままではちょっとスペックのいいMinikube程度の機能しか無いので、どんどんコンポーネントをデプロイしていきます。

 まず、やりたいことを決め、それを実現できるコンポーネントをデプロイすることにしました。自分が当初決めた要件は以下の通りです。

  • type: LoadBalancer なServiceを利用できる。これには指定した特定のレンジのIPアドレスを割り当てることができること。
  • 自動でSSL証明書を発行・更新し、httpsでサービスを提供できる。
  • なんらかのIngress controllerによってリクエストのルーティングができる。
  • PersistentVolumeClaim を使ってデータの永続化領域を動的に切り出して利用できる。ノード障害耐性があるとよい。
  • GitOpsによって外部からアクセスさせることなくサービスをCDできる。
  • メトリクスの収集・可視化、および監視ができる。

これらを実現するためには様々なコンポーネントが利用可能ですが、一体何を選択すればいいのか……
うーん……

あっ、cybozu-go/neco-appsで実際に使われているコンポーネントが公開されてるじゃーーん! github.com

ということで勝手に参考にしました。

 注意 筆者はNecoのプロジェクトメンバーではありません。以下は勝手に参考しているだけです。

Metallb

metallb.universe.tf

 おうちKubernetesでLoadBalancerを使うためには実質ほとんど選択肢がありませんでした。
 MetallbはKubernetesにおいてtype: LoadBalancer なServiceを提供するためのコンポーネントです。BGPで経路を広告するBGPモードと、ARP*5を使ってVIPに紐付くノードの付け替えを行うL2モードがあります。BGPモードの場合はルータ側でECMP(Equal-cost multipath)を使うことで複数のノードに分散してトラフィックを送ることができますが、L2モードの場合はリーダーとなった特定のノードのみにトラフィックが集中します。今回は、一般的なルータでも利用可能なL2モードを利用することにしました。
 Kubesprayのオプションを有効にすることでインストールできるのでそれを使いました。設定方法は https://kubespray.io/#/docs/metallb にある通りです。指定したIPアドレスレンジからtype: LoadBalancerなServiceにアドレスを割り当てるので、他と被らないレンジを指定します*6。MAASのUIからDHCPで割り当てたくないIPアドレスレンジを指定できるので、それを使ってLoadBalancer用のIPアドレスレンジを確保しておく必要がありました。

cert-manager

cert-manager.io

 おなじみ証明書管理のためのコンポーネントです。特に説明することは無いと思います。
 これもKubesprayでインストールできるため、それを使いました https://kubespray.io/#/docs/ert_manager。証明書発行はLet's Encryptを、マネージドDNSサービスとしてGoogle Cloud DNSを利用しています。便利な時代に感謝🙏

Contour

projectcontour.io

 ContourはHTTPProxyというカスタムリソースを使うIngress controllerです。その実態としてはEnvoyのコントロールプレーンです。HTTPProxyリソースは個人的にはIngressよりも好きです。
 インストールする方法は非常に簡単で、以下にあるものをほぼそのまま利用するだけです。自分はkustomizeを使ってイメージのバージョンを固定、リソースの設定を追加しました。

github.com

 HTTPProxyリソースはIngressリソースよりも簡便に書けて良いと自分は思っていますが、一方で他のコンポーネントとのインテグレーションはIngressと比べると弱いです。例えばcert-manager用にアノテーション付与することにより証明書を自動発行する機能などは利用できません。そのためCybozuではHTTPProxyリソースを監視し、ExternalDNSのためにDNSEndpointリソースを、cert-managerのためにCertificateリソースを自動で生成する機能を追加したカスタムコントローラであるcybozu-go/contour-plusを開発・利用しています。これによりHTTPProxyリソースを作るだけで以下を追加で自動的に行うようにしています。

  • DNSレコードの登録
  • 証明書の発行

今回自分は外部へのサービス公開をほとんど考えていない*7ことから採用をスキップしましたが、非常に便利なので外部にサービス公開を考えている場合は是非使ってみてください。

github.com

TopoLVM

github.com

 Cybozuが開発・運用している、LVMを使ってPersistentVolumeを提供するCSIプラグインです。詳しくは以下をご覧下さい。

blog.cybozu.io

 後述するRookでPVC basedなクラスタを構築したり、mocoでMySQLクラスタを構築するために利用しています。PVとノードが紐付くため、そのノードが利用できなくなった場合そのPVの情報は失われてしまいます。利用する側でデータの冗長性を確保する必要があります。
 Kubernetes v1.21からbetaとなったGeneric Ephemeral Volumeとして利用することもできます。TopoLVM用のVGからボリュームを切り出すため、emptyDirでホストOSのストレージ領域が圧迫されてしまうことを防ぐことができます。

kubernetes.io

Rook

rook.io

 分散ストレージソフトウェアであるCephをKubernetesで管理するためのオペレータです。CephやRookについては以下の記事をご参照ください。

blog.cybozu.io

blog.cybozu.io

 TopoLVMによってPVCからDynamic Provisioningできるようになっているため、RookではPVC basedなクラスタを作成しました。id:tenzen_hgst さんの以下の記事を参考にしました。

tenzen.hatenablog.com

とはいえ今回のクラスタには3つのワーカーノード、各ノードに1台のストレージが載っているのみなので、大きめのボリュームのOSDが各ノードに一台いるという感じの簡単な構成になっています。あんまりOSDの割り当てなどで悩む余地もありませんでした。Rook/Ceph全然わからんと言いながら使っています。

 TopoLVMとは異なり、可用性のあるPVを提供することができます。また、S3互換なAPIを備えたオブジェクトストレージも提供しており、Minioなどを別途建てなくてもオブジェクトストレージを利用できます。やはりオブジェクトストレージがあるとぐっとクラウドネイティブっぽくなりますね(?)。

https://rook.io/docs/rook/v1.7/ceph-object.html

SealedSecret

github.com

 GitOpsのためにはSecretもGit管理したいところですが、プライベートリポジトリにしたとしても流出に備えて暗号化できるならしておきたいところです。BitnamiのSealedSecretコントローラとkubesealコマンドを使えば、簡単に既存のSecretを暗号化して利用することができます。
 詳しい導入・利用方法については他のサイトに譲りますが、ArgoCDでいつまで経ってもSealedSecretのSyncが完了しない問題がv0.17.0で解消している*8ので紹介しておきます。

github.com

ArgoCD

argo-cd.readthedocs.io

 GitOpsのためのコントローラです。おうちKubernetesでは可能な限り外側に露出するアタックポイントを減らして、毎日メンテできない不安を軽減したいものです。GitOpsならば、外側からアクションを実行するような口を開けることなく継続的にアプリケーションをデプロイすることができます。他に有名なGitOpsのためのツールとしてFlux 2などが知られています。

 基本的なインストール方法は公式ドキュメントの通りです。ただし一部いじりたい箇所もあったので、kustomizeを使って一部変更しています。

GitHubアカウントでSSOする

 一人なのでadminのパスワードをkubectlコマンドでぶっこ抜いて使っても良いのですが、せっかくなのでGitHubでSSOすることにしました。まず認証のためのorgが必要なので、適当なorgを用意します。そのorgの設定から、GitHubのOAuth Applicationを用意します。

docs.github.com

用意したOAuth AppのClient IDとClient Secretを用意し、Secretリソースとして作成します。実際にはこれをkubesealコマンドを使ってSealedSecretリソースとして暗号化しています。

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: argocd-github-client-secret
  namespace: argocd
spec:
  stringData:
    clientID: Client ID
    clientSecret: Client Secret

上記Secretを使うようにArgoCDを設定します。以下は最低限の設定です。必要な他の設定は公式のサンプルを見てください。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
data:
  # ArgoCDにアクセス可能なURLを指定する。コールバックのFQDNもここを指す。
  url: https://argocd.sample.com
  # falseならばadminユーザを作成しない
  admin.enabled: "false"
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          # $シークレット名:key の形で指定する
          clientID: $argocd-github-client-secret:clientID
          clientSecret: $argocd-github-client-secret:clientSecret
          orgs:
          # どのorgを許可するか
          - name: org名
          teamNameField: slug
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
data:
  # どのorgのどのteamにどの権限を割り当てるか。roleはadminとread-onlyのみがデフォルトで用意されている。
  # https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles
  policy.csv: |
    g, org名:team名, role:admin
  # どのチームにも所属しないユーザに何を割り当てるか。空文字にすると何も割り当てない == 閲覧権限すら無し。
  policy.default: ""

これらを適用することでArgoCDにGitHubでログインするボタンが表示されるようになります。

gRPC用とWeb UI用でサービスを分ける

 neco-appsを見ていると、ArgoCD Server用になぜか2種類のserviceがあることに気付きました。

片方には projectcontour.io/upstream-protocol.tls: 443,https が、もう片方には projectcontour.io/upstream-protocol.h2: 443,https が付いています。これらはContour用のannotationで、EnvoyでTLS終端せずにupstreamのサービスにプロキシするための設定です。

https://projectcontour.io/docs/v1.19.1/config/upstream-tls/

どうもContourは同じホスト・ポートに対して異なるプロトコル(http/https or gRPC (http/2))でサーブすることを許していないようで、うまく動かなくなってしまうという挙動に対するワークアラウンドのようです。2つのアノテーションを一つのServiceに付与して動かしてみたりもしましたが、実際にargocdコマンドがうまく動かなかったりしたため自分も同様に二つのServiceに分けました。HTTPProxyリソースの書き方は下記の通りです。

neco-apps/httpproxy.yaml at release-2021.12.01-27858 · cybozu-go/neco-apps · GitHub

VictoriaMetrics

docs.victoriametrics.com

 普通にPrometheusを使っても良かったのですが、下記の記事を読んでVictoriaMetricsを使ってみることにしました。

blog.cybozu.io

 おうちKubernetesをやる上で嬉しいこととしては、以下の辺りでしょうか。

https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#prominent-features

  • It uses 10x less RAM than InfluxDB and up to 7x less RAM than Prometheus, Thanos or Cortex when dealing with millions of unique time series (aka high cardinality).
  • It provides high data compression, so up to 70x more data points may be crammed into limited storage comparing to TimescaleDB and up to 7x less storage space is required compared to Prometheus, Thanos or Cortex.
  • It is optimized for storage with high-latency IO and low IOPS (HDD and network storage in AWS, Google Cloud, Microsoft Azure, etc). See disk IO graphs from these benchmarks.

まとめるとRAMの使用量が少ない!データの圧縮率が良い!遅いストレージでも大丈夫!(今回はNVMe SSDだけど)
特にストレージ容量は比較的小さめであることが多いと思うので、データ圧縮率が良いのは嬉しいですね。加えて比較的構成がシンプルであることも地味に嬉しい点です。理解が楽なので。

 今回はVictoria Metrics operatorを使ってクラスタを構築しました。導入自体は割と簡単です。

github.com

全然不要なのに調子に乗ってHA構成で組んだので、そのうち飽きたら解体します。今のところちゃんと動いていて良さそう。

Grafana operator

github.com

 VictoriaMetricsはWeb UIを持たないので、可視化のためにGrafanaを入れます。GitOpsしたいのでGrafana operatorを導入し、ダッシュボードやデータソースをコードで管理します。また、せっかくArgoCDではGitHubでSSOするようにしたので、Grafanaも同様にします。↓いろいろ省略して認証周りのみに絞っています。

apiVersion: integreatly.org/v1alpha1
kind: Grafana
metadata:
  name: grafana
spec:
  config:
    auth:
      disable_login_form: False
      disable_signout_menu: True
    auth.anonymous:
      enabled: False
    # GitHubでのSSOの設定
    auth.github:
      enabled: true
      allow_sign_up: true
      scopes: user:email,read:org
      auth_url: https://github.com/login/oauth/authorize
      token_url: https://github.com/login/oauth/access_token
      api_url: https://api.github.com/user
      allowed_organizations: org名
    server:
      domain: 公開するドメイン名
      root_url: https://公開するドメイン名
    users:
      viewers_can_edit: true
      auto_assign_org_role: Viewer
  deployment:
    envFrom:
      - secretRef:
          # このsecretにclient idやclient secretを入れておく
          name: grafana-github-client-secret

Secretでは特定の環境変数に値を渡します。この辺りの設定はこことかここにあるので、その辺りを参照すれば他のプロバイダでもSSO出来ると思います。

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: grafana-github-client-secret
spec:
  stringData:
    GF_AUTH_GITHUB_CLIENT_ID: Client ID
    GF_AUTH_GITHUB_CLIENT_SECRET: Client Secret

DataSourceも適当に足します。今のところVictoriaMetricsしかいないのでこれだけです。

apiVersion: integreatly.org/v1alpha1
kind: GrafanaDataSource
metadata:
  name: vm-source
spec:
  name: victoriametrics.yaml
  datasources:
    - name: victoriametrics
      type: prometheus
      access: proxy
      # vmselectのserviceを指定
      url: http://vmselect-vmcluster.monitoring.svc:8481/select/0/prometheus
      version: 1
      isDefault: true
      editable: false
      jsonData:
        tlsSkipVerify: true
        timeInterval: "30s"

ダッシュボードも grafana.comで公開されているダッシュボード は以下の様に簡単に追加できます。例えばNode Exporterのダッシュボードのrevision 23をデプロイする場合は以下のようにします*9

apiVersion: integreatly.org/v1alpha1
kind: GrafanaDashboard
metadata:
  name: node-exporter
spec:
  url: "https://grafana.com/api/dashboards/1860/revisions/23/download"
  datasources:
    - inputName: "DS_PROMETHEUS"
      datasourceName: "victoriametrics"

各NodeにNode Exporterをたてておき、VMNodeScrapeリソースなどを使って情報を収集させておけば以下の様にダッシュボードを表示できます。

f:id:pudding_info:20211204140948p:plain
Node Exporterのダッシュボード

moco

 ここまでで基本的なことはだいたいできるKubernetesクラスタができました。ついでなのでもう一個使えるコンポーネントをデプロイしておきます。
 mocoはCybozuが開発しているMySQLオペレータで、MySQLのSemi-sync replicationを使ったクラスタを提供します。使い勝手が通常のMySQLと変わらないこと、最悪どうしようもなくなったら社内で聞けばなんとかなるやろと思ったので導入してみました。インストールも↓のドキュメントに従うだけでよく、非常に簡単です。

cybozu-go.github.io

 作成するMySQLClusterリソースで永続化ボリュームの大きさを指定できるのですが、例では1GBとなっています。自分の手元では容量不足で起動しなかったりしたため、5~10GB程度は割り当てておくと良いと思います。またnameは mysql-data で固定です。最初適当な名前を付けて失敗しました*10

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  name: test
spec:
  ...
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

開発環境

 大きな変更をいきなり本番クラスタに当てるのは怖いですよね。というわけでおうちKubernetesクラスタも開発環境を用意するようにしました。
 クラウドインスタンスを作ることも考えましたが、たまたま手元にそこそこスペックの良いWindowsデスクトップマシンがあるので、これにVMを立てまくることにしました。

  • CPU:Ryzen7 3700X(8コア 16スレッド)
  • RAM:64GB
  • SSD:2TB

VMを5つ建てます。それぞれ以下の役割を持ちます。

  • maas-master:MAASをインストールしておくサーバ
  • maas-node1:KubernetesのMasterノード
  • maas-node2:KubernetesのWorkerノードその1
  • maas-node3:KubernetesのWorkerノードその2
  • maas-node4:KubernetesのWorkerノードその3

f:id:pudding_info:20211204141409p:plain
HyperVで建てたVMたち

雑に作っていったらスペックがまちまちになってしまい、でもまぁ困ってないのでいいかなということでそのままになっているという雑な図です。動作確認のためにデスクトップPCを起動しておく必要があるとか、GitOpsでデプロイはできるが外部からのアクセスは許可していないので実質開発環境がこのデスクトップPCに固定化されているところがイケてないところです。

 基本的にこの環境で動作を検証し、問題なければ変更をreleaseブランチにマージ→本番環境のArgoCDがそれをsyncして適用という流れになっています。とはいえ本番環境でしか起きない問題なども度々引いており、なかなかうまくいかないなという感じです*11

今動いているもの

 汎用的なWebクローラーのようなものを書いて新着通知をしたりしています。特に速報性は求めていないのでかなり緩やかなペースでの通知ですが、DBがないとつらくAWS LambdaやGoogle Cloud Functionsに地味に乗せづらかったものです。DBスキーマの変更までGitOpsで完結しているので、コードを書いてリリースまでクラスタに触れる必要がありません。このあたりについてはまたいずれ記事を書ければ良いなと思っています。
 他は気になった物をときどきデプロイする程度で、あまり安定的に動いてるコンポーネントはありません。何か面白いものを思いつくのを待っています。

これからやりたいこと

バックアップとリストア

 今のところRook/Cephが崩壊するとデータを全ロストします。うちにはNASもあるので、定期的にデータのバックアップを取りたいところです。また、バックアップは取っただけでは意味が無く、それをリストアできる必要があるのでリストアの方法についても探求していきたいと思っています。  おうちクラスタでは軽い気持ちでバージョン上げたり新しいものを入れたりしがちな上、壊れると直すのが面倒になって放置しがちです。最悪クラスタを一度壊して作り直してもOKという体制を整えて長く使ってあげたいですね。

監視の充実

 VictoriaMetricsとGrafanaを入れたとはいえ、まだまだ中身が追いついていません。少しずつ拡充していければなと思っています。PromQLが難しすぎる……

まとめ

以上です。皆さんの素敵なおうちKubernetesクラスタ情報お待ちしております。

明日以降のCybozuアドベントカレンダーもお楽しみください!


*1:真面目なことを言っておくと、現実世界の複雑な部分をクラウドプロバイダに押し付けっぱなしにするだけでなく、自分で体感した方がよいと思っているためです。やればやるほどマネージドサービスのすごさを体感できて安く感じるのでお得です。

*2:インテル® アクティブ・マネジメント・テクノロジー (インテル® AMT) | インテル

*3:NUCでvPro対応しているものは非常に少ないためレアですが、LenovoDELL・HPなどから出ている小型PCには対応しているものが豊富にあります。入手性はそれらの方が良いでしょう。

*4:poyo.hatenablog.jp

*5:IPv4の場合。IPv6の場合はNDP。

*6:ここで割り当てるIPアドレスはプライベートIPアドレスです。外部にサービスを公開したい場合、ルータのNAPTを使ってルーティングする必要があります。

*7:主に家の中で引ければ良いので、名前解決にはルータ搭載の簡易DNSサーバを使っています。証明書は必要ですがDNS-01チャレンジならばHTTPサーバを公開しておく必要もなく、外部に一切公開せずに運用が可能です

*8:正確にはv0.16.0の時点で追加のフラグを与えることで解消できるようになり、v0.17.0でそのオプションがデフォルトで有効化されました。 github.com

*9:適用直後しばらくはダッシュボードの取得時に Too Many Requests のようなエラーが出ていたのですが、しばらく放置すると取得できたのかダッシュボードが見れるようになっていました

*10:ちゃんとドキュメントを読め……

*11:逆にGrafana operatorでは自分の開発環境でのみ起きる問題を引き当ててしまい、現状本番環境に当てないと動作確認できないという最悪な状況になったりもしています。 github.com

マルチアーキテクチャ対応イメージのビルドをどうにか早くしたかった

マルチアーキテクチャ対応イメージって何?

 最近ではApple Siliconの登場や、Oracle CloudのAmpere A1 Computeインスタンスなど、ARMアーキテクチャ採用のプロセッサに実際の開発などで触れる機会も増えてきました。 現在広く流通するx86_64に対応するIntelAMD製のプロセッサと比べて、ARM系のCPUには高い省電力性能などに期待が集まっています。趣味開発レベルの話をすると、Oracle CloudのAlways FreeなA1インスタンスは非常に魅力的であり、是非活用したいところです。

 ただしARM系CPUではx86_64向けにビルドされたバイナリをそのまま動かすことはできません。当然Dockerイメージについてもx86_64向けのイメージをarm64のマシンで動かすことはできない*1ため、arm向けにビルドされたDockerイメージを別で用意する必要があります。簡単に思いつく方法としては

  • イメージごと分けてしまう方法
    • 例:hoge/fuga-arm64:latest, hoge/fuga-amd64:latest
  • タグごとに分けてしまう方法
    • 例:hoge/fuga:latest-arm64, hoge/fuga:latest-amd64

などがあると思います。しかし、これは利便性を大きく損ないます。docker-compose.ymlだったり、コンテナを起動するスクリプトだったり、Kubernetesマニフェストだったりには基本的にイメージ名やタグがハードコードされています。環境ごとにこれを書き換えるのは非常に面倒でしょう。

 マルチアーキテクチャ対応イメージとは、ある単一のイメージおよびそのタグに、複数のアーキテクチャ向けのイメージを紐付けたものを指します。利用者側で自身のアーキテクチャに合わせて最適なイメージが選択されて使用されるため、アーキテクチャを意識してイメージ・タグを書き換える仕組みを用意する必要がありません。例えばDocker Hubで配布されているベースイメージ( docker.io/library/* )などが多数のアーキテクチャに対応したマルチアーキテクチャ対応イメージになっています*2

どのアーキテクチャに対応しているか、Dockerとしてのサポートの範囲などは以下に記述があります。 https://github.com/docker-library/official-images#architectures-other-than-amd64

どうやって作るか

単に別アーキテクチャでビルドして同じイメージ・タグでレジストリにpushすると、後からpushされた方で上書きされて単一のアーキテクチャ向けのイメージになってしまいます。正しいマルチアーキテクチャ対応イメージの作り方には大きく分けて2つの方法があります。

docker manifestコマンドを使う

 どうにかして各アーキテクチャで動く環境を手に入れ、その上でdocker buildしたイメージを別々のイメージ名ないしタグでレジストリにpushしておきます*3。ここでは例えば amd64/image:latestarm64/image:latest としておきます。これらをまとめて一つの my/image:latest としたい場合、下記の様にコマンドを実行します。

docker manifest create my/image:latest \
    amd64/image:latest \
    arm64/image:latest

これで my/image:latestマニフェストができたのでこれをレジストリにpushします。

docker manifest push my/image:latest

 この方法は(おそらく)Docker Hubのオフィシャルイメージで使用されています。例えばUbuntuのイメージは対応アーキテクチャごとにそれぞれ別々のorgに存在します。

利用者側では単にubuntu:20.04などのイメージを指定するだけで、適したアーキテクチャのイメージがpullされます。

docker buildxコマンドでビルドする

 buildxとは、BuildKit*4に基づいてより発展的なビルド方式を提供するdockerコマンドのプラグインです。

github.com

 buildxではQEMUを利用した他アーキテクチャのエミュレーション環境や、実際にそのアーキテクチャで動作するノードを仮想的な Builder に紐付け、それらを使ってビルドを行い、最初からマルチアーキテクチャに対応したマニフェストを用意することが出来ます。 ただし、ローカルのDocker環境からマルチアーキテクチャマニフェストをそのまま取り扱うことはできないため、一度レジストリにpushした後に自身のアーキテクチャに対応するイメージをpullすることでしかそのビルドしたイメージを使うことは出来ません。

 試した環境の情報は以下の通りです。

 まずはBuilderを用意します。ここではDocker Desktop for Macを使っています。WindowsのDocker Desktopではおよそ同じ手順で出来るとは思いますが、Linuxでは追加の手順が必要になると思います*5。本当はLinuxでも確かめるつもりだったのですが、諸事情でデスクトップが死んでいるので諦めました*6

 ローカルでマルチアーキテクチャ対応イメージをビルドする場合、docker-container ドライバの Builder を作ることが必要です。

docker buildx create \
     --name multi-arch-builder \
     --driver docker-container \
     --platform linux/arm64,linux/amd64

今回はサンプルとしてGo製のcowsayであるNeo-cowsay*7をビルドするだけのDockerfileを用意してみました。 golangイメージはマルチアーキテクチャ対応しているので、Dockerfile自体に工夫がなくともamd64/arm64両方に向けてビルドできます。

FROM golang:1.17.1 as builder

ARG VERSION=latest

RUN go install github.com/Code-Hex/Neo-cowsay/cmd/cowsay@${VERSION}

FROM gcr.io/distroless/static

COPY --from=builder /go/bin/cowsay /usr/bin/cowsay

ENTRYPOINT ["/usr/bin/cowsay"]

先ほどの Builder を指定して、linux/amd64linux/arm64 向けにビルドしてみます。

$ docker buildx build \
    --builder multi-arch-builder \
    --platform linux/amd64,linux/arm64 \
    -o type=image,push=false \
    -t cowsay:latest \
    --no-cache .
[+] Building 91.3s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                    0.4s
 => => transferring dockerfile: 274B                                                                                                                    0.0s
 => [internal] load .dockerignore                                                                                                                       0.3s
 => => transferring context: 2B                                                                                                                         0.0s
 => [linux/arm64 internal] load metadata for gcr.io/distroless/static:latest                                                                            1.0s
 => [linux/arm64 internal] load metadata for docker.io/library/golang:1.17.1                                                                            1.4s
 => [linux/amd64 internal] load metadata for docker.io/library/golang:1.17.1                                                                            1.3s
 => [linux/amd64 internal] load metadata for gcr.io/distroless/static:latest                                                                            1.4s
 => CACHED [linux/amd64 stage-1 1/2] FROM gcr.io/distroless/static@sha256:912bd2c2b9704ead25ba91b631e3849d940f9d533f0c15cf4fc625099ad145b1              0.0s
 => => resolve gcr.io/distroless/static@sha256:912bd2c2b9704ead25ba91b631e3849d940f9d533f0c15cf4fc625099ad145b1                                         1.0s
 => CACHED [linux/amd64 builder 1/2] FROM docker.io/library/golang:1.17.1@sha256:285cf0cb73ab995caee61b900b2be123cd198f3541ce318c549ea5ff9832bdf0       0.0s
 => => resolve docker.io/library/golang:1.17.1@sha256:285cf0cb73ab995caee61b900b2be123cd198f3541ce318c549ea5ff9832bdf0                                  1.1s
 => CACHED [linux/arm64 stage-1 1/2] FROM gcr.io/distroless/static@sha256:912bd2c2b9704ead25ba91b631e3849d940f9d533f0c15cf4fc625099ad145b1              0.0s
 => => resolve gcr.io/distroless/static@sha256:912bd2c2b9704ead25ba91b631e3849d940f9d533f0c15cf4fc625099ad145b1                                         1.1s
 => CACHED [linux/arm64 builder 1/2] FROM docker.io/library/golang:1.17.1@sha256:285cf0cb73ab995caee61b900b2be123cd198f3541ce318c549ea5ff9832bdf0       0.0s
 => => resolve docker.io/library/golang:1.17.1@sha256:285cf0cb73ab995caee61b900b2be123cd198f3541ce318c549ea5ff9832bdf0                                  1.1s
 => [linux/amd64 builder 2/2] RUN go install github.com/Code-Hex/Neo-cowsay/cmd/cowsay@latest                                                          17.1s
 => [linux/arm64 builder 2/2] RUN go install github.com/Code-Hex/Neo-cowsay/cmd/cowsay@latest                                                          80.5s
 => [linux/amd64 stage-1 2/2] COPY --from=builder /go/bin/cowsay /usr/bin/cowsay                                                                        1.0s
 => [linux/arm64 stage-1 2/2] COPY --from=builder /go/bin/cowsay /usr/bin/cowsay                                                                        0.9s
 => exporting to image                                                                                                                                  5.5s
 => => exporting layers                                                                                                                                 4.0s
 => => exporting manifest sha256:d1ee75cd239140bd0210554ada9707162750d4766635e6abc17725aa70bac5d3                                                       0.3s
 => => exporting config sha256:afe4e4a473376655e0377c92aa75ef7bdcddc3a3fffd95e285fda7d0db4ce1df                                                         0.3s
 => => exporting manifest sha256:df3198ab96b70b1e5b0584d5cc39065772c1268946e088e0bb2f1f038ff81400                                                       0.3s
 => => exporting config sha256:af3de61b4d4202ee4c8fa513b189e620443a462b5487d39cc491fee08776d39d                                                         0.3s
 => => exporting manifest list sha256:e1e7b18094b953f584370083971a13fdcc12290239e575739e0b7f91a9d83ca1                                                  0.3s

無事イメージがビルドできたようです。とはいえ、これを使うためにはレジストリにpushする必要があるのですが…… 。現状生成されたマニフェストdocker manifest inspect等で見ることもできないような気がします。見る方法を知っている方、教えてください🙏

 ちなみに docker ドライバーのBuilderを使い、単一のplatformを指定し、-o type=image,push=false ではなく --load というオプションを組み合わせると、 docker image ls などで表示されるイメージ一覧にビルドされたイメージが追加されます。動作確認したい場合は試してみてください。

buildxとQEMUによるビルドは遅い

 非常に簡単にマルチアーキテクチャ対応イメージをビルドすることができました。しかし、実行時間を見てみると、 go install してcowsayをビルドしている部分の時間は以下の様になっていました。

およそ5倍近い差が開いています。複数回実行しても、おおよそ4〜6倍程度、arm64向けのビルドが遅いことがわかりました。QEMUの分のオーバーヘッドがあるため仕方ないのですが、ビルド対象の規模が大きくなるほど遅くなる他、apt-getなど他の操作も有意に遅くなりました。場合によっては10倍、20倍と差が開くこともあり、CIなどで長時間待たされることになりました。

CIでのビルドを速くしたい

 当初、プライベートリポジトリでマルチアーキテクチャ対応イメージをCIでビルド・pushしたいと考えていました。このリポジトリではCIにGitHub Actionsを用いています。GitHub Actionsは従量課金で毎月2000分のビルドまでは無料ですが、色々複雑なことをしていたら一回のビルド時間が容易に15分、20分と伸びていってしまいました。できれば課金はしたくないですし、手作業でビルド・pushもしたくありません。何より一度コードをpushしてから結果が分かるまで20分もかかるというのはかなり苦痛です。

 どうにか高速化できないかなと少し足掻いてみました。試したリポジトリ・アップロードしたイメージは以下にあります。

github.com

https://hub.docker.com/repository/docker/pddg/multi-arch-image-sample

アーキテクチャごとに別ジョブでビルドする

 buildx自体はビルドをリモートマシンに委譲できるなどスケーラブルな仕組みになっているのですが、CIの仕組みとは非常に相性が悪いです。CIでは定義された複数のジョブを決められたスペックのインスタンスで実行します。スケールする単位はジョブやワークフローごとであり、ある一つのジョブ内で計算リソースをスケールさせるためには動的にクラウドのリソースを作るようなことが必要になります*8。そんな面倒なことはしたくないので、対象アーキテクチャごとにジョブを分けて並列にビルド・レジストリにpushして、それらが全て完了した後に後続のジョブで docker manifest コマンドを叩く方法を検討してみました。

ただ今回は各アーキテクチャごとに別ジョブでビルドしても、ワークフロー全体に占める時間はほとんど変わりませんでした。より大規模なプロジェクトや、多数のアーキテクチャをサポートなどするなどすると差が出てくると思います。

クロスビルドを活用する

 Goは幸いクロスビルドできる言語です。QEMUなんて挟まなくとも、Pure Goで記述されていればx86_64のマシンからarm向けのバイナリをビルドすることが出来ます。これを使えば、わざわざ重い環境でビルドしなくてもよくなるはずです。

github.com

実際に今回はこの方法で2分近くかかっていたCIを40秒程度まで短縮することが出来ました。

 ただし嬉しくない点がいくつかあります。

  1. Dockerfile内でアーキテクチャを認識する必要があるかもしれない
  2. docker buildで完結しないため、開発者の個々の環境に左右される可能性がある

 まず1についてですが、Goのアーキテクチャの指定方法と、Dockerのアーキテクチャの指定方法に差異があるため、これをどこかで吸収する必要があります。各アーキテクチャ向けにビルドしたい場合、ビルド時に以下の様に環境変数を設定します*9。arm64で GOARM=8 を付けると Invalid GOARM value. Must be 5, 6, or 7. というエラーが出るので注意が必要です。

GOOS=linux GOARCH=amd64 go build .
GOOS=linux GOARCH=arm GOARM=7 go build .
GOOS=linux GOARCH=arm64 go build .

Dockerfile内からこれらの値について知ることができる特殊な引数は以下の通りです。

# linux
ARG TARGETOS

# amd64, arm, arm64
ARG TARGETARCH

# linux/arm/v7 のみ v7 。他は空文字。
ARG TARGETVARIANT

arm/v7の場合は TARGETVARIANT に値が存在しますが、amd64やarm64の場合は現状 TARGETVARIANT は空文字列です。もしかしたら将来的にarm64/v9とかが出てくるかもしれないと思うと、結構嬉しくない状態だなと思っています(ARMの事情に明るくないので適当言っています)。x86とarm以外については今回検討しなかったため、もしかしたら他にも厄介な点があるかも知れません。

 2については言うまでも無く、これまではmulti stage buildによってDockerさえあればビルド出来る状態でした。しかしクロスビルドによってDockerイメージの生成とバイナリの生成を分離したことで、DockerだけでなくGoの環境が必要になってしまいました。幸いGoはインストールも簡単なので、他言語と比べると非常に楽な方ではあると思います。

buildxに対する雑感

 buildx周りのコマンドは洗練されていない印象が強いです。buildx のサブコマンドは対応するリソースとアクションが分かりづらく、また未だ知見も少ないことからかなり手探りでした。一方でGitHub Actionsで使えるように用意されているactionは広く使われることを意識しているのか、かなりわかりやすく整えられていると感じました。とはいえスケールする構成にするためにはdocker manifestコマンドなどを知っていなければいけないなど、まだ発展途上な仕組みであることには違いないようです。

 また、複数プラットフォームのイメージをラップトップで一度にビルドするのは計算リソースの問題から厳しいことが分かってきました。検証しているとひっきりなしにMacBook Proのファンが唸るのでかなり厳しい気持ちになってしまいました。力こそパワー、ラップトップを捨ててつよつよワークステーションを買いましょう。

結論

  • 強い計算リソースを買えば問題は解決

こうすると良いよ的な情報があればどしどし寄せて頂けると幸いです。

参考

  • www.docker.com
    • docker manifestによる方法を「The hard way」と言っていますが、正直こっちの方がわかりやすいかつスケーラブルな仕組みにしやすいと思います。

 

*1:QEMUなどによってパフォーマンスを犠牲にして動かすことはできます

*2:www.docker.com

*3:実際には後述するbuildxを使って別プラットフォームを指定しても良いです

*4:github.com

*5:www.yukkuriikouze.com

*6:どうもメモリがぶっ壊れたようでメモリテストで異常が検知されるのですが、32GBモジュール×2の交換品は果たしてちゃんと確保されているのでしょうか……

*7:github.com

*8:もしかしたら将来的にはGitHub Actionsで動的にbuildxのBuilderノードを追加して叩くようなAPI/actionができるかもしれませんが、わざわざそこまでしてくれるかは微妙だろうと自分は思っています

*9:github.com