Posts for: #Design

RWMutex の試用

0時に寝て3時半に起きてネットみたてたら6時半になってそれから寝て7時半に起きた。やっぱり生活のリズムがおかしい。

今日の筋トレは腹筋:20x1,腕立て:10x1,スクワット10x1をした。あと1kmほど散歩した。

go の RWMutex を使う

テストツールでちょっとしたプールを実装していて mutex を使うコードを書いた。書き込みはロックしないといけないけど、読み込みはロックする必要がないようなものは RWMutex を使うと普通の Mutex よりは読み込みが並行に動くので効率がよくなる。実際に RWMutex を使ってコードを書いたことがなかったので試しに書いて単体テストを実行して振る舞いを確認してみた。

type pool struct {
	l  []string
	m  map[string]struct{}
	mu sync.RWMutex
}

func (p *pool) exists(dn *ldap.DN) bool {
	p.mu.RLock()
	defer p.mu.RUnlock()
	_, ok := p.m[dn.String()]
	return ok
}

func (p *pool) get() *ldap.DN {
	p.mu.RLock()
	defer p.mu.RUnlock()
	length := len(p.l)
	if length == 0 {
		return nil
	}
	s := p.l[rand.Intn(length)]
	return mustParseDN(s)
}

func (p *pool) put(dn *ldap.DN) {
	p.mu.Lock()
	defer p.mu.Unlock()
	s := dn.String()
	p.l = append(p.l, s)
	p.m[s] = struct{}{}
}

4年ぶりの神戸ルミナリエ

たまたま 第29回 神戸ルミナリエ が1月の19日から28日にかけて行われるという記事をみかけた。そう言えば、いつも12月に開催されていたのが今回から1月に変わったみたい。毎年今回が最後みたいな含みをもたせて、なんやらかんやらでもう29回も続いているんやね。今回が第29回だからおそらく来年に第30回もやるのでしょう。「4年ぶりの開催が決定」と書いてあって、あれ?やってなかったっけ?と思ったらコロナ禍のために2020-2023年まで中止という意思決定はされていて、代替イベントとして小さいルミナリエっぽいイルミネーションをやっていたらしい。ルミナリエの展示場となる東遊園地がうちの近所なのでこれまでもやっていたような記憶があるのはそのせいかもしれない。今年は規模を拡張してやるのかな?久しぶりなので時間があるときに観に行ってみようと思う。

RemoteAddr はほとんど役に立たない

0時に寝て何度か起きて7時に起きた。寒くて朝起きれなくなってきた。

ローカルネットワークからのリクエストのみを受け付けるミドルウェア

Request 構造体の中にある RemoteAddr を参照すればすぐできるだろ思って、すぐにできた。すぐにできたんだけど、これは実際の運用で使えるものではない。

type LocalNetworkConfig struct {
	Skipper middleware.Skipper
}

func localNetworkWithConfig(cfg LocalNetworkConfig) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if cfg.Skipper(c) {
				return next(c)
			}

			req := c.Request()
			addr, _, err := net.SplitHostPort(req.RemoteAddr)
			if err != nil {
				slog.Error("failed to get remote address",
					"err", err, "addr", addr)
				return echo.ErrForbidden
			}
			ip := net.ParseIP(addr)
			if ip.IsPrivate() || ip.IsLoopback() {
				return next(c)
			}

			slog.Error("requested from an external network", "ip", ip.String())
			return echo.ErrForbidden
		}
	}
}

func NewLocalNetwork() echo.MiddlewareFunc {
	return localNetworkWithConfig(LocalNetworkConfig{
		Skipper: func(c echo.Context) bool { return false },
	})
}

過去にも同じようなことをやらかしたような既視感がある。

golangのnet.Request構造体内にはRemoteAddr属性があり、当然のことながら要求元のリモートアドレスが含まれています。これで仕事は終わりですね?しかし、リバースプロキシやロードバランサーをアプリケーションに使っている場合はそうではありません。これはgoサーバには常に、すべてのリクエストがロードバランサから来ているかのように見えます。これは、これを何らかのスロットリングの指標として使いたいのであれば、最悪なことです。ですから、何らかの形のリバースプロキシの後ろでなく、goサーバーが1つだけ動いているのでない限り、私たちの目的には役に立たないとして、すぐにこれを捨てることができます。

https://husobee.github.io/golang/ip-address/2015/12/17/remote-ip-go.html

