Posts for: #Debug

モジュールは複数データソースに依存させない

デバッグしていて遅くなり、帰ってから慌ててスーパーに買いものへ行ったりしていて時間がなくなったので今日はバドミントン練習をお休み。

データベースとメッセージキューの整合性を考える

昨日の続き。トランザクションを導入したことで mongodb と mq でのデータフローが意図せず変わってしまっていることが調査してわかった。

従来は次のように動いていた。

  1. api サーバーがエントリー情報を受け取る
  2. api ハンドラーがエントリー情報を mongodb に保存する
  3. api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する
  4. api ハンドラー内の producer が rabbitmq にメッセージを送信する
  5. consumer がメッセージを受信する
  6. consumer が mongodb からジョブ情報を参照する
  7. api ハンドラーがレスポンスを返す
  8. consumer がメッセージを処理する

トランザクションを導入したことで mongodb にデータが保存されるタイミングが変わってしまった。

  1. api サーバーがエントリー情報を受け取る
  2. api ハンドラーがトランザクションを開始する
  3. api ハンドラーがエントリー情報を mongodb に保存する (この時点では未コミット)
  4. api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する (この時点では未コミット)
  5. api ハンドラー内の producer が rabbitmq にメッセージを送信する
  6. consumer がメッセージを受信する
  7. consumer が mongodb からジョブ情報を参照するが、トランザクションがコミットされていない可能性がある
  8. api ハンドラーがトランザクションをコミットする
  9. api ハンドラーがレスポンスを返す
  10. consumer でエラーが発生する

処理の流れを見直して次のように改修した。内部実装の都合があってやや煩雑な変更となった。

  1. api サーバーがエントリー情報を受け取る
  2. api ハンドラーがトランザクションを開始する
  3. api ハンドラーがエントリー情報を mongodb に保存する (この時点では未コミット)
  4. api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する (この時点では未コミット)
  5. api ハンドラーがトランザクションをコミットする
  6. api ハンドラー内の producer が rabbitmq にメッセージを送信する
  7. consumer がメッセージを受信する
  8. consumer が mongodb からジョブ情報を参照する
  9. api ハンドラーがレスポンスを返す
  10. consumer がメッセージを処理する

これは単純にトランザクションのコミットタイミングと mq へのメッセージ送受信のタイミングを見直せばよいという話しではない。本質的に mongodb で管理しているジョブ情報と rabbitmq へ送信しているメッセージの整合性を保証することはできないということを表している。producer はメッセージ送信に失敗する可能性があるから、そのときにジョブ情報を書き換える必要はあるが、その前にトランザクションをコミットしてしまっているため、api ハンドラー内でデータのコミットタイミングが複数になってしまう。トランザクションを導入したメリットが失われてしまい、Unit of work のパターンも実現できない。consumer の処理に必要な情報を mongodb にあるデータとメッセージの2つに分割しているところが整合性の問題を引き起こしている。アーキテクチャ上の設計ミスと言える。consumer の処理に必要な情報はすべてメッセージに含めてしまい、メッセージを処理した後に mongodb に結果を書き込むといった設計にすべきだった。

初期実装のときからジョブ情報を mongodb で管理する必要はあるのか?という懸念を私はもっていた。要件や機能が曖昧な状況でもあり、メンバーもなんとなく db に管理情報を残しておいた方が将来的な変更に対応できて安心といった理由だったと思う。当時は整合性の問題が起きることに、私が気付いていなかったためにこの設計を見直すように強く指摘できなかった。トランザクションを導入したことで consumer が必要な情報を db に保持すると、db とメッセージ処理のタイミングにおける整合性の問題が生じるという学びになった。

いまとなってはこのジョブ情報を使う他の機能もあるため、この設計を見直すことはできない。今後の開発プロジェクトで db とメッセージを扱うときはこの経験を活かすためにふりかえりとして書いておく。

増上寺の東京タワー

増上寺の東京タワー

今日の運動は縄跳び(両足跳),散歩,ハンドグリップをした。統計を 運動の記録 にまとめる。

msgraph-sdk-go のデバッグ

