PITRで復元したDynamoDBテーブルをAWS SAM管理に加える(CFnでインポート）

## はじめに
Brilliantcryptoのファンゲーム的に作ったDiscordアプリ
[Dayily Mine インストールリンク](https://discord.com/oauth2/authorize?client_id=1229496306360123392)

![play.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781648/97b2d468-f967-e581-850c-c3bf07088e83.gif)

この開発で使った技術方法を
[DiscordアプリをAWS SAMで作った話(概要・システム構成編)](https://qiita.com/robo20240430/items/5af426c2fe9e426ee9ba)

で紹介していますが、今回は上記システムのバックアップに関連した記事です。
初めはバックアップだけで１つの記事にしようとしたのですが、長くなりそうなので、記事タイトルの部分だけ独立した記事にしました。

本題ですが、DynamoDBにてバックアップから復元を行った場合、新規テーブルが作成されます。
そのテーブルはAWS SAMのスタック管理外にあるため、復元したテーブルをスタックにインポートする必要があります。



### 既存SAMテンプレートをCloudFormationからダウンロード
既存のスタックへのインポートはSAMにその機能がないため、CloudFormationから行います。
(SAM経由ではなく直接CloudFormationでスタックをいじる形になります）
ただしし、SAMで普段利用しているテンプレートファイルは、SAM用に拡張されたもので、CloudFormationでは扱えません。

そこでSAMがCloudFormationで作成したテンプレートを使います。
```zsh:
aws cloudformation get-template --stack-name [スタック名] | jq .TemplateBody > template.json 
```
上記のように不要な構造を取り除いてダウンロードします。

### template.jsonの編集

続けて既存のテーブルに加えて、復元したDBを追記します。
基本全く同じ定義です。
しかし、復元テーブルはPITRバックアップ設定が停止になっている点と
```"DeletionPolicy": "Retain",```
を追記する必要がある点に注意してください。

```template.json
{
  "AWSTemplateFormatVersion": "2010-09-09",

  [略]
  
  "Resources": {
    "DynamoDBTable": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "TableName": "DynamoTable",
        "BillingMode": "PAY_PER_REQUEST",
        "PointInTimeRecoverySpecification": {
          "PointInTimeRecoveryEnabled": true
        },
        "AttributeDefinitions": [
          {
            "AttributeName": "Id",
            "AttributeType": "S"
          }
        ],
        "KeySchema": [
          {
            "AttributeName": "Id",
            "KeyType": "HASH"
          }
        ]
      },
    "DynamoDBTable2": {
      "Type": "AWS::DynamoDB::Table",
      "DeletionPolicy": "Retain",
      "Properties": {
        "TableName": "DynamoTable2",
        "BillingMode": "PAY_PER_REQUEST",
        "AttributeDefinitions": [
          {
            "AttributeName": "Id",
            "AttributeType": "S"
          }
        ],
        "KeySchema": [
          {
            "AttributeName": "Id",
            "KeyType": "HASH"
          }
        ]
      },
      "Metadata": {
        "SamResourceId": "DynamoDBTable2"
      }
    },

    [略]
  }
}
```
※ テーブル構造は簡略化しています

### 変更セットの作成

編集したファイルに問題ないか確認します。
そして、変更セットを作成するのに必要となる情報を取得します。

```
aws cloudformation get-template-summary --stack-name [スタック名]
```
```LogicalResourceId```, ```ResourceIdentifier```が何かこんがらがるのですが、上記コマンドの出力を参考にするとわかりやすいです。

以下を実行します。```change-set-name```はお好きなものを。

```
aws cloudformation create-change-set \
    --stack-name [スタック名] --change-set-name ImportChangeSet20240608 \
    --change-set-type IMPORT \
    --resources-to-import "[{\"ResourceType\":\"AWS::DynamoDB::Table\",\"LogicalResourceId\":\"DynamoDBTable2\",\"ResourceIdentifier\":{\"TableName\":\"DynamoTable2\"}}]"\
    --template-body file://template.json   --capabilities CAPABILITY_IAM
```
公式資料はs3に template.jsonを配置するサンプルが多いですが、今回はローカルのファイルをそのまま指定します。



続けて、作成した変更セットの内容を確認します。
```
aws cloudformation describe-change-set --change-set-name ImportChangeSet20240608 --stack-name [スタック名]
```

問題なければ実行します。

```
aws cloudformation execute-change-set --change-set-name ImportChangeSet20240608 --stack-name [スタック名]
```

### 実行結果が正しいか確認（ドリフト検出）

ドリフトとは、ざっくり言えば実際のリソースとCFn定義（テンプレート)の差分です。

※ 詳しくはこちらをご覧ください
[AWS CloudFormationユーザーガイド - ドリフトとは](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html#what-is-drift)

検出開始
```
aws cloudformation detect-stack-drift --stack-name [スタック名]
```

状態確認
```
aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id [１つ前のコマンドの出力結果から指定]
```

結果確認
```
aws cloudformation describe-stack-resource-drifts --stack-name [スタック名]
```

３つのコマンドがある意味、私は最初混乱したんですが
要は、開始してもすぐ完了しないので、状態確認があり、完了後結果確認するといった流れです。
今回のサンプルはシンプルなので、すぐに終わりますが。

### バックアップ元のテーブルをスタックから削除する
実際の運用では復元前の時点のバックアップが必要になる可能性があるので、すぐに削除することはないかと思います。（別途オンデマンドバックアップがあるかなども関係しますが）

今回はバックアップ元のテーブルをすぐに削除します。（削除後の動作のチェックのため）

今度はSAMのtemplate.yamlを編集します。
旧テーブルに
```DeletionPolicy: Retain```
```UpdateReplacePolicy: Retain```
を加えます。
前者は削除時のDB実体の扱い（Retainなら管理から外すがテーブル自体は削除しない）
後者はスタックによる更新処理で例えるなら　Delete/Insert的な更新が必要な場合の削除対象の扱いです。
※ 他の方の記事を読むと ```UpdateReplacePolicy: Retain``` をやってない人も多いのですが、私の場合はこれがないとエラーになったので付けています。あって困るものではないので深追いしてませんが、今回の構成だと不要な気もして少しモヤモヤしてますが。


そして、新テーブルの定義を加えるのを忘れないようにしましょう。
（そもそも、そのための作業だったわけなので）
今回は変更を加えないのでimport時の内容と同じです。

なお、新DBには
```DeletionPolicy: Retain```
```UpdateReplacePolicy: Retain```
は不要です。

```
  DynamoDBTable:
    Type: "AWS::DynamoDB::Table"
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      TableName: DynamoTable
      BillingMode: PAY_PER_REQUEST
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      AttributeDefinitions:
        - AttributeName: Id
          AttributeType: S
      KeySchema:
        - AttributeName: Id
          KeyType: HASH
  DynamoDBTable2:
    Type: "AWS::DynamoDB::Table"
    Properties:
      TableName: DynamoTable2
      BillingMode: PAY_PER_REQUEST
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: false
      AttributeDefinitions:
        - AttributeName: Id
          AttributeType: S
      KeySchema:
        - AttributeName: Id
          KeyType: HASH
```

```
sam deploy
```

実行すると
新DBはDeletionPolicyが削除され
旧DBはDeletionPolicyとUpdateReplacePolicyが追加されます。


次に旧DBに関する情報を丸ごと削除して
再び実行します。

```
sam deploy
```

これで旧DBがスタックから消えます。
（ただし管理外になるだけでテーブル自体は存続します）


### 旧DBのリソースが残っているか確認
```
 aws dynamodb list-tables 
```

CLIで直接一覧で確認してみます。
残っていることが確認できました。

少ししつこいですが念の為ドリフト検出もしてみます。
コマンドは省略しますが問題ないことを確認してみましょう。

## 後始末
今回のテスト用スタックを削除し、スタックから外したがリソース自体は残っている旧テーブルを削除します。
```
sam delete
aws dynamodb delete-table --table-name [旧テーブル名]
```



## 最後に
万が一の備えは必要なものの、個人開発のサービスで、実際にバックアップが役立つ機会はあまりないかもしれません。

本業でも物理サーバーを使う機会が全くなくなったので、数える程度しか記憶にありません。

わかりやすくなるように、簡易なアプリケーションで検証した作業のため、実際のサービスだと勝手が違うこともあるかもしれません。

そのため、実際の経験上お気づきの点があれば、ご指摘・ご助言いただけると大変ありがたいです。

## 参考資料

[AWS CloudFormation ユーザーガイド - 既存リソースを CloudFormation 管理に取り込む](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import.html)

[AWS CloudFormation よくあるユースケースと質問編【AWS Black Belt】](https://www.youtube.com/watch?v=FHFfWMddd-0)