途中のロードバランサーやアプリケーションゲートウェイ、リバースプロキシなど、中継するサーバーの ip アドレスに置き換わってしまうため、ネットワークのインフラを管理していないとどこからリクエストされたかを追跡することはできない。X-Real-IPX-Forwarded-For という http ヘッダーに中継したサーバーの ip アドレスを記録するという慣習があるようだけど、これはクライアント側で書き換えできるのでこれ単体でアクセス制御のようなものに使うことはできない。

次に同じ失敗をしないようにここに書いておく。

mongodb のサードパーティのコンテナイメージ

23時に寝て3時に起きて寝たかどうか覚えていないうちに6時半になっていて7時半に起きた。

json を介した go の bool 値のバリエーション

go-playground/validator のバリデータには required というバリデーションオプションがある。しかし、このオプションは go のゼロ値でないことをチェックするという仕様になっている。bool のゼロ値は false となるため、リクエストした JSON データに false を設定していたのか、未設定だったのかの違いを検出できない。これはバリデータの問題ではなく、go の json ライブラリの制約のようなもので使い勝手のよい仕様とは言えない。私もこの振る舞いに起因する不具合に遭遇したこともあるし、こういうときにどうしたらよいかも過去に3回ぐらいは調べている気がする。

現時点での私の最適化は次のコードになる。データ構造として *bool 型にすれば、ポインタ型のゼロ値は nil となるため、true, false, nil の3値でバリデーションできる。しかし、私はこのデータ構造を好ましく思わない。というのは、内部的には true/false の2値でしか管理しないメンバーを、json のバリデーションのためだけに nil も許容する3値にすることがよい設計だと私は思えない。そこでバリデータによるバリデーションは諦めて、json の Unmarshal 処理をフックしてバリデーション相当の処理を自分で実装する。このやり方のデメリットはメンバーが追加されたときに自分で UnmarshalJSON() メソッドを保守する必要がある点になる。しかし、メリットとして内部のデータ構造の型は bool 型で扱える。一概にどちらがよいとは言いにくいかもしれないし、設計上の好みかもしれない。

type reqMyData struct {
	Name       string `json:"name"`
	View       *bool  `json:"view"`
}

type MyData struct {
	Name       string `json:"name"`
	View       bool   `json:"view"`
}

func (d *MyData) UnmarshalJSON(data []byte) error {
	var tmp reqMyData
	if err := json.Unmarshal(data, &tmp); err != nil {
		return fmt.Errorf("failed to unmarshal as reqMyData")
	}
	if tmp.View == nil {
		return fmt.Errorf("required view field")
	}
	d.Name = tmp.Name
	d.View = *tmp.View
	return nil
}

サードパーティの mongodb コンテナイメージ

先日の mongodb のレプリカセット調査 の続き。コードレビューをしていて bitnami/mongodb というサードパーティのコンテナイメージを使った方がよいのではないか?というコメントがあったのでその調査をしてみた。VMware 社が提供しているサードパーティのコンテナイメージらしい。

MongoDB(R) is run and maintained by MongoDB, which is a completely separate project from Bitnami.

まず MongoDB プロジェクトとはまったく別管理であることが書いてある。

Bitnami イメージを使用する理由

  • Bitnamiはアップストリームソースの変更を綿密に追跡し、自動化されたシステムを使用してこのイメージの新しいバージョンを迅速に公開します。
  • Bitnami イメージでは、最新のバグ修正と機能をできるだけ早く利用できます。
  • Bitnamiのコンテナ、仮想マシン、クラウドイメージは、同じコンポーネントと構成アプローチを使用しているため、プロジェクトのニーズに応じて形式を簡単に切り替えることができます。
  • Bitnamiのイメージはすべて、minideb(最小限のDebianベースのコンテナイメージ)またはscratch(明示的に空のイメージ)をベースにしています。
  • Docker Hubで利用可能なすべてのBitnamiイメージは、Docker Content Trust(DCT)で署名されています。DOCKER_CONTENT_TRUST=1 を使用して、イメージの完全性を確認できます。
  • Bitnamiコンテナイメージは定期的にリリースされ、最新のディストリビューションパッケージが利用可能です。

MongoDB®を本番環境で使用したいですか?Bitnami Application Catalogのエンタープライズ版であるVMware Tanzu Application Catalogをお試しください。

mongo の公式イメージは ubuntu をベースイメージにしている。ubuntu よりは minideb の方が軽いのかな?そしてちゃんと upstream にも追随しているみたい。このベースイメージの違いによるものかは定かではないが、結合テストのイメージも移行してみたところ、10-20秒ほど結合テストの実行時間が速くなった。割合にすると10%程度かな。