me-id の調査 を経て msgraph-sdk-go の delta クエリを使って変更差分を取得できるようになった。デバッグしていて、プロパティの値を変更したときに差分を取得できているものの、変更する値を空の値にする (null) にしたときの変更を検知できていないことに気付いた。実際のレスポンスは null が連携されているものの、sdk 側のコードで null を無視しているようにみえた。おそらく sdk のバグなんじゃないかと思って discussion ではなく issue として登録してみた。

もしかしたら他の api で代替できるのかもしれないけれど、そうじゃなかったら、これは致命的な不具合だと思う。こんな実運用で使ったらすぐ気付く不具合がこれまでに修正されていないとしたら go の sdk で delta クエリの運用を誰もやっていないの?とか思ってしまう。

スーパーホテル

今日から スーパーホテル 東京・芝 に2泊する。スーパーホテルというホテルチェーンがあることは知っていたけれど、実際に泊まるのは初めてだった。避けていたわけではないのにこれまで機会がなかった。泊まってみてとてもよかった。施設・設備は整っているし、大浴場ではないけど個室よりは広いお風呂があるし、朝食も無料でついてきて、その上に宿泊料金が安い。コスパという視点では、いつも宿泊している京王プレッソインよりも高いと思う。他の場所のスーパーホテルにも泊まってみようという気持ちにさせるぐらいの、今回の宿泊体験はよかった。

しばの上の運動

今日もお仕事を終えてから散歩に出掛けた。芝公園 や増上寺の付近をぐるぐるまわりながら縄跳びができそうな場所を探していた。芝生の広場もあったけれど、早い時間帯は人がたくさんいて、縄跳びすると変な人にみられそうだったので、弁天池という人気のないところで縄跳びをしていた。22時頃に芝生の広場の前を通ったらかなり人が減っていたので遅い時間帯ならここでもよいかもしれない。20代後半のときにこの付近に住んでいたので増上寺や東京タワーを眺めるのも懐かしかった。出張したときの運動するコースとして芝公園周りはちょうどよいと思う。

仕事も運動も満足な金曜日

22時過ぎから休んでいるうちに寝てしまい、24時過ぎに起きてトイレ行って、寝る準備を整えてまた寝て6時前に起きた。

今日の運動は腹筋ローラー,スクワット,縄跳び(両足跳),散歩をした。統計を 運動の記録 にまとめる。

agent のバグ修正

前の開発フェーズの QA テスト中に agent の致命的なバグ に気付いた。非同期/並行にしてはいけないデータストリームの処理を分割してしまったがためにそれぞれのデータストリームの依存関係を保証できないといった不具合をみつけていた。1つのデータストリームで扱えばいいだけではあるのだけど、既存の設計を見直しながら、効率も考慮していくつか最適化もしながら、関連するところをあちこち直した。今日はチームのメンバー2人がお休みだったのでメンバーのサポートも必要なくて、朝から晩まで集中して自分のコードを書いていた。テストツールを実行して QA テストのときにみつけた依存関係に関する不具合は解消したが、まだ一部で発生するエラーもみつけている。おそらくそれらはデータストリームの不具合とは関係ない、テストツールの不具合か、別の処理の不具合な気はする。せっかく再現できる状況にあるのでまた来週デバッグして背景を調べて直す。

スマート縄跳びの運用開始

ここ最近雨降りの日が多くて縄跳びできなかった。コンクリートのところで縄跳びすると足を痛める懸念があるからなるべく土の上で飛びたい。土の地面で雨に濡れない場所が身近になくて雨降りだと縄跳びできない。体育館のような場所を気軽に使えればよいのだけど、まだ調査不足。

今日は天気がよかったので縄跳びしてきた。試行期間 を経て縄跳びのワークアウトをこれからちゃんと作っていく。タイマーも一緒にもっていって15分をセットする。しばらくは15分間で跳べるだけ跳ぶ。とはいえ、私がいま跳び続けるのは最長1分間になる。1分たったら休んで呼吸を整える。休憩を1分、2分と疲れ具合にあわせて時間をあけながら1分間跳ぶといったサイクルをしている。おそらく今日は5-6回跳んだのかな?合計で800回になった。現在の私のペースだと、1分間で110-120回ほど跳んでいるみたい。きっと慣れてきたり体力がついてくれば同じ時間でもっと回数を跳べるようになるはず。急にやると足を痛めるかもしれないから、最初はそのぐらいのノリでカラダを慣らしていこうと思う。

