ServerlessFramework에서 DynamoDB의 정렬 키를 변경해 보십시오

결론


Cloudformation을 실행하는 중 오류가 발생했습니다.
정렬 키를 변경하려면 잘 복원하십시오.
그래서 우리는 다음 순서에 따라 검증을 하고 복구를 시도했다.

해본 일


DynamoDB 테이블 만들기


serverless.yml의 Resources에서 다음 DynamoDB의 정보를 정의하고 디자인합니다.
serverless.yml
service: dynamo-test
frameworkVersion: '2'
provider:
  name: aws
  runtime: nodejs12.x
  lambdaHashingVersion: 20201221
  region: ap-northeast-1
resources:
 Resources:
   Test:
     Type: AWS::DynamoDB::Table
     Properties:
       TableName: Test
       AttributeDefinitions:
         - AttributeName: pKey
           AttributeType: S
         - AttributeName: sKey
           AttributeType: S
       StreamSpecification:
         StreamViewType: NEW_AND_OLD_IMAGES
       KeySchema:
        - AttributeName: pKey
          KeyType: HASH
        - AttributeName: sKey
          KeyType: RANGE
       BillingMode: PAY_PER_REQUEST
npx serverless deploy
test의 표를 만든다.

sortKey의 변경 사항


sKey 이름을 sKey 2로 변경하고 실행
resources:
 Resources:
   Test:
     Type: AWS::DynamoDB::Table
     Properties:
       TableName: Test
       AttributeDefinitions:
         - AttributeName: pKey
           AttributeType: S
-         - AttributeName: sKey
+         - AttributeName: sKey2
           AttributeType: S
       StreamSpecification:
         StreamViewType: NEW_AND_OLD_IMAGES
       KeySchema:
        - AttributeName: pKey
          KeyType: HASH
-        - AttributeName: sKey
+        - AttributeName: sKey2
          KeyType: RANGE
       BillingMode: PAY_PER_REQUEST
npx serverless deploy
에 다음 오류가 표시됩니다.
An error occurred: Test - CloudFormation cannot update a stack when a custom-named resource requires replacing. Rename Test and update the stack again..
링크에서 설명한 바와 같이 DynamoDB의 경우 TableName은 같은 물체를 변경할 수 없습니다.
https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-custom-name/
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html
참고로 테이블Name을 수정하여 depro를 진행하면 원래의 표 (Test) 가 삭제됩니다.
resources:
 Resources:
   Test:
     Type: AWS::DynamoDB::Table
     Properties:
-       TableName: Test
+       TableName: Test2
       AttributeDefinitions:
         - AttributeName: pKey
           AttributeType: S
         - AttributeName: sKey
           AttributeType: S
       StreamSpecification:
         StreamViewType: NEW_AND_OLD_IMAGES
       KeySchema:
        - AttributeName: pKey
          KeyType: HASH
        - AttributeName: sKey
          KeyType: RANGE
       BillingMode: PAY_PER_REQUEST