KubernetesにMongoDB®をデプロイするには?

Bitnami アプリケーションを Helm Chart としてデプロイすることは、Kubernetes 上で当社のアプリケーションを使い始める最も簡単な方法です。インストールの詳細については、Bitnami MongoDB® Chart GitHub リポジトリを参照してください。

Bitnami コンテナは、クラスタへの Helm Charts のデプロイと管理に Kubeapps と一緒に使用できます。

helm chart も提供しているようで、いずれクラウド版を作るときに MongoDB も k8s 上にデプロイする上でこのことは都合がよいように思える。

レプリケーションを前提とした初期設定があり、entrypoint スクリプトもいくつか読んでみた感じだと、きれいに管理されていて保守もちゃんとやってくれそうにみえる。

昨日、導入したばかりの公式イメージ + 自作スクリプトによるレプリケーション設定を廃止して、Bitnami のコンテナイメージを使うことに決めた。

rbac なロールでの動的な権限チェック

23時に寝て何度か起きて7時に起きた。夜に作業するつもりが、メイドインアビス を見ながら寝落ちした。

ldap で認証したユーザーの権限管理

rbac なロール管理 の仕組みを api サーバーに実装した。初期実装したときは静的にリソース (url) に対して acl から権限が許可されているかどうかのみをチェックするだけでよかった。ldap で認証したユーザーは自分の情報は更新できるが、他人の情報は更新できないといったリクエスト単位に権限が許可されているかどうかをミドルウェアで判別したい。いわば、リクエスト時に動的にリソースの情報をチェックしないといけない。

いくつか試行錯誤して、あーでもないこーどもないとやった結果、次のように落ち着いた。

  • リソースは動的な権限チェックのために AuthorizeFunc を取得するための AuthorizeKey をもつ
  • AuthorizeFunc は Context から取得する
  • AuthorizeFunc が true を返す場合は権限を付与する
func (r *Role) CanAccess(ctx context.Context, resourceName, target string) bool {
	for _, ac := range r.ACL {
		if ac.Resource.Match(resourceName) {
			for _, perm := range ac.Permissions {
				if perm.IsGranted(target) {
					return true
				}
			}
			if authorize, ok := ctx.Value(ac.Resource.Key).(AuthorizeFunc); ok {
				if authorize(ctx, resourceName, target) {
					return true
				}
			}
		}
	}
	return false
}

ロールに渡す Context にはあらかじめ、AuthorizeKey と AuthorizeFunc を登録しておく。

ctx = context.WithValue(ctx, role.LDAPResource, role.IsLDAPResourceOwner)

リソースを生成するときに AuthorizeKey をセットしておく。

ldapUserResource = rbac.NewResource(
	"ldap-user-specific-operation",
	"(/api/ldap/entry|/api/ldap/entry/.*)",
	LDAPResource,
)

ちょっと設定は煩雑になるが、ひとまずこれで要件を満たせた。

論理の通じない人たち

23時に寝て何度か起きて8時に起きた。ホテルの部屋が暗いと朝になった気がしなくて2度寝したら寝坊した。

組織の対応と sns の議論

先日の sns 騒ぎ の続き。公式からの声明も出たので軽くまとめておく。

簡潔な文章に事実の記述、責任の所在、関係者への配慮が含まれていて十分な内容にみえる。法律なども関係するため、弁護士チェックが必要なことを考慮すると、こんな短期間で組織の見解を出せたことは運営側の体制を鑑みることができる。それが適正かどうかは人によって判断は異なるかもしれないが、私はコミュニティ運営というボランティア主体の組織であれば十分なものだと思えた。その後のネット上の議論も、ちゃんと終えてはいないが、様々な見解で議論は進んでいるようにみえる。

今回みていて感じたことの1つに、コミュニケーションが成り立たない人が世の中にはたくさんいるということ。議論の前提や論理の出発点が異なる人たちは、一定の論理を含む全体や大局を理解できず、細部や詳細のところだけを拠り所に自身の論理を組み立てる。意見の差異があることはなんら問題はないが、論理が通じないのは議論の余地すらないようにみえた。そういう人たちを会話するときは前提条件を同じにしたり、思想の背景を共有したり、もっと時間をかけて丁寧にすり合わせていく作業が必要になる。そして sns のような、流れが速い不特定多数の議論はそういった丁寧な作業にまったく向いていない。だから sns で議論することは時間の無駄である。