ordered map 開発のきっかけ

0時に寝て6時半に起きた。神戸に戻ってきてようやく落ち着けた。

今日の運動はスクワットをした。統計を 運動の記録 にまとめる。

ordered map の開発

金曜日は打ち合わせが何もない曜日の1つ。疲労困憊で神戸に戻ってきたので今日はずっとコーディングに集中していた。本当はお仕事終えてからプールへ行こうと思っていたものの、逆にコードを書くのに集中し過ぎて晩ご飯に一旦帰って食べた後も、もう一度オフィスに来て、さらに1時ぐらいまでコードを書いてた。

go の map をイテレートすると意図的にランダムに key-value を返す仕様になっている。これは開発者がキーの順序に依存した実装をしてしまって潜在的バグを混入させてしまう懸念を取り除くため。その設計思想は理解できるものの、go の map を json で返すときに ux の視点からキーの順序を保証したい場面がある。そんなときに ordered map のようなものが必要になる。go のコード内で ordered map を実装しているライブラリはいくつかあるものの、json のシリアライズ/デシリアライズも考慮してキーの順序を保証するライブラリはあまりみつけられなかった。それを考慮しているライブラリの1つに mapslice-json がある。しかし、このライブラリの実装はイケてなくて致命的なバグを1つみつけて PR を送ったものの、おそらく3年ぐらい保守されていない。あとジェネリクスを使うと使い勝手がよくなるからコードを書き換えたい。

結局このライブラリからアイディアだけもらって自分で再実装することにした。そして、array や map が入れ子になるような、複雑なデータ構造のときに map (json で言えばオブジェクト) のキーの順序を保持できないことにもテストを書いていて気付いた。そこの部分も作り直さないといけないとデバッグしたり設計をやり直したりしていた。これは会社の oss ライブラリとして作ってもよいかもしれない。

致命的バグをみつけた

2時頃に寝て5時過ぎに起きた。お風呂に入るときに fitbit を外して、その後付けるの忘れて寝てしまったから睡眠時間を計測できなかった。

今日の運動はレッグレイズ(椅子),腹筋ローラー,腕立て,スクワットをした。統計を 運動の記録 にまとめる。

agent の致命的なバグ

先週から QA テストをしていて agent の致命的なバグに気付いた。もともとあった java 製の agent から私が設計して作り直した go 製の agent になる。ldap エントリーの更新リクエストのテストツール を作ったことでシビアなタイミングによるバグを検出できた。

ツールを使ってテストしていて、成功するはずのリクエストが失敗して、ログを調査しながらデバッグしていた。ldap エントリーを扱う難しさの1つに、ldap サーバーはエントリー間の依存関係やデータの整合性といったものを検証しない。そういった用途はアプリケーションの役割であって、ldap プロトコルはあくまで id を管理することに特化したものという役割分担になっているのだと推測する。そうすると、(open) ldap サーバーへ登録できたエントリーが次の agent や api サーバーといったアプリケーションのレイヤーでエラーになることがある。このとき、ldap サーバーではエラーが発生していないため、直接的なエラーを検知することができなくて、デバッグや調査に時間がかかる。

agent の実装として、ユーザーエントリのストリームとグループエントリーのストリームの2つに分割して、非同期にそれぞれのエントリーを id 連携する設計にしていた。というのは、ldap エントリーにはユーザーやグループといった概念は原則として存在しない。そのエントリーがユーザーなのか、グループなのかはアプリケーションが判断している。アプリケーションの用途としてはこの2つを明確にわけないと不便なことから、ワークアラウンドもしくは実務的な解決策として検索するときのフィルターと base dn で管理するようにしていた。そして、これらをそれぞれ別のストリームとして扱うよう、私が設計していた。このことがタイミングによってはユーザーエントリーとグループエントリーに依存関係がある場合、データの整合性を保証できないことに気付いた。なぜならば、ユーザーエントリーとグループエントリーそれぞれ非同期/並行に処理されてしまうから。

結論としては、ldap サーバーからエントリーの更新の順序を保証するには1つのストリームを subscribe しないといけない。そして、ストリームから取り出したエントリーがユーザーなのか、グループなのかはアプリケーションが判別しないといけない。テストツールを作ったことでシビアなバグも検出できた。

