やわらかテック

仕事の話などを書きます。執筆にはAIを使っていません

独自ホスティングのブログへ移行しました

前の記事でお伝えしていた通り、移行しました。
今後は「はてなブログ」の方には新規記事の投稿をしませんが、既存の記事は URL そのままです。 読者の方にはご迷惑をおかけしますが、RSS フィードは用意しましたのでご活用ください。

やわらかテック

RSS

構成

Cloudflare から Hugo を使って静的ファイル化した記事を配信しています。
もちろん無料運用 ✌️

Secure by Designを読んだ

社内で設計に明るい方がオススメされていたので読んでみました。
この生成 AI 時代では勝手にセキュアなデータがコミットされてしまっていたり、インジェクション攻撃が可能なコードが知らぬ間に実装されてしまっていたりと、よりセキュリティに対して意識を強くもたねばなりません。 かなり分厚い書籍ですが、翻訳書に多い海外のジョークや癖のある言い回しもほとんどなく、簡潔な表現がされており読みやすかったです。

Secure by Design

タイトルにもなっている「Secure by Design」とはセキュリティを高める設計(デザイン)を指していました。どうしてもセキュリティの対策は後回しになりがちで、セキュリティ診断サービスを受けたり、考えたくもないですが、実際に攻撃を受けてからインシデントとなり、結果的にセキュリティの重要さに気づくことがあります。開発とセキュリティ対策が分離されるのが従来のやり方であり、実際に求人を見てみると、開発エンジニアとは別にセキュリティエンジニアが募集されているケースが多いです。

著者達はこの現状を問題だと考えており、解決のために「Secure by Design」を提唱しています。Secure by Design を実践することで、開発とセキュリティが分離せず、自然と高いセキュリティをもったコードが実現されていくとのことです。

ドメイン駆動設計をする

どのように Secure by Design を実現するのか?という問いに対してドメイン駆動設計が登場してきます。 ドメイン駆動設計ではドメイン知識を値オブジェクトやエンティティといったモデルとして定義することで、ドメイン知識をコードへそのまま反映させます。すでに各所で値オブジェクト・エンティティの解説があるので、こちらでは省略します。

さて、どのようにドメイン駆動設計がセキュリティと関わってくるのでしょうか。
次のコードは、ユーザー名とメールアドレスを受け取り新規ユーザーを作成する API の実装です。ドメイン駆動設計では、知識や制約をきちんとモデル化する事が重要ですが、このコードではユーザー名とメールアドレスは、ただのStringとして扱われています。

// POST: /users に対応するサービス
class UserService {
    fun createUser(name: String, email: String): User {
        val connection = DriverManager.getConnection("jdbc:mysql://localhost/mydb", "user", "pass")
        val statement = connection.createStatement()

        val sql = "INSERT INTO users (name, email) VALUES ('$name', '$email')"
        statement.executeUpdate(sql)
        
        return User(name, email)
    }
}

ただのStringだということはメモリ容量に収まる限り、あらゆるどんな文字列であっても制限なく受け入れてしまいます。 この仕様を悪用すれば SQL インジェクションや、巨大な文字列をリクエストして DB をダウンさせたりといった攻撃ができてしまいます。つまり、攻撃が成り立ってしまうのは、悪意のあるリクエストがどこでもブロックされていないからです。

悪意のあるリクエスト

{ 
  "name": "山田'); DROP TABLE users; --",
  "email": "aaaaaaaaaaaaaaaaaaa...@example.com" 
}
INSERT INTO users (name, email)
VALUES ('山田'); DROP TABLE users; --', 'aaaaaaaaaaaaaaaaaaa...@example.com')

プリミティブ型をそのまま使わない

ユーザ名とメールアドレスをStringで定義するべきなのか?というのが、ドメイン駆動設計で何度も問われていることです。 メールアドレスには国際標準規格があります。ユーザー名はどうでしょうか。このサービスのコンテキストでは、ユーザー名に記号を使えない・最大 100文字のような制限があるかもしれません。こういった知識・制約をコードに正しく反映させてモデル化することで、自然とコードは堅牢でセキュアな実装となっていきます。

ドメインプリミティブ

