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)

強制 (検査・構造)

証拠 (実測記録)

Infrastructure as Code がインフラの状態を宣言としてコード化したのに対して、 これは不変条件を検査としてコード化する。IaC は「望む姿」を書く。こちらは 「破ってはいけない線」を書く。architecture as code という既存の言葉に乗るなら、 この記事の主張は「制約強制系の AaC に、証拠(実測)と判断(ADR)を束ねて 1つの成果物にする」という変奏で、新しいのはせいぜいその束ね方だけだ。

3点のどれが欠けても弱い。判断だけ(ADR だけ)だと最初の実例のように1行で破られる。 強制だけだと、なぜその検査があるのか分からなくなった頃に消される。 証拠が無いと、成功しているテストが「何も見ていない」ことに気づけない。

たぶん効かないところ

検証ビルドという置き場

もうひとつ気に入っているのは、この3点セットが検証ビルドの成果物として きれいにパッケージになることだ。本番とは別の検証用リポジトリで 制約 + 検査 + 実測を作り込み、揃ったものを本流へアップストリームする。 コードの雛形だけ持ち帰るのではなく、「この制約はこの検査で守られ、 効くことはこう測って確かめた」まで一式で渡す。

アーキテクチャの導入とは制約の導入で、制約は判断・強制・証拠が揃って初めて 生きたものになる — 当面この整理で運用してみる。

← Back to all posts