ぽよメモ

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

イケてる高級キーボードmountain ergoを買った

My new keyboard...

KBDfansのmountain ergoを買いました。

[Restock] KBDfans mountain ergo keyboard kitkbdfans.com

f:id:pudding_info:20220306105849j:plain

f:id:pudding_info:20220306113821j:plain
背面のロゴ

f:id:pudding_info:20220306113842j:plain
リストレストのロゴは彫り込み

キースイッチ

DUROCK POM T1 Sunflowerを採用しました。

talpkeyboard.net

基本的にリモートワークをしているのでマイクが音をできるだけ拾わないよう静音軸、かつリニアは苦手なのでタクタイルで探していました。以前Choco60を組む時に採用したZilent V2*1でも良かったのですが、ガタつきを感じる点を以前から残念に思っており、せっかくなので新しいものに手を出すことにしました。

最初はDUROCK T1 Shrimp*2を使うつもりだったのですが、ついでに試したT1 Sunflowerの滑らかさにハマったのでこれにしました*3

f:id:pudding_info:20220306113454j:plain
ビルド過程を撮るの忘れてた

このキースイッチは軸の擦れるような感覚がほぼ無く、非常に滑らかに押し込める感触が素晴らしいです。更にガタつきもほぼなく、精度の良さを感じました。押下直後に比較的大きなパンプを感じる点は人を選ぶかも知れませんが、タクタイルスイッチとしての出来は非常に良いと感じました。ファクトリールブ済みで初心者でもオススメしやすいのも良いところですね。願わくばこの静音版が出ますように。

基本的にはファクトリールブの状態で満足だったのですが、スプリングに軽くKrytox GPL 105を塗布するとスプリングの甲高い音の響きが緩和されるような気がしたので、わざわざ一度全部バラしました*4

f:id:pudding_info:20220306112537j:plain
バラすのは10分、戻すのに2時間

総じて非常に満足感の高いスイッチでしたのでオススメです。

キーキャップ

KAT BOW PBTにしました。

KAT BOW PBT Keycaps setkbdfans.com

f:id:pudding_info:20220306113721j:plain
比較的柔らかい印象のキーキャップ

mountain ergoと一緒に発送してもらうと安上がりだったのでKBDfansで買いましたが、国内では今のところ遊舎工房で手に入るようです。

KAT BOW PBT Keycaps setshop.yushakobo.jp

KATはKAT Alphaの印字にじみ問題*5があり少し警戒していましたが、今のところ品質は良さそうで問題は感じていません。
強いて言うなら、思ったより暖色系の白でmountain ergoの硬い白色とはちょっとイメージが違った*6のが残念ではありました。

他のかわいらしいキーキャップと悩みはしましたが、必要なコンベックスキー*7の長さ(2uと2.75u)の合うセットがこれくらいしかなく、実際のところ選択肢がほぼないという状況でした。GMK Nimbus*8の色が好みなので、届いたらそれに変えようかなと思っています。

ファームウェア

公式で案内されている通り、ydkb.io を使うことになります。 ydkb.io

正直言うとVIAとかにして欲しかったという気持ちはありますが、仕方ありません。書き込み方は簡単で

  1. ydkb.ioでキーマップを作る
  2. ydkb.ioから作ったファームウェアをダウンロード(MOUNTAIN.BINみたいなファイルをダウンロードできる)
  3. mountain ergoのesc(一番左上のキー)を押しながらUSBケーブルを接続
  4. 書き込み可能な外付けディスクとして見えるようになるので、MOUNTAIN.BINをコピペ
  5. 既にあるMOUNTAIN.BINを上書き
  6. USBケーブルを挿し直し

これで適用が完了します。公式の手順ではMacの場合は上書きせず消してから書き込めと書いてあったんですが、これをすると空き容量不足で書き込めなかったため単に上書きしました。それでもうまく動いているのでたぶん大丈夫っぽいです。

