Claude Media
Claude CodeでContentfulの移行スクリプトを書き、sandboxで検証してから本番へ流す

Claude CodeでContentfulの移行スクリプトを書き、sandboxで検証してから本番へ流す

contentful-migrationのスクリプトをClaude Codeに書かせ、別環境で実行してから環境エイリアスを切り替える流れを、許可ルールの設定例つきで示します。

Claude CodeでContentfulの移行スクリプトを書き、sandboxで検証してから本番へ流す

Contentfulのコンテンツモデルを変えるとき、管理画面で手作業するか、contentful-migrationのスクリプトにするかの二択があります。スクリプトにすると差分がコードとして残り、同じ変更を別の環境に再実行できます。

このスクリプトを書く作業は、Claude Codeに向いています。DSLの語彙が限られ、変換ロジックは普通のJavaScript関数だからです。難しいのは書くことより、本番に触れさせない構成を先に作ることです。本記事では、フィールドの追加・名前変更・既存エントリの変換を例に、別環境での実行から環境エイリアスの切り替えまでを順に見ます。

移行スクリプトの部品と、Claudeに任せる範囲

contentful-migrationは、migrationオブジェクトを受け取る関数をエクスポートするだけのスクリプトです。READMEの最小例は次の形です。

module.exports = function (migration, context) {
  const dog = migration.createContentType('dog')
  const name = dog.createField('name')
  name.type('Symbol').required(true)
}

READMEが挙げる主な操作は、コンテンツタイプの作成・編集、フィールドの作成・編集・削除・ID変更・並び替え、エントリの変換、タグの操作です。日常の変更で使うのは、次の4つに絞れます。

やりたいこと使う操作注意点
フィールドを足す使う操作createField注意点nameとtypeが必須。Arrayにはitems、LinkにはlinkTypeも要る
フィールドの定義を変える使う操作editField注意点指定はcreateFieldと同じ
フィールドIDを変える使う操作changeFieldId注意点現在のIDと新しいIDを渡す
既存エントリの値を詰め替える使う操作transformEntries注意点ロケールごとに関数が呼ばれる

READMEは、deleteFieldについて「フィールドを外し、コンテンツタイプを公開し、そのうえでフィールドを削除する近道」であり、関連するコンテンツは失われると説明しています。削除は取り消せない操作です。Claudeに書かせる場合も、削除を含む移行は別ファイルに分けて、人が内容を確かめたうえで流すのが運用しやすい形です。

記法には2種類あります。メソッドをつなぐ書き方と、オブジェクトで渡す書き方です。READMEは前者を勧めています。バリデーションエラーが出たとき、行番号つきでどこが誤りかを示せるからです。Claudeに書かせるときは、この点を指示に入れておくと、エラーを読み返して直す反復が速くなります。

フィールドを足して、既存エントリの値を詰め替える

「authorとauthorCityを結合したbylineを作りたい」といった変更では、フィールドを足してから既存エントリを詰め替えます。READMEのtransformEntriesの例が、そのままの形です。

module.exports = function (migration) {
  const article = migration.editContentType('newsArticle')
  article.createField('byline').name('Byline').type('Symbol')
 
  migration.transformEntries({
    contentType: 'newsArticle',
    from: ['author', 'authorCity'],
    to: ['byline'],
    transformEntryForLocale: function (fromFields, currentLocale) {
      if (currentLocale === 'de-DE') {
        return
      }
      const newByline = `${fromFields.author[currentLocale]} ${fromFields.authorCity[currentLocale]}`
      return { byline: newByline }
    }
  })
}

仕様のうち、実行前に押さえておきたい点は3つです。

  • 変換関数は、エントリごとに、スペースのロケールの数だけ呼ばれる
  • 関数がundefinedを返したロケールでは、その値に手が加わらない
  • shouldPublishの既定は'preserve'で、元のエントリの公開状態を引き継ぐ

上の例では、de-DEだけ何もせず戻っています。ロケールの扱いを書き忘れると、値のないロケールでundefinedという文字列がbylineに入る事故になりえます。Claudeが書いた変換関数は、このロケール分岐と欠損値の扱いを最初に確かめる場所です。

変換ロジックをtransformEntryForLocaleの中に直接書かず、純粋な関数として別ファイルに切り出させると、Node.jsの標準テストで単体検証できます。入力を{ author: { 'en-US': 'Ann' }, authorCity: { 'en-US': 'Oslo' } }のように固定し、期待する戻り値を比べるだけです。Contentfulに接続せずに済むので、Claudeに書かせて、テストを実行させて、失敗を直させる反復をここで完結できます。