本書では知識・制約を正しく反映させた値オブジェクトのことをドメインプリミティブと定義しています。 試しにユーザー名をドメインプリミティブとして定義するとこんな感じでしょうか。記号は使えず 100文字以内の制約が表現されました。この時点で、先ほどの SQL インジェクションと巨大データのリクエストでユーザー名のインスタンスを作ることは不可能になりました。後続の書き込み処理でUserNameを扱うようにすれば、攻撃をブロックすることができます。

@JvmInline
value class UserName private constructor(val value: String) {
    companion object {
        // 英数字、ひらがな、カタカナ、漢字、スペースのみ許可
        private val ALLOWED_PATTERN = Regex("^[a-zA-Z0-9\\p{IsHiragana}\\p{IsKatakana}\\p{IsHan} ]+$")
        
        fun of(value: String): UserName {
            require(value.isNotBlank()) { "ユーザー名は空にできません" }
            require(value.length <= 100) { "ユーザー名は100文字以内にしてください" }
            require(ALLOWED_PATTERN.matches(value)) {  "ユーザー名に使用できない文字が含まれています" }

            return UserName(value)
        }
    }
}

インフラや設定で守るべき箇所はどうするのか

Infrastructure as Code (IaC)や継続的なCI/CD パイプラインなど、近年では当たり前となった手法が紹介されていました。
イテレーティブな開発の中に侵入テストや脆弱性診断などを取り組み、短いスパンで発見・学習をできる仕組みづくりをすることが推奨とされていましたが、特に目新しいトピックはなかったのかなと思うので、気になる方は実際に目を通してみてください。

詰まる所

これはセキュリティの本というよりもドメイン駆動設計の本です。
本書のおもしろい点は、従来から広く支持されているドメイン駆動設計にセキュリティの観点を組み合わせている点です。自分も今まで考えたことがなかったのですが、言われてみると確かにな...と感じました。一方で、ドメイン駆動設計をすでに知っている前提で書かれているのと「プリミティブ型をそのまま使わないで〜」という一貫した主張を 500P ほどのボリュームを使って書いているので、この本を真っ先に読んだ方が良いよ!とは思いませんでした。

しかし、冒頭でも書いた通り生成AI が著しいこの時代こそ、セキュリティに関心を持ち Secure by Design でセキュアなコードを書く意識は重要さを増しています。
実装が楽になった分、ドメインプリミティブを使ったコードを生成させるようにしたいですね。そのためにはドメインエキスパートの会話が不可欠です。ここは結局、サボれないので、今日も今日とてドメインに向き合っていきたい。

4月の振り返り

もう暑い...
春はどこへ行ってしまったのかと思いきや、急に冷える日もあり服装に迷う日々です。 ありがたいことに体調を崩すこともなく元気に過ごしています。ギリ汗をかかない心地よい気温なので散歩が捗るのですが、少し前に話題になったセブンイレブンの紅茶マシーンが家の近所?(徒歩30分)にも設置されて、定番の散歩コースになりつつあります。数百円でこのクオリティーはすごい。

4月度の成果

品質

- 専門的なテスト知識の獲得
- アプリケーション品質の向上
  - E2E テストの棚卸し(Flaky テストの撲滅とテストピラミッドのバランシング)
  - リグレッションテストの再設計(初期のものから更新ができていない)
  - テスト戦略の定義・チームでの合意
  - テストガバレッジの確立(品質の数値化を検討)

先月の振り返りに記載した通り、体制が変わったことで別部署がオーナーをとる形になりました。
引き続き安全で高品質な開発を続けていきたい気持ちですが、特別な成果物はありませんでした。感想は別の記事に書こうと思っていますが、社内で設計に強いメンバーから「Secure by Design」という書籍を紹介して頂き、こちらを読み進めています。

反省点

特にありません。
新年度の考えた品質へのコミットはある程度、達成したのかなと思います。JSTQB の資格も取れましたし、テストピラミッドのバランシングの方針を示して実施したことで、E2E テストの拡充とトレードオフについて社内で認知されるようになってきたのかなと感じています。生成 AI 時代ということもあり、従来の E2E テストサービスでは自由度が低く扱いが難しいため、自分の観測している感じだとplaywrightに移行する事例が増えています。一方、テストレポートや実行基盤の構築、メンテナンス(playwrightに限ったことではないですが)などの課題には向き合っていかねばなりません。

プロダクトと向き合う

