Architecture as Code という捉え方
アーキテクチャは図ではなく「変更に晒されても保たれる制約の集合」で、文書にしか無い制約は保たれない。判断(ADR)・強制(検査)・証拠(実測)の3点セットをコードとして持つ、という整理のメモ。
検証用のモノレポをしばらく育てていて、「自分が作っているのはサービスではなく 制約の集合だ」という感覚がだんだん強くなってきた。その整理のメモ。
アーキテクチャは図ではない
アーキテクチャの説明としてよく出てくるのは構成図だ。箱と矢印。だが図は現在の状態の スケッチであって、システムが変更に晒されたときに何も守ってくれない。
実例をひとつ。複数のサービスが同一の MySQL インスタンスに同居し、サービスごとに 専用の database を持つ構成を考える(インスタンスを分けるほど予算がない、 モノリスから切り出した直後、などでよくある形)。設計文書には 「他サービスの database のテーブルを参照しない」と書いてあるとする。
ところが MySQL は、database を跨ぐ外部キーをエラーも警告もなく許す。
-- 注文サービスの database から在庫サービスの database への参照。
-- 規約違反だが、MySQL は普通に通す
ALTER TABLE orders.order_items
ADD CONSTRAINT fk FOREIGN KEY (product_id) REFERENCES inventory.products(id);
この1行が入ると何が起きるか。外部キーの親テーブル(inventory.products)に DDL を
流すと、MySQL は整合を保つために子テーブル(orders.order_items)のメタデータ
ロックも取る。つまり在庫チームがマイグレーションを流している間、注文チームの
マイグレーションは ERROR 1205 Lock wait timeout exceeded で落ちる(実測済み。
通常の INSERT/SELECT は通るので、壊れるのはデプロイだけ。だから日常では気づけない)。
「サービスごとに独立してデプロイできる」というアーキテクチャ特性が、 規約文の存在とは無関係に、1行で静かに壊れる。
だからアーキテクチャの定義をこう置きたい。
アーキテクチャとは、変更に晒されても保たれる制約の集合である。 そして文書にしか無い制約は保たれない。
制約は層をまたぐ
「システムアーキテクチャ」というとき、普通はコードの構造かインフラの配置のどちらかを 指すことが多い。だが制約で見ると、層は4つある。
| 層 | 制約の例 |
|---|---|
| コード | モジュール境界。core はドメインを知らない |
| 実行系 | 1サービス=1バイナリ、サービスごとに専用 database |
| デプロイ・運用 | migrate → release → healthcheck の順序。同一サービス直列・別サービス並列 |
| 強制 | 上の3層の制約を機械に守らせる仕掛け |
面白いのは4層目で、ここは普通のアーキテクチャ図に描かれない。だが上の3層の制約は、 4層目が無いとただの願望になる。
制約を機械に住まわせる
同じモノレポでやってきたことを制約→強制の対応で並べるとこうなる。
| 制約 | 住んでいる場所 |
|---|---|
| コードの境界: サービスは他サービスの内部を import しない | go.mod の分割 + GOWORK=off でのビルド(跨ぐとコンパイルが通らない) |
| データの境界: 他ドメインのテーブルに触れない | database 分割 + GRANT(規約をすり抜けたクエリは実行時に権限エラー) |
| 跨ぐ FK を張らない | CI の検査(information_schema を全サービス適用後の DB に対して照会) |
| デプロイの境界: 同一サービスは直列 | GitHub Actions の concurrency group |
| エラーは必ず同じ形式 | エラー生成の経路を1つに束ね、panic さえ同じ形式に変換するミドルウェア |
共通するのは、**破ったときに「レビューで指摘される」のではなく「機械が拒否する」**こと。 文章で禁じたものは、機械でも禁じる。
これは新しい発想ではない。Building Evolutionary Architectures が言う fitness function がほぼこれだし、ArchUnit や dependency-cruiser のような 依存検査ツールも同じ思想だ。ここで言いたいのは、それを特別な儀式ではなく ふつうの CI ジョブとして日常に置くと、アーキテクチャ文書の性格が変わるということ。
用語としての “Architecture as Code” 自体も既にある。大きく2系統で、 Structurizr / C4 のようにアーキテクチャのモデルをコードで記述する系 (diagrams as code の延長。FINOS の Architecture as Code イニシアチブ = CALM も 機械可読モデル + 検証という形でこちら寄り)と、fitness function のように 制約をテストで強制する系。この記事で言っているのは後者だ。
「成功した」は「何も見ていない」と区別がつかない
ただし強制層には固有の罠がある。検査自体が壊れたとき、検査は失敗ではなく 成功の側に落ちる(英語で言う fail open)。
2回踏んだ。1回目は、跨ぐ FK の検査クエリが存在しないカラムを参照していて エラーになっていたのに、stderr を捨てていたため「FK は無い」と読んでいた。 2回目は、検査をサービス単位の CI ジョブに置いたら、そのジョブでは相手サービスの テーブルが空で、跨ぐ FK は構造的に存在できない = 検査が必ず通る配置になっていた。
どちらも検査は成功していた。成功の意味が「制約が守られている」ではなく 「検査が何も見ていない」だったのだ。だから検査には前提の検証を持たせる必要がある。 「跨ぐ FK は無い」ではなく「この database 群を見た上で、跨ぐ FK は無い」と 言わせる。見たものを出力に含め、前提が崩れていたら検査自体を失敗させる。
これはテストで「わざと壊して落ちることを確かめる」のと同じ運動だ。 制約の検査は、破ってみせて初めて信用できる。
3点セットで持つ
ここまでを合わせると、アーキテクチャをコードとして持つには3つが要る。
- 判断 (ADR) — なぜこの制約なのか。捨てた代替案は何か
- 強制 (検査・構造) — 制約を機械に守らせる実装。CI・GRANT・型・ビルド構成
- 証拠 (実測記録) — その強制が本当に効くことを、破ってみせて測った記録
Infrastructure as Code がインフラの状態を宣言としてコード化したのに対して、 これは不変条件を検査としてコード化する。IaC は「望む姿」を書く。こちらは 「破ってはいけない線」を書く。architecture as code という既存の言葉に乗るなら、 この記事の主張は「制約強制系の AaC に、証拠(実測)と判断(ADR)を束ねて 1つの成果物にする」という変奏で、新しいのはせいぜいその束ね方だけだ。
3点のどれが欠けても弱い。判断だけ(ADR だけ)だと最初の実例のように1行で破られる。 強制だけだと、なぜその検査があるのか分からなくなった頃に消される。 証拠が無いと、成功しているテストが「何も見ていない」ことに気づけない。
たぶん効かないところ
- 測れない特性がある。概念的整合性、命名の質、認知負荷。ここは今もレビューと 文書の領分で、無理に検査化すると検査のための検査になる
- 検査は変更を固定する力でもある。制約が古くなったとき、検査が「正しい変更」を 弾く。ADR に判断の理由を残すのは、検査を安全に外せるようにするためでもある
- 完成したシステムに後付けするのは高くつく。制約はすでに破られていて、検査を入れると 既存の不整合が大量に検知される。新規か、境界を作り直すタイミングでないと入れにくい
検証ビルドという置き場
もうひとつ気に入っているのは、この3点セットが検証ビルドの成果物として きれいにパッケージになることだ。本番とは別の検証用リポジトリで 制約 + 検査 + 実測を作り込み、揃ったものを本流へアップストリームする。 コードの雛形だけ持ち帰るのではなく、「この制約はこの検査で守られ、 効くことはこう測って確かめた」まで一式で渡す。
アーキテクチャの導入とは制約の導入で、制約は判断・強制・証拠が揃って初めて 生きたものになる — 当面この整理で運用してみる。