저는 다른 다양한 모델을 시도했지만 단순한 전환은 어렵다는 것을 깨달았습니다.
  • 콘솔에서 테이블을 제거하고 Cloudformation 실행
  • Cloudformation은 리소스를 만들기 전에 이전 처리와 차이가 발생하며 이와 같은 오류가 발생합니다.
  • 자원 이름 변경 시도(Test→Test2)
  • 다른 자원(Test)에서 Test를 사용한 테이블 오류
  • 되살리다


    DynamoDB의 주요 부분을 변경하려면 다음 절차가 필요합니다.
  • 새 테이블 정의로 테이블 만들기 (Test2)
  • 데이터 전송(Test→Test2)
  • listItem에서 데이터를 읽는 동시에 변환 처리를 한 후 BatchWriteItem으로 값을 쓰는 처리.
  • 먼저 CloudFormation을 사용하여 원본 테이블을 삭제합니다(Test)
  • CloudFormation을 사용하여 새 정의로 테이블 만들기(Test)
  • 데이터 전송(Test2→Test)
  • Test2 테이블 삭제
  • 백업


    표 정의의 제작 - 데이터가 전송되기 전에 이런 처리로 완성할 수 있다고 생각합니다.
    migrate.ts
    import DynamoDB, { KeySchema } from 'aws-sdk/clients/dynamodb';
    
    const AWS_INFO = {
      region: 'ap-northeast-1',
      accessKeyId: 'xxx',
      secretAccessKey: 'xxx',
    };
    
    const backupInfo = {
      tableName: 'Test',  // backup対象のテーブル名
      oldKeyName: 'sKey',  // 変換対象のkey
      keyName: 'sKey2',  // 変換後のkey
      KeyType: 'RANGE',  // keyのtype
      AttributeType: 'S',  // keyのAttribute
      converter: {
        sKey2: item => `${item.sKey}2`,
      },  // 変換処理。keyの名前でValue側の処理を実行して格納する
    };
    
    const client = new DynamoDB.DocumentClient(AWS_INFO);
    
    const dynamoDb = new DynamoDB({
      apiVersion: '2012-08-10',
      region: AWS_INFO.region,
      credentials: {
        accessKeyId: AWS_INFO.accessKeyId,
        secretAccessKey: AWS_INFO.secretAccessKey,
      },
    });
    
    /**
     * backup前のテーブルを再帰的に全件取得する
     * 件数が多い場合はOOMになるので注意
     * @param tableName
     * @param pre
     * @param lastEvaluatedKey
     * @returns
     */
    const listItems = async <T>(tableName: string, pre: T[], lastEvaluatedKey?: DynamoDB.DocumentClient.Key) => {
      console.log('list item');
    
      const items = await client
        .scan({
          TableName: tableName,
          ExclusiveStartKey: lastEvaluatedKey,
        })
        .promise();
    
      const result = [...pre, ...items.Items];
      if (items.LastEvaluatedKey) {
        return await listItems(tableName, result, items.LastEvaluatedKey);
      }
    
      return result;
    };
    
    /**
     * tableにデータを25件ずつ入れる
     * batchWriteの仕様で25件ずつしかいれられない
     * @param tableName
     * @param items
     */
    const insertItems = async <T>(tableName: string, items: T[]) => {
      const batch25 = async (items: T[]) => {
        if (items.length === 0) {
          return;
        }
        await client
          .batchWrite({
            RequestItems: {
              [tableName]: items.slice(0, 25).map(item => ({ PutRequest: { Item: item } })),
            },
          })
          .promise();
        return await batch25(items.slice(25));
      };
    
      await batch25(items);
    };
    
    /**
     * backup時にconvertする
     * @param item
     * @returns
     */
    const converter = (item: any) => ({
      ...item,
      ...Object.entries(backupInfo.converter).reduce(
        (pre, [key, value]) => ({
          ...pre,
          [key]: value(item),
        }),
        {},
      ),
    });
    
    /**
     * backup用のテーブルを作成する。名前は_bakで固定
     * @param tableInfo
     */
    const createTable = async (tableInfo: DynamoDB.DescribeTableOutput) => {
      await dynamoDb
        .createTable({
          TableName: `${tableInfo.Table.TableName}_bak`,
          KeySchema: createKeySchema(tableInfo.Table.KeySchema),
          AttributeDefinitions: createAttributeDefinitions(tableInfo.Table.AttributeDefinitions),
          LocalSecondaryIndexes: tableInfo.Table.LocalSecondaryIndexes?.map(idx => ({
            IndexName: idx.IndexName,
            KeySchema: idx.KeySchema,
            Projection: idx.Projection,
          })),
          GlobalSecondaryIndexes: tableInfo.Table.GlobalSecondaryIndexes.map(idx => ({
            IndexName: idx.IndexName,
            KeySchema: idx.KeySchema,
            Projection: idx.Projection,
            // billingModeがPAY_PER_REQUESTの場合、0になるので入れるとエラーになる
            // ProvisionedThroughput: {
            //   ReadCapacityUnits: idx.ProvisionedThroughput.ReadCapacityUnits,
            //   WriteCapacityUnits: idx.ProvisionedThroughput.WriteCapacityUnits,
            // },
          })),
          BillingMode: tableInfo.Table.BillingModeSummary.BillingMode,
          // billingModeがPAY_PER_REQUESTの場合、0になるので入れるとエラーになる
          // ProvisionedThroughput: {
          //   ReadCapacityUnits: tableInfo.Table.ProvisionedThroughput.ReadCapacityUnits,
          //   WriteCapacityUnits: tableInfo.Table.ProvisionedThroughput.WriteCapacityUnits,
          // },
          StreamSpecification: tableInfo.Table.StreamSpecification,
          SSESpecification: tableInfo.Table.SSEDescription,
        })
        .promise();
    };
    
    const createKeySchema = (keySchema: KeySchema): KeySchema => {
      return [
        ...keySchema.filter(key => key.AttributeName !== backupInfo.oldKeyName),
        {
          AttributeName: backupInfo.keyName,
          KeyType: backupInfo.KeyType,
        },
      ];
    };
    
    const createAttributeDefinitions = (attributeDefinitions: DynamoDB.AttributeDefinitions) => {
      return [
        ...attributeDefinitions.filter(def => def.AttributeName !== backupInfo.oldKeyName),
        {
          AttributeName: backupInfo.keyName,
          AttributeType: backupInfo.AttributeType,
        },
      ];
    };
    
    const sleep = async (ms: number) => {
      return new Promise(resolve =>
        setTimeout(() => {
          resolve(null);
        }, ms),
      );
    };
    
    const migrate = async () => {
      const tableInfo = await dynamoDb
        .describeTable({
          TableName: backupInfo.tableName,
        })
        .promise();
    
      await createTable(tableInfo);
    
      // tableの作成が終わるまで待つ
      while (true) {
        console.log('wait ...');
    
        await sleep(5000);
        const tableInfo = await dynamoDb
          .describeTable({
            TableName: `${backupInfo.tableName}_bak`,
          })
          .promise();
        if (tableInfo.Table.TableStatus === 'ACTIVE') {
          break;
        }
      }
    
      const result = await listItems(backupInfo.tableName, []);
      await insertItems(
        `${backupInfo.tableName}_bak`,
        result.map(ret => converter(ret)),
      );
    };
    
    migrate();
    
    npx ts-node migrate.ts
    
    이후cloudformation에서Test의 테이블을 삭제한 상태에서 실행한 다음replace 정보로 실행합니다.
    resources:
     Resources:
    -   Test:
    -     Type: AWS::DynamoDB::Table
    -     Properties:
    -       TableName: Test
    -       AttributeDefinitions:
    -         - AttributeName: pKey
    -           AttributeType: S
    -         - AttributeName: sKey
    -           AttributeType: S
    -       StreamSpecification:
    -         StreamViewType: NEW_AND_OLD_IMAGES
    -       KeySchema:
    -        - AttributeName: pKey
    -          KeyType: HASH
    -        - AttributeName: sKey
    -          KeyType: RANGE
    -       BillingMode: PAY_PER_REQUEST
    
    npx serverless deploy
    
    resources:
     Resources:
       Test:
         Type: AWS::DynamoDB::Table
         Properties:
           TableName: Test
           AttributeDefinitions:
             - AttributeName: pKey
               AttributeType: S
    -         - AttributeName: sKey
    +         - AttributeName: sKey2
               AttributeType: S
           StreamSpecification:
             StreamViewType: NEW_AND_OLD_IMAGES
           KeySchema:
            - AttributeName: pKey
              KeyType: HASH
            - AttributeName: sKey
              KeyType: RANGE
           BillingMode: PAY_PER_REQUEST
    
    는 제작된 Test의 표에 대해 상기 코드의 일부분을 사용하여Test2의 데이터를 집행하면 된다.
    migrate.ts(↑의 일부 변경판)
    npx serverless deploy
    
    import DynamoDB, { KeySchema } from 'aws-sdk/clients/dynamodb';
    
    const AWS_INFO = {
      region: 'ap-northeast-1',
      accessKeyId: 'xxx',
      secretAccessKey: 'xxx',
    };
    
    const backupInfo = {
      tableName: 'Test',  // backup対象のテーブル名
    };
    
    const client = new DynamoDB.DocumentClient(AWS_INFO);
    
    /**
     * backup前のテーブルを再帰的に全件取得する
     * 件数が多い場合はOOMになるので注意
     * @param tableName
     * @param pre
     * @param lastEvaluatedKey
     * @returns
     */
    const listItems = async <T>(tableName: string, pre: T[], lastEvaluatedKey?: DynamoDB.DocumentClient.Key) => {
      console.log('list item');
    
      const items = await client
        .scan({
          TableName: tableName,
          ExclusiveStartKey: lastEvaluatedKey,
        })
        .promise();
    
      const result = [...pre, ...items.Items];
      if (items.LastEvaluatedKey) {
        return await listItems(tableName, result, items.LastEvaluatedKey);
      }
    
      return result;
    };
    
    /**
     * tableにデータを25件ずつ入れる
     * batchWriteの仕様で25件ずつしかいれられない
     * @param tableName
     * @param items
     */
    const insertItems = async <T>(tableName: string, items: T[]) => {
      const batch25 = async (items: T[]) => {
        if (items.length === 0) {
          return;
        }
        await client
          .batchWrite({
            RequestItems: {
              [tableName]: items.slice(0, 25).map(item => ({ PutRequest: { Item: item } })),
            },
          })
          .promise();
        return await batch25(items.slice(25));
      };
    
      await batch25(items);
    };
    
    const migrate = async () => {
      const result = await listItems(backupInfo.tableName, []);
      await insertItems(
        `${backupInfo.tableName}_bak`,
        result.map(ret => converter(ret)),
      );
    };
    
    migrate()
    

    종결어.


    쉬울 것 같아. 귀찮아.
    partitionKey와sortKey의 정보는 변하지 않는 값입니다. 변할 수 있는 조회는 GSI를 사용하는 것이 좋습니다.

    좋은 웹페이지 즐겨찾기