|
| 1 | +/** |
| 2 | + * GutPacer を us-east-1 → ap-northeast-1 へ移す(データ部分)。 |
| 3 | + * |
| 4 | + * node scripts/migrate-to-tokyo.mjs # 下見(何もしない) |
| 5 | + * node scripts/migrate-to-tokyo.mjs --apply # 実行 |
| 6 | + * |
| 7 | + * ⚠️ **旧リージョン(us-east-1)には一切書き込まない。読むだけ。** |
| 8 | + * 失敗しても現状は壊れない。切り替えは frontend/config.js を |
| 9 | + * 東京の Function URL に向けたときに初めて起きる。 |
| 10 | + * |
| 11 | + * 何度実行しても同じ結果になるように書く(テーブルは存在すれば作らない、 |
| 12 | + * 項目は put で上書き)。 |
| 13 | + */ |
| 14 | +import { |
| 15 | + DynamoDBClient, CreateTableCommand, DescribeTableCommand, ListTablesCommand, |
| 16 | + ScanCommand, BatchWriteItemCommand, waitUntilTableExists, |
| 17 | +} from "@aws-sdk/client-dynamodb"; |
| 18 | + |
| 19 | +const FROM = "us-east-1"; |
| 20 | +const TO = "ap-northeast-1"; |
| 21 | +const APPLY = process.argv.includes("--apply"); |
| 22 | + |
| 23 | +// 旧リージョンで確認した定義。**GSI もTTLも無い、オンデマンド。** |
| 24 | +const TABLES = [ |
| 25 | + { name: "gutpacer-logs-v2", keys: [["userId", "HASH", "S"], ["fullDate", "RANGE", "S"]] }, |
| 26 | + { name: "gutpacer-logs", keys: [["fullDate", "HASH", "S"]] }, |
| 27 | + { name: "gutpacer-settings", keys: [["settingKey", "HASH", "S"]] }, |
| 28 | + { name: "gutpacer-users", keys: [["userId", "HASH", "S"]] }, |
| 29 | +]; |
| 30 | + |
| 31 | +const src = new DynamoDBClient({ region: FROM }); |
| 32 | +const dst = new DynamoDBClient({ region: TO }); |
| 33 | + |
| 34 | +async function existsInTokyo(name) { |
| 35 | + try { |
| 36 | + await dst.send(new DescribeTableCommand({ TableName: name })); |
| 37 | + return true; |
| 38 | + } catch (e) { |
| 39 | + if (e.name === "ResourceNotFoundException") return false; |
| 40 | + throw e; |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +async function createTable(t) { |
| 45 | + await dst.send(new CreateTableCommand({ |
| 46 | + TableName: t.name, |
| 47 | + BillingMode: "PAY_PER_REQUEST", |
| 48 | + AttributeDefinitions: t.keys.map(([n, , type]) => ({ AttributeName: n, AttributeType: type })), |
| 49 | + KeySchema: t.keys.map(([n, kt]) => ({ AttributeName: n, KeyType: kt })), |
| 50 | + })); |
| 51 | + await waitUntilTableExists({ client: dst, maxWaitTime: 120 }, { TableName: t.name }); |
| 52 | +} |
| 53 | + |
| 54 | +async function scanAll(client, name) { |
| 55 | + const items = []; |
| 56 | + let key; |
| 57 | + do { |
| 58 | + const r = await client.send(new ScanCommand({ TableName: name, ExclusiveStartKey: key })); |
| 59 | + items.push(...(r.Items ?? [])); |
| 60 | + key = r.LastEvaluatedKey; |
| 61 | + } while (key); |
| 62 | + return items; |
| 63 | +} |
| 64 | + |
| 65 | +async function copy(t) { |
| 66 | + const items = await scanAll(src, t.name); |
| 67 | + if (!APPLY) return { count: items.length, wrote: 0 }; |
| 68 | + for (let i = 0; i < items.length; i += 25) { |
| 69 | + const chunk = items.slice(i, i + 25).map((Item) => ({ PutRequest: { Item } })); |
| 70 | + let req = { [t.name]: chunk }; |
| 71 | + // 未処理分は必ず再送する。**黙って落とさない。** |
| 72 | + for (let attempt = 0; attempt < 5 && Object.keys(req).length; attempt++) { |
| 73 | + const r = await dst.send(new BatchWriteItemCommand({ RequestItems: req })); |
| 74 | + req = r.UnprocessedItems ?? {}; |
| 75 | + if (Object.keys(req).length) await new Promise((s) => setTimeout(s, 300 * (attempt + 1))); |
| 76 | + } |
| 77 | + if (Object.keys(req).length) throw new Error(`${t.name}: 書き込めなかった項目が残った`); |
| 78 | + } |
| 79 | + return { count: items.length, wrote: items.length }; |
| 80 | +} |
| 81 | + |
| 82 | +console.log(`${FROM} → ${TO}${APPLY ? "" : "(下見。--apply で実行)"}\n`); |
| 83 | +let ng = 0; |
| 84 | +for (const t of TABLES) { |
| 85 | + const has = await existsInTokyo(t.name); |
| 86 | + if (!has) { |
| 87 | + if (APPLY) { await createTable(t); console.log(` 作成 ${t.name}`); } |
| 88 | + else console.log(` 作成予定 ${t.name}`); |
| 89 | + } else { |
| 90 | + console.log(` 既にある ${t.name}`); |
| 91 | + } |
| 92 | + if (!APPLY && !has) { console.log(` (テーブルが無いので件数の照合は実行時に)`); continue; } |
| 93 | + |
| 94 | + const { count } = await copy(t); |
| 95 | + if (APPLY) { |
| 96 | + const after = (await scanAll(dst, t.name)).length; |
| 97 | + const ok = after >= count; |
| 98 | + if (!ok) ng++; |
| 99 | + console.log(` ${count} 件 → 東京 ${after} 件 ${ok ? "✅" : "★不一致"}`); |
| 100 | + } else { |
| 101 | + console.log(` ${count} 件を移す予定`); |
| 102 | + } |
| 103 | +} |
| 104 | +console.log(ng ? `\n★ ${ng} 件のテーブルで不一致。切り替えないこと。` : "\n照合 OK。旧リージョンはそのまま残してある。"); |
0 commit comments