ã¿ãªããããã«ã¡ã¯ãXïŒã¯ãã¹ïŒ ã€ãããŒã·ã§ã³ æ¬éšããœãããŠã§ã¢ãã¶ã€ã³ã»ã³ã¿ãŒã®éŽæšã§ãã ããŒã¿ããŒã¹ã« Amazon DynamoDB ãæ¡çšããŠããWebã¢ããªã±ãŒã·ã§ã³ãã Amazon RDSãæ¡çšããWebã¢ããªã±ãŒã·ã§ã³ã«ãªãã¬ã€ã¹ããŠããéã«ãããŒã¿ç§»è¡ã§èŠæŠããããšãèšäºã«æ®ããŸãã èæ¯ å®çŸãããããšãšãèŠæŠããããš å®çŸãããããš ã¢ããªã±ãŒã·ã§ã³ãšããŒãã«å®çŸ© èŠæŠããããš è§£æ±ºæ¹æ³ èŠç¹ DynamoDBâS3ãžã®ãšã¯ã¹ããŒã éçºç°å¢ãžã®ããŠã³ããŒãã倿åŠç RDSãžã®ã€ã³ããŒã ãŸãšã èæ¯ åŸæ¥ã®ã¢ããªã±ãŒã·ã§ã³ã§ã¯ãããŒã¿ããŒã¹å±€ã«NoSQLåã®ããŒã¿ããŒã¹ã§ããDynamoDBãæ¡çšããŠããŸããã ããããå©çšãŠãŒã¶ãŒãå¢å ã㊠ããŒãã£ã·ã§ã³ ããŒä»¥å€ã§ã®æ€çŽ¢èŠä»¶ãåºãŠããããšã§ããã«ã¹ãã£ã³ãããããåŸãªãã±ãŒã¹ããã³ãã³çºçããããã¬ã¹ãã³ã¹ã¿ã€ã ãã³ã¹ãã®åé¡ãçºçããŠããŸããã 以äžã®ããšãããNoSQLåã®ããŒã¿ããŒã¹ããæè»ã«æ€çŽ¢ãå¯èœãªã RDB åã®ããŒã¿ããŒã¹ã§ããRDSã«ç§»è¡ããããšãšãªããŸããã å®çŸãããããšãšãèŠæŠããããš å®çŸãããããš ç§»è¡å
ã®DynamoDBããŒãã«ããããŒã¿ããšã¯ã¹ããŒãããç§»è¡å
ã®ã¢ããªã±ãŒã·ã§ã³ã®ããŒã¿ããŒã¹ïŒ PostgreSQL ïŒã®ããŒãã«å®çŸ©ã«åºã¥ããŠããŒã¿ã倿ããŸããæçµçã«ã¯ãRDSãžããŒã¿ãã€ã³ããŒãããããšãç®æããŸãã ããŒã¿ã®å€æã»ç§»è¡ã«ã¯ã AWS Glueã AWS DMS(Database Migration Service)ãªã©ã® AWS ãµãŒãã¹ãå©çšããããšãã§ããŸããä»åã¯ãéçºç°å¢ã§ããŒã¿ã®äžéšã䜿ãããã£ãããšããããéçºç°å¢ã§TypeScriptãçšããŠå€æããŸãã ã¢ããªã±ãŒã·ã§ã³ãšããŒãã«å®çŸ© ã¢ããªã±ãŒã·ã§ã³ã¯ãNext.js à Prisma ãçšããŠå®è£
ããŠããŸãã ããŒã¿ããŒã¹ã«ã¯ PostgreSQL ãæ¡çšããŠãããèšäºçšã«ç°¡ç¥åããç§»è¡åŸã®ããŒãã«æ§æã¯ä»¥äžã®ãšããã§ãã userããŒãã«ãšaccountããŒãã«ããããuserããŒãã«ã®idãå€éšããŒã«ãaccountããŒãã«ãšãªã¬ãŒã·ã§ã³ãç¯ããŠããŸãã èŠæŠããããš DynamoDBãããšã¯ã¹ããŒãããããŒã¿ã倿ããŠããŒã¿ããŒã¹ã«ã€ã³ããŒãããéçšã§ãSERIALåã§ããuserããŒãã«ã®idã¯ã¬ã³ãŒããäœæããããŸã§ç¢ºå®ããŸãããaccountããŒãã«ã«ã¬ã³ãŒããäœæããéã«ãaccountããŒãã«ã®å€éšããŒã§ããuser_idã¯ãuserããŒãã«ã®ã¬ã³ãŒããäœæåŸã«userããŒãã«ããååŸããªããã°ãªããŸããã ãŸããWebã¢ããªã±ãŒã·ã§ã³ã® ãœãŒã¹ã³ãŒã ããã¢ããªã±ãŒã·ã§ã³ãåç
§ããŠããããŒã¿ããŒã¹ã® ã¹ããŒã ã«ã¯ãªãã¹ãæãå ããªãããã«ãããã§ãã è§£æ±ºæ¹æ³ èŠç¹ ä»åã¯æ¬¡ã®æµãã§è§£æ±ºããŸããã â accountããŒãã«ãšã¯å¥ã«äžæçãªaccount_tmpããŒãã«ãããŒã¿ããŒã¹ã«äœæããã â¡userããŒãã«ãšaccount_tmpããŒãã«ã«å€æåŸã®ããŒã¿ãINSERTããã â¢userããŒãã«ããaccount_tmpããŒãã«ã®ã¬ã³ãŒãã«å¯Ÿå¿ããuser_idãååŸããŠãaccount_tmpããŒãã«ã®ã¬ã³ãŒããUPDATEããã â£account_tmpããŒãã«ããaccountããŒãã«ã«INSERTããã 以éã詳ããã¿ãŠãããŸããïŒ AWS ã®æ§æãããŒã¿æ§é ã¯èšäºçšã«ç°¡ç¥åããŠããŸãïŒ DynamoDBâS3ãžã®ãšã¯ã¹ããŒã èšäºçšã«DynamoDBããŒãã«ãäœæããŸããã ããŒãã£ã·ã§ã³ ããŒã«ã¯emailãèšå®ããŠããŸãã é
ç®ã¯ã以äžã®ãšããã§ãã email (S) account (L) updated_at (N) created_at (N) test-user1@ example.com [{"name": "test-user1-1"}, {"name": "test-user1-2"}] 1696129200 1696129200 test-user2@ example.com [{"name": "test-user2-1"}] 1696129200 1696129200 test-user3@ example.com [{"name": "test-user3-1"}, {"name": "test-user3-2"}] 1696129200 1696129200 ãã®ã㌠ãã«ã㌠ã¿ãéçºç°å¢ã«ç§»åããããããS3 ãã±ãã ã«ãšã¯ã¹ããŒãããŸãã ãŸãããšã¯ã¹ããŒãçšã«S3ã« ãã±ãã ãäœæããŸãã ãã®åŸãDynamoDBã®ã³ã³ãœãŒã«ç»é¢ãããããšã¯ã¹ããŒãããã³ã¹ããªãŒã ãã¿ããããS3ãžã®ãšã¯ã¹ããŒãããéžæããããšã§ãå
ã»ã©äœæããS3 ãã±ãã ã«ããŒã¿ããšã¯ã¹ããŒãããŸãã 詳ããæé ã¯ã詳现㯠AWSã®å
¬åŒèšäº ãåç
§ããŠãã ããã éçºç°å¢ãžã®ããŠã³ããŒãã倿åŠç S3ã®ã³ã³ãœãŒã«ç»é¢ãããå
ã»ã©ãšã¯ã¹ããŒãããDynamoDBããŒãã«æ
å ±ãããŠã³ããŒãããŠãã ããã AWS CLI ãã aws s3 cp s3://[ãã±ããå]/[ããŒã¿ãé
眮ããããã©ã«ããã¹] [ããŠã³ããŒãå
ã®ãã©ã«ããã¹] --recursive ã§ãããŠã³ããŒãå¯èœã§ãã ããŠã³ããŒãåŸã®ããŒã¿ã¯è§£åããŠãããŠãã ãããDynamoDBã®ããŒã¿éãå€ãå Žåã¯è€æ°ãã¡ã€ã«ã«åãããŠããããšããããŸãã gunzip ./input/fsjiydg6by6o5cjalruephgpca.json.gz -k è§£ååŸã®ããŒã¿ãéããšãDynamoDBããåºåããããŒã¿ã¯ä»¥äžã®ãããªåœ¢åŒãšãªã£ãŠããŸããDynamoDB JSON 圢åŒãšåŒã°ãããããã§ãã // fsjiydg6by6o5cjalruephgpca.json { " Item ": { " email ": { " S ":" test-user1@example.com " } ," updated_at ": { " N ":" 1696129200 " } ," account ": { " L ": [{ " M ": { " name ": { " S ":" test-user1-1 " }}} , { " M ": { " name ": { " S ":" test-user1-2 " }}}]} ," created_at ": { " N ":" 1696129200 " }} } { " Item ": { " email ": { " S ":" test-user2@example.com " } ," updated_at ": { " N ":" 1696129200 " } ," account ": { " L ": [{ " M ": { " name ": { " S ":" test-user2-1 " }}}]} ," created_at ": { " N ":" 1696129200 " }} } { " Item ": { " email ": { " S ":" test-user3@example.com " } ," updated_at ": { " N ":" 1696129200 " } ," account ": { " L ": [{ " M ": { " name ": { " S ":" test-user3-1 " }}} , { " M ": { " name ": { " S ":" test-user3-2 " }}}]} ," created_at ": { " N ":" 1696129200 " }}} ããŒã¿ããŒã¹( PostgreSQL )ã«ããŒã¿ãã€ã³ããŒãããããã CSV 圢åŒã«å€æããŸãã倿ã®éçšã§ãDynamoDB JSON 圢åŒãæ±ãããã JSON 圢åŒã«å€æãããã @aws-sdk/util-dynamodb ãšããããã±ãŒãžãçšããŸãã 倿ã«çšããèªäœã® ã¹ã¯ãªãã ãäžäŸãšããŠæ²èŒããŸãã äžèšã®ã³ãŒããã¿ãŒããã«äžã§å®è¡ããããŒã¿ã CSV 圢åŒã§åºåããŸãã ./node_modules/.bin/ts-node convert.ts ./input â»ã./inputãã¯DynamoDBããŒãã«æ
å ±ããããã©ã«ã // convert.ts import * as fs from "fs" ; import * as readline from "readline" ; import { unmarshall } from "@aws-sdk/util-dynamodb" ; import { Parser } from "json2csv" ; // åãå®çŸ© type Account = { name: string ; } type UserTableFormat = { email: string ; createdAt: Date ; updatedAt: Date ; } ; type AccountTableFormat = { name: string ; createdAt: Date ; updateAt: Date ; email: string ; } ; // åºåå
ã®csvãã¡ã€ã«ã®ããããŒãšå€ãçŽã¥ãã const userTableFields = [ { label: "email" , value: "email" } , { label: "createdAt" , value: "createdAt" } , { label: "updatedAt" , value: "updatedAt" } , ] ; const accountTableFields = [ { label: "name" , value: "name" } , { label: "createdAt" , value: "createdAt" } , { label: "updatedAt" , value: "updatedAt" } , { label: "email" , value: "email" } , ] ; // åãã©ã«ãã»ãã¡ã€ã«ãå®çŸ© const inputFolder = process .argv [ 2 ] ; const outputFilePath = "./output" ; const outputUserTable = outputFilePath + "/userTable.csv" ; const outputAccountTable = outputFilePath + "/accountTable.csv" ; // åºåå
ã®ãã£ã¬ã¯ããªãäœæ fs.mkdirSync ( outputFilePath , { recursive: true } ); // åºåãã¡ã€ã«ã®åæå fs.writeFileSync ( outputUserTable , "" ); fs.writeFileSync ( outputAccountTable , "" ); // DynamoDBããŒãã«æ
å ±ãã¡ã€ã«ã®äžèЧãååŸ const fileList = fs.readdirSync ( inputFolder ) .filter (( f ) => f.endsWith ( ".json" )); // å€æã»æžã蟌ã¿åŠç for ( const inputFile of fileList ) { const rs = fs.createReadStream ( inputFolder + "/" + inputFile ); const rl = readline.createInterface ( { input: rs } ); rl.on ( "line" , ( line ) => { // 倿åã«äœåãªæååãåé€ const inputLine = line .slice ( 0 , -1 ) .replace ( /{"Item":/ , "" ) .replace ( /\r?\n/g , "" ); // DynamoDB JSONâJSONã«å€æ const inputData = unmarshall ( JSON .parse ( inputLine )); // ããŒãã«ããšã«ããŒã¿ãåå² const userTableRecord: UserTableFormat = { email: inputData.email , createdAt: new Date ( inputData.created_at * 1000 ), updatedAt: new Date ( inputData.updated_at * 1000 ), } ; let accountTableRecords: AccountTableFormat [] | null ; if ( ! inputData.account ) { accountTableRecords = null ; } else { accountTableRecords = inputData.account.map (( a: Account ) => { return { name: a.name , createdAt: new Date ( inputData.created_at * 1000 ), updatedAt: new Date ( inputData.updated_at * 1000 ), email: inputData.email , } } ) } // csv圢åŒã«ããŒã¹ const userTableParser = new Parser ( { fields: userTableFields , header: false , withBOM: true } ); const userTableCsv = userTableParser.parse ( userTableRecord ); let accountTableCsv if ( accountTableRecords && accountTableRecords.length > 0 ) { const githubAccountTableParser = new Parser ( { fields: accountTableFields , header: false , withBOM: true } ); accountTableCsv = githubAccountTableParser.parse ( accountTableRecords ); } // ããŒãã«ããšã«ãã¡ã€ã«åºå fs.writeFileSync ( outputUserTable , userTableCsv.slice ( 1 ) + "\n" , { flag: "a" } ); if ( accountTableCsv ) { fs.writeFileSync ( outputAccountTable , accountTableCsv.slice ( 1 ) + "\n" , { flag: "a" } ); } } ); } 以äžã§ãããŒã¿ã®å€æã¯å®äºããoutputãã©ã«ãã«å€æåŸã®ããŒã¿ãããŒãã«åäœã§ãã¡ã€ã«ãšããŠåºåãããŠããŸãã S3 ãã±ãã ã®ã«ãŒãã«ãuploadããã©ã«ããäœã£ãŠããããã®ãã¡ã€ã«ãã¢ããããŒãããŸããS3ã®ã³ã³ãœãŒã«ç»é¢ããã§ããæ¬¡ã® AWS CLI ããïŒä»¥äžã³ãã³ãïŒã§ãã¢ããããŒãå¯èœã§ãã aws s3 cp ./output/ s3://[ãã±ããå]/upload/ --recursive RDSãžã®ã€ã³ããŒã RDSã«å¯ŸããŠã psql ã³ãã³ãã©ã€ã³ ã䜿çšããŠS3ã«ã¢ããããŒããã csv ãã¡ã€ã«ããŒã¿ãåæ ãããŠãããŸãã 詳现ã¯çããŸãããRDSããã©ã€ããŒããµããããå
ã«äœæããŠããå Žåãèžã¿å°ãµãŒãã䜿çšãããªã©ã§ããŒã¿ããŒã¹ã«ã¢ã¯ã»ã¹ããŠãã ãããä»åã¯èžã¿å°ãµãŒãã§ããŒã ãã©ã¯ ãŒãã£ã³ã°ãè¡ããRDSã®å¯Ÿè±¡ããŒã¿ããŒã¹ãžéä¿¡ã§ããç¶æ
ã§ã以äžã®ã³ãã³ããå®è¡ããŠããŒã¿ããŒã¹ã«æ¥ç¶ããŸãã ãã¹ã¯ãŒãã®å
¥åãæ±ãããããããããŒã¿ããŒã¹ãŠãŒã¶ãŒã«å¯Ÿå¿ãããã¹ã¯ãŒããå
¥åããŠãã ããã psql --host=localhost --port=5432 --username=[RDSã®ãŠãŒã¶ãŒå] --dbname=[RDSã®ããŒã¿ããŒã¹å] --password postgres 次ã«ã psql ãçšããŠS3ã®ãã¡ã€ã«ããŒã¿ãRDSã«ã€ã³ããŒãããããã以äžã®ã³ãã³ããã aws _s3 æ¡åŒµæ©èœ ãã€ã³ã¹ããŒã«ããŸãã CREATE EXTENSION aws_s3 CASCADE; \dx ã³ãã³ããå®è¡ãã aws_s3 æ¡åŒµæ©èœ ãã€ã³ã¹ããŒã«ãããŠããããšã確èªããŸãã postgres=> \dx List of installed extensions Name | Version | Schema | Description -------------+---------+------------+--------------------------------------------- aws_commons | 1.2 | public | Common data types across AWS services aws_s3 | 1.1 | public | AWS S3 extension for importing data from S3 plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language (3 rows) ãããä»åã®æ¬é¡ã§ãã 課é¡ã«ãèšèŒããŸããããuserããŒãã«ã®idã¯ã¬ã³ãŒããäœæããããŸã§çæãããªããããaccountããŒãã«ã®å€éšããŒã§ããuser_id㯠CSV 圢åŒã«å€æããã¿ã€ãã³ã°ã§ã¯ç¢ºå®ãããããšãã§ããŸããã§ããã ä»åã¯ä»¥äžã®æ¹æ³ã§è§£æ±ºããŸããã userããŒãã«ã®ãŠããŒã¯ã«ã©ã (email)ããã£ãŒã«ãã«æã€ account_tmpããŒãã«ãäœæããã S3ã«ã¢ããããŒããã csv ãã¡ã€ã«ããuserããŒãã«ãšaccount_tmpããŒãã«ã«åã蟌ãã userããŒãã«ã«äœæãããã¬ã³ãŒãããaccount_tmpããŒãã«ã«èšå®ãããŠããŒã¯ãã£ãŒã«ããããšã«idãååŸããaccount_tmpããŒãã«ã®user_idãæŽæ°ããã account_tmpããŒãã«ã®ã¬ã³ãŒããaccountããŒãã«ã«INSERTããã account_tmpããŒãã«ãåé€ããã å
·äœçã«ã¯ä»¥äžã® SQL ãã¡ã€ã«ã psql ããå®è¡ããŸãã \i data_migration.sql -- data_migration.sql -- 1. account_tmpããŒãã«ãäœæãã CREATE TABLE " account_tmp " ( " id " SERIAL NOT NULL , " name " TEXT NOT NULL , " user_id " INTEGER , " created_at " TIMESTAMP ( 0 ) NOT NULL DEFAULT CURRENT_TIMESTAMP , " updated_at " TIMESTAMP ( 0 ) NOT NULL DEFAULT CURRENT_TIMESTAMP , " email " TEXT NOT NULL , CONSTRAINT " account_tmp_pkey " PRIMARY KEY ( " id " ) ); -- 2. S3ã«ã¢ããããŒãããcsvãã¡ã€ã«ããuserããŒãã«ãšaccount_tmpããŒãã«ã«åã蟌ã SELECT aws_s3.table_import_from_s3( ' "account_tmp" ' , -- ããŒãã«å ' "name", "user_id", "created_at", "updated_at", "email" ' , -- ã«ã©ã å ' (format csv) ' , -- ãã¡ã€ã«åœ¢åŒ ' bucket-name ' , -- ãã±ããå ' upload/accountTable.csv ' , -- ãã±ããã«ãŒãããã®ãã¡ã€ã«ãã¹ ' ap-northeast-1 ' -- ãªãŒãžã§ã³ ); SELECT aws_s3.table_import_from_s3( ' "user" ' , ' "email", "created_at", "updated_at" ' , ' (format csv) ' , ' bucket-name ' , ' upload/userTable.csv ' , ' ap-northeast-1 ' ); -- 3. ãŠããŒã¯ãã£ãŒã«ããããšã«idãååŸããaccount_tmpããŒãã«ã®user_idãæŽæ°ãã UPDATE " account_tmp " SET " userId " = " user " . " id " FROM " user " WHERE " account_tmp " . " email " = " user " . " email " ; -- 4. account_tmpããŒãã«ã®ã¬ã³ãŒããaccountããŒãã«ã«INSERTãã INSERT INTO " account " ( " name " , " user_id " , " created_at " , " updated_at " ) SELECT " name " , " user_id " , " created_at " , " updated_at " FROM " account_tmp " ; -- 5. account_tmpããŒãã«ãåé€ãã DROP TABLE " account_tmp " ; 以äžã§ç¡äºã«RDSãžããŒã¿ãç§»è¡ã§ããŸããã ã¡ãªã¿ã«éçºç°å¢ã®ããŒã¿ããŒã¹ã«åã蟌ãéã¯ã以äž2ç¹ã倿ŽããŠãã ããã psql ã§ããŒã¿ããŒã¹ã«ãã°ã€ã³ããéãéçºç°å¢ã®ããŒã¿ããŒã¹ãã¹ãåã»ããŒãçªå·ã»ããŒã¿ããŒã¹åã»ãŠãŒã¶ãŒåã«çœ®ãæããŠå®è¡ããã ãdata_migration. sql ãã®ã2. S3ã«ã¢ããããŒããã csv ãã¡ã€ã«ããuserããŒãã«ãšaccount_tmpããŒãã«ã«åã蟌ããéšåãéçºç°å¢ã® csv ãã¡ã€ã«ããããŒã¿ãåã蟌ãããã«ä¿®æ£ããŠã \i data_migration.sql ãå®è¡ããã -- data_migration.sql -- (çç¥) -- 2. csvãã¡ã€ã«ãããŒãã«ã«åã蟌ã \COPY " account_tmp " ( " name " , " user_id " , " created_at " , " updated_at " , " email " ) FROM ' ./output/accountTable.csv ' DELIMITER ' , ' CSV \COPY " user " ( " email " , " created_at " , " updated_at " ) FROM ' ./output/userTable.csv ' DELIMITER ' , ' CSV -- 3. ãŠããŒã¯ãã£ãŒã«ããããšã«idãååŸããaccount_tmpããŒãã«ã®user_idãæŽæ°ãã -- (çç¥) ãŸãšã ä»åã¯ãDynamoDBããRDSãžããŒã¿ãç§»è¡ããéã«èŠæŠããããšãèšäºã«æ®ããŸãããuser_idåé¡ä»¥å€ã«ããDynamoDB JSON ã®å€ææ¹æ³ãªã©è²ã
ãšå匷ã«ãªããŸããã åãå Žé¢ã«åºãããæ©äŒã¯å°ãªããšæããŸãããã©ãªããã®åèã«ãªãã°å¹žãã§ãã æåŸãŸã§ãèªã¿ããã ããŸããŠããããšãããããŸããã ç§ãã¡ã¯äžç·ã«åããŠããã仲éãåéããŠããŸãïŒ ãã«ãµã€ã¯ã«ãšã³ãžã㢠å·çïŒ @suzuki.takuma ãã¬ãã¥ãŒïŒ @yamashita.tsuyoshi ïŒ Shodo ã§å·çãããŸãã ïŒ