定額減税

定額減税 特設サイト が公開されたらしいというニュースをあちこちでみつける。従業員の税金が安くなるので経営者としての私がなにかしないといけないと思っているけれども、まだ何をしていいのかよくわかっていない。時間をみつけて調べないといけない。税制が変わるとこういった事務手続きが突発的に入ってくるのがマイクロ法人の面倒なところ。

年明けからコーポレート業務いろいろ

23時に寝て4時半頃に起きてそのまま6時半までだらだらして起きた。早寝早起き。

今日の筋トレは腹筋:20x1,腕立て:15x1,スクワット20x1をした。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。今日の議題はこれら。内容が多くて1時間超えた。

  • 電子帳簿保存法対応の事務処理規定の共有
    • まだ始まったばかりで税理士さんの温度感も低い
    • 事務処理規定が省令に沿っているかどうかの判断はプロセス監査で行われる
    • 電子帳簿保存法には規定されていないため、事務処理規定の妥当性の検証は行われない
  • 融資を受ける構想作り
    • 日本政策金融公庫のみで検討していたが、融資実績を作りたいなら信用金庫も加えた方がよい
    • 借金のメンタリティ、担当者との折衝や審査など余裕のあるときに経験を積んでおくことはよいように思えた
  • ファイナンシャルプランナーさんとのやり取り
    • 会社の経費で役員のための保険に入ろうと考えている
    • 個人の保険控除は8万円らしい
  • ローカル複業化プロジェクトの考察
    • IT コミュニティに老人や子どもたちは入りにくい。農業なら老若男女誰でも入れる気がする
    • 農業や地元の特産品を切り口にコワーキング (コミュニティ) で街の人たちと田舎の人たちをつなげるのはすごいことだと思う (関係人口の創出)
    • 地元の有力者と仲良くなると、行政の口利きもしてくれて活動しやすくなる

はらさんが よりひろいフロントエンド を始めたそうでその話しも聞いていた。個人のブログサイトにするのか、複数人で記事を共有するサイトにするのかもまだ曖昧だという。Contentful + Next.js + Cloudflare Pages という構成らしい。Contentful というツールを私が知らなかったのでまた時間のあるときに調べてみようと思う。

母が一人暮らしをしていて体調もよくないことから要介護状態になるリスクがそこそこあると考えている。最悪の場合、会社を休眠させてしばらく介護をするかもしれない。はらさんが仰るには休眠はよいアイディアだという。会社員に例えると退職した日の帰り道を想像するとよいと話されていたのだけど、私は過去の記憶があまりないというのもあるが、これまで6回も退職してきたのに退職日を特別に思ったことはあまりない。退職日と他の日に大きな違いはなくて、次のお仕事の準備や調査をしていることが私は多かったと思う。それでも退職にあわせて有休を1-2ヶ月とってゆっくり過ごしていたことには変わりない。私もそういう、メリハリのある働き方が好きだ。

smtp 接続のタイムアウト

たまたまメンバーが誤った設定で smtp サーバーに接続したときにタイムアウトするまで5分ほどかかるという現象を発見した。タイムアウトの設定をせずに接続しようとするからそんなことが起きるのかな?と考えて smtp クライアントのタイムアウト設定を調べてみると net.DialTimeout を使えばよいという。

多めに見積もって30秒のタイムアウトを設定して接続するようにして再度メンバーに再現検証してもらったら直っていないという。接続そのものは出来ていたのだ。ソースを読んでみると、smtp クライアントを生成するときに 220 というレスポンスを読むことがわかる。誤った接続設定でもコネクションは確立するが、レスポンスが返ってこなくて待ち状態になっていた。

func NewClient(conn net.Conn, host string) (*Client, error) {
	text := textproto.NewConn(conn)
	_, _, err := text.ReadResponse(220)
	if err != nil {
		text.Close()
		return nil, err
	}
	c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
	_, c.tls = conn.(*tls.Conn)
	return c, nil
}

調べてみると Conn インターフェースにデッドラインを設定する API が提供されている。注意事項としては接続した後にデッドラインをリセットしないといけない。