古いフィールドの削除は、新しいフィールドを読むアプリ側のリリースが済んでから、別の移行として出します。1つのスクリプトに追加・変換・削除を詰めると、途中で失敗したとき元に戻す判断が難しくなります。

作業用の環境を作って、そこで流す

移行を本番の環境に直接流さないために、環境を複製して作業用にします。contentful space environment createは、複製元を--sourceで指定できます。

contentful space environment create \
  --environment-id 'release-2' --name 'Release 2' \
  --source master --await-processing

--await-processingを付けると、環境の準備が済むまで待ちます。待ち時間の上限は--processing-timeoutで、既定は5分です。

移行の実行はcontentful space migrationで、実行先の環境を--environment-id(-e)で指定します。省略すると既定値のmasterに流れる点に注意してください。

contentful space migration \
  --space-id "$CONTENTFUL_SPACE_ID" \
  --environment-id release-2 \
  migrations/002-add-byline.js

--yes(-y)を付けると、適用前の確認が省略されます。付けなければ、確認を挟んで実行します。作業用の環境ではClaudeに-y付きで流させてよい一方、本番側の環境に向けた実行では付けないのが運用の分かれ目です。-eを書き忘れると既定のmasterに向かうので、後述のとおり許可ルール側でも防ぎます。

READMEとCLIのドキュメントのどちらにも、実際には何も書き込まない専用の実行モードは載っていません。「dry-runで確かめる」と言う場合の実体は、複製した環境で本物の移行を流して結果を確認する、という手順になります。複製が失敗の巻き戻し先になるため、これが実質的な予行演習として機能します。

確認は、複製した環境のエントリを管理画面で開き、変換後の値とロケールごとの欠けを目視する形が基本です。件数が多いときは、確認したい観点(空のbylineが残っていないか、公開状態が元のままか)をClaudeに箇条書きで出させ、その観点を人が画面で突き合わせると抜けが減ります。

検証した環境を本番にする

環境エイリアスは、masterのような名前を、実際の環境に向けるための仕組みです。CLIのenvironment-alias updateは、エイリアスの向き先を付け替えます。

contentful space environment-alias update \
  --alias-id master --target-environment-id release-2

この機能は、Contentfulの管理画面の設定(Settings > Environments)で事前に有効化しておく必要があります。有効化していない空間では、このコマンドは使えません。

アプリ側がmasterというエイリアス経由で読んでいれば、付け替えた瞬間に読む先がrelease-2に移ります。問題が見つかったときは、同じコマンドで向き先を前の環境に戻せます。この付け替えは、Claudeではなく人が実行する操作にしておくと、本番に影響する最後の一歩だけが人の手に残ります。

Claude Codeの許可ルールで、本番に向かう道を塞ぐ

移行スクリプトの内容より先に決めたいのは、Claudeが実行できるコマンドの範囲です。Claude Codeの許可ルールは、拒否、確認、許可の順に評価され、最初に当たったものが採用されます。広い拒否ルールは、より狭い許可ルールで例外にできません。この性質を使うと、次の設定になります。

{
  "permissions": {
    "allow": [
      "Bash(npm run migrate:sandbox)",
      "Bash(node --test *)"
    ],
    "deny": [
      "Bash(contentful space environment-alias *)",
      "Bash(contentful space migration *)",
      "Read(./.env)",
      "Read(./.contentfulrc*)"
    ]
  }
}

考え方は、contentfulコマンドを直接叩く道を閉じて、作業用の環境にだけ向けたnpmスクリプトを許可する、というものです。package.json側は次のように書きます。

{
  "scripts": {
    "migrate:sandbox": "contentful space migration -s $CONTENTFUL_SPACE_ID -e release-2 -y migrations/current.js"
  }
}

許可ルールは、Claudeが書いたコマンドの文字列に対して当たります。npm runの中で動くcontentfulは、Claudeが書いたコマンド文字列そのものではないため、拒否ルールの照合対象になりません(公式も、sh -cの内側には当たらないと説明しています)。これは抜け道ではなく、人がpackage.jsonで固定した実行先だけを通すための設計です。ただし公式のドキュメントは、Bashのルールを「セキュリティ境界ではない」と明記しています。別の呼び出し方には当たらないため、Claudeがpackage.jsonを書き換える経路は別に考える必要があります。

