ドキドキするとき無敵でしょ

映画とプログラミングの話

2024年のふりかえり

年末コインランドリーで乾燥機を使っていたところ、終了した直後にカップルが間違えてしまい、追加で40分乾燥がはじまりました。  

というわけで今年のふりかえりです。あんま文章にすることもないので雑に箇条書きする

仕事

  • 別チームのリーダーからヘッドハントされて 5 月に突然部署異動になった
  • それまでアプリケーションの API ばっか書いてた
  • と思ったら異動後はずっと CI / CD を整えてた
  • でもいろいろあって CI / CD 整えながらアプリケーションコードを読んで直したりしてた
  • Kubernetes 周りは業務で触れるに越したことはないな~と思っていたら本当に触ることになってしまいちょっとびっくりした
  • ベテランに囲まれる 3 年目のひよっこをやっている
  • 来年はアプリケーションもさらにゴリゴリ書くことになりそうだ
  • 場合によっては RFC やら論文やら漁ることになる仕事もありそうではある
  • 難易度がたけぇ。面白いからいいけど
  • ネットワーク周りに関わるようなもの作れたら作りたいな~

プライベート

  • いろいろあった
  • 筋肉がめっちゃ増えた。というかガタイがめちゃくちゃ良くなった
  • 体力の上限値が伸びた
  • 懸垂をワイドで3,4回程度ならできるようになった。これはだいぶうれしい
  • ただ体格が変わりすぎて服をめっちゃ捨てた

全体的な感想

仕事の難易度がいきなり上がって草だった。ヘッドハンティングって本当にあるんだ...
いきなりフル出社になって知ってる人たちがいなくなってしまったり、別れもそれなりにあった。
ただ、部署異動したのは結果的に良かった。学習の必要な量は大幅に増えたが、がっつりやればそれだけ返ってくるともいえるだけの技量は手に入る。
いろいろあったし、来年は目標立ててがっつり潜るぞ、の選択をする覚悟ができたので良かったかも。

最後に

カップルが「どうしよう...」としばらく呆然としていたので観察していたら去っていった。というわけでささっと洗濯物を取り出して事なきをゲットした。
40 分だと乾き具合が微妙だったので、ちょうどいい感じに洗濯物が乾いてよかった。

『Design It!』を読んだ

年末は積読を消化するのにとても良い....
というわけで2本目です。『Design It! ―プログラマーのためのアーキテクティング入門』を読みました。

www.oreilly.co.jp

感想

小さなチームに異動したので、やることが増えたのもあり、自分自身設計を少しでもかじっておかないと来年困っちゃうな~とか考えてたら会社にあったので持ち帰って読んだ。
アーキテクチャをどう考えるか?ということの入門としてかなり良かった。
ステークホルダーやチームメンバーを巻き込んでガンガンディスカッションしていこうぜ!みたいな感じだった。
あとやっぱり図に起こすって超大事なんだ...ってなった。複数種類作ったっていい。俺もそうしていきたい。

個人的には「選ばなかった道」の話が好きだった。なんでそうしたのか?俺ならこうするが?みたいな気持ちになることはあるので、ちゃんとだめだった理由を後から知られるのはいいことだなぁ、と思った。

これもまた読み返すと面白い本だ。

『プログラミング作法』を読んだ

Tidy First? 読みたいなぁ、と思っていたら t-wada さんのツイートが流れてきて気になったので読んだ。

感想

めちゃくちゃ面白かった......
書籍自体は純粋に全部面白かったが、第4章のインターフェースの話が本当に面白くて興奮した。
1つの機能を例に挙げて、ステップバイステップで改善していくという内容だった。
考え方が言語化されていて、これは自分も意識してやった方がいいなと思わされた。結構フレームワークに近いエッセンスを感じた。うまみがすごすぎた。

全体的に根底の考え方を固めるような内容で、また読みたいと思った。インターフェースの章はあらゆる人に読ませたい。
Tidy First? も近いうちに読むぞ、になった。