デッドラインとは、I/O操作がブロックされずに失敗する絶対時間のことである。デッドラインは、ReadやWriteを呼び出した直後のI/Oだけでなく、将来の保留中の I/O すべてに適用される。デッドラインを超過した後は、未来にデッドラインを設定することで、接続をリフレッシュすることができる。

デッドラインを超えた場合、ReadやWrite、その他のI/Oメソッドの呼び出しは、os.ErrDeadlineExceeded をラップしたエラーを返します。これは、errors.Is(err, os.ErrDeadlineExceeded)を使用してテストすることができます。errorのTimeoutメソッドはtrueを返しますが、期限を超過していなくてもTimeoutメソッドがtrueを返すエラーが他にもあることに注意してください。

アイドルタイムアウトは、ReadまたはWrite呼び出しに成功した後、デッドラインを繰り返し延長することで実装できる。tの値が0であれば、I/O操作はタイムアウトしない。

https://pkg.go.dev/net#Conn

次のように SetReadDeadline() を使ってタイムアウトを5分から30秒に短縮できた。

func (s *Clinet) connectWithReadDeadline(conn net.Conn) (*smtp.Client, error) {
	if err := conn.SetReadDeadline(time.Now().Add(dialTimeout)); err != nil {
		return nil, fmt.Errorf("failed to set read deadline: %w", err)
	}
	c, err := smtp.NewClient(conn, s.config.Host)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to the smtp server: %w", err)
	}
	// clear read deadline
	if err := conn.SetReadDeadline(time.Time{}); err != nil {
		return nil, fmt.Errorf("failed to reset read deadline: %w", err)
	}
	return c, nil
}

gitlab ci/cd のローカルデバッグ

23時頃から寝始めて3時に起きて5時半に起きて8時過ぎに起きた。久しぶりに寝坊した。

gitlab-runner のデバッグ

mongodb のレプリカセット対応して、ローカルでは結合テストが動くものの、gitlab ci/cd 環境では動かなくなった。gitlab ci/cd は GitLab Runner によって提供されている。そのデバッグのため、ローカルに gitlab-runner をインストールして調査した。

GitLab Runner のインストール ドキュメントにそれぞれの OS 向けのドキュメントがある。Debian/Ubuntu/Mint 向けのインストール手順を行う。

$ curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
$ sudo apt-get install gitlab-runner
$ gitlab-runner --version
Version:      16.6.1
Git revision: f5da3c5a
Git branch:   16-6-stable
GO version:   go1.20.10
Built:        2023-11-24T21:11:36+0000
OS/Arch:      linux/amd64

.gitlab-ci.yml があるディレクトリへ移動して、ジョブを指定して実行する。ローカルでの変更内容を検証するときはブランチにコミットしないといけない。コミットしていないと次のワーニングが表示される。

WARNING: You most probably have uncommitted changes. 
WARNING: These changes will not be tested.         

dind なジョブを実行するときは --docker-privileged で特権を付けて実行する。環境変数は --env KEY=VALUE で渡せるが、CI_JOB_TOKEN のような組み込みの環境変数は上書きできない。

$ cd path/to/repo
$ gitlab-runner exec docker --docker-privileged ${ジョブ名}

svelte コンポーネントの実装は簡単

1時に寝て何度か起きて7時に起きた。日曜日の夜に業務スーパーへ行ったら生鮮系は売り切れているのが多かった。日持ちするようなものを購入した。呪術廻戦ゲーム の初心者ミッションをクリアしたのでゲームの時間を減らしていく。

kit/vite アプリケーションのデバッグ

先日の続き の続き。

ある kit アプリケーションの svelte コンポーネントから外部の kit アプリのコンポーネントやモジュールを埋め込むことができるかどうかを調査した。ドキュメントの Loading data をみながらコンポーネントを書いてみる。フロントエンドの開発はすべてメンバーに委譲しているので私はほとんど開発していない。ドキュメントみないとまったくどう実装していよいかわからない。

svelte コンポーネントをレンダリングするときにサーバー側で動かすのは +page.server.ts に、クライアント側で動かすのは +page.ts に実装する。今回の場合、外部の node.js プロセスに起動したサーバーに対してリクエストして index.html に相当するものを取得するのでサーバー側で取得したレスポンスから html を取り出して、それをコンポーネント側でレンダリングする。+page.server.ts は次のように実装する。