そこで、次の2つを足します。

  • 管理トークンは、作業用の環境だけを操作できる権限のものを渡す。本番のトークンは.envに置かず、Claudeの作業ディレクトリに存在させない
  • Readの拒否ルールで、トークンを書いた.envや設定ファイルを読ませない。公式の説明では、ファイルツールだけでなくcatのようなBashの読み取りコマンドにも効く。ただしPythonやNodeのスクリプトが自分でファイルを開く経路には効かない

最後の点は、拒否ルールだけでは守り切れない部分です。OSレベルでアクセスを遮りたい場合は、サンドボックスを併用します。MCP経由で本番に接続する場合の権限設計はMCPで本番環境に接続する権限設計にまとめています。

CLAUDE.mdに書いておく運用の約束

許可ルールで止められるのは実行です。「どの種類の変更を1ファイルに入れるか」といった書き方は、CLAUDE.mdで伝えます。

# Contentful移行スクリプトの規約
 
- 移行は`migrations/`に連番のファイルで置く。1ファイルにつき1種類の変更だけにする
- フィールドの削除(`deleteField`)は、追加・変換とは別ファイルにする
- メソッドをつなぐ記法で書く(エラー時に行番号が出るため)
- `transformEntries`の変換関数は`lib/`の純粋関数に切り出し、`node --test`で検証する
- 実行は`npm run migrate:sandbox`だけ。環境エイリアスの切り替えは人が行う

規約が1つ増えるごとに、Claudeの書くスクリプトのばらつきが減ります。特に「純粋関数に切り出す」の一行が、レビューの負担を大きく変えます。

実行から切り替えまでの順番

5つの手順のうち、人の手に戻るのは1・4・5番目です。それぞれ、次へ進む条件を決めておきます。

手順

本番に流すまでの手順

  1. 1

    環境を複製する(人)

    release-2が使える状態になったら、次へ進みます。

  2. 2

    スクリプトとテストを書かせる(Claude)

    node --testが通ったら、次へ進みます。

  3. 3

    複製環境で流す(Claude)

    npm run migrate:sandboxがエラーなく終わったら、次へ進みます。

  4. 4

    結果を確かめる(人)

    変換後の値、ロケール、公開状態に問題がなければ、次へ進みます。

  5. 5

    エイリアスを付け替える(人)

    付け替え後に、アプリ側の表示を見て完了とします。

ContentfulのMCP連携との使い分け

エントリの確認や小さな修正であれば、MCP経由の操作で足ります。接続方法と権限の絞り方はClaudeとContentfulを連携する方法で扱っています。一方、コンテンツモデルの変更は、差分をレビューし、複数の環境へ同じ内容を流し直せることが価値になります。スクリプトにする理由はそこにあります。

マイグレーションをコードとして残し、本番適用の前に往復や差分で確かめるという考え方は、データベースでも共通です。Alembicのマイグレーションでは、upgradeとdowngradeの往復で検証する流れを、EF Coreのマイグレーションでは適用前のスクリプトレビューを扱っています。Contentfulの移行スクリプトにはdowngradeに当たる記述がなく、その代わりに、環境の複製とエイリアスの付け替えで元の状態に戻れる構成にします。

つまずきやすい点

  • 再実行: 変換を途中まで適用した環境にもう一度流すと、結果が想定とずれます。失敗したら環境を作り直すのが確実です。複製には待ち時間が伴うため、--await-processingを付けた作成コマンドをpackage.jsonの別スクリプトにしておくと、やり直しが軽くなります
  • 付け替えたあとの次の移行: 上の構成では、migrate:sandboxはrelease-2に固定されています。エイリアスを付け替えた後のrelease-2は本番として読まれる環境なので、そのまま次の移行を流すと本番に直接当たります。次の移行の前に、新しい環境をmasterから複製し、package.jsonの-eの値とCLAUDE.mdの記述を新しい環境IDに合わせます

まとめ

フィールドを1つ足す程度の変更が年に数回なら、管理画面の手作業でも足ります。スクリプト化が見合うのは、同じ変更を複数の環境へ流し直す場面や、既存エントリの値を一括で変換する場面です。その場合に限って、許可ルールとpackage.jsonで実行先を固定してからClaudeに書かせると、本番に届くのは人が見たものだけになります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. github.com/contentful/contentful-migration
  2. github.com/contentful/contentful-cli/tree/master/docs/space/migration
  3. github.com/contentful/contentful-cli/tree/master/docs/space/environme…
  4. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn