設定も列挙型も、壊れるのは「値」と「意味」を混同したとき

Ryusei Nakamura

Hatched by Ryusei Nakamura

Aug 23, 2026

1 min read

88%

0

「その設定は、いま本当に正しいのか」。この問いに、私たちは意外なほど自信を持って答えられない。

クラウドのリソースはコンソールから変更できる。アプリケーションの状態は文字列としてデータベースに保存できる。どちらも簡単で、どちらも便利だ。しかし、その便利さがシステムを不安定にする瞬間がある。設定ファイルに書かれた状態と、実際に動いている環境がずれるとき。そして、単なる値が、いつの間にかアプリケーションの意味そのものを背負わされるときだ。

一見すると、インフラストラクチャのドリフトと、PHPの値に依存した列挙型は別々の話に見える。前者はクラウド運用の問題であり、後者は型設計の問題である。だが両者の奥には、同じ問いがある。

システムの「正しさ」は、どこに置かれるべきなのか。

この問いを突き詰めると、安定したソフトウェアを作るために必要なのは、変更を禁止することではなく、意味のある変更が、どの場所を通り、どの表現に変換されるかを設計することだとわかる。

システムは二つの顔を持っている

CloudFormationのような宣言的な仕組みでは、テンプレートが望ましい状態を記述する。たとえば、タスク定義のコンテナイメージ、CPU、メモリ、環境変数などをテンプレートに書く。CloudFormationは、その記述を現実のAWSリソースへ反映する。

ここで重要なのは、テンプレートが単なるメモではないことだ。テンプレートは、環境の正規の表現である。実際のリソースがテンプレートと一致している限り、システムは「何が正しい状態か」を判断できる。

しかし、誰かがコンソールからタスク定義を直接変更すると、二つの世界が生まれる。テンプレートにはメモリが512MiBと書かれているのに、実際のリソースは1024MiBになっている。テンプレートは古いイメージを指しているのに、稼働環境だけが新しいイメージを使っている。この差がドリフトである。

ドリフトの本質は、単なる設定ミスではない。正しさの基準が二つ以上に分裂することである。

同じ構造は、アプリケーションの値にも現れる。たとえば注文の状態を、次のような文字列で管理するとする。

$order->status = 'shipped';

この文字列は、データベースに保存しやすく、APIでも送信しやすい。一方で、文字列は何でも受け入れてしまう。shipedという綴り間違いも、deliveredという別の状態も、技術的には代入できる。値そのものは存在していても、それがドメイン上の正しい意味を持つとは限らない。

値に依存した列挙型は、この問題に境界を引く。たとえば、次のように状態を定義できる。

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
}

ここでは、pendingshippedが外部に保存される値であり、OrderStatus::Shippedがプログラム内で扱う意味を持つ。値は運搬しやすい。列挙型のケースは、許される意味の集合を表す。

この二つを重ねると、重要な対応関係が見える。CloudFormationのテンプレートは、環境の望ましい状態を表す。Backed Enumのケースは、ドメインで許される意味を表す。どちらも、自由な現実を制限し、正しさを一つの基準へ戻すための仕組みなのである。

「直接変更」が危険なのは、変更そのものが悪いからではない

現場では、直接変更はしばしば合理的に見える。障害が起きたので、コンソールから環境変数を変える。負荷が高いので、タスクのメモリを増やす。動作確認のために、稼働中のリソースを一時的に調整する。

問題は、その変更が即座に反映されることではない。問題は、変更の経路が記録されず、正規の表現へ戻されないことにある。

直接変更した直後のシステムは、むしろ正常に見えることがある。障害も収まり、ユーザーも操作できる。ところが次回のデプロイで、テンプレートが環境を以前の状態へ戻す。あるいは、更新処理が想定外の差分に遭遇し、失敗する。緊急対応が、未来の障害の種になる。

列挙型の世界でも似たことが起きる。アプリケーション内のあちこちで文字列を直接操作すると、ひとつの変更が複数の場所に分散する。shippedという値をsentに変えたいとき、データベース、API、画面表示、条件分岐、テストをすべて探さなければならない。さらに、どこかに古い文字列が残れば、システムは複数の語彙を同時に受け入れ始める。