感動した Go のテクいコード

この前上司の PR を読んでいたら「理解できるけどどうすればこんなこと思いつくんだ」と思ったコードがあり、聞いたら出典を教えてくれたので紹介する。

というわけでこちら。 strechr/testify のコードにそれはある。

github.com

私が感動したコードはこの getLen() 関数。

// getLen tries to get the length of an object.
// It returns (0, false) if impossible.
func getLen(x interface{}) (length int, ok bool) {
    v := reflect.ValueOf(x)
    defer func() {
        ok = recover() == nil
    }()
    return v.Len(), true
}

Named return values

まずこの関数を一目見て、最初に気づくのは Named return values かと思う。 このコードを読んだ時に、まずこの書き方をほぼ見かけたことがなかったので、挙動を追うのに手間取った。 使いどころとしては、引数がめっちゃ長いときに補完を効かせるとかそういったところらしい。

go-tour-jp.appspot.com

A Tour of Go のこのページの説明で Goでの戻り値となる変数に名前をつける( named return value )ことができます。戻り値に名前をつけると、関数の最初で定義した変数名として扱われます。 とあるように 戻り値に名前をつけるとゼロ値で初期化された変数として扱われる。

Effective Go の Named result parameters の項目にも named results are initialized and tied to an unadorned return という記載がある。

初期値が入っていることを確認するために A Tour of Go の関数をちょっと書き換えて実行してみる。 https://go.dev/play/p/xzmXKpjH5jk

package main

import "fmt"

func split(sum int) (x, y int) {
    if sum == 0 {
        return
    }

    x = sum * 4 / 9
    y = sum - x
    return
}

func main() {
    fmt.Println(split(17))

    // ゼロ値で初期化されているので 0 0 が返ってくる
    fmt.Println(split(0))
}

この関数の実行結果は

7 10
0 0

となり、戻り値に名前をつけるとゼロ値で初期化されるということを確認できる。

defer func() 内の recover()

getLen() で引数に渡されたreflect でオブジェクトが取得できなかった場合このコードは panic が起きる。 しかし、この関数では defer func() することにより panic が起きた場合 recover() して戻り値のゼロ値を返すようになっている。そのため getLen() は 0, false を返すことにより、関数が失敗したことを通知できる。 stretchr/testify はテストフレームワークだから無邪気に recover() して返すことができる、という背景があるからこそ書けるコードでもあるので、めちゃくちゃ合理的やん…!!!! とテンションがぶち上がった。

この関数自体は panic() を起こさなかった場合 recover() は nil を返すので問題がない。だいぶ気持ち良い。

感想

今までに Named return values を使ったコードをほとんど見たことがなかったのでかなり驚いた。 なんなら自分でも書いたことはないと思う。 Naked return できることを考えると正直あまり気が進まない。このコードの製作者はその点にも気を配って書いていて、洗練された美しいコードに出会えて感動した。 他にもテクいコードの話をしてくれたので、自分でも見つけたら紹介する。

上司が「 Go の標準パッケージや OSS のソースコードにはこういうテクいコードが色々あるから読んでみると良い」とアドバイスされたので、自分も定期的に読みたいと思う。

参考

Go Conference 2024 に運営スタッフとして参加した

今回は Go Conference 2024 に運営スタッフとして参加したので、その記録。

事の発端はこのツイートが流れてきたこと。

北海道にいるときは当日スタッフがメインで、運営スタッフでお手伝いしたりしていたイベントもあるのですが、東京に来てからは全然スタッフをやっていなかったのでこれはもうやるしかないと思い @tomato3713 さんを 巻き込んで 誘って一緒に応募しました。

数日後、二人共応募が通ったのでやりました✌️

スタッフとしてやったこと

自分の担当はスポンサー班でした。

スポンサー班はスポンサーに関すること全般だったので「あれ、思ったよりやることが多いぞ!?」となりました。