コネクションを共有しないプール

go の非同期処理であまり使われることはないが、semaphore が準公式ライブラリとして提供されている。私はセマフォを気に入っていてたまに使う。

ldap プロトコルではコネクションの確立とログインに相当する bind の操作が分かれている。コネクションを確立したまま、ログアウトに相当する処理ができればプールを設けることでコネクションの再利用ができる。

しかし、このドキュメントの説明によると、unbind という操作は用意されているものの、ログアウトに相当する機能ではなく、クローズする前に通知するといった用途だと書いてある。unbind のリクエストをした後にはクローズするしかないといったものになる。それを踏まえて、プールはセマフォで同時接続数のみを制御するのでよいのではないかと思う。そんなワーカープールを実装してみた。

type ClientPool struct {
	config *config.LDAP
	sem    *semaphore.Weighted
}

func (p *ClientPool) Get(
	ctx context.Context,
) (*LDAPClient, error) {
	if !p.sem.TryAcquire(1) {
		return nil, fmt.Errorf("failed to acquire, wait and get later")
	}
	client := NewLDAPClient(p.config)
	if err := client.Connect(ctx); err != nil {
		p.sem.Release(1)
		return nil, fmt.Errorf("failed to connect: %w", err)
	}
	return client, nil
}

func (p *ClientPool) GetAuthenticated(
	ctx context.Context,
) (*LDAPClient, error) {
	client, err := p.Get(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get: %w", err)
	}
	dn := p.config.BindDN
	passwd := p.config.BindPasswd.String()
	if err := client.Bind(ctx, dn, passwd); err != nil {
		p.sem.Release(1)
		return nil, fmt.Errorf("failed to bind: %w", err)
	}
	return client, nil
}

func (p *ClientPool) Close(client *LDAPClient) error {
	err := client.Close()
	p.sem.Release(1)
	return err
}

func NewClientPool(cfg *config.LDAP) *ClientPool {
	return &ClientPool{
		config: cfg,
		sem:    semaphore.NewWeighted(cfg.ClientPoolSize),
	}
}

権限管理ができるようになった

1時に寝て何度か起きて7時半に起きた。なんかしんどい夢をみたが、もう覚えていない。

rbac なミドルウェアの実装

昨日の続き 。rbac (role-based access control) なライブラリが一通り実装できたのでそれを使って echo のミドルウェア を実装した。やりたいことは次のようなこと。たったこれだけだが、これを実装するために、ここ1週間ほど、あれやこれやの実装をしてきた。ようやくそれが動くようになったというところ。私にとってはミドルウェアの仕組みは過去に何度も実装してきたものだが、頭に描いたイメージのまま、実装できたのがよかったと思う。

func rbacWithConfig(cfg rbacConfig) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if cfg.Skipper(c) {
				return next(c)
			}

			req := c.Request()
			ctx := req.Context()
			sessionStore := c.Get(keySession).(session.Store)
			userName := c.Get("username").(string)
			s, err := session.Get(ctx, sessionStore, userName)
			if err != nil {
				return fmt.Errorf("failed to get session: %w", err)
			}
			if !s.Role.CanAccess(ctx, req.URL.Path, req.Method) {
				return echo.ErrForbidden
			}

			return next(c)
		}
	}
}

node.js のアップグレード

ちょうど管理画面も少し触ろうと思って環境を構築し始めた。たまたま node.js のバージョンをみると、ちょうど10月18日で 18 (LTS) の active support 期間が終了していた。security support は2025年4月まであるのでそんな急がなくてもよいが、それに気付いたついでなので 20 (LTS) にアップグレードすることにした。幸い、大半のライブラリは 20 (LTS) でもそのまま動いた。しかし、依存ライブラリのアップデートをしていて、一部 conflict してうまく動かないライブラリもあった。細かい原因調査をしていないが、フロントエンドの方がこまめにバージョンを上げていかないと何が原因でアプリケーションが正常に動かなくなるのか、分からなくなる気がする。

非日常を提供するという価値

1時半に寝て3時に起きてもう1回起きて6時半に起きた。

interface はデシリアライズできない

