<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Migration on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/migration/</link>
    <description>Recent content in Migration on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Thu, 05 Mar 2026 09:16:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/migration/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Kubernetes環境でDBスキーマ変更を止めずに進める：ゼロダウンタイム移行の実践戦略</title>
      <link>https://www.ai2core.com/posts/2026-03-05-kubernetes-zero-downtime-db-migration-strategy/</link>
      <pubDate>Thu, 05 Mar 2026 09:16:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-kubernetes-zero-downtime-db-migration-strategy/</guid>
      <description>本番KubernetesでDBマイグレーションを安全に実施するためのExpand-Contract戦略、手順、ロールバック設計を具体例つきで解説。</description>
      <content:encoded><![CDATA[<h1 id="kubernetes環境でdbスキーマ変更を止めずに進めるゼロダウンタイム移行の実践戦略">Kubernetes環境でDBスキーマ変更を止めずに進める：ゼロダウンタイム移行の実践戦略</h1>
<p>「カラムを追加するだけだから大丈夫」──この油断が、本番障害の入口になります。Kubernetes のように複数バージョンの Pod が同時に存在する環境では、DB スキーマ変更はアプリ変更よりも慎重に扱う必要があります。</p>
<p>本記事では、<strong>Expand-Contract パターン</strong>を中心に、ゼロダウンタイムを目指すための具体手順を解説します。実際の運用では、DDLの速さより「互換性のある期間をどう作るか」が勝負です。</p>
<h2 id="1-なぜkubernetesでdb移行が難しいのか">1. なぜKubernetesでDB移行が難しいのか</h2>
<p>Kubernetesでは、ローリングアップデート中に新旧Podが混在します。つまり次の状態が同時に発生します。</p>
<ul>
<li>新アプリは新スキーマを期待</li>
<li>旧アプリは旧スキーマしか知らない</li>
<li>DBは1つしかない</li>
</ul>
<p>このとき破綻するのが「破壊的変更を先に適用する」ケースです。たとえば旧カラムを即削除すると、旧Podがエラーを連発します。</p>
<h2 id="2-基本戦略expand--migrate--contract">2. 基本戦略：Expand → Migrate → Contract</h2>
<p>ゼロダウンタイム移行の原則はこの3段階です。</p>
<ol>
<li><strong>Expand</strong>: 互換性を壊さない変更を先に入れる（新カラム追加など）</li>
<li><strong>Migrate</strong>: アプリを段階的に切替え、データを移行する</li>
<li><strong>Contract</strong>: 旧仕様を最終削除する（十分な監視後）</li>
</ol>
<p>この順序なら、どの時点でも旧新どちらのアプリも動作可能にできます。</p>
<h2 id="3-具体例usersfull_name-を-first_name--last_name-へ分割">3. 具体例：<code>users.full_name</code> を <code>first_name</code> / <code>last_name</code> へ分割</h2>
<h3 id="31-expand-フェーズ">3.1 Expand フェーズ</h3>
<p>まず破壊的でないDDLを適用します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> first_name text;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> last_name text;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この時点で旧アプリは <code>full_name</code> を使い続けられます。新アプリは新カラムに対応した実装を持っていても、まだ必須にしません。</p>
<h3 id="32-アプリを両対応にする">3.2 アプリを「両対応」にする</h3>
<p>書き込み時は両方へ保存（dual write）し、読み込み時は新カラム優先 + 旧カラムフォールバックにします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">save_user_name</span>(user_id: str, full_name: str):
</span></span><span style="display:flex;"><span>    first, last <span style="color:#f92672">=</span> split_name(full_name)
</span></span><span style="display:flex;"><span>    db<span style="color:#f92672">.</span>execute(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        UPDATE users
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SET full_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            first_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            last_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WHERE id = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>        (full_name, first, last, user_id),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">read_user_display_name</span>(row):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> row<span style="color:#f92672">.</span>first_name <span style="color:#f92672">and</span> row<span style="color:#f92672">.</span>last_name:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>row<span style="color:#f92672">.</span>first_name<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>row<span style="color:#f92672">.</span>last_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> row<span style="color:#f92672">.</span>full_name
</span></span></code></pre></td></tr></table>
</div>
</div><p>この両対応期間を作るのが、ゼロダウンタイムの本質です。</p>
<h3 id="33-バックフィル既存データ移行">3.3 バックフィル（既存データ移行）</h3>
<p>大規模テーブルでは一括更新を避け、チャンク更新します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- 疑似コードイメージ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> users
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> first_name <span style="color:#f92672">=</span> split_part(full_name, <span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#ae81ff">1</span>),
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> split_part(full_name, <span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">&gt;</span> :last_id
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> id <span style="color:#f92672">&lt;=</span> :last_id <span style="color:#f92672">+</span> <span style="color:#ae81ff">10000</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> (first_name <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> last_name <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>ジョブ実装時のポイント:</p>
<ul>
<li>1チャンクごとに commit</li>
<li>再開可能なチェックポイント（last_id）を保存</li>
<li>実行時間帯を制御（ピーク時間回避）</li>
</ul>
<h3 id="34-contract-フェーズ">3.4 Contract フェーズ</h3>
<p>全Podが新実装になり、フォールバックが不要と判断できたら旧カラム削除へ進みます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">DROP</span> <span style="color:#66d9ef">COLUMN</span> full_name;
</span></span></code></pre></td></tr></table>
</div>
</div><p>削除は必ず最後です。ここを急ぐとロールバック不能になります。</p>
<h2 id="4-マイグレーション実行方式の選び方">4. マイグレーション実行方式の選び方</h2>
<p>Kubernetesでは主に3パターンがあります。</p>
<h3 id="a-cicdで先行実行推奨">A. CI/CDで先行実行（推奨）</h3>
<p>デプロイ前に migration Job を走らせ、成功後にアプリを更新。</p>
<ul>
<li>メリット: 実行順序を固定しやすい</li>
<li>デメリット: 長時間 migration の扱いが難しい</li>
</ul>
<h3 id="b-initcontainer-実行">B. initContainer 実行</h3>
<p>Pod起動時に migration を走らせる方式。</p>
<ul>
<li>メリット: 実装が単純</li>
<li>デメリット: 複数Pod同時起動で競合しやすい</li>
</ul>
<h3 id="c-専用-migration-controller--job">C. 専用 Migration Controller / Job</h3>
<p>Argo Workflows などで明示的に管理。</p>
<ul>
<li>メリット: 大規模運用で監査しやすい</li>
<li>デメリット: 初期構築コストが高い</li>
</ul>
<p>中規模までなら A が最も事故が少ないです。</p>
<h2 id="5-alembicflyway運用の実務ポイント">5. Alembic/Flyway運用の実務ポイント</h2>
<h3 id="51-1-migration--1責務">5.1 1 migration = 1責務</h3>
<p>「追加 + データ移行 + 削除」を1ファイルに詰め込むと失敗時に戻しづらくなります。Expand/Migrate/Contract を別 migration に分けて、各段階を検証可能にしてください。</p>
<h3 id="52-lock-timeout-を設定する">5.2 lock timeout を設定する</h3>
<p>DDL はロック待ちでアプリを止めることがあります。PostgreSQL なら <code>lock_timeout</code> を設定して、危険な待機を避けます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> lock_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;5s&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> ...;
</span></span></code></pre></td></tr></table>
</div>
</div><p>失敗時は即中断し、メンテナンス時間帯に再実行する判断が可能です。</p>
<h3 id="53-破壊的変更前に計測窓を設ける">5.3 破壊的変更前に計測窓を設ける</h3>
<p>旧カラム参照がゼロになったことを、メトリクスやログで一定期間確認してから削除します。目安として 7〜14日程度の観測期間を取ると安全です。</p>
<h2 id="6-ロールバック戦略">6. ロールバック戦略</h2>
<p>ゼロダウンタイム設計では、ロールバック可能性を最初に決めます。</p>
<ul>
<li>Expand段階: ほぼ即ロールバック可能</li>
<li>Migrate段階: dual write を維持していればロールバック可能</li>
<li>Contract段階: 削除後は復元コスト高（バックアップ依存）</li>
</ul>
<p>したがって、Contract 実施前に以下を満たす必要があります。</p>
<ul>
<li>直近スナップショット取得済み</li>
<li>復元手順を演習済み</li>
<li>影響範囲（API・バッチ・BI）を棚卸し済み</li>
</ul>
<h2 id="7-監視項目と成功判定">7. 監視項目と成功判定</h2>
<p>移行中は「成功したか」より「安全に進んでいるか」を見ます。</p>
<ul>
<li>APIエラー率（4xx/5xx）</li>
<li>DBロック待ち時間</li>
<li>スロークエリ件数</li>
<li>migration ジョブ進捗（処理済み件数、残件数）</li>
<li>旧カラム参照回数</li>
</ul>
<p>成功判定の例:</p>
<ol>
<li>新旧Pod混在中のエラー率に有意な悪化なし</li>
<li>バックフィル完了率100%</li>
<li>旧カラム参照0が連続7日</li>
<li>Contract後24時間で異常なし</li>
</ol>
<h2 id="8-よくある失敗と回避策">8. よくある失敗と回避策</h2>
<h3 id="失敗1-非null制約を早く付けすぎる">失敗1: 非NULL制約を早く付けすぎる</h3>
<p>バックフィル完了前に <code>NOT NULL</code> を付けると、旧データで失敗します。まずは nullable で追加し、データ移行後に制約追加が正解です。</p>
<h3 id="失敗2-インデックス作成で書き込み停止">失敗2: インデックス作成で書き込み停止</h3>
<p>大きいテーブルで通常インデックス作成を行うとロックが重くなります。PostgreSQL では <code>CREATE INDEX CONCURRENTLY</code> を使って影響を下げます。</p>
<h3 id="失敗3-migration-の実行主体が複数">失敗3: migration の実行主体が複数</h3>
<p>同時に2つのJobが走ると競合します。Kubernetes Job は排他制御（Lease/Lock）を持たせるか、CI側で単一実行を保証してください。</p>
<h2 id="9-実運用テンプレートmigration-job-と段階リリース">9. 実運用テンプレート：Migration Job と段階リリース</h2>
<p>最後に、現場でそのまま使える最小テンプレートを示します。ポイントは「DDLを一気にやらない」「アプリ側フラグで読取切替を制御する」の2点です。</p>
<h3 id="91-migration-jobexpand専用">9.1 Migration Job（Expand専用）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">batch/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Job</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">users-expand-20260305</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">backoffLimit</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">restartPolicy</span>: <span style="color:#ae81ff">Never</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">migrate</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/example/api:1.42.0</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>: [<span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-lc&#34;</span>, <span style="color:#e6db74">&#34;alembic upgrade 20260305_expand_users_name&#34;</span>]
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">envFrom</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">secretRef</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-db-secret</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Expand 用 Job を分離しておけば、失敗時にアプリデプロイを止める判断が明確になります。<code>backoffLimit: 0</code> にして「失敗を隠さない」運用にするのも実務で有効です。</p>
<h3 id="92-段階リリース手順例">9.2 段階リリース手順（例）</h3>
<ol>
<li>Expand Job 実行（DDLのみ）</li>
<li>アプリ v1（dual write + fallback read）を10%配信</li>
<li>エラー率・ロック待ちを30分監視</li>
<li>50% → 100%へ段階拡大</li>
<li>バックフィル Job 実行</li>
<li>旧参照ゼロ確認後に Contract 実施</li>
</ol>
<p>Kubernetes では <code>kubectl rollout status deployment/api -n prod</code> を必ず監視に組み込み、配信完了判定を人間が明示的に確認する運用が安全です。</p>
<h2 id="まとめ">まとめ</h2>
<p>KubernetesでのDBマイグレーションは、DDLテクニックだけでは成功しません。重要なのは、</p>
<ul>
<li>互換性期間を意図的に作る</li>
<li>段階を分けて進める</li>
<li>ロールバック可能性を先に設計する</li>
<li>監視で「削除してよい」根拠を取る</li>
</ul>
<p>この4点です。Expand-Contract を守るだけで、移行の失敗率は目に見えて下がります。スキーマ変更は怖い作業ですが、手順化すれば再現可能な運用にできます。次回の変更から、ぜひこの流れで試してみてください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>Database</category>
      <category>Migration</category>
      <category>SRE</category>
      <category>DevOps</category>
    </item>
  </channel>
</rss>