事前準備では質疑応答や相談、当日のブース配置、前日は届いた荷物のチェック、当日は受付業務や質問などの対応から搬出のチェックなど、もりもりでした。

他のメンバーが色々やっていたので、自分も積極的にメールの対応をしました。届いた質問をひたすら共有したり、回答内容を確認して返答したりが中心でした。

なんだかんだ楽しかったので良かったです。

当日

自分はあまりセッションを見ず、スポンサーブースを中心に回ったりしていました。ほとんどカンファレンスに一般参加したことがなく、こういうときに暇だとソワソワしてしまいがちなので受付の近くについついいたりしていました。

スポンサーブースは大盛況でセッション中は流石に減るかな?と思っていたらそれほど減っていませんでした。すごい

懇親会では TinyGo で自作キーボードされてる @sago35tk さんとお話していました。自作キーボード設計してる人がスタッフにいるのを知らなかったので驚きでした。
その後なんだかんだ4,5人ぐらいでずっと自作キーボードの話をしていて、自作キーボードはやっぱりいいなあ…になりました。

キメラスイッチ作ってルブしてる人もいたのでだいぶよかったです。

@tomato3713 さんを自作キーボードの沼に引きずりこむことに成功し、大変良い時間を過ごせました。

感想

毎回イベントのスタッフをするたびに「ハァハァ…疲れたから次は普通に参加するぞ…」と思うのですが、次の日には「またやるか~」になりがちです。 例のごとく来年はもっと早くから関わるぞ、になりました。 セッションはあんまり見られていないので、上がった資料を読みながら良…という気持ちで過ごそうと思います。

カンファレンススタッフはいいぞ。

OOC2024 当日スタッフ参加してきた

最近全然ブログが書けていなかった Gunzi です。OOC の前に投稿しようと思っていたブログが思ったより重くて間に合わなかった..

というわけで OOC に当日スタッフ参加してきたのでその備忘録を忘れないうちにまとめておく。

今回は事前準備、前夜祭、本編のフル参加をしてきた。 今回は友人を一人スタッフに誘っていたのだが、知り合いがいっぱいいたので全然アウェー感がなかった。 https://ooc.connpass.com/event/305258/ https://ooc.connpass.com/event/305241/

事前準備はいつもの蟹工船をしつつ、他のスタッフと談笑していた。Emacsユーザーを2人見つけることができたのでとても良かった。

前夜祭では早めに集まってスタッフ業務の説明を受けたり、前夜祭中にスタッフたちと談笑して明日の準備をしていた。

本編では8:45にお茶女に集合し、スタッフ業務をしながらカンファレンスに参加した。スタッフの朝は早い____

本編の感想

興奮してあまり眠れなかったので、受付業務でだいぶ疲れてしまい、スポンサー全部を回るのと、セッションを1つ聞いて終わってしまいもったいなかったなぁ…となってしまった。 あとから X で他の人のツイートを見たり、スライドを見ながらリアルタイムで聞かなかったことをかなり後悔した。次のスタッフは興奮せずにしっかり寝たい。 一緒に参加していたスタッフと、動画が公開されたら感想戦をしながら見よう、という話をしたのでそこでキャッチアップする。

セッションやスライドで自分が全然できてないポイントをいくつも見つけられたのと、体感でも勉強必要な部分だったので、モチベーションを得ることができた。 また、スポンサーブースでも普段聞けないような話を聞くことができてとても面白かった。ブース毎に色々取り組みが違っていて面白かった。
勘違いしてクイズのがしてしまったりもあったので、次はしっかり話を聞こう...

オフラインの一体感最高すぎたので、また機会を見つけてスタッフをやるぞ〜となった。こういうところで圧倒されながらモチベーションを獲得するのはとても気持ちがいい。維持していきたい。

P.S. お茶女のクロちゃんがすごいかわいい。

flannelのcni-pluginを読む