昨日の続き 。rbac なライブラリを使ってアプリケーションを実装していく。ログイン時にユーザーにロールを割り当ててセッションにロールを保持するのが都合よさそうに思えた。ロールの実装で一部の型は interface にして後から拡張できるような設計にしていた。例えば encoding/json ライブラリだと、Marshaler/Unmarshaler の interface を満たすことで任意の json のシリアライズ/デシリアライズをフックできる。調べたり、実際に動かしていていて気付いたのだけど、interface の場合はシリアライズは任意にできるけど、デシリアライズはできない。当たり前と言えば当たり前だが、interface を満たす複数の型がある中で json ライブラリがどの型でデシリアライズしていいか判別できないからだ。当初の interface を用いた設計が誤りだったことに気付いて、一部の型を汎用の構造体で設計し直すようなことをしていた。

またデシリアライズするときに一部の値を初期化したいといった要件がある。例えば mutex を初期化したい。このときに処理の内部で派生型を宣言して、それにキャストした上でデシリアライズの処理を実行した上で差分の処理を実装するというテクニックを学んだ。スコープが限定されて、コードがシンプルになって保守性も高い、久しぶりに頭のよいスマートなコードをみた。

func (r *Role) UnmarshalJSON(b []byte) error {
	type Alias Role
	if err := json.Unmarshal(b, (*Alias)(r)); err != nil {
		return err
	}
	r.mu = &sync.Mutex{}
	return nil
}

コワーキングのオンラインイベント

月例のカフーツさんのオンラインイベントに参加した。前回の所感はここ 。今日は参加者が2人だけだった。コワーキングスペースを運営するコワーキングスペースマネージャーの連携を強化することで、コワーキングスペースの付加価値が上がったりしないか?といった内容を話したりしていた。コワーキングスペースマネージャーは、普通はお仕事で自分のコワーキングスペースにいないといけないから、なかなか他所のコワーキングスペースへ訪問すること自体が難しい。コワーキングスペース同士の連携により、お仕事でコワーキングスペースマネージャーが自分ところのコワーキングスペースの利用者を連れて、他所のコワーキングスペースへ訪問して、そこでイベントをしたりすればいいんじゃないかという案が出た。

いとうさんがよく コワーキングツアー と称して、全国各地のコワーキングスペースへ訪問して、そこでイベント開催をしたり、その地域の取り組みなどを紹介したりしている。それと全く同じことを、コワーキングスペースの利用者に対してもその付加価値というのはあるかもしれないと私もよいアイディアだと思った。例えば、大阪のコワーキングスペースの利用者を広島へ連れていって、そこでイベントやって交流する。その逆も然り。通常のコワーキングスペースの利用者は自ら広島のコワーキングスペースへ行ってコラボレーションを行ったりしない。いや、いとうさんみたいに自らやる人もいるんだけど、そんな人は対象の利用者ではない。自分からは行かないが、誘われたら行ってもいいかなと考える人 (私もそんな1人だ) を移動させることで、新しい価値やアイディアが生まれるかもしれないと思える。私もいまはフルタイムのお仕事があるから自由に移動はできないが、いずれ会社の投資期間に入って、自分でスケジュールを決められる状況になれば、コワーキングツアーにも出掛けてみようと思う。

あと勉強会やイベント以外でコワーキングスペースで出来ることはないか?という話題でも盛り上がった。私は主催者の準備が大変だと出来ないから、主催者のコストが低いものという視点から考えて猫コワーキングがいいんじゃないかと提案してみた。ある週だけコワーキングスペースに猫が10匹ぐらいいますといった取り組み。課題は猫をどこから連れてくるかだけだが、そういう機会があれば確かに私も行ってみたい。そのアイディアの発散で非日常の体験ができるような取り組みがよいんじゃないかとまとめられていた。

  • 猫コワーキング (猫がたくさんいる)
  • 深夜コワーキング (深夜に開いている)

深夜コワーキングスペースのモデルとなる 弐拾dB さんというコワーキングスペースが広島の尾道にあるらしい。23-翌5時という営業時間だという。いとうさんが絶賛していたのでおもしろいオーナーが運営されているのだと思う。そのオーナーが執筆したエッセイの 頁をめくる音で息をする を購入してみた。

echo のミドルウェア関数のスタイル

1時に寝て何度か起きて6時半に起きた。起きてから軽く部屋の掃除をした。

echo のミドルウェア開発

go の api サーバーの開発に echo という定番のフレームワークを使っている。少し前にメンバーに認証の処理をミドルウェアとして実装してもらった。いま認可の仕組みもミドルウェアで実装しようと、いくつかソースコードを読んでいて、echo のフレームワークが提供しているミドルウェアの関数名や config には共通点があることに気付いた。echo middleware によると、20個ぐらいのミドルウェアが提供されている。例えば、適当にそのうちの3つほどを取り出すが XxxWithConfig という命名規則で config を受けとって echo.MiddlewareFunc を返すというインターフェースになっている。