ここでBacked Enumが提供するのは、単なる入力補完ではない。外部の単純な値と、内部の意味を分離する接合面である。

外部では、データベースに文字列を保存する。APIでは文字列を受け取る。だが内部では、任意の文字列ではなく、OrderStatusのいずれかとして扱う。変換に失敗した値は、境界で拒否できる。つまり、システムの中心部に曖昧さを持ち込まない。

この考え方をインフラに移すと、運用の原則も変わる。コンソール操作を全面的に悪とするのではなく、緊急の観測や一時的な診断と、恒久的な変更を分ける。恒久的な変更はテンプレートへ戻し、そこから再適用する。変更の入口を一つにすることで、現実の環境と記述の意味を一致させる。

直接変更を禁じるだけでは、システムは安全にならない。変更を正規の表現へ戻す道を用意して初めて、安全な変更管理になる。

「値を保存する」と「意味を管理する」は別の仕事である

Backed Enumの特徴は、各ケースがスカラー値に支えられていることだ。これは、列挙型が人間にとって意味のある抽象でありながら、データベースや通信では単純な値として扱えることを意味する。

この二重性は、ソフトウェア設計で非常に強力である。内部のコードは、文字列の綴りではなく、意味に依存できる。一方、外部の世界は、文字列や数値という単純な形式のまま扱える。

たとえば、次の処理を考えてみる。

$status = OrderStatus::tryFrom($row['status']);

if ($status === null) {
    throw new UnexpectedValueException('未知の注文状態です');
}

データベースに未知の値が存在していた場合、システムは黙って処理を続けない。境界で異常を検知する。これは、値を拒否しているというより、値が意味を持つという契約を守っているのである。

インフラのテンプレートも同じ役割を持つ。テンプレートの各項目は、単なる設定値の一覧ではない。どの種類のリソースを、どの構成で、どの依存関係のもとに動かすかという契約である。実際のリソースがそこから外れたなら、その差分は偶然の違いではなく、契約違反の可能性がある。

ここから、システムの表現を三層に分ける mental model が得られる。日本語で呼ぶなら、意味層、正規化層、実行層である。

第一の意味層は、何が許されるかを定義する。注文状態なら、支払い済み、発送済み、配送済みなどの概念である。インフラなら、サービスが満たすべき可用性やリソース構成がこれにあたる。

第二の正規化層は、意味を機械的に扱える形式へ固定する。Backed Enumの文字列値や、CloudFormationのテンプレートがこの役割を担う。ここが、記憶や口頭の指示ではなく、レビュー可能な成果物になっていることが重要だ。

第三の実行層は、実際に動く現実である。データベースの行、APIのリクエスト、稼働中のコンテナ、AWSリソースがここにある。

障害は、実行層が変化すること自体から生じるのではない。実行層の変更が正規化層に戻されず、さらに意味層との関係が見えなくなることで生じる。

ドリフトを見つけることは、組織の記憶を回復すること

設定の差分を検出する機能は、単なる監査のためにあるのではない。それは、システムが何を経て現在の状態になったのかを回復するためにある。

あるサービスの動作が不安定になったとする。テンプレートには変更がない。しかし、実環境では環境変数が追加され、タスク定義のメモリが変更され、セキュリティグループにルールが加わっている。これらが見つからなければ、調査は「いま動いているもの」を前提に進むしかない。だが、その状態が誰によって、なぜ作られたのかはわからない。

列挙型に未知の値が現れた場合も同じである。問題は、単に新しい文字列が増えたことではない。その値がどのサービスから来たのか、どのバージョンで導入されたのか、正式な意味を持つのかを問い直す必要がある。

この観点では、ドリフト検出と列挙型の変換エラーは、同じ種類の警報である。どちらも、現実が正規の語彙から外れたことを知らせるシグナルだ。

ただし、警報を消すだけではいけない。差分を見つけたら、次の三つを判断する必要がある。

  1. その変更は意図的だったのか。
  2. 意図的なら、正規の定義へ反映すべきか。
  3. 反映できないなら、互換性や移行期間をどう設計するか。