Windowsを起動すると4、5分でブルースクリーン、Gunziです。とてもつらい。

お盆休み中にプロトコル・スタック自作をしてだいぶモチベーションが回復したので、
しばらくk8sのネットワーク部分の探検をしようと思い立ち、とりあえずcni-pluginがよくわからんので、作ってみることにした。

色々やってみた結果、よくわからなかったので、よく聞くflannelのソースを読んで他のCNIプラグインがどのように動作しているのか?
をまずは調べつつ、自作CNIプラグインチャレンジをすることにした。

ただ、前提知識がやたらに多く、説明が抜けていたり若干間違えている可能性が高いので、ご了承いただきたい。

とりあえず手を動かす

どうやらShellで自作した人がいるようなので、自宅にk8sの環境を構築してやってみた。

www.altoros.com

ノード間の通信はうまくいかなかったが単一ノード内の通信はうまくできた。
どうやら入力自体は標準入力で受け取る関係上、理論上はどんな言語でも構わないようだ。

コードを読むとどうやらpodに仮想ニックを刺して作成したネットワーク名前空間に接続しているようだった。何をして動いてるかはなんとなく掴めた。

CNIプラグインとは

というわけでこいつがなんなのかについての話。

公式ドキュメントはこちら。

Network Plugins

よくわからん…
色々な資料やソースコードを読み漁ったところ、コンテナのネットワークを作成・削除するための仕組みらしい。ContainerNetworkingInterfaceだもん、そりゃそうか。

いい感じにk8sネットワークの仕組みの全体を解説している日本語資料があったので、ググりつつ、こちらで全体像を学ばせてもらった。見切り発車で始めたので、概要を掴むのにとても良かった。

speakerdeck.com

cni-plugin は仕様により実装するコマンドが決まっている。これはcni-pluginの仕様書で、この仕様書にあるCNI operationsの項目が実装する必要のあるコマンドになる。

仕様書によれば

  • ADD
  • DEL
  • CHECK

の3つを実装する必要がある。ここからはflannelのcni-pluginにある、ADD,DELCHECKに対応しているコマンドの部分を読んでいこうと思う。

なぜflannelかというと、有名なイメージが強く、システム自体がとてもシンプルなので参考にするのに向いていると思ったからだ。クラスターネットワークの仕組みもシンプルなので、実際に作成する際には参考にする。

実装を読んでみる

というわけでflannelのcni-pluginのソースコードを実際に読んでみた。 読んで思ったことや、処理についてコメントを思い思いに残しているので、いい感じにみなさんも読みとっていただければと思う。

github.com

cmdAdd()

cmdAdd()はコンテナをネットワークに追加する。
flannelではcmdAdd()→doCmdAdd→delegateCmdAddの順で処理している。
delegateCmdAddではinvoke.DelegateAddを呼び出している。
パッケージに説明があり、この関数はCNI ADD、もしくはJSONコンフィグを使用して指定されたdelegate pluginを呼び出している。
デフォルトではブリッジプラグインのため、構成時に指定されたブリッジプラグインのADDコマンドを実行している。
”bridge”以外が指定されていた場合はそれらを呼び出す。はず。多分。

delegateAddの名前の通り、最後にinvoke.DelegateAddを呼び、ブリッジプラグインのADDを実行している。
invoke package - github.com/containernetworking/cni/pkg/invoke - Go Packages