- 複雑なドメイン知識と向き合う
  - 医療制度(特に負担額計算領域)の理解
  - 知識をコードに落とし込む経験をより積む(eg: いわゆるDDDの実践的トライ)
- 歴史あるコードとの向き合い知識の獲得
  - レガシーコード改善ガイドを読む(購入済み)
  - 単体テストの考え方/使い方

今まで実装を始めようにも、期待値が不明なケース・なぜこの期待値になるのかの根拠を理解できない事が多かったですが、かなりドメイン知識が身についてきた成果なのか、詰まることが減りました。直近では労災の点数・負担額計算の開発をしており、どんなケースをサポートしないといけないのか・既存の健保の点数・負担額計算の処理を使いまわせるのかを判断んするために、改めてコードを読み直す機会が増えました。コードを理解するにあたって、今まで流し見程度だった厚生労働省が公開している資料を読み漁っています。

反省点

手を動かす際はコードファーストで実装から始めていましたが、ドメイン知識(制度や期待値)に詳しくないことで何度もバグを出してしまい遠回りしてしまいました。イテレーティブな開発をしているので、動く成果物を作る・見せるという観点では良かったのですが、レセコン開発では設計をミスるとあっという間にモデルが破綻してしまいます。 改めてドメイン知識を事前に得ることの重要性を感じましたが、ややディープな領域なので根拠のある情報を見つけるのが難しいという課題もあります。

総評

やることをやった1ヶ月でした。
労災の開発にフィットすることに全力投球をしたので、自発的な成果物を生み出せなかったのは反省点。ここには詳しく書けないですが、かなりバタバタと忙しい月でした。目標がやや形骸化してきているので、改めようと思います。せっかくの GW なので時間を確保したい。

お知らせ

近々、はてなブログから独自ホスティングのブログに移行しようと思います。
今まで「はてなブログ Pro」を使っていましたが、記事を有料サービスを使ってまで書くモチベーションがなくなりました。PV を狙った記事を書いていない(書く気もない)のと、生成 AI の影響かブログの PV が激落しておりGoogle Adsenceで「月の支払い額 < 収入」を実現できなくなりました。独自ドメインが使えるのと、広告が自由に配置できることをメリットに「はてなブログ Pro」を使っていましたが、あまり魅力ではなくなりました。

また、はてなの振込み詐欺事件のこともあり、この会社のサービスを使い続けることに不安も感じています。

3月の振り返り

ようやく過ごしやすい季節になってきましたね。
やや寒さを感じるものの、何枚も着込んで外に出ていたのが嘘のように暖かくなりました。すっかりと外の様子も変わり、早咲きの桜は散り、枝垂れ桜が花を咲かせていました。美しい...

3月度の成果

品質

- 専門的なテスト知識の獲得
- アプリケーション品質の向上
  - E2E テストの棚卸し(Flaky テストの撲滅とテストピラミッドのバランシング)
  - リグレッションテストの再設計(初期のものから更新ができていない)
  - テスト戦略の定義・チームでの合意
  - テストガバレッジの確立(品質の数値化を検討)

引き続きテストピラミッドのバランシングに取り組みました。 先月分と合わせて全体の 10% ほどを単体テスト化することで、E2E のクレジット削減・テストサイクルの高速化を達成することができました。 Flaky な E2E テストが削減されたことで管理のコストも減って万々歳です。特段、新しいことをやっていたわけではありませんが、地道に取り組んだことによって成果を感じられるようになりました。

反省点

一方で体制が変わったこともあり、テスト戦略やその他、施策に私が取り組む必要性が低くなってきました。 とはいえ、品質の可観測性を高めたいという思いがありましたが、ゴールのビジョンを描けておらずここまで目立った成果を上げることができませんでした。知見のない領域ではありますが、思いが先行して先走ってしまったことを反省しています。
(やる気があるのはいい事と思いたい)

プロダクトと向き合う

- 複雑なドメイン知識と向き合う
  - 医療制度(特に負担額計算領域)の理解
  - 知識をコードに落とし込む経験をより積む(eg: いわゆるDDDの実践的トライ)
- 歴史あるコードとの向き合い知識の獲得
  - レガシーコード改善ガイドを読む(購入済み)
  - 単体テストの考え方/使い方