func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc
func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc
func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc

また config の中身をみていると、ミドルウェアの処理に必要な関数を渡すような設計になっている。複数のミドルウェアにとって共通なのは、ミドルウェアの処理を迂回する条件を実装するため middleware.Skipper という型が次のように型で定義されている。

e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{
	Skipper: func (c echo.Context) bool {
		// Skipper defines a function to skip middleware.
    },
	Validator: func(string, string, echo.Context) (bool, error) {
		// Validator is a function to validate BasicAuth credentials.
		// Required.
    },
	Realm: "Restricted",
}))

典型的なミドルウェアは次のように実装する。最初に Skipper を呼び出して処理の有無を確認する。

return func(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		if config.Skipper(c) {
			return next(c)
		}

        // TODO: ミドルウェア本体の処理

		return next(c)
    }
}

認証のミドルウェアを実装してもらったときに、私がここまでみていなかったなということに気付いて、既存のミドルウェアを echo のそれと同じスタイルにあわせるようにリファクタリングした。自分でソースコードを読んでいるとコードレビューで気付かなかったことに気付くことが多い。自分がコードを書いているときと、コードレビューをしているときでなにかしら視点が違う。

トークン認証のプロバイダ実装

22時から寝始めて何度か起きて6時に起きた。最近は早寝早起きにしている。

client ライブラリのトークン認証対応

いまの開発の新機能の1つにローカルアカウントの管理機能がある。普通のパスワード認証により、JWT によるアクセストークンを発行し、リフレッシュトークンを使ってアクセストークンの再取得を行う。api サーバーで実装してもらったこれらの web api を使って、api client 側でもログインしてアクセストークンを取得して web api のリクエストができるようにする。一般的にアクセストークンは有効期限が短いため、有効期限が切れたときは透過的にリフレッシュトークンを使ってアクセストークンを再取得する。またリフレッシュトークンの有効期限が切れたときは再ログインして、アクセストークンとリフレッシュトークンを再取得する。

文章で書けばこれだけの機能だけど、このための AuthProvider を実装した。最終的には次のインターフェースになった。

type AuthProvider interface {
	CanRefresh() bool
	GetAuthorization() (string, error)
	GetType() AuthType
	Refresh() error
}

先週たまたま Azure/azure-sdk-for-goAuthProvider のソースコードを読んだ。やりたいことに対して、かなり複雑なことをしているようにみえたが、アクセストークンのキャッシュ、有効期限が切れたときのリフレッシュを透過的に行うコードだった。このライブラリの実装が読みにくいコードで、私だったらもっとシンプルに実装するというイメージが先週からあったのでそのイメージ通りに実装して1日で対応を終えた。本当はこの機能をいまの開発フェーズで提供する予定はなかったんだけど、うまく簡潔に実装できたので一部 agent で導入してテストで検証することにした。

go の処理系も驚く sdk のコード生成

0時に寝て何度か起きて6時に起きた。そのまま7時過ぎまでだらだらしてた。しんどい。

msgraph-sdk-go のサイズ問題

先週 msgraph-sdk-go を使った開発 を終えてデプロイする段階になってライブラリのサイズが大きくて、コンパイル速度が遅くなったり、バイナリサイズが大きくなったりする弊害があることに気付いた。コンパイル速度は2-3倍遅くなり (3分が7-10分ぐらい)、バイナリサイズも2-3倍大きくなる (30 MiB が 100 MiB とか) 。たまたまこのリポジトリは他のツール類からも依存パッケージとして使われるものなので想定よりも影響が大きいことに気付いた。

朝からチームのメンバーとミーティングして、本来は qa に入ったこの時期にこんな変更をすべきではないが、これは放置するデメリットが大きいのでリポジトリ分割 (モジュール分割) しようと提案して了承を得た。私がやれば作業は1日もあれば完了するだろうと見積もって、見積もり通り、夕方には分割したモジュールをテスト環境にデプロイして当面の解決を得た。アプリケーションのモジュール構造をちゃんとレイヤー化して作ってあるから、今回みたいに急遽、モジュール分割が必要になってもほぼ変更する必要はなかった (たった1箇所だけ) 。

この本質的な問題は次の issue のコメントで説明されている。

ざっと機械翻訳してみる。

コンテキスト

