- 技術 💻
SSOTとは|信頼できる唯一の情報源の意味と、4か所のずれを消す実例
結論
SSOT(Single Source of Truth)とは、ひとつの情報について「正しい値はここにある」と決めた場所を1つだけにする考え方です。日本語では「信頼できる唯一の情報源」と訳されます。コピーを持つことは許されますが、書き換えるのは正の場所だけで、ほかはそこから生成・参照します。
この記事でわかること
- 同じ価格を4か所に持つとずれ、1か所に寄せると消える様子(実行結果)
- Pydantic→OpenAPI→TypeScript の型生成と、CI でずれを止める方法
- DRY・正規化・MDM・キャッシュとの違いと、やり過ぎを避ける判断表
同じ値が何か所にもあると、どれかを直し忘れた瞬間に「どれが正しいのか」が分からなくなります。SSOT は、正の場所を1つに決め、ほかの場所をそこから作る(生成する)か、そこを見に行く(参照する)形に変えることで、この問題を起こさないようにします。次の図は、この記事で動かした版 B の形です。
flowchart TD
S[(DB の price<br>正は1か所)] --> API[注文API<br>DB を読む]
S --> X[価格表.xlsx<br>DB から生成]
API --> F[画面<br>API から受け取る]
API --> C[キャッシュ<br>期限つきの写し]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| SSOTとは | ひとつの情報の「正」を置く場所を1つに決める考え方 | SSOTとは |
| single source of truth の訳 | 信頼できる唯一の情報源(唯一の信頼できる情報源) | SSOTとは |
| SSOTが無いと何が起きる | 直し忘れた場所だけ前の値が残り、画面と請求額が食い違う | 4か所に持つとずれる |
| コピーやキャッシュは禁止か | 禁止ではない。書き換えは正の場所だけにし、コピーは捨てて作り直す | キャッシュ・コピーとSSOT |
| 開発でのSSOTの例 | DB・スキーマ・設定ファイル・IaC のコードを正にして、型や文書を生成 | スキーマからの型生成 |
| DRY・DWH・MDMとの違い | DRYはコードの知識の重複、DWHは集計用の写し、MDMはマスターの正を決める仕組み | SSOTとDRY・正規化・MDM |
| SSOTの作り方 | 棚卸し→正を決める→生成・参照に変える→書き込み口を絞る→ずれを検知 | SSOTの作り方 |
| やり過ぎの見分け方 | 意味が違うだけの「似た値」まで1つにまとめていないか | やり過ぎを避ける判断表 |
確認環境:Ubuntu 24.04.5 LTS の上で、Python 3.13.18 の venv(作業場所)に FastAPI 0.162.0・Pydantic 2.14.2・uvicorn 0.54.0・openpyxl 3.1.5 を入れ、SQLite 3.53.1 を使った小さな商品アプリを 127.0.0.1 で動かしました。同じ「価格」を DB・注文 API・画面の JavaScript(Node.js 22.23.4 で実行)・Excel の価格表の4か所に持つ版と、DB だけを正にした版を作り、同じ3回の価格改定を行って4か所の値を読み比べています。型生成は openapi-typescript 7.13.0 と TypeScript 5.9.3、生成物のずれの検知は git 2.43.0 で確かめました。Terraform のドリフト、マスターデータ管理、OpenAPI と JSON Schema の版は、HashiCorp・Pydantic・FastAPI・openapi-typescript の公式ドキュメントと Wikipedia の定義で確かめた範囲です。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SSOTとは|信頼できる唯一の情報源の意味と、4か所のずれを消す実例」
SSOTとは(Single Source of Truth・信頼できる唯一の情報源の意味)
この節では、SSOT の正確な意味と、「唯一」が何を指しているのかが分かります。
SSOT(Single Source of Truth)は、情報システムの中で、各データを「1か所でだけ管理(編集)する」ようにする設計の考え方です。 英語版 Wikipedia は、SSOT を「各データ要素を1か所でだけ mastered(or edited)する」情報システムの構成と説明し、似た言葉に SPOT(Single Point of Truth)を挙げています。読み方は「エスエスオーティー」で、日本語の資料では「信頼できる唯一の情報源」と訳されることが多い言葉です。
大切なのは、「データを1か所にしか置かない」ではなく「正を1か所にしか置かない」という点です。画面にも、キャッシュにも、Excel にも同じ値が載っていてかまいません。ただし、それらは正から作られた写し(コピー)であり、写しを直接書き換えることはしません。値を変えたいときは正の場所を書き換え、写しは作り直します。
1正の場所は1つ
「この値の正しさはどこで決まるか」と聞かれたら、全員が同じ場所を答えられる状態。
2ほかは生成か参照
画面・帳票・型定義・文書は、正から作るか、正を見に行く。手で書き写さない。
3書き込み口を絞る
正を書き換える道を1本にし、写しを書き換える道をふさぐ。ずれは自動で検知する。
どんな「情報」が対象になるか
SSOT はデータベースの話だけではありません。次のように、ソフトウェア開発でも会社の業務でも使われます。
- 業務データ:商品の価格、顧客の住所、社員の所属。基幹システムや CRM のどれを正にするか。
- API の形:項目名と型。サーバーのモデル・OpenAPI・フロントの型のどれを正にするか。
- 設定:税率・タイムアウト・機能フラグ。コードの定数・環境変数・設定ファイル・DB のどれを正にするか。
- インフラ:サーバーやネットワークの構成。クラウドの管理画面と IaC のコードのどちらを正にするか。
- 文書:手順書・議事録・仕様。Wiki・共有フォルダー・チャットのどこに正を置くか。
SSOTとDRY・正規化・マスターデータ管理(MDM)・DWHの違い
この節では、SSOT と一緒に出てくる言葉との関係が、表で分かります。
どの言葉も「同じ情報を重ねて持たない」点では同じ方向を向いていますが、対象と目的が違います。
| 言葉 | 対象 | 中身 | SSOTとの関係 |
|---|---|---|---|
| DRY(Don't Repeat Yourself) | コード・知識 | 同じ知識をコードの中で繰り返さない | SSOT をコードに当てはめた形に近い |
| 正規化 | DB のテーブル | 同じ事実を1つの行・列にだけ持つ | DB の中で SSOT を作る手段 |
| MDM(マスターデータ管理) | 顧客・商品などのマスター | 複数システムの中から「正のレコード」を決めて配る | システムをまたぐ SSOT の仕組み |
| DWH・データ基盤 | 分析用のデータ | 各システムのデータを集めて集計に使う | 集計の「唯一の版」にはなるが、データの発生元ではない |
| キャッシュ・レプリカ | 読み取りの写し | 速さや可用性のために正を写す | SSOT と両立する(書き込みは正へ) |
- DRY は、書籍『The Pragmatic Programmer』(Andy Hunt・Dave Thomas、1999年)の原則で、「すべての知識は、システムの中で、ただ1つの、曖昧さのない、権威ある表現を持たなければならない」という文です。SSOT とほぼ同じことを、コードと設計の側から言っています。
- 正規化は、たとえば「商品名を注文テーブルにも書く」をやめて商品テーブルだけに置くことです。DB の中の SSOT は正規化で作れます。手順は関連記事「第一正規化から第三正規化までわかりやすく|データベースの正規化を図とSQLで」で扱っています。
- DWH について、英語版 Wikipedia は、DWH は集計のための「single version of the truth(唯一の版)」として使われることが多いが、データを生み出す場所ではなく、元のシステムには別の版が残る、と区別しています。DWH を作っただけでは、各業務システムの「どれが正か」は決まりません。
同じ価格を4か所に持つとずれる(小さなアプリで再現)
この節では、SSOT が無い状態で何が起きるのかを、実際に動かしたアプリの出力で確かめられます。
商品「ノートA5」の価格 1,200円を、DB・注文 API・画面の JavaScript・Excel の価格表の4か所に書いた版(A)を作りました。 よくある構成をまねたものです。注文 API は請求額の計算に自分の価格表を持ち、画面は表示用に定数を持ち、営業は見積もりに Excel を使っています。
# 4か所に同じ値(A)
# 1. DB
CREATE TABLE products(id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
INSERT INTO products VALUES(1, 'ノートA5', 1200);
# 2. 注文API(api_prices.py)
PRICES = {1: 1200}
# 3. 画面(front.js)
const PRICE = 1200;
console.log(`ノートA5 ${PRICE.toLocaleString('ja-JP')}円`);
# 4. 価格表.xlsx の C2 セル = 1200
4か所の値を読み比べるスクリプトを用意しました。DB は SELECT、注文 API は 127.0.0.1 に起動した FastAPI に注文を1件送り、画面は Node.js で front.js を実行した出力、価格表は openpyxl でセルを読みます。
db = sqlite3.connect("shop.db").execute("SELECT price FROM products WHERE id=1").fetchone()[0]
api = json.load(urlopen(Request(f"http://127.0.0.1:{port}/api/orders?product_id=1&qty=1", method="POST")))["amount"]
out = subprocess.run(["node", "front.js"], capture_output=True, text=True).stdout
front = int(re.search(r"([\d,]+)円", out).group(1).replace(",", ""))
xlsx = openpyxl.load_workbook("price_list.xlsx").active["C2"].value
ここで価格改定を3回行います。担当者はそれぞれ「自分が知っている場所」だけを書き換えます。DB 担当と画面担当、DB 担当と営業、最後は開発チームが3か所を直したが価格表を忘れた、という流れです。
$ python audit_a.py 45736
DB=1200 注文API=1200 画面=1200 価格表=1200 -> 一致
$ python change_a.py 1500 DB,画面
改定 1500円:DB・画面 を更新
DB=1500 注文API=1200 画面=1500 価格表=1200 -> ずれ
$ python change_a.py 1480 DB,価格表
改定 1480円:DB・価格表 を更新
DB=1480 注文API=1200 画面=1500 価格表=1480 -> ずれ
$ python change_a.py 1550 DB,注文API,画面
改定 1550円:DB・注文API・画面 を更新
DB=1550 注文API=1550 画面=1550 価格表=1480 -> ずれ
| 改定 | 書き換えた場所 | 起きたこと | 4か所の値の種類 |
|---|---|---|---|
| 1回目 1,500円 | DB・画面(2か所) | 画面は 1,500円なのに請求は 1,200円 | 2種類 |
| 2回目 1,480円 | DB・価格表(2か所) | 画面 1,500円・請求 1,200円・見積 1,480円 | 3種類 |
| 3回目 1,550円 | DB・注文API・画面(3か所) | 見積もりだけ 1,480円のまま | 2種類 |
3回の改定で合計7か所を書き換えましたが、4か所がそろった時点は一度もありませんでした。 2回目のあとは、同じ商品について 1,200円・1,480円・1,500円の3つの値が同時に存在しています。この状態で「正しい価格はいくらか」と聞かれても、どれかを選ぶ根拠がありません。DB の値が最新だと思っても、実際に請求しているのは注文 API の値です。
利用者から見えること
画面の価格と請求額が違う
見積書と請求書の金額が違う
問い合わせのたびに説明が変わる
作る側で起きること
変更のたびに「どこに書いてあるか」を探す
直し忘れはテストでも見つけにくい
ずれたあと、どれを正とするかで揉める
1か所に寄せるとずれが消える(DBを正にした版)
この節では、同じアプリを「価格の正は DB だけ」に作り変え、同じ3回の改定でずれが出ないことを確かめられます。
版 B では、価格を持つのは DB の products.price だけにしました。 注文 API は毎回 DB から読み、画面は API から受け取り、価格表は DB から生成します。税率は設定ファイル config.toml の1か所にだけ書きました。
| 場所 | 版A(4か所に書く) | 版B(正はDB) |
|---|---|---|
| DB | price を持つ | price を持つ(正) |
| 注文API | 自分の価格表 PRICES を持つ | DB を読む(期限つきのキャッシュあり) |
| 画面 | 定数 PRICE を持つ | API の応答を表示するだけ |
| 価格表.xlsx | 人が手で直す | DB から生成。シートを保護し「生成物」と明記 |
| 税率 | (版Aでは扱わず) | config.toml の1か所 |
API の中心の部分です。価格を変える道は PUT /api/products/{id}/price の1本だけにし、DB を書き換えたらキャッシュを捨てます。
CONF = tomllib.loads((D / "config.toml").read_text())
TAX = Decimal(CONF["tax_rate"]) # 税率の正は config.toml
TTL = CONF["cache_ttl_seconds"]
_cache: dict[int, tuple[float, Product]] = {} # 写し(読み取り専用・期限つき)
def load(pid: int) -> Product:
hit = _cache.get(pid)
if hit and time.monotonic() - hit[0] < TTL:
return hit[1]
row = sqlite3.connect(D / "shop.db").execute(
"SELECT id, name, price FROM products WHERE id=?", (pid,)).fetchone()
with_tax = int((Decimal(row[2]) * (1 + TAX)).to_integral_value(ROUND_DOWN))
p = Product(id=row[0], name=row[1], price=row[2], price_with_tax=with_tax)
_cache[pid] = (time.monotonic(), p)
return p
@app.put("/api/products/{pid}/price", response_model=Product)
def change_price(pid: int, price: int):
db = sqlite3.connect(D / "shop.db")
db.execute("UPDATE products SET price=? WHERE id=?", (price, pid)); db.commit()
_cache.pop(pid, None) # 正を書き換えたら写しを捨てる
return load(pid)
画面の JavaScript は価格を持たず、受け取った値を表示するだけです。
// 商品ページ:価格を持たず、API から受け取って表示する
const r = await fetch(`http://127.0.0.1:${port}/api/products/1`);
const p = await r.json();
console.log(`${p.name} ${p.price.toLocaleString('ja-JP')}円(税込 ${p.price_with_tax.toLocaleString('ja-JP')}円)`);
版 A と同じ 1,500円→1,480円→1,550円の改定を行いました。改定は API への PUT を1回と、価格表の生成だけです。
$ python audit_b.py 45737 -v
DB=1200 注文API=1200 画面=1200 価格表=1200 -> 一致
画面の表示: ノートA5 1,200円(税込 1,320円)
$ ./change_b.sh 45737 1500
{"id":1,"name":"ノートA5","price":1500,"price_with_tax":1650}
DB=1500 注文API=1500 画面=1500 価格表=1500 -> 一致
画面の表示: ノートA5 1,500円(税込 1,650円)
$ ./change_b.sh 45737 1480
{"id":1,"name":"ノートA5","price":1480,"price_with_tax":1628}
DB=1480 注文API=1480 画面=1480 価格表=1480 -> 一致
画面の表示: ノートA5 1,480円(税込 1,628円)
$ ./change_b.sh 45737 1550
{"id":1,"name":"ノートA5","price":1550,"price_with_tax":1705}
DB=1550 注文API=1550 画面=1550 価格表=1550 -> 一致
画面の表示: ノートA5 1,550円(税込 1,705円)
ずれが消えたのは、担当者が注意深くなったからではなく、ずれようがない形にしたからです。 画面と注文 API は価格を持っていないので、直し忘れる場所がありません。税込価格も、税率と価格の2つの正から毎回計算しているため、税込価格をどこかに保存して食い違うこともありません。
価格表の Excel は、DB から作る「生成物」にしました。生成日時と「編集は DB(管理画面)で」という一文をシートに書き、シートを保護しています。誰かが手で直しても、次の生成で上書きされます。
# 価格表は DB から作る「生成物」。手で直さない
for row in sqlite3.connect(D / "shop.db").execute("SELECT id, name, price FROM products ORDER BY id"):
ws.append(list(row))
ws["E1"] = "このファイルは shop.db から生成。編集は DB(管理画面)で"
ws["E2"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " 生成"
ws.protection.sheet = True
マスターとして持つ値と、注文のように発生ごとに記録する値の分け方は、関連記事「マスタ設計の考え方|トランザクションとの違いと判定フロー・DDL例」で扱っています。注文した時点の価格を注文の行に保存するのは、SSOT に反しません。「その時点でいくらで売ったか」という別の事実だからです。
キャッシュ・コピーとSSOT(コピーはしてよいが正は1つ)
この節では、キャッシュや複製が SSOT と両立する条件と、書き込みの道を外れたときに何が起きるかが分かります。
SSOT は「コピーを持つな」ではありません。速さや障害への備えのためのキャッシュ・レプリカ・CDN は、正から作られ、正を書き換えたら捨てられる限り、SSOT と両立します。 問題になるのは、写しを直接書き換えることと、正を書き換えたことが写しに伝わらないことです。
版 B の API には、30秒の期限つきキャッシュがあります。正しい道(PUT)を通さずに DB を直接 UPDATE すると、どうなるかを試しました。
$ curl -s http://127.0.0.1:45737/api/products/1
{"id":1,"name":"ノートA5","price":1550,"price_with_tax":1705}
DB を直接 UPDATE(API を通さない): price=1600
0.0秒後 API の price=1550
5.0秒後 API の price=1550
10.0秒後 API の price=1550
15.0秒後 API の price=1550
20.0秒後 API の price=1550
25.0秒後 API の price=1600
30.0秒後 API の price=1600
35.0秒後 API の price=1600
$ curl -s -X PUT "http://127.0.0.1:45737/api/products/1/price?price=1650"
{"id":1,"name":"ノートA5","price":1650,"price_with_tax":1815}
DB を直接書き換えたときは、キャッシュの期限が切れるまでの約25秒間、API は更新前の 1,550円を返し続けました。 PUT を通したときは、キャッシュを捨てるので、すぐに 1,650円になりました。正の場所が1つでも、書き込みの道が2本あると、写しとの間に一時的なずれが生まれます。
写しの扱い
見分ける問い:「この写しが消えても、正から作り直せるか」
- 写しの期限(TTL)は「どれだけ前の値を見せてよいか」の約束です。 価格のように請求に関わる値は、注文の確定時に正から読み直すと安全です。版 B の注文 API も
load()を通しているため、キャッシュの期限内はキャッシュの値で請求します。請求に使う値はキャッシュを通さない、という作り方もあります。 - 英語版 Wikipedia は、SSOT の実装方法として「マスターを複製せず参照だけ持つ」「読み取り専用の複製を持ち、更新はマスターだけ」(CQRS に近い形)「イベントソーシング」などを挙げています。どれも、書き込みの正を1つに保つための工夫です。
スキーマからの型生成(Pydantic→OpenAPI→TypeScript)
この節では、API の「形」の SSOT として、サーバーのモデル1つから JSON Schema・OpenAPI・TypeScript の型を作る手順と、その実行結果が分かります。
値だけでなく、データの形(項目名・型・制約)も SSOT にできます。 サーバーの Pydantic モデルを正にすると、FastAPI がそこから OpenAPI を作り、openapi-typescript が OpenAPI から TypeScript の型を作ります。フロントの型を手で書かないので、サーバーとフロントで項目名が食い違うことがなくなります。
# models.py(形の正)
from pydantic import BaseModel, Field
class Product(BaseModel):
"""商品(API の応答の形の正)"""
id: int
name: str = Field(max_length=40)
price: int = Field(ge=0, description="税抜価格(円)")
price_with_tax: int = Field(ge=0, description="税込価格(円・切り捨て)")
生成は1本のスクリプトにまとめました(venv と node_modules のパスを省いています)。API サーバーを起動しなくても、app.openapi() で OpenAPI を取り出せます。
#!/bin/sh
# 正(Pydantic のモデル)から OpenAPI・JSON Schema・TypeScript の型を作る
set -e
python -c "import json; from api_b import app; print(json.dumps(app.openapi(), ensure_ascii=False, indent=2))" > openapi.json
python -c "import json; from models import Product; print(json.dumps(Product.model_json_schema(), ensure_ascii=False, indent=2))" > product.schema.json
npx openapi-typescript openapi.json -o api-types.ts
$ ./gen.sh
✨ openapi-typescript 7.13.0
🚀 openapi.json → api-types.ts [41.7ms]
$ head -3 openapi.json
{
"openapi": "3.1.0",
"info": {
model_json_schema() が出した JSON Schema です。ge=0 が minimum に、description がそのまま説明に入りました。
{
"description": "商品(API の応答の形の正)",
"properties": {
"id": { "title": "Id", "type": "integer" },
"name": { "maxLength": 40, "title": "Name", "type": "string" },
"price": { "description": "税抜価格(円)", "minimum": 0, "title": "Price", "type": "integer" },
"price_with_tax": { "description": "税込価格(円・切り捨て)", "minimum": 0, "title": "Price With Tax", "type": "integer" }
},
"required": ["id", "name", "price", "price_with_tax"],
"title": "Product",
"type": "object"
}
openapi-typescript が作った型(api-types.ts の一部)と、それを使うフロントのコードです。
// api-types.ts(生成物。手で直さない)
Product: {
/** Id */
id: number;
/** Name */
name: string;
/**
* Price
* @description 税抜価格(円)
*/
price: number;
/**
* Price With Tax
* @description 税込価格(円・切り捨て)
*/
price_with_tax: number;
};
// front.ts
import type { components } from "./api-types";
type Product = components["schemas"]["Product"];
export function label(p: Product): string {
return `${p.name} ${p.price.toLocaleString("ja-JP")}円(税込 ${p.price_with_tax.toLocaleString("ja-JP")}円)`;
}
サーバーで項目名を変えると、フロントのビルドが止まる
サーバー側で price を unit_price に変え、生成し直してから tsc を実行しました。
$ npx tsc -p . # 変更前
(出力なし・終了コード 0)
$ sed -i 's/price: int/unit_price: int/' models.py && ./gen.sh
$ npx tsc -p .
front.ts(5,25): error TS2339: Property 'price' does not exist on type '{ id: number; name: string; unit_price: number; price_with_tax: number; }'.
$ echo $?
2
型を手で書いていたら、この変更は本番で undefined が表示されるまで気づけません。 生成した型を使うと、フロントのどの行が変更の影響を受けるかを、ビルドの時点で教えてくれます。FastAPI の公式ドキュメントも、OpenAPI からクライアントを生成すると、バックエンドを変えて生成し直したときにデータの食い違いがビルドのエラーになり、開発の早い段階で多くの誤りを見つけられる、と説明しています。
| 正にするもの | 生成する先 | 道具の例 |
|---|---|---|
| サーバーのモデル(Pydantic) | OpenAPI・JSON Schema・TS の型 | FastAPI・openapi-typescript |
| OpenAPI の YAML(設計を先に書く) | サーバーの雛形・クライアント・文書 | OpenAPI Generator・Hey API |
| Zod のスキーマ(TS が正) | TS の型(z.infer)・入力検証 | Zod |
| DB のスキーマ | ORM のモデル・型 | Prisma・Drizzle など |
| .proto ファイル | 各言語のメッセージ型 | protoc(gRPC) |
サーバーのコードを正にする(コードファースト)か、OpenAPI の YAML を正にする(スキーマファースト)かは、チームの体制で決めます。どちらでも、正は1つにして、もう片方は生成物にします。OpenAPI を先に書く場合の書き方は、関連記事「OpenAPIの書き方|YAMLのテンプレートとチームで使う運用」で、Pydantic のモデルの書き方は関連記事「Pydanticとは|何に使う・dataclassとの違いと使い方を実測で解説」で扱っています。
検証のルールも同じモデルから
同じモデルは、入力の検証にも使えます。price=-1 を渡したときの Pydantic のエラーと、DB 側に置いた CHECK 制約のエラーです。
1 validation error for Product
price
Input should be greater than or equal to 0 [type=greater_than_equal, input_value=-1, input_type=int]
For further information visit https://errors.pydantic.dev/2.14/v/greater_than_equal
IntegrityError: CHECK constraint failed: price >= 0
「0以上」というルールが Pydantic と DB の2か所にあるのは、厳密にはルールの重複です。ここでは、利用者に見せる検証の正はモデル、DB の CHECK はモデルを通らない書き込み(管理ツールからの直接の UPDATE など)を止める最後の柵、と役割を分けています。役割が違うので、両方を残す判断になります。
生成物とドキュメントのずれをCIで止める(スキーマ・README)
この節では、正から作った生成物や README が、正と食い違ったまま残らないようにする仕組みが、実行結果で分かります。
生成の仕組みを作っても、生成し忘れれば写しは前のまま残ります。 そこで、CI(自動テスト)で「生成し直しても差分が出ないこと」を確かめます。
#!/bin/sh
# CI で実行:生成物がモデルと一致しているか
./gen.sh > /dev/null 2>&1
git diff --exit-code --stat -- openapi.json product.schema.json api-types.ts
モデルに stock(在庫数)を足し、生成をし忘れたままコミットしようとした状態で実行しました。
$ ./check_generated.sh; echo "exit=$?" # 生成済みのとき
exit=0
$ git diff --stat models.py
models.py | 1 +
$ ./check_generated.sh; echo "exit=$?" # モデルだけ変えたとき
api-types.ts | 5 +++++
openapi.json | 9 ++++++++-
product.schema.json | 9 ++++++++-
3 files changed, 21 insertions(+), 2 deletions(-)
exit=1
終了コードが1なので、CI はここで失敗します。生成物をリポジトリに入れない(ビルドのたびに作る)方法もあります。入れる場合は、このような確認で「正と写しが一致していること」を機械に見張らせます。
README と設定ファイルのずれ
文書も写しの一種です。README に設定の一覧を手で書くと、設定ファイルを変えたときに README だけ前の値が残ります。版 B の README には税率を 0.08 と書いたまま、config.toml は 0.10 でした。設定表を config.toml から作るスクリプトに --check を付けて実行しました。
# README の設定表を config.toml から作る。--check なら差分があると終了コード1
conf = tomllib.loads(pathlib.Path("config.toml").read_text())
table = "| キー | 値 |\n|---|---|\n" + "".join(f"| {k} | {v} |\n" for k, v in conf.items())
src = pathlib.Path("README.md").read_text()
new = re.sub(r"(<!-- BEGIN settings -->\n).*?(<!-- END settings -->)",
lambda m: m[1] + table + m[2], src, flags=re.S)
if "--check" in sys.argv:
if new != src:
print("README.md の設定表が config.toml と違います。python gen_readme.py を実行して更新")
sys.exit(1)
$ python gen_readme.py --check; echo "exit=$?"
README.md の設定表が config.toml と違います。python gen_readme.py を実行して更新
exit=1
$ python gen_readme.py && python gen_readme.py --check; echo "exit=$?"
README.md は config.toml と一致
exit=0
- 1生成物に「生成物」と書くファイルの先頭や Excel のセルに、どこから作ったか・手で直さないことを書く
- 2生成のコマンドを1本にまとめるgen.sh のように、誰が実行しても同じものができるようにする
- 3CI で「生成し直しても差分なし」を確かめるgit diff --exit-code や --check で、ずれたままのコミットを止める
- 4文書はコードから作るか、リンクにする値を書き写さず、「設定は config.toml を参照」と場所を示すだけでもよい
IaC・設定ファイルとSSOT(Terraformのドリフト・環境変数)
この節では、インフラと設定の世界で SSOT がどう使われるか、ずれ(ドリフト)をどう見つけるかが分かります。
IaC(Infrastructure as Code)は、インフラの構成をコードに書き、コードを正にする考え方です。 クラウドの管理画面から手で設定を変えると、コードと実物がずれます。Terraform の公式チュートリアルは、手で変更すると状態ファイルが実際のインフラと同期しなくなる(ドリフトする)と説明し、terraform plan -refresh-only で、状態ファイルにどんな変更が入るかを先に確かめるよう勧めています。ずれがあると「Objects have changed outside of Terraform」と表示されます。
| ずれの起き方 | 見つけ方 | 直し方の方針 |
|---|---|---|
| 管理画面で手で変えた | terraform plan -refresh-only |
変更をコードに書き戻すか、コードで上書きする |
| 同じ値を複数の .tf に書いた | コードレビュー | variable・locals・module で1か所に寄せる |
| 環境ごとにコピーした設定がずれた | 環境間の差分を出す | 共通部分を1つにし、環境の差だけを別に書く |
Terraform の state の扱いと、既存のリソースを取り込む手順は、関連記事「Terraform入門|基本の流れからstate管理・既存リソースの取り込みまで」で扱っています。
設定値はどこを正にするか
アプリの設定値も、同じ値をコードの定数・環境変数・設定ファイル・DB に散らすとずれます。版 B では税率を config.toml の1か所にし、API と README がそこを読む形にしました。値ごとに「正はどこか」と「上書きしてよい順番(優先順位)」を決めておくと、「環境変数で上書きしたつもりが、コードの定数が使われていた」という事故を防げます。どの値をどこに置くかの決め方は、関連記事「設定値の管理設計|環境変数・設定ファイル・DBの使い分けと優先順位」で扱っています。
データ基盤・業務でのSSOT(MDM・DWH・ナレッジの一元化)
この節では、会社全体のデータや文書で SSOT が語られるときの意味と、よく挙がるメリットが分かります。
業務の世界では、SSOT は「部門ごとのシステムや Excel に散ったデータのうち、どれを正とするかを全社で決めること」を指します。 顧客の住所が CRM・請求システム・営業の Excel で違う、という状態を正すのが典型です。
マスターデータ管理(MDM)
英語版 Wikipedia は、MDM のシステムが中心(ハブ)になって、どの更新を正とするかを決め、「ゴールデンレコード」を作って他のシステムへ配る形を、SSOT の実現方法として挙げています。同時に、多くの企業では、市販のシステムがそれぞれ自分の写しを持ち、作り変えも許されないため、SSOT を完全には実現できないことも書いています。現実には、項目ごとに「この項目の正はこのシステム」と決めるところから始めることになります。
| 項目 | 正にするシステムの例 | ほかのシステムの扱い |
|---|---|---|
| 顧客の連絡先 | CRM | 請求・サポートは CRM から同期(読み取り専用) |
| 商品の価格 | 商品マスター(基幹システム) | EC サイト・見積書は参照か生成 |
| 社員の所属 | 人事システム | アカウント管理・社内名簿は人事から同期 |
| 売上の集計値 | DWH の集計テーブル | 各部門のダッシュボードはここを見る |
よく挙がるメリット
- 判断が速くなる:会議のたびに「どの数字が正しいか」を確かめ直す時間が減る。
- 部門間の認識がそろう:同じ指標を同じ定義で見る。
- データの分断(サイロ化)を防ぐ:部門ごとに別のデータを持ち続ける状態を減らす。
- AI に渡すデータの質が上がる:生成 AI に社内の情報を読ませるとき、版の違う文書が混ざると答えも揺れる。正の場所が決まっていれば、読ませる範囲も決めやすい。
文書・ナレッジのSSOT
手順書や議事録も、Wiki・共有フォルダー・チャットに同じ文書の版違いがあると、どれが最新か分からなくなります。Atlassian の解説は、保存場所を1つに決め、更新の担当者を割り当て、テンプレートで形をそろえ、続けて見直すことを、チームの SSOT の作り方として挙げています。文書でも、「正の場所」「更新する人」「写しを作らない決まり(リンクで共有する)」の3つがそろって初めて SSOT になります。
SSOTの作り方(5つの手順)
この節では、すでに同じ値が散っているシステムを、SSOT に寄せていく順番が分かります。
一度に全部を作り直さなくても進められます。ずれて困っている値から1つずつ寄せます。
同じ値を持っている場所を棚卸しする
価格・税率・項目名など、困っている値について、コード・DB・設定・Excel・文書を検索し、持っている場所を一覧にする。版 A なら DB・注文 API・画面・価格表の4か所。
正の場所を1つ決め、担当者を決める
「どこで決まった値が本当の値か」を基準に選ぶ。価格なら商品マスター、API の形ならモデルか OpenAPI。値を変える権限を持つ人(オーナー)も決める。
ほかの場所を「生成」か「参照」に変える
画面の定数を消して API から受け取る、Excel を DB から生成する、型をスキーマから生成する。写しには「生成物」と書く。
書き込みの道を1本に絞る
正を書き換える道を API や管理画面に限る。写し(キャッシュ・Excel)を直接書き換える道はふさぐ。直接の UPDATE が要るときの手順(キャッシュを捨てる)も決める。
ずれを機械で見つける
生成物の差分を CI で確かめる、4か所を読み比べる監査スクリプトを定期的に動かす、IaC ならドリフトを確かめる。人の注意に頼らない。
SSOTのやり過ぎを避ける判断表
この節では、SSOT に寄せるべきものと、寄せないほうがよいものの見分け方が分かります。
SSOT の対象は「同じ意味の値」です。見た目が同じでも意味が違う値を1つにまとめると、片方の変更がもう片方を壊します。 DRY も「知識」の重複をなくす原則で、偶然同じ形をしているだけのコードまで1つにする原則ではありません。
| 状況 | 判断 | 理由 |
|---|---|---|
| 同じ商品の価格が画面・API・Excel にある | 寄せる | 同じ意味の値。ずれると請求を誤る |
| API の項目名をサーバーとフロントで別に書いている | 寄せる(型生成) | 形がずれると表示が壊れる |
| 注文の行に、注文時点の価格を保存している | 寄せない | 「その時いくらで売ったか」という別の事実 |
| 送料無料の基準とポイント付与の基準が、たまたま同じ 5,000円 | 寄せない | 意味が違う。片方だけ変わる日が来る |
| 速さのためのキャッシュ・検索用のインデックス | 写しとして持つ | 正から作り直せる。期限と破棄の仕組みを付ける |
| 障害に備えた DB のレプリカ・バックアップ | 写しとして持つ | 書き込みはプライマリだけにする |
| 市販のシステムが自分の写しを持つ | 同期の向きを決める | 作り変えられないなら、正→写しの一方向にする |
| 2人だけの小さな試作 | 後で寄せる | 値が少ないうちは、生成の仕組みのほうが重いこともある |
- 1同じ意味の値か?違うなら寄せない。名前を分けて、別々に持つ
- 2ずれたら誰が困るか?請求・法令・利用者の表示に関わるなら優先して寄せる
- 3正はどこで決まっているか?値を決める人・システムがいる場所を正にする
- 4写しは生成できるか、参照で足りるか?参照で済むなら写しを持たない。持つなら期限と破棄を決める
- 5ずれを機械で見つけられるか?CI・監査スクリプト・ドリフト検知のどれかを付ける
SSOTでよくあるつまずきとエラー
この節では、SSOT に寄せる途中で出会いやすい問題と、手元で出た表示の原文、直し方が分かります。
| つまずき | 出た表示・起きたこと | 直し方 |
|---|---|---|
| サーバーで項目名を変えた | error TS2339: Property 'price' does not exist on type ... |
生成した型に合わせてフロントを直す(ビルドで気づけた状態) |
| モデルを変えて生成を忘れた | 3 files changed ...・exit=1 |
gen.sh を実行してから生成物もコミットする |
| README の値が前のまま | README.md の設定表が config.toml と違います |
生成スクリプトで作り直す。値を書かずリンクにする |
| DB を直接 UPDATE した | 約25秒、API が更新前の値を返した | API を通して更新するか、キャッシュを捨てる手順を足す |
| 負の価格を入れようとした | Input should be greater than or equal to 0・CHECK constraint failed |
モデルで止める。DB の CHECK は最後の柵として残す |
| 正の場所をみんなが知らない | Excel を手で直す人が残る | 生成物にその旨を書き、シートを保護する |
| 正を2つ作ってしまった | OpenAPI の YAML とモデルの両方を手で直している | どちらかを正にし、もう片方を生成物にする |
- 「正を1つにしたはずが、正の候補が2つになる」のが最も多いつまずきです。OpenAPI を手で書きながら、FastAPI の自動生成の OpenAPI も公開している、などです。どちらが正かを README に1行書くだけでも防げます。
- 生成の道具の版が人によって違うと、生成物に余計な差分が出ます。package.json や requirements に版を固定し、CI と同じ版で生成します。
よくある質問
Q. SSOTとは何ですか? A. Single Source of Truth の略で、ひとつの情報について「正しい値はここにある」と決めた場所を1つだけにする考え方です。日本語では「信頼できる唯一の情報源」と訳されます。ほかの場所は、正から生成するか、正を参照します。
Q. SSOTの読み方は? A. 「エスエスオーティー」と読むのが一般的です。SSoT と書かれることもあります。
Q. SSOTにすると、データを1か所にしか置けないのですか? A. いいえ。キャッシュ・レプリカ・帳票などの写しは持てます。決まりは「書き換えるのは正の場所だけ」「写しは正から作り直せる」の2つです。手元の版 B でも、API のキャッシュと Excel の価格表という写しを持っています。
Q. SSOTとDRYの違いは? A. DRY は「同じ知識をコードの中で繰り返さない」というプログラミングの原則で、SSOT はデータや情報全体について「正の場所を1つにする」考え方です。DRY の原文の「ただ1つの、曖昧さのない、権威ある表現」は、SSOT とほぼ同じ意味です。
Q. SSOTとDWH(データウェアハウス)の違いは? A. DWH は、各システムのデータを集めて分析に使う場所です。集計の「唯一の版」にはなりますが、データが生まれる場所ではありません。顧客や商品の「どれが正か」を決めるのは、MDM や各業務システムの役割です。
Q. 開発でのSSOTの具体例は? A. DB のスキーマから ORM の型を作る、Pydantic のモデルから OpenAPI と TypeScript の型を作る、税率を設定ファイルの1か所に置く、インフラを Terraform のコードで管理する、などです。この記事では、価格の正を DB にした版と、型の正を Pydantic のモデルにした版を動かしました。
Q. 型生成のSSOTは、コードとOpenAPIのどちらを正にすべきですか? A. サーバーを先に作るチームはコード(モデル)、API の設計を先に合意したいチームや複数の言語で実装するチームは OpenAPI を正にすることが多いです。大事なのは、両方を手で直さないことです。
Q. 既存のシステムを全部作り直さないとSSOTにできませんか? A. いいえ。ずれて困っている値を1つ選び、正を決め、写しを生成か参照に変える、を繰り返します。作り変えられないシステムは、正からの一方向の同期にします。
Q. 小さなチームや個人の開発でも必要ですか? A. 人数より、同じ値を何か所に持っているかで決まります。手元の再現では、4か所に持っただけで、3回の改定のすべてでずれが残りました。値が少ない試作のうちは、後で寄せる判断もあります。
Q. SSOTにすると、正の場所が止まったときに全部止まりませんか? A. 正の場所に読み取りが集中すると、その場所の障害や遅さが全体に広がります。そのためにキャッシュやレプリカという写しを持ちます。写しは「読み取り用」で、「書き込みは正へ」を守れば SSOT と両立します。
Q. SSOTと生成AIの関係は? A. 社内の文書やデータを生成 AI に読ませるとき、版の違う文書が混ざると答えも食い違います。正の場所が決まっていれば、AI に読ませる範囲もそこに絞れます。
まとめ
- SSOT(Single Source of Truth・信頼できる唯一の情報源)は、ひとつの情報の正を置く場所を1つに決め、ほかは生成か参照にする考え方。写しを持つことは許される。
- 同じ価格を DB・注文 API・画面・Excel の4か所に持つと、3回の改定で7か所を書き換えても、4か所がそろうことは一度もなかった。DB を正にすると、同じ改定で一度もずれなかった。
- キャッシュは SSOT と両立する。ただし DB を直接書き換えると、期限が切れるまで約25秒、更新前の値が返った。書き込みの道は1本に絞る。
- Pydantic のモデルを正にすると、OpenAPI 3.1・JSON Schema・TypeScript の型を生成でき、項目名の変更は
tscのエラーで見つかった。生成物と README のずれは CI の差分確認で止められる。 - やり過ぎに注意する。意味が違う「似た値」、注文時点の価格のような別の事実は、寄せずに別々に持つ。
参考・出典
確認日はいずれも 2026年10月29日です。
- Wikipedia「Single source of truth」:https://en.wikipedia.org/wiki/Single_source_of_truth
- Wikipedia「Don't repeat yourself」:https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
- FastAPI「Generating SDKs」:https://fastapi.tiangolo.com/advanced/generate-clients/
- openapi-typescript「Introduction」:https://openapi-ts.dev/introduction
- Pydantic「JSON Schema」:https://pydantic.dev/docs/validation/latest/concepts/json_schema/
- HashiCorp「Manage resource drift」(Terraform):https://developer.hashicorp.com/terraform/tutorials/state/resource-drift
- Atlassian「信頼できる唯一の情報源 [+ 構築方法]」:https://www.atlassian.com/ja/work-management/knowledge-sharing/documentation/building-a-single-source-of-truth-ssot-for-your-team
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SSOTとは|信頼できる唯一の情報源の意味と、4か所のずれを消す実例」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。