import type { PageServerLoad } from './$types';
import { apps } from '$lib/index';

export const load: PageServerLoad = async ({ params }) => {
	const res = await fetch(apps['kit-demo1'].entrypoint);
	const html = await res.text();
	return { html };
};

この html をクライアント側の +page.svelte から参照してレンダリングする。

<script lang="ts">
	import type { PageData } from './$types';
	export let data: PageData;
</script>

<div>{@html data.html}</div>

これで一応は意図した kit アプリケーションを埋め込むことはできるが、実際にはスクリプトなどはなにかが競合して動かないようだ。これは node.js から取得するスクリプトやスタイルなどが複数の kit アプリケーションで競合してしまうからではないかと推測する。

これが ssg ならば adapter-static を使ってビルドして、その成果物を static ディレクトリ配下に置くだけでそのまま動く。これは特別ななにかではなく、kit アプリケーションとして意図した振る舞いにはなる。これが出来て嬉しいことはあまり思いつかないが、想像した通りに動くかどうかの検証のために確認した。

次のリポジトリに調査した内容のサンプルコードを作った。ここまでの調査内容でまたテックブログを書いてみようと思う。

複数の kit アプリケーションを共存させる仕組みの考察

1時に寝て3時に起きて5時に起きて7時半に起きた。なんとなく布団に入らずにベッドの上でそのまま寝てた。それでもあまり寒くはなかった。

kit/vite アプリケーションのデバッグ

先日の続き の続き。

kit アプリケーションを kit アプリケーションに埋め込むといったことができないかどうかの調査をしている。いろいろ調べている中で kit の discussions でもそういった議論はいくつか行われている。マイクロフロントエンドというキーワードも出てくる。

これらの議論をみていても kit の ssr はそれ自体が1つのアプリケーションとして動かすことを前提にビルドされているため、kit アプリケーション内に別の kit アプリケーションを埋め込んだり、一部のコンポーネントを外部のアプリケーションと組み合わせて動かすことはなかなか難しいようにみえる。マイクロフロントエンドのような思想で設計されていない。しかし、既存のアプリケーションを動かしつつ、少しずつ kit アプリケーションへ移行するといった運用をしたいという世の中のニーズも根強いことが伺える。

ここで svelte.config.js でエントリーポイントを置き換えるぐらいはできる。デフォルトは / がエントリーポイントになるのを /myapp に置き換えるには次のように設定する。relative は es モジュールのインポートを相対パスで行うか、絶対パスにするかの設定も変更できる。これもデプロイ先のインフラの都合にあわせて調整できるようになっている。この設定を切り替えられるのだからエンドポイントをハックすること自体はそう難しくないのかもしれない。

kit: {
  paths: {
    relative: false,
    base: '/myapp'
  },
}

さらに調査していて、adapter-node を使ってビルドするとデフォルトでは polka というアプリケーションサーバーが起動するコードが生成される。

function polka (opts) {
	return new Polka(opts);
}

const path = env('SOCKET_PATH', false);
const host = env('HOST', '0.0.0.0');
const port = env('PORT', !path && '3000');

const server = polka().use(handler);

server.listen({ path, host, port }, () => {
	console.log(`Listening on ${path ? path : host + ':' + port}`);
});

ここで任意のアプリケーションサーバーを使いたいという issue があって、それに対する回答から adapter-node のドキュメントにカスタムサーバーについて書かれていることに気付く。

アダプタは、ビルドディレクトリにindex.jsとhandler.jsの2つのファイルを作成します。index.js を実行すると (デフォルトのビルドディレクトリを使用している場合は node ビルドなど)、設定されたポートでサーバが起動します。

あるいは、Express、Connect、Polka(あるいは組み込みのhttp.createServer)に適したハンドラをエクスポートするhandler.jsファイルをインポートして、独自のサーバをセットアップすることもできます。

handler.js さえインポートすればそのまま動くことはデバッグしていて知ってはいたのだけど、この自前のアプリケーションサーバーを hooks を使って起動すれば任意のサーバーに置き換えできると issue の中で回答されていた。kit アプリケーションは1つのサーバーが1つのシステムとして動かすことを前提に設計されているが、サーバーを複数起動することでそれらを共存できるのではないか?と考えた。検証のために node.js から子プロセスを生成するには次のようなコードで起動する。些事だけど adapter-node の生成したコードが shell を介しないとポート番号を設定できなかったので shell: true もセットしている。

