deprecation-and-migration について
システムの非推奨化と移行を計画するためのソフトウェア開発スキルです。代替策の判断、利用者の段階的な移行、データベース変更の手順を扱い、旧システムをリスクを抑えて終了する方法を示します。
主な機能
- 01非推奨化の価値と影響の評価
- 02勧告型・強制型の選択
- 03利用者の段階的な移行計画
- 04ストラングラーパターン、アダプターパターン、機能フラグ
- 05拡張・移行・縮小によるスキーマ変更
活用シーン
- 01レガシーシステム、API、ライブラリの置き換え
- 02不要になった機能の終了
- 03重複実装や所有者不在コードの統合
- 04データベーススキーマの安全な変更
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ addyosmani/ agent-skills --skill deprecation-and-migrationインストール後、Claude Code で/deprecation-and-migrationを入力して読み込まれたか確認します。
詳細
非推奨化と移行
概要
コードは資産ではなく、負債です。コードのすべての行には継続的な保守コストがかかります。修正すべきバグ、更新すべき依存関係、適用すべきセキュリティパッチ、オンボーディングが必要な新しいエンジニアがいます。非推奨化とは、役割を果たさなくなったコードを削除する取り組みです。移行とは、ユーザーを古いものから新しいものへ安全に移行するプロセスです。
多くのエンジニアリング組織は、何かを作ることには長けています。しかし、それを取り除くことに長けている組織はほとんどありません。このスキルは、そのギャップを埋めます。
使用する場面
- 古いシステム、API、ライブラリを新しいものに置き換えるとき
- 不要になった機能を終了するとき
- 重複した実装を統合するとき
- 誰も所有していないのに誰もが依存しているデッドコードを削除するとき
- 新しいシステムのライフサイクルを計画するとき(非推奨化の計画は設計時に始まります)
- レガシーシステムを維持するか、移行に投資するかを判断するとき
基本原則
コードは負債です
コードのすべての行には継続的なコストがかかります。テスト、ドキュメント、セキュリティパッチ、依存関係の更新が必要であり、近くで作業する人には認知的な負担もかかります。コードの価値は、それ自体ではなく、コードが提供する機能にあります。同じ機能を、より少ないコードや複雑さで、あるいはより優れた抽象化によって提供できるなら、古いコードは削除すべきです。
ハイラムの法則により削除は難しくなります
ユーザーが十分に増えると、バグ、タイミングの癖、文書化されていない副作用も含め、あらゆる観察可能な動作が依存対象になります。そのため、非推奨化には単なる告知ではなく、積極的な移行が必要です。置き換え先が再現しない動作に依存している場合、ユーザーは「ただ切り替える」ことはできません。
非推奨化の計画は設計時に始まります
新しいものを作るときは、「3年後にこれをどうやって削除するか?」と考えてください。明確なインターフェース、フィーチャーフラグ、最小限の公開範囲を備えて設計されたシステムは、実装の詳細があらゆる場所に漏れ出すシステムよりも、非推奨化しやすくなります。
非推奨化の判断
何かを非推奨にする前に、次の質問に答えてください。
1. Does this system still provide unique value?
→ If yes, maintain it. If no, proceed.
2. How many users/consumers depend on it?
→ Quantify the migration scope.
3. Does a replacement exist?
→ If no, build the replacement first. Don't deprecate without an alternative.
4. What's the migration cost for each consumer?
→ If trivially automated, do it. If manual and high-effort, weigh against maintenance cost.
5. What's the ongoing maintenance cost of NOT deprecating?
→ Security risk, engineer time, opportunity cost of complexity.
強制的な非推奨化と勧告的な非推奨化
| 種類 | 使用する場面 | 手段 |
|---|---|---|
| 勧告的 | 移行は任意で、古いシステムが安定している | 警告、ドキュメント、移行の促し。ユーザーは自分の都合に合わせて移行します。 |
| 強制的 | 古いシステムにセキュリティ上の問題がある、進行を妨げている、または保守コストが持続不可能である | 厳格な期限を設定します。古いシステムは日付Xまでに削除されます。移行ツールを提供します。 |
基本的には勧告的な方法を選んでください。 保守コストやリスクを考慮し、移行を強制する必要がある場合に限り、強制的な方法を使用してください。強制的な非推奨化では、移行ツール、ドキュメント、サポートを提供する必要があります。期限を告知するだけでは不十分です。
移行プロセス
ステップ1:置き換え先を構築する
動作する代替手段がない状態で、非推奨化してはいけません。置き換え先には、次の条件が必要です。
- 古いシステムの重要なユースケースをすべてカバーしている
- ドキュメントと移行ガイドがある
- 本番環境で実証されている(「理論上は優れている」だけでは不十分です)
ステップ2:告知して文書化する
## Deprecation Notice: OldService
**Status:** Deprecated as of 2025-03-01
**Replacement:** NewService (see migration guide below)
**Removal date:** Advisory — no hard deadline yet
**Reason:** OldService requires manual scaling and lacks observability.
NewService handles both automatically.
### Migration Guide
1. Replace `import { client } from 'old-service'` with `import { client } from 'new-service'`
2. Update configuration (see examples below)
3. Run the migration verification script: `npx migrate-check`
ステップ3:段階的に移行する
一度にすべてではなく、コンシューマーを1つずつ移行します。各コンシューマーについて、次の手順を実行します。
1. Identify all touchpoints with the deprecated system
2. Update to use the replacement
3. Verify behavior matches (tests, integration checks)
4. Remove references to the old system
5. Confirm no regressions
変更負担のルール: 非推奨にするインフラストラクチャを所有しているなら、ユーザーの移行を担当するか、移行が不要な後方互換性のある更新を提供する責任があります。非推奨化を告知して、ユーザーに対応を丸投げしてはいけません。
ステップ4:古いシステムを削除する
すべてのコンシューマーが移行した後に限り、次の手順を実行します。
1. Verify zero active usage (metrics, logs, dependency analysis)
2. Remove the code
3. Remove associated tests, documentation, and configuration
4. Remove the deprecation notices
5. Celebrate — removing code is an achievement
移行パターン
ストラングラーパターン
古いシステムと新しいシステムを並行して稼働させます。トラフィックを段階的に古いシステムから新しいシステムへ振り分けます。古いシステムが処理するトラフィックが0%になったら、古いシステムを削除します。
Phase 1: New system handles 0%, old handles 100%
Phase 2: New system handles 10% (canary)
Phase 3: New system handles 50%
Phase 4: New system handles 100%, old system idle
Phase 5: Remove old system
アダプターパターン
古いインターフェースから新しい実装へ呼び出しを変換するアダプターを作成します。バックエンドを移行している間も、コンシューマーは古いインターフェースを使い続けられます。
// Adapter: old interface, new implementation
class LegacyTaskService implements OldTaskAPI {
constructor(private newService: NewTaskService) {}
// Old method signature, delegates to new implementation
getTask(id: number): OldTask {
const task = this.newService.findById(String(id));
return this.toOldFormat(task);
}
}
フィーチャーフラグによる移行
フィーチャーフラグを使って、コンシューマーを1つずつ古いシステムから新しいシステムへ切り替えます。
function getTaskService(userId: string): TaskService {
if (featureFlags.isEnabled('new-task-service', { userId })) {
return new NewTaskService();
}
return new LegacyTaskService();
}
データベーススキーマのマイグレーション(拡張/縮小)
スキーマ変更は、デプロイをロールバックしても元に戻せない唯一のものがデータであるため、最もリスクの高いマイグレーションです。失敗の原因は、スキーマ変更とコード変更を密結合させることです。新しい名前を使い始めるリリースと同時にカラム名を変更すると、古いコードと新しいコードが同時に動作するロールアウト期間中に、どちらか一方が存在しないカラムをクエリしてしまいます。解決策は、カラムをその場で変更しないことです。加算的な段階に分けてマイグレーションし、どの段階でも古いコードと新しいコードの両方が有効な状態を保ちます。
EXPAND ──────────────→ MIGRATE ──────────────→ CONTRACT
add the new column, backfill existing rows, once no code reads the
nullable, alongside dual-write old+new from old column, drop it in
the old one the app a later, separate deploy
実例 — name を full_name に変更する場合:
- 拡張。
full_nameを NULL 許容で追加します。デプロイします。(古いコードはこれを無視するため、何も問題は起きません。) - 二重書き込み。 アプリが挿入/更新のたびに
nameとfull_nameの両方に書き込みます。デプロイします。 - バックフィル。 テーブルをロックしないよう、既存の行の
name → full_nameをバッチでコピーします。 - 読み取り先を切り替え。 アプリの参照先を
full_nameに変更し、両方への書き込みは続けます。デプロイして安定稼働を確認します。 - 縮小。
nameへの書き込みを停止し、その後、別の後続デプロイでカラムを削除します。
各ステップは個別にデプロイでき、元に戻せます。ステップ 4 で問題が起きた場合は、コードをロールバックすれば、full_name には引き続き値が設定されています。各段階を薄い垂直スライスとして扱います。incremental-implementation スキルを参照してください。
ルール:
- 最初に追加し、破壊的変更は最後に単独で行います。 新しい NULL 許容カラム、新しいテーブル、新しいインデックスの追加は、どのデプロイでも安全です。削除と名前変更は、古い形状を参照するコードがなくなった後に、それぞれ独立したデプロイで行います。
- すべてのマイグレーションに、テスト済みのロールバック手順を用意します。 元に戻せないマイグレーションは、ロールバックできないデプロイです。マージ前に
downを作成して実行します。 - バックフィルはホットパスを避け、バッチで行います。 数百万行に対する単一の
UPDATEはテーブルをロックします。処理を分割し、速度を制限してください。 - 書き込みをブロックせずに大きなインデックスを作成します(例:Postgres の
CREATE INDEX CONCURRENTLY)。 - 切り替えにリスクがある場合は、上記のフィーチャーフラグによるマイグレーションパターンと同様に、フィーチャーフラグを使ってコードとの依存を切り離します。
ゾンビコード
ゾンビコードとは、誰も所有していないのに、誰もが依存しているコードです。積極的に保守されておらず、明確な所有者もいないため、セキュリティ脆弱性や互換性の問題が蓄積します。次のような兆候があります。
- 6 か月以上コミットがないのに、利用者が存在する
- 保守担当者やチームが割り当てられていない
- 誰も修正しないテスト失敗がある
- 既知の脆弱性がある依存関係を誰も更新しない
- すでに存在しないシステムを参照するドキュメントがある
対応: 担当者を割り当てて適切に保守するか、具体的な移行計画を立てて非推奨にします。ゾンビコードを宙ぶらりんのままにしてはいけません。投資して保守するか、削除するかのどちらかです。
よくある合理化
| 合理化 | 実態 |
|---|---|
| 「まだ動いているのに、なぜ削除するのですか?」 | 誰も保守していない動作中のコードは、セキュリティ上の負債と複雑さを蓄積します。保守コストは気づかないうちに増大します。 |
| 「いつか誰かが必要とするかもしれません」 | 後で必要になれば、作り直せます。「念のため」に使われていないコードを残す方が、作り直すよりコストがかかります。 |
| 「移行にはコストがかかりすぎます」 | 移行コストと、2~3 年にわたる継続的な保守コストを比較してください。長期的には、通常、移行の方が低コストです。 |
| 「新しいシステムが完成したら、非推奨にします」 | 非推奨化の計画は設計時に始めます。新しいシステムが完成する頃には、新たな優先事項ができています。今すぐ計画してください。 |
| 「ユーザーは自分で移行してくれるでしょう」 | そうはなりません。ツール、ドキュメント、インセンティブを用意するか、自分で移行してください(Churn Rule)。 |
| 「両方のシステムをいつまでも保守できます」 | 同じことをするシステムが 2 つあると、保守、テスト、ドキュメント、オンボーディングのコストが倍になります。 |
| 「カラム名を変えるだけです。1 行で済みます」 | ロールアウト中は古いコードと新しいコードが同時に動作するため、存在しなくなったカラムを参照するコードが出てきます。インプレースで名前を変更せず、必ず拡張/縮小の手順を踏んでください。 |
| 「カラムを追加して、同じマイグレーションで古いものを削除します」 | それでは、安全な追加と破壊的な削除を結びつけてしまいます。削除は、古い構造を参照するコードがなくなった後、独立したデプロイで行ってください。 |
| 「必要になったらロールバックを書きます」 | ダウンパスのないマイグレーションは、元に戻せないデプロイです。マージ前に down を書いて実行してください。 |
危険信号
- 利用可能な代替手段がない非推奨システム
- 移行ツールやドキュメントを伴わない非推奨の告知
- 何年も勧告にとどまり、進展のない「緩やかな」非推奨化
- オーナーがいない一方で、利用者がいるゾンビコード
- 非推奨システムへの新機能の追加(代わりに移行先へ投資してください)
- 現在の利用状況を測定せずに行う非推奨化
- アクティブな利用者がゼロであることを確認せずに行うコードの削除
- スキーマ変更と、それに依存するコードを同じデプロイでリリースすること
- 拡張/縮小の手順ではなく、インプレースでカラム名を変更または削除すること
- ダウンパスのテストなしでマージされたマイグレーション、またはテーブルをロックするバックフィル
検証
非推奨化の完了後:
- 代替手段が本番環境で実証済みで、重要なユースケースをすべてカバーしている
- 具体的な手順と例を含む移行ガイドがある
- アクティブな利用者がすべて移行済みである(メトリクス/ログで確認済み)
- 古いコード、テスト、ドキュメント、設定がすべて削除されている
- コードベースに非推奨システムへの参照が残っていない
- 非推奨の告知が削除されている(役割を終えたため)
データベーススキーマのマイグレーション後:
- 変更は一度のインプレース編集ではなく、追加的な段階(拡張 → バックフィル → 縮小)でリリースされる
- どのデプロイ段階でも、古いコードと新しいコードの両方がスキーマに対して有効である
- 各マイグレーションにテスト済みのダウンパスがあり、バックフィルは負荷を抑えたバッチで実行される
- 破壊的な手順(削除/名前変更)は、古い構造を参照するコードがなくなった後、独立したデプロイでリリースされる