今まで得てきた知見が積もり積もって、本格的に不具合修正や機能追加に取り組みました。 実装面では Claude Code の活用や、テストファーストで再実施可能な状態を用意する工夫で、手が詰まることはあまりないと感じています。ただ、ドメインが難しすぎて、期待値が分からず手が止まるというケースが多いです。特に医療費の負担額を算出するケースでは、特例や特殊ルールが多く、これらが重なりあった場合、果たしてどうなるのか?を開発者だけの知識で判断することは困難を極めます。

宣伝

ここらへんの話を技術書典20 にて頒布される書籍に執筆しました。
情報が公開されて次第、こちらにも記載します。乞うご期待!

公開されました!

techbookfest.org

反省点

先ほどの話と関連しますが、何かを修正した時に全体の計算モデルとして破綻していないか?は常に気を付ける必要があります。 しかし、ドメイン知識がまだまだ弱いので、こういったケースでヨシ!の判断を自信を持ってすることができません。自分ができる事としては、知識を引き続きインプットしていくことと、テストを拡張してデグレを検知できる確率を上げることでしょうか。

総評

あっという間に 1Q が終わりました。
成果を感じている・そうではないものが明確に別れてきたので、自分の強みや興味関心が少し透けてきたのかなと思います。 品質に関しての具体的なアクションは前述の通り、減ってくるかなと思いますが、引き続きドメインにはしっかりと向き合っていきたい所存です。

以上です。

おまけ 最近やめたこと

わざわざ別記事にするほどの内容でもないので、ここに書いておきます。

バーベルスクワット

ある日のこと、膝に謎の違和感と痛みが...
真っ先に思いついたのは週に一度、取り組んでいるバーベルスクワットです。トレーニング中に膝からパキパキと音がすることがあって、痛みはなかったので気にしていなかったのですが、おそらく膝にダメージがあったのかなと思います。爆発力と扱える重量の観点では、バーベルスクワットに勝る種目はないと思いますが、膝の持続性を考えると微妙かも...と思い止めてみました。

www.youtube.com

とはいえ、下半身のトレーニングをしないのは嫌なので、メニューをゴブレットスクワットとブルガリアンスクワットに変更しました。なぜこの 2種目なのかは話すと長くなるので、興味があればコメントなどで教えてください🙏

エナジードリンク(清涼飲料)

シンプルに味が好きで週に一度ほど、飲んでいました。
ただ、明らかに体に悪いだろうなぁ...と自覚はあったので、控えるようにしてみた結果、肌荒れ・下痢の頻度が減ったと感じています。 気づいたら清涼飲料も飲まなくなっていました。ただ、どうしても飲みたくなった時は糖分の含まれていない炭酸水を飲んでいます。

UPDATEクエリの実行時間をどう予測するか

実行時間を知りたい

サービスの運用中には、テーブル構造を変更するマイグレーションがよく発生します。
頻出なのは新規カラムを追加するケースでしょうか。テーブルに新たなカラムを nullable で追加して、DDL の完了後に手動でUPDATEを実行して null を埋めるような対応です。 その際に、対象となるレコードが 100万件を超えるような場合、どれぐらいクエリの実行時間が必要なのかを事前に把握して、ユーザー影響が少ないように実行タイミングの判断をする必要があります。

考えられる選択肢

  • 何も気にせず実行する
  • アクセスの少ない時間帯(夜間)に実行する
  • バッチ分割して少しずつ実行する
  • メンテナンスモード中に実行する

コストを知ることはできる

クエリ実行のコストを分析するにはEXPLAINを使います。
しばしばSELECTのクエリに対してEXPLAINを実行してきましたが、実はUPDATEDELETEであってもEXPLAINは有効であり、実際にはデータの更新、削除はされないため、安心して実行をすることができます。そのため、大量のレコードに対して更新クエリを実行したい場合、事前に副作用なく安心してコストを知ることが可能です。

EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement

EXPLAIN

しかし、実行計画で知れるのはコストであって、実際の実行時間の情報ではありません。 EXPLAIN ANALYZE UPDATE...とすれば、実際の実行時間を知ることができますが、実際にデータが更新されてしまうため採用できません。

仮説: 実行時間は対象レコード数に比例する