import { spawn } from 'child_process';

export function start_server() {
  console.log('called start_server');
  const opts = {
    shell: true,
    env: {
      ...process.env,
      PORT: '3005',
        ORIGIN: 'http://localhost:5174',
      NODE_ENV: 'production'
    }
  };
  const node = spawn('node', ['apps/myapp/build/index.js'], opts);
  node.stdout.on('data', (data) => {
    console.log(data.toString());
  });

  node.stderr.on('data', (data) => {
    console.error(data.toString());
  });

  node.on('exit', (code) => {
    console.log(`Child exited with code ${code}`);
  });
}

この node.js の子プロセスを起動する処理を hooks で呼び出すことである kit アプリケーションを起動したときに、別の kit アプリケーションを提供するアプリケーションサーバーの node.js プロセスも起動できる。そしてパスを解決できるようにするため、さらに es モジュールのインポートパスにあわせたプロキシを実装する。

import { start_server } from '$lib/index';

start_server();

import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  if (event.url.pathname.startsWith('/myapp')) {
    if (event.request.method == 'GET') {
      return fetch('http://localhost:3005' + event.url.pathname);
    } else if (event.request.method == 'POST') {
      const data = await event.request.formData();
      const endpoint = 'http://localhost:3005' + event.url.pathname + event.url.search;
      return fetch(endpoint, { method: 'POST', body: data });
    }
  }
  const response = await resolve(event);
  return response;
};

これは kit のデモアプリが動くことを確認するためだけに実装したプロキシで GET/POST のリクエストを localhost:3005 に起動した node.js のプロセスへプロキシしている。これで2つの kit アプリケーションが1つのサーバーで共存しているかのように振る舞うことは確認できた。この延長上に私のやりたいことが実現できるかどうかをさらに調査する必要がある。

sveltekit/vite アプリケーションの調査を再開

1時に寝て起きたか起きてないか覚えてない感じで6時に起きた。起きてちょっとゲームして気付いたら7時だった。

kit/vite アプリケーションのデバッグ

先週公開したテックブログ の続き。

vite アプリケーションのバックエンドインテグレーション の詳細を調査している。丸1日デバッグしていていくつか振る舞いがわかってきて、designer アプリケーションを作りたいという要件に対して、こうすればできるんじゃないかという仮説も立てられるようになった。いまやりたい要件は kit の ssr アプリケーションを埋め込みたい。これは要件に満たないが、kit の ssg アプリケーションならば static ディレクトリに置くだけで参照できるし、インポートパスさえ書き換えてやれば別の kit アプリに埋め込むこともできるのを確認した。意図した通りの振る舞い。

vite アプリケーションはビルドオプションで manifest.json を出力し、エントリーポイントやどのファイルがどのファイルをインポートしているかといった情報を管理している。sveltekit はこれらの manifest.json から rollup でバンドルするために manifest.js を生成している。厳密には、sveltekit では production ビルド向けのチューニングをしたビルドツールを adapter と呼び、vite のビルドをフックする場所に1つになっている。node.js サーバー向けに production ビルドするときは adapter-node を使う。この実装を読んでみると、vite がビルドした成果物に対して、再度 rollup でバンドルして成果物を作り直すといったことをしている。そして、vite の成果物 (manifest.json も含む) を抽象化したものが Builder となる。adapter は Builder のインスタンスを使ってビルドの成果物を制御できる。先の manifest.js もこのときに生成していて、rollup でバンドルするためのパラメーターの1つとして使っているようにみえる。しかし、rollup のドキュメントをみても直接的に manifest.js の説明はなく、rollup の拡張の仕組みで manifest.js を作っているというよりは、sveltekit の要件によるもののようにもみえる。ここの背景はまだよくわからない。

私はフロントエンドのことが全然わからないのでライブラリのソースコードを読みながら、ドキュメントとあわせて調べて、1つずつ理解を深めていくというアプローチで進めている。こういった調査のやり方もメンバーへ伝えていければと考えている。

小規模企業共済オンライン手続きポータル