この SDK は kiota を使用してメタデータから自動的に生成されます。オリジナルのメタデータは、Microsoft Graph の配下にあるすべてのサービスチーム(v1用とベータ用)によって入力された CSDL です。この CSDL は最終的に OpenAPI のフォーマットに変換されますが、これは非常に大きなものです(1k 以上のエンドポイント、1.5k のモデル …)。API のサイズが大きいため、完全な API surface の SDK を手作りすることは実現不可能でしょう。

私たちは、SDK を複数のサブモジュール(ファイル用、メール用など)に “スライス” して、人々が関心のあるものだけを簡単に入手できるようにすることをしばらく考えてきました。実際、私たちは PowerShell でこれを実現しました。しかし、“グラフ” の性質(すべてのモデルは互いにある程度関連している)と構築されるアプリケーションの多様性により、スライスは誰にとっても “正しい” ものにはならない(大きすぎたり、小さすぎたり、モデルの重複につながったり…)。

そのような理由から、私たちは「完全なSDK」を提供することにしました。すべての人にとって理想的とは言えないかもしれませんが、Go開発者の中には「アプリケーションを作るためのSDKが欲しいだけ」という人もいると感じています(以下で説明する2つ目のオプションとは対照的です)。

Go の欠点

Go の探求を通して、いくつかの欠点に気づいた。現時点では、私たちのプロジェクトやパッケージが適切にセットアップされていないせいなのか、Go や大規模プロジェクトの制限のせいなのかはわからない:

go build は、変更されておらず、依存関係も変更されていないサブパッケージをリビルドすることが多い。go build が直前に実行されていても、go test がリビルドすることがよくあります。なぜある種のキャッシュに頼らないのでしょうか?同じ問題が go lint にもある。
私には、たくさんのサブパッケージがある大きなプロジェクトをビルドするコストは、依存関係が更新されたり、キャッシュが削除されたり、コードが変更されたりしない限り、「セッションごとに一度」だけ支払われるべきだと感じます。

私たちのプロジェクト構成/構造において、そのような状況を改善するための最適化について、自由に概説してください。また、Goコミュニティ(Goコンパイラを開発している人たちなど)と関わって、そのようなフィードバックを提供する方法があれば、喜んでそうします。私たちのプロジェクトは、その規模の大きさから、世の中にあるほとんどのGoパッケージと比べると、ちょっと変わり者だとわかっています。

適切なサイズの SDK

最後に、すべてのエンドポイントを備えた完全な SDK を持つことは、様々な理由からすべての人に適しているわけではないことを認識しています。私たちは新しい “適切なサイズのセルフサービスSDKエクスペリエンス” を可能にするために取り組んでいます。そこでは、APIユーザーは誰でも、この SDK と同じように見え、同じように感じる SDK を生成することができますが、完全な API サーフェスの代わりに、彼らのアプリケーションのために彼らが気にするエンドポイント/モデルのみが含まれています。 私たちは今、そのような取り組みに本当に早くから取り組んでいますが、それでもフィードバックをいただけるとうれしいです。大まかな手順はこんな感じだ:

  1. 新しいgoプロジェクトを作成するか、既存のプロジェクトを特定する。
  2. kiotaの依存関係を追加するか、msgraph-sdk-go-coreを追加します(これはKiotaの依存関係をプルし、いくつか追加します)。
  3. グラフエクスプローラで必要なリソースを選択(左パネル、2番目のタブ、…、“コレクションに追加”)。
  4. コレクションをプレビューをクリックし、postmanコレクションとしてエクスポートします。
  5. hidi を postmanコレクションと先ほど共有したOpenAPIの完全な説明文と一緒に使って、“フィルタリングされた” OpenAPI フォーマットを生成する。
  6. kiotaを使って、プロジェクトにMicrosoft Graph用のGoクライアントを生成する。
  7. APIの呼び出しを開始する。

この時点で、私たちはこれらのステップをすべて文書化し、効率化するために取り組んでいます(おそらくステップ4~5を圧縮しています)。このアプローチの素晴らしいところは、ステップ5から7までが、Microsoft Graphだけでなく、呼び出したいOpenAPIで記述されたAPIで動作することだ。
繰り返しますが、この最後の提案はまだ初期段階です。自由に試して、様々な場所でフィードバックを提供してください。

この長い投稿で、私たちがどこに向かっているのかが明らかになり、Goコミュニティからこれらの側面すべてについてさらにフィードバックが得られると本当に助かる!

簡単に言えば、ms graph api の体系が巨大過ぎて、その定義は openapi.yaml にあるが、この定義からすべてコード生成すると巨大なモデル定義をもつ sdk が出来上がってしまったという話しである。後半に書いてあるワークアラウンドとして kiota で必要なモデルだけを選択して専用 sdk を生成すればサイズを小さくできるとある。しかし、それはそれで graph explorer で選択しないといけなかったりして面倒そうではある。次のドキュメントでもその手順について書いてある。

うちの用途ではモジュール分割により、局所化したのでひとまずこの問題は大きな影響をもたないようになった。また余裕があるときにモデルを選択して専用 sdk を自動的に生成する仕組みを構築できるならそれに挑戦してもよいかもしれない。

室温と集中力との相関関係

2時に寝て2回ぐらい起きて7時に起きた。朝からやや疲れ気味。

ストレッチ

疲労と暑さと出張でバテバテ。今日の開脚幅は開始前157cmで、ストレッチ後160cmだった。数値はよい感じ。トレーナーさんが言うにはお尻と肩が硬かったらしい。私の感覚ではそれらに加えて、ふくらはぎの後ろの筋がかなり痛かった、とくに左足。さらに体全体がだるくて疲れが溜まっているなーという印象も受けた。実は8月21日の週にまた出張する予定になったので体力がもつか、不安も感じるようになってきた。

空調工事の結果

先日の 暑さ対策委員会 の続き。

出張から帰ってきて、エアコンの冷媒切り替えを終えたはずのオフィスの室温はどう変わったか?その結果が楽しみにで昨日、出張帰りにオフィスに寄ってみたというのもあった。結論から言うと

何の成果も!!得られませんでした!!

温湿度計を買っておいてよかった。ちゃんと数値でどう変わったかを測れるもんね。だって午前中は34℃、お昼から32℃、夜は28℃、なんも変わってない。この部屋が暑い理由は冷媒が原因ではなかったという切り分けはできた。がっかりして、また運営会社に電話して、成果がなかったことと、前に断熱のブラインドに変えてくれると言っていた件はどうなったの?とツッコミまで入れてしまったよ。もう建物の構造的にこの区画は涼しくならないんやろか?

オフィスの wifi 不通

昨日の午前中、運営会社のスタッフとやり取りしていて wifi が不通になっていることに気付いた。デスクトップマシンは有線ネットワークを使っていてそちらは疎通しているものの、wifi のアクセスポイントまでは接続できるが、なぜかその先のインターネット接続が不通になるという現象が発生していた。たまたま部屋の外に出たときにコワーキングスペースの利用者が運営会社のサポートに電話して、やや強い口調でクレームしていた。コワーキングスペースの利用者向けには wifi ネットワークしかないため、コワーキングスペースにわざわざ来てインターネット繋がりませんで怒る気持ちは分からないでもない。

ネットワーク障害が発生することそのものは仕方ないものの、発生してから翌日の20時時点でその wifi ネットワークの障害が解消していない。同じフロア内にある有線ネットワークが疎通していることから、このネットワーク障害は小規模な原因であることが推測される。それこそ通信機器を再起動すれば直るかもしれない。1日以上放置している運営体制を懸念に思ってしまったのだけど、これは職業病?週末に知らずにコワーキングスペースへ来られる方がいるのではないだろうか?

エージェントアプリケーション開発

今週中に完了させておきたい機能開発 が全然進捗しなかったので週末に取り組む。昼間は暑くて (34℃) 集中力が出なかったので15時から19時まで家に帰ってエアコンの効いた部屋で寝てた。ただただ寝てた。私が4時間起きずに眠れることは稀なので自分でも驚いた。その後、オフィスに戻って19時半ぐらいから開発に臨んだ。夜のオフィスの室温は28℃前後なので十分に涼しい (と適応している自分がいる) 。

ldap サーバーでのユーザーとグループのエントリー、それぞれの変更を検知して id 連携しないといけない。既存の実装は1つのクエリにユーザーとグループの検索条件を OR 条件にして両方のエントリーを取得するようにしている。このフィルター自体は問題ないが、その後のユーザーとグループの判別に DN の接尾辞の部分マッチで判定していた。これは微妙な判定方法だ。このやり方だとユーザーとグループの DN の接尾辞が同じときに運用できない。ユーザーとグループは分けて管理した方が要件がシンプルで運用も実装もわかりやすいだろうと考え、それぞれのクエリを非同期/並行に動かすようにした。こういうのは go 言語の得意とするところ。一通り動くようになったら3時半ぐらいになってた。涼しい方が集中できる。