func cmdAdd(args *skel.CmdArgs) error {
// loadFlannelNetConf
// 標準入力でNetConfを受取り、ロードしてJSONをアンマーシャルする
    n, err := loadFlannelNetConf(args.StdinData)
    if err != nil {
        return fmt.Errorf("loadFlannelNetConf failed: %w", err)
    }

// /run/flannel/subnet.env からネットワーク構成を読み取る
    fenv, err := loadFlannelSubnetEnv(n.SubnetFile)
    if err != nil {
        return fmt.Errorf("loadFlannelSubnetEnv failed: %w", err)
    }

// delegateの値はflannelプラグインはデフォルトでブリッジプラグインに移譲
// 追加の設定値をブリッジプラグインに渡す必要がある場合はDelegateフィールドを利用
    if n.Delegate == nil {
        n.Delegate = make(map[string]interface{})
    } else {
// それぞれDelegateマップにキーが存在するかをチェックしている
// typeが存在するかつ値が文字列でない
        if hasKey(n.Delegate, "type") && !isString(n.Delegate["type"]) {
            return fmt.Errorf("'delegate' dictionary, if present, must have (string) 'type' field")
        }
// nameが存在する
        if hasKey(n.Delegate, "name") {
            return fmt.Errorf("'delegate' dictionary must not have 'name' field, it'll be set by flannel")
        }
// ipamが存在する
        if hasKey(n.Delegate, "ipam") {
            return fmt.Errorf("'delegate' dictionary must not have 'ipam' field, it'll be set by flannel")
        }
    }

// runtimeConfigはomitemptyになっているため、空のときはスキップされる
    if n.RuntimeConfig != nil {
        n.Delegate["runtimeConfig"] = n.RuntimeConfig
    }

    return doCmdAdd(args, n, fenv)
}

// doCmdAddの実装
func doCmdAdd(args *skel.CmdArgs, n *NetConf, fenv *subnetEnv) error {
// テストだとcni-flannelなどを渡している
    n.Delegate["name"] = n.Name

// キーが存在しない場合はデフォルトでブリッジプラグインを選択
    if !hasKey(n.Delegate, "type") {
        n.Delegate["type"] = "bridge"
    }

// 
    if !hasKey(n.Delegate, "ipMasq") {
        // if flannel is not doing ipmasq, we should
        // subnetEnv構造体のipmasqを取得し(flannelの設定のデフォルトではTrueっぽい)、反転させて代入
        ipmasq := !*fenv.ipmasq
        n.Delegate["ipMasq"] = ipmasq
    }

// subnetEnv構造体の値を参照して代入
    if !hasKey(n.Delegate, "mtu") {
        mtu := fenv.mtu
        n.Delegate["mtu"] = mtu
    }

// ブリッジタイプが指定されている場合はisGatewayをtrueにする
    if n.Delegate["type"].(string) == "bridge" {
        if !hasKey(n.Delegate, "isGateway") {
            n.Delegate["isGateway"] = true
        }
    }

// CNIVersionが0でなければDelegateにも同様に設定
    if n.CNIVersion != "" {
        n.Delegate["cniVersion"] = n.CNIVersion
    }

// netconf構造体にipamの値が存在すれば入力の値を使用し、置換もしくは補完する
    ipam, err := getDelegateIPAM(n, fenv)
    if err != nil {
        return fmt.Errorf("failed to assemble Delegate IPAM: %w", err)
    }
    n.Delegate["ipam"] = ipam
    fmt.Fprintf(os.Stderr, "\n%#v\n", n.Delegate)

    return delegateAdd(args.ContainerID, n.DataDir, n.Delegate)
}

func delegateAdd(cid, dataDir string, netconf map[string]interface{}) error {
// netconfのマーシャル
    netconfBytes, err := json.Marshal(netconf)
    fmt.Fprintf(os.Stderr, "delegateAdd: netconf sent to delegate plugin:\n")
    os.Stderr.Write(netconfBytes)
    if err != nil {
        return fmt.Errorf("error serializing delegate netconf: %v", err)
    }

// cmdDel用に一時NetConfの保存
    // save the rendered netconf for cmdDel
    if err = saveScratchNetConf(cid, dataDir, netconfBytes); err != nil {
        return err
    }

// 指定されたプラグインでADDを実行する
    result, err := invoke.DelegateAdd(context.TODO(), netconf["type"].(string), netconfBytes, nil)
    if err != nil {
        err = fmt.Errorf("failed to delegate add: %w", err)
        return err
    }
    return result.Print()
}