たとえば、新しい注文状態returnedが必要になったとする。まず列挙型にケースを追加し、各処理がその状態をどう扱うかを決める。次に、データベースやAPIの契約を更新し、最後に実際のデータを移行する。順番を逆にして、先に値だけを投入すれば、アプリケーションは未知の状態に遭遇する。

インフラの変更も同様だ。先にコンソールで恒久的な変更を行い、後からテンプレートを直すのではなく、変更の意図をコード化し、レビューし、適用する。緊急操作をした場合は、その時点で一時状態として記録し、正規定義との差分を解消する。

この運用は、ソフトウェアに記憶を持たせる行為でもある。正規の定義は、現在の状態だけでなく、組織が何を意図したかを保存するからだ。

実務で使える「正規の表現」設計

ここまでの議論を、日々の開発と運用に落とし込もう。ポイントは、すべてを一つのファイルに集めることではない。正規の定義がどこにあり、実行環境からそこへどう戻るかを明確にすることである。

アプリケーションでは、次のような設計が有効だ。

  • ドメイン上の有限な選択肢には列挙型を使う。
  • データベースやAPIとの境界で、文字列や数値を列挙型へ変換する。
  • 未知の値を黙って既定値へ変換しない。異常として記録し、必要なら処理を止める。
  • 表示名や翻訳文をBacking Valueに埋め込まず、意味と表示を分離する。
  • ケース追加を、すべての分岐や外部契約を見直すイベントとして扱う。

インフラでは、次の習慣が有効だ。

  • 変更可能なリソースの定義をテンプレートやコードで管理する。
  • コンソールで変更した場合は、一時的な応急処置として記録する。
  • 定期的にドリフトを検出し、差分の意図を確認する。
  • 実環境だけに存在する設定を、正規定義へ戻すか、明確に削除する。
  • タスク定義や環境変数を、手作業ではなくレビュー可能な変更として扱う。

ここで大切なのは、正規化を「変更を遅くする作業」と誤解しないことだ。正規の経路は、短期的には手順を増やす。しかし長期的には、誰が何を変えたかを推測する時間を削る。速さとは、変更を一秒で反映することではない。変更後も、その状態を説明できることである。

Key Takeaways

  • 値と意味を分離する: 外部では文字列や数値を使っても、内部では列挙型やドメイン型で許される意味を表現する。
  • 正規の定義を一つ決める: インフラの状態はテンプレートへ、ドメインの選択肢は列挙型へ戻し、現実を複数の基準で管理しない。
  • 境界で差分を検出する: 未知の列挙値や、テンプレートと実リソースの不一致を早期に警告し、黙って受け入れない。
  • 緊急変更を忘れない: コンソール操作や直接のデータ更新を行ったら、それを一時状態として記録し、正規定義へ反映する。
  • 変更を意味の変更としてレビューする: 新しい値や設定を追加することは、単なる編集ではなく、システムの語彙と契約を拡張する行為である。

最後に、正しさは「いま動くこと」ではない

動いているから正しい。そう考えると、直接変更は魅力的に見える。目の前の障害を消し、ユーザーの操作を復旧させるからだ。しかし、動作は実行層の事実にすぎない。その状態が意図されたものか、再現できるものか、次の変更でも保たれるものかは別の問題である。

列挙型は、値の世界に意味の境界を作る。宣言的なインフラ管理は、環境の世界に意図の境界を作る。両者が教えてくれるのは、堅牢性とは自由を奪うことではなく、自由な現実を、説明可能な形へ何度でも戻せる能力だということだ。

だから、システムを設計するときは「どんな値を受け入れるか」だけでなく、「その値が何を意味するのか」を問いたい。そして運用するときは「何を変更できるか」だけでなく、「変更後の現実を、どの正規の定義に戻せるか」を問いたい。

最も信頼できるシステムとは、決してずれないシステムではない。ずれたときに、そのずれを発見し、意味を問い直し、正しい場所へ戻せるシステムである。

Sources

← Back to Library

Hatch New Ideas with Glasp AI 🐣

Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)

Start Hatching 🐣