どうすれば更新クエリの実行時間を知ることができるでしょうか。
実際に実行してみる以外で、よく見かけるアプローチは、事前に任意件を更新対象としたクエリを実行・計測をして、件数を増やした場合の実行時間を予測するというものです。 何となく良さそうに聞こえますが、この予測は「実行時間が対象レコード数に比例する」という前提の元で成り立っています。本当にそうなのでしょうか。

データベースにもよりますが、更新といっても実際にはWALへの書き込み、VACUUMの実行などの処理が実行されており、これらを無視して比例を前提とするのは、やや楽観的だと感じました。

計測する

ということで、実際に計測してみたいと思います。
更新対象となるデータ件数を 10倍ずつ増やしながら、実際の実行時間を信頼性を高めるため、EXPLAIN ANALYZEを用いて5回ずつ計測します。 完全にクリーンなテーブルに対して実行するケースと、既存のテーブルに対して実行するケースでは結果に差異がありそうなので、合わせて検証してみます。

  • 毎回テーブル再作成: 計測ごとにTRUNCATEを実行して計測する
  • VACUUM FULL: データはそのままでVACUUM FULLを実行後、計測する
  • クリーンアップなし: 何もせずデータを増やして計測する

シナリオ

  • RLS(RowLevelSecurity) を導入するため、全てのテーブルに組織IDを定義したい
  • ユーザーが投稿した記事を管理するテーブルに組織IDがないため、DDL で organization_id: nullable を追加した
  • 手動でorganization_idを埋めるクエリの実行をする
UPDATE user_posts
SET organization_id = users.organization_id
FROM users
WHERE
  user_posts.user_id = users.id AND user_posts.organization_id IS NULL
;

詳細なスペック

  • マシン: MacBook Air (Apple M1, 8コア, 16GB RAM)
    • Docker Desktop for Mac
    • PostgreSQL 16 (公式Dockerイメージ)
    • ストレージ: tmpfs (RAM上, 4GB)
      • ディスクI/Oノイズ排除のため
    • 共有メモリ: 512MB (shm_size)
  • テーブル構成
    • organizations: 10件 (固定)
    • users: 1,000件 (固定, 1組織あたり約100人)
    • user_posts: 100 / 1,000 / 10,000 / 100,000 / 1,000,000 件 (可変)
      • organization_id に関するインデックスはなし

結果と考察

図1より、実行時間とデータ件数には、ほぼ比例(原点を通っていないですが、0件で計測すれば0となる)の関係があることが分かります。 実行ケースによる違いもほとんどなかったのは思わぬ結果でした。一方で、図2からはクリーンアップなしのケースでは実行時間に悪化があり、何らかの影響を受けていると考えられますが、データ件数が要因だと結論を出すことはできません。1件あたりの実行時間はデータ件数との明確な関係性はなさそうです。

あまり考察しがいのない結果となりました...
今回の計測結果からは「実行時間は対象レコード数にほぼ比例する」と判断できそうです。

注意点

今回の計測はtmpfs(RAM)で行っており、HDD / SSD では1件あたりの実行コストは大きくなります。件数に対する増加パターン(線形性)はリソース境界を超えない限り同様と考えられますが、同じような結果になる保証はありません。

あくまで理想状態での計測

今回は、データベースに対して他のアクセスがない、言ってみれば理想状態での計測を行いました。 実際の本番環境では、複数プロセスからテーブルへの読み込み・書き込みやロックを扱っていたり...と複雑な状況があります。より信頼性のある計測をするには、同じような複雑な状態を再現したい気持ちはありますが、手元で簡単に再現できるものではありません。 結局、稼働状況によっては比例の関係を覆す変数が容易に存在するし、その影響はデカいという事実からは逃げられません。

なので、参考値程度に比例を前提に予測をするのが、現実的な受け止め方でしょうか。

実行時間は予測できないもの

そもそも、計測できないものを予測しようという考えが良くないかもしれません。
実行時間は予測できないものとして制約を受け入れてアプローチを検討した方が良いのではないでしょうか。 たとえば、1つのクエリとして実行するのではなく、データ整合性に注意しつつ、バッチ単位で小さく更新を行うといった方針は検討できます。
nullable のカラムを足すようなテーブル変更の段階で求められる整合性は、外部キー制約ぐらいな気がするので、前提を切り替えてこのアプローチを第一に考えた方が良さそうです。