一つだけ罠ポイントを紹介しておきます。Windowsで自作キーボードをUS配列として使っている人はご存じかと思いますが、変換・無変換キーはうまく認識されません。ydkb.ioを使っていても例外ではなく、キーマップで変換・無変換キーを割り当ててもWindowsからは別のキーとして認識されます。自分は代わりにF13、F14を割り当て、PowerToysのKeyboard managerを使ってそれぞれ無変換/変換にリマップしています。

f:id:pudding_info:20220306120841p:plain
PowerToysマジ便利

かかった金額

項目名 値段
mountain ergo本体
(Top/bottom E-white, aluminium plate, Hot swap PCB)
$462
KAT BOW PBT $79
Route package protection*9 $11.33
小計 66297

キースイッチが

項目名 値段
お試し購入 2050円
DUROCK T1 Sunflower 7710円
小計 9760円

総額76057円でした。

届く過程で一度日本に入ってきてそのままハノイに出て行ってしまい、また日本に入ってくるみたいな謎ルートを通っているのを見てゲラゲラ笑えた点などを加味しても高いですね。まぁ仕方ない。
キーキャップとキースイッチ以外はほんとにほぼ全部入っているので、こんなもんかなという印象です。

なんで買ったの?

Choco60には非常に満足していました。慣れ親しんだ物理配列、カスタマイズできる機能、あとGopherくんキーキャップ*10がかわいい。

f:id:pudding_info:20220306114439j:plain
おだんごGopherくん

一番大きな理由としてはメンタル的なところでした。ここしばらく特に良いことがなく、むしろ世界情勢はどんどん悪い方向にいっており、気分が滅入ってきていたことでパフォーマンスが下がり気味になっていました。ちょうど賞与も入ったので、自分の精神的な鼓舞のために前から気になっていたAlice配列のお高いキーボードを買ってみました。
どの程度効果があるかは分かりませんが、少なくとも使っていて楽しいですし、作っている最中も(いろいろあったけど)わくわくしたので体験としては非常に良かったです。

あとは前述したとおり前からAlice配列のキーボードは気になっており、昨年はMeridian R2を買うかどうか二週間くらい悩んで結局やめていたというのもありました。

ai03.com

基本的には分割キーボード以外を使うつもりはなかったのですが、どうしても筐体が軽く打鍵感がおもちゃっぽくなりがちである点などから、ちょっと気分転換してみてもよいかなと思っています。今年は後半にKinesis Advantage 360のカスタム可能モデルが発売される(かも)という話もあり、それまでしばらくはmountain ergoとChoco60を併用してみるつもりです。

まとめ

f:id:pudding_info:20220306123132j:plain
作業領域をもう少し俯瞰で見た様子

mountain ergoのいいところ

  • 非常に重くしっかりしたつくりの美しい筐体(鈍器)
  • 心地よい打鍵感
  • 全部入りなので後はキーキャップとキースイッチを揃えれば組めるという初心者設計
  • USB-C対応・キーマップはブラウザがあれば編集可能

mountain ergoの微妙なところ

  • ydkb.ioに依存している
  • 値段が高い
  • 合うキーキャップセットが限られている
  • リストレストはもう少し明るい色の選択肢が欲しかった
  • 初心者向けではあるけど、ビルドガイドは付属していないのでやっぱり難易度は高め

メンタルを救う代わりに財布は犠牲になったのだ……これは致し方のない犠牲。
皆さんにも気分転換にお高いキーボードに変えてみるというソリューション、おすすめしておきます。


*1:poyo.hatenablog.jp

*2:talpkeyboard.net

*3:@社の人 今度から会議中にカタカタ聞こえたらたぶん僕です

*4:気のせいかも知れません

*5:talpkeyboard.net

*6:写真では思ったよりわからないですね

*7:スペースバーのような、上に凸の形状になっているキー

*8:geekhack.org

*9:輸送途中の貨物の紛失や破損に対する保証。気がついたら付いてました(よく読んでなかった……)。

*10:kochikeyboard.stores.jp

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