2021年度から小規模企業共済 に加入している。今年から掛け金を7万円/月に変更した。年間で84万円の所得控除となる。ちょうど2023年9月1日からポータルサイトが作成されたらしい。いずれマイナポータルと紐付くのかもしれない。

利用登録しようと思って、メールアドレスを登録しようとしたら会社のメールアドレスはなぜかバリデーションエラーになって gmail のアドレスなら登録できた。その後も氏名の半角カナ入力を強制されたりしながら、マイナンバーカードを読み取って認証チェックして利用登録の申請はできた。しかし、自動で本登録されるわけではなく、おそらく申請内容が先方に届いてなんらかの運用があって本登録されるみたい。オンラインポータルのホームでも半角カナを使っていたり、<title> タグには「マイナ手続きポータル」とあったり、申請しただけでいくつも不備がわかるようなひどいサイトになっている。2023年にまともな開発者が作ったサイトとは思えない。デジタル庁に作り直してもらった方がよいと思う。

selinux はなるべく有効にして使うもの

22時ぐらいから寝始めて何度か起きて6時に起きた。早く寝たから早く起きた。

selinux の微妙な振る舞い

今日は火曜日なのでチームの定例会議をやって、ドキュメントを書いて、その後はインフラの細かい作業をわちゃわちゃやって、ドキュメントを書いてとわちゃわちゃやってた。

先週、最新の almalinux 8 をインストールして、その後、lvm の論理ボリュームの結合 とか、rootless コンテナ の設定とか、テスト環境を構築していた。gitlab ci/cd から ssh で公開鍵認証を使ってデプロイしている。作り直したこのテスト環境に対してその公開鍵認証がうまく動かない現象に遭遇した。よくある設定や権限のトラブルではなく、デバッグ用の sshd を起動すると公開鍵認証できた。なにかしら systemd 経由で起動する sshd の設定ミスなんじゃないかと、2-3時間デバッグしてもわからなくて社内の有識者に尋ねてみた。

$ sudo /usr/sbin/sshd -d -p 2222

selinux を無効にしてみたら?というアドバイスをいただいて、試しに enforced から disabled にしたら動いたので selinux のなにかしらの設定を変えてしまったのかな?とそのときは思っていた。しかし、別の開発者からデフォルト設定で enforced でも動くはずという情報をもらって、もう一度 disabled から enforced に戻して再起動したら普通に動いて、その前の公開鍵認証の失敗を再現できなくなった。私にはこの先のデバッグはまったくわからない。お手伝い先のシニアエンジニアの方にみてもらって次のようなことを教えてもらった。

SElinuxが怪しいなと思ったら、/var/log/audit/audit.log とかausearch -m avcコマンドを確認。
authorized_keysのアクセスが拒否されているので確かにSELinuxの問題があったことがわかります。
type=AVC msg=audit(1696315292.258:1446): avc: denied { read } for pid=446534 comm=“sshd” name=“authorized_keys” dev=“dm-0” ino=201836096 scontext=system_u:system_r:sshd_t:s0-s0:c0.c1023 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
現在、authorized_keysのコンテキストは期待通りunconfined_u:object_r:ssh_home_t:s0となっているけど、問題が起きていたときは、unconfined_u:object_r:default_t:s0 だったことがわかります。
詳しい経緯はわからないけど、.ssh/authorized_keysを作成した時点でopenssh用のselinuxポリシーが適用されていなかったと考えられます。
その後なにかのイベント(再起動?)でrestorecon 相当が行われて、コンテキストがssh_home_tに変更され問題は解消した。
なんだかよくわかないけど、OSのマイナーバージョンアップで微妙にセキュリティコンテキストが変更されてrestoreconすると解決する、ってのは時々起きてますね。
たぶんopensshインストール前にrsyncしたのでコンテキストがdefault_tになってたんじゃないかと。なかなかの罠ですね。

おそらく lvm の論理ボリュームのバックアップ/リストアに rsync -a を使った (本当は cp -aの方がよい) ことによる問題ではないかということ。私が報告した状況と selinux のログからすぐ助言できるのが素晴らしいと思う。まだまだ私のインフラエンジニアとしての未熟さを実感した瞬間でもあった。一昔前は selinux は disabled にするものという常識だったが、最近は初期設定で動くようになっているのでなるべく selinux は有効にして運用するものという意識に変わってきているらしい。