cmdDel()

ADDと同じ引数を渡し、コンテナを削除する。
最後にdelegateDelを呼び出してブリッジインターフェースを削除している。
doCmdAdd()でsaveScratchNetConf()を呼び、一時的に保存したデータを削除するところまでがワンセット。

func cmdDel(args *skel.CmdArgs) error {
// 標準入出力から値を読み込みパースする
    nc, err := loadFlannelNetConf(args.StdinData)
    if err != nil {
        return err
    }

// runtimeConfigをロードする
    if nc.RuntimeConfig != nil {
// nc.Delegateの値を代入する必要があるのでmakeし領域を確保
        if nc.Delegate == nil {
            nc.Delegate = make(map[string]interface{})
        }
        nc.Delegate["runtimeConfig"] = nc.RuntimeConfig
    }

    return doCmdDel(args, nc)
}

// saveScratchNetConf で保存したファイルをロードして利用
func consumeScratchNetConf(containerID, dataDir string) (func(error), []byte, error) {
    path := filepath.Join(dataDir, containerID)

    // cleanup will do clean job when no error happens in consuming/using process
    cleanup := func(err error) {
        if err == nil {
            // Ignore errors when removing - Per spec safe to continue during DEL
            _ = os.Remove(path)
        }
    }
    netConfBytes, err := os.ReadFile(path)

    return cleanup, netConfBytes, err
}

func doCmdDel(args *skel.CmdArgs, n *NetConf) error {
    cleanup, netConfBytes, err := consumeScratchNetConf(args.ContainerID, n.DataDir)
    if err != nil {
        if os.IsNotExist(err) {
            // Per spec should ignore error if resources are missing / already removed
            return nil
        }
        return err
    }

// deferなので最後にerrの発生がなければクリーンアップ(保存されているファイルの削除)が実行される
    // cleanup will work when no error happens
    defer func() {
        cleanup(err)
    }()

// 保存されているファイルを読み込み
    nc := &types.NetConf{}
    if err = json.Unmarshal(netConfBytes, nc); err != nil {
        // Interface will remain in the bridge but will be removed when rebooting the node
        fmt.Fprintf(os.Stderr, "failed to parse netconf: %v", err)
        return nil
    }

// 
    return invoke.DelegateDel(context.TODO(), nc.Type, netConfBytes, nil)
}

cmdCheck()

func cmdCheck(args *skel.CmdArgs) error {
    // TODO: implement
    return nil
}

まとめてきなやつ

あまりにもわからないのでとりあえず他のCNIプラグイン調べるか…と思い、flannel-io/cni-plugin を読んで正解だった。かなり理解が進んだ。
containernetworkingのパッケージにあるinvokeをなぜ呼び出しているのか?について調べたところ、どうやらインターフェースの作成をしてくれるものだったらしい。

www.cni.dev

flannelは前段で設計ファイルのパース→バリデーションを行い、実行時に問題のない形式にしている、ということが理解できた。
参考にしつつ、小さいCNIプラグインをまずは作ってみようと思う。

余談だが、k8sが出た当初、ネットワークはここまで自由ではなかったらしい。

終わりに

k8sは軽く勉強はしたが、実際にコアな部分(といっていいかはわからないが)に近いところに触れることができてとても面白い。
k8sのネットワークはどうやらプラガブルにごちょごちょできる、ということを知っていきなり触り始めたので、だいぶわからないところが多くてとても良い。
ただ、CNIプラグインを自作してる人がなかなかおらず、ブログもほとんど見つからなかった。クラスターネットワークの自作は…と思ったけど流石にあまりいなさそうな気もする。
ともあれ、仕様書、OSSになっている各種CNIプラグインのソースコードといった、ナレッジとドキュメントがある。
読めばどうとでもなるので、脳筋でいけそう!ということがわかった。ひとまず続けてみる。