AWS RDSをS3に自動バックアップする方法

AWS RDSのスナップショットをS3にエクスポートする自動化の設定を行ったので、備忘録として作業の流れなどを記載します。
作業中に直面した問題とその解決策を記載しています。

## **1. 目的**

AWS RDSのスナップショットをS3へ自動エクスポートし、以下を実現します：

- **アプリデータの保全**：RDSスナップショットをS3に保存することで、データを長期的に保管可能。
- **コスト削減**：S3のストレージ料金はRDSスナップショットのストレージに保管するより安価で、長期保存でのコスト効率が向上。
- **運用効率化**：古いスナップショットの自動削除で不要なストレージ消費を防止。
- **完全自動化**：LambdaとEventBridgeで、バックアップ作成からエクスポート、削除までを自動化。

## **2. 使用する技術とその目的**

この設定では、以下のAWSサービスを使用します：

- **Amazon RDS**  
  - **理由**：アプリデータを保存するデータベース。スナップショットを作成し、S3にエクスポートする対象。  

- **Amazon S3**  
  - **理由**：エクスポートしたスナップショットを低コストで長期間保管するストレージ。  

- **AWS Lambda**  
  - **理由**：スナップショットの作成、S3へのエクスポート、古いスナップショットの削除などを自動で実行する関数。  

- **Amazon EventBridge**  
  - **理由**：スケジュールベースでLambda関数を定期実行し、バックアップを自動化。  

- **AWS KMS**  
  - **理由**：S3にエクスポートするデータを暗号化し、セキュリティを確保。  

- **IAM**  
  - **理由**：各サービス間のアクセス権限を管理し、必要なポリシーを付与。

## **3. 作業手順**

### コード内の記述補足
`<region>`: 使用しているAWSリージョンを指定（例: ap-northeast-1）。
`<your_account_id>`: AWSアカウントIDを指定。
`<your_key_id>`: 作成したKMSキーのIDを指定。
`<your_role_name>`: Lambdaで使用するIAMロール名を指定（例: lambda-rds-s3-backup-role）。

### **Step 1: S3バケットの作成**

1. **AWS Management Console**で**S3サービス**を開き、新しいバケットを作成する。
   - **バケット名**は一意の名前を指定（例: `my-rds-backup-bucket`）。
   - **リージョン**はRDSと同じリージョンを選択。
   - **バケットタイプ**は汎用を選択。

2. **パブリックアクセスをすべてブロック**を有効化する（セキュリティ対策のため推奨）。


**補足**：
- **新しいバケットを作成する理由**  
  スナップショット専用バケットを使用することで、他のデータと分離し、管理しやすくなる。  
- **フォルダ構造を設定する目的**  
  データの管理を簡単にするために、`exports/`のようなプレフィックスを指定することで整理がしやすくなる。  
- **KMSキーの設定について**  
  後で作成するKMSキーを利用してバックアップデータを暗号化するための準備。

---

### **Step 2: IAMロールの作成**

#### **ロール設定**

1. **IAMコンソール**で新しいロールを作成。  
   - ロール名：`lambda-rds-s3-backup-role`  
   - 信頼されたエンティティタイプ：**AWSサービス**  
   - サービスには以下を指定：  
     - **Lambda**  
     - **RDS Export Task**（`export.rds.amazonaws.com`）  
     - **EventBridge Scheduler**（`scheduler.amazonaws.com`）  

#### **信頼関係の設定**
作成したロールに以下の信頼関係ポリシーを設定。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "export.rds.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "scheduler.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

#### **インラインポリシーの設定**
以下のインラインポリシーをロールに追加。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt",
                "kms:Encrypt",
                "kms:ReEncrypt*",
                "kms:GenerateDataKey*",
                "kms:DescribeKey"
            ],
            "Resource": "arn:aws:kms:<region>:<your_account_id>:key/<your_key_id>"
        },
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "arn:aws:iam::<your_account_id>:role/<your_role_name>"
        }
    ]
}
```
### **補足**

- **なぜ`iam:PassRole`が必要か**  
  RDSのS3エクスポートタスクで、IAMロールを引き継ぐために必要。

- **`kms:Decrypt`などKMS操作が必要な理由**  
  S3エクスポートで暗号化されたスナップショットを処理するため。

- **`logs:*`権限の目的**  
  LambdaがCloudWatch Logsにログを出力するために必要。

- **信頼関係の重要性**  
  LambdaやRDS Export Task、EventBridge Schedulerがロールを引き受けられるよう設定する。

---

### **Step 3: KMSキーの作成**

1. **KMSコンソール**を開いて「キーの作成」を選択。  
   - **キータイプ**は「対称暗号キー」を選択。  
   - **使用法**は「暗号化と復号化」を指定。  

2. **エイリアスを設定**。例：`rds-s3-export-key`。  

3. **キーポリシーを設定**。以下のポリシーを追加し、Lambdaロールに必要な権限を付与。  
   - `lambda-rds-s3-backup-role`をキーの使用者として指定。  
   - RDS Exportタスクのエクスポート処理でキーを利用するために適切な権限を設定。  

```json
{
    "Id": "key-consolepolicy-3",
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Enable IAM User Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<your_account_id>:root"
            },
            "Action": "kms:*",
            "Resource": "*"
        },
        {
            "Sid": "Allow access for Key Administrators",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<your_account_id>:user/<your_user_name>"
            },
            "Action": [
                "kms:Create*",
                "kms:Describe*",
                "kms:Enable*",
                "kms:List*",
                "kms:Put*",
                "kms:Update*",
                "kms:Revoke*",
                "kms:Disable*",
                "kms:Get*",
                "kms:Delete*",
                "kms:TagResource",
                "kms:UntagResource",
                "kms:ScheduleKeyDeletion",
                "kms:CancelKeyDeletion",
                "kms:RotateKeyOnDemand"
            ],
            "Resource": "*"
        },
        {
            "Sid": "Allow use of the key",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<your_account_id>:role/<your_role_name>"
            },
            "Action": [
                "kms:Encrypt",
                "kms:Decrypt",
                "kms:ReEncrypt*",
                "kms:GenerateDataKey*",
                "kms:DescribeKey"
            ],
            "Resource": "*"
        },
        {
            "Sid": "Allow attachment of persistent resources",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<your_account_id>:role/<your_role_name>"
            },
            "Action": [
                "kms:CreateGrant",
                "kms:ListGrants",
                "kms:RevokeGrant"
            ],
            "Resource": "*",
            "Condition": {
                "Bool": {
                    "kms:GrantIsForAWSResource": "true"
                }
            }
        }
    ]
}

```

4. **AWSサービスがキーを利用できるように信頼関係を設定**。  
   - RDS Exportタスク、EventBridge Schedulerの利用を許可。  

### **補足**

- **なぜKMSキーが必要か**  
  RDSのスナップショットを暗号化するため。AWSのセキュリティベストプラクティスに準拠する。

- **どのキーを選ぶべきか**  
  S3エクスポート専用にカスタマーマネージドキーを作成する。AWS管理キーは利用不可。

- **許可のポイント**  
  Lambdaロールに「Decrypt」や「GenerateDataKey」などの操作権限が必要。RDS Exportタスクがキーを利用する権限も設定する。

--- 
### **Step 4: Lambda関数の作成**

#### Lambda関数の設定

1. **AWS Lambdaコンソール**で新しい関数を作成。  
   - **ランタイム**：Python 3.x（例: Python 3.9）  
   - **実行ロール**：`lambda-rds-s3-backup-role`  
   - 必要に応じて**タイムアウト**を延長（推奨: 10分以上）。  

2. **コードの記述**

以下のようなコードで環境に応じて変数を設定する。`<replace_with_...>`の部分を適切な値に置き換える。

```python
import boto3
from datetime import datetime as dt

def lambda_handler(event, context):
    rds_client = boto3.client('rds')
    s3_bucket_name = "<replace_with_s3_bucket>"
    s3_prefix = "<replace_with_s3_prefix>"
    kms_key_arn = "<replace_with_kms_key_arn>"
    iam_role_arn = "<replace_with_lambda_role_arn>"
    db_cluster_identifier = "<replace_with_db_cluster_identifier>"
    
    snapshot_identifier = f"{db_cluster_identifier}-snapshot-{dt.now().strftime('%Y%m%d%H%M%S')}"
    now = dt.now()
    
    try:
        # RDSクラスターの状態をチェック
        print("Checking RDS cluster state...")
        cluster_info = rds_client.describe_db_clusters(DBClusterIdentifier=db_cluster_identifier)
        cluster_status = cluster_info['DBClusters'][0]['Status']
        
        if cluster_status != 'available':
            print(f"Cluster is not in available state: {cluster_status}")
            return {
                'statusCode': 400,
                'body': f"Cluster is not in available state: {cluster_status}"
            }

        # 古い手動スナップショットを削除
        print("Checking for old manual snapshots to delete...")
        snapshots = rds_client.describe_db_cluster_snapshots(DBClusterIdentifier=db_cluster_identifier)
        for snapshot in snapshots['DBClusterSnapshots']:
            if snapshot['SnapshotType'] == 'manual':
                creation_time = snapshot['SnapshotCreateTime']
                if (now - creation_time.replace(tzinfo=None)).days > 1:
                    snapshot_id = snapshot['DBClusterSnapshotIdentifier']
                    print(f"Deleting old manual snapshot: {snapshot_id}")
                    rds_client.delete_db_cluster_snapshot(
                        DBClusterSnapshotIdentifier=snapshot_id
                    )

        # 新しいスナップショットを作成
        print(f"Creating new snapshot: {snapshot_identifier}")
        rds_client.create_db_cluster_snapshot(
            DBClusterSnapshotIdentifier=snapshot_identifier,
            DBClusterIdentifier=db_cluster_identifier
        )

        # スナップショットが利用可能になるまで待機
        print("Waiting for snapshot to become available...")
        waiter = rds_client.get_waiter('db_cluster_snapshot_available')
        waiter.wait(
            DBClusterSnapshotIdentifier=snapshot_identifier,
            DBClusterIdentifier=db_cluster_identifier
        )
        print(f"Snapshot {snapshot_identifier} is now available.")

        # スナップショットをS3にエクスポート
        export_identifier = f"{snapshot_identifier}-export"
        print(f"Exporting snapshot {snapshot_identifier} to S3 bucket {s3_bucket_name}...")
        rds_client.start_export_task(
            ExportTaskIdentifier=export_identifier,
            SourceArn=f"arn:aws:rds:<replace_with_region>:<replace_with_account_id>:cluster-snapshot:{snapshot_identifier}",
            S3BucketName=s3_bucket_name,
            IamRoleArn=iam_role_arn,
            KmsKeyId=kms_key_arn,
            S3Prefix=s3_prefix
        )

        print("Export task initiated successfully.")
        return {
            'statusCode': 200,
            'body': f"Snapshot {snapshot_identifier} successfully exported to S3."
        }
    
    except Exception as e:
        print(f"An error occurred: {str(e)}")
        return {
            'statusCode': 500,
            'body': f"Error: {str(e)}"
        }
```

#### 環境変数の説明

- **`<replace_with_s3_bucket>`**: バックアップ先のS3バケット名。  
- **`<replace_with_s3_prefix>`**: 保存時のプレフィックス（例: `exports/`）。  
- **`<replace_with_kms_key_arn>`**: 作成したKMSキーのARN。  
- **`<replace_with_lambda_role_arn>`**: 使用するIAMロールのARN。  
- **`<replace_with_db_cluster_identifier>`**: RDSクラスターの識別子。  
- **`<replace_with_region>`**: AWSリージョン（例: `ap-northeast-1`）。  
- **`<replace_with_account_id>`**: AWSアカウントID。

#### VPCとセキュリティグループの設定

1. **VPCの設定**: LambdaがVPC内のRDSにアクセスする必要がある場合、VPCサブネットを指定する。  
   - **プライベートサブネット**を選択することが推奨される。  

2. **セキュリティグループのルール**: 必要に応じてインバウンド/アウトバウンドルールを設定する。
   - **インバウンドルール**: RDSのポート（例: 3306）を許可する。  
   - **アウトバウンドルール**: S3アクセス用にHTTP（80）とHTTPS（443）を許可する。  

補足を加えることで、設定ミスや迷いやすいポイントを事前に解消できる。以下の補足を追加すると記事の完成度がさらに向上する。

### **補足**

#### **Lambda関数のタイムアウト設定**  
スナップショットの作成やエクスポートは時間がかかる場合があるため、Lambdaのタイムアウトを**10分以上**に設定することを推奨。デフォルトの3秒では処理が途中で終了してしまう可能性が高い。

#### **RDSクラスターの状態チェックの重要性**  
RDSが`available`以外の状態（例: `backing-up`や`stopped`）のときにはスナップショット作成が失敗する。このため、コード内で状態を確認し、利用可能でない場合は処理をスキップする仕組みが含まれている。

#### **VPCとセキュリティグループの注意点**  
Lambda関数がVPC内で動作する場合は、以下に注意する：
- **サブネット選択**: プライベートサブネットを選ぶことでセキュリティを確保する。  
- **セキュリティグループの設定ミス**: RDSへのインバウンドポート（例: 3306）が正しく設定されていないと通信エラーが発生する。アウトバウンドでS3やKMSへのアクセスが必要な点も見落としやすい。

#### **KMSキーの設定について**  
S3にエクスポートする際、**暗号化キーのARN（KMS Key ARN）**が必須。KMSキーのポリシーでLambdaロールを許可していないとエラーが発生する。具体的には`kms:GenerateDataKey`や`kms:Decrypt`の許可が必要。

#### **IAMロールの`iam:PassRole`許可**  
エラー「User is not authorized to perform: iam:PassRole」が出た場合、LambdaのロールがIAMロールの委任を許可されていない可能性がある。インラインポリシーで適切に設定する必要がある。

---
### **Step 5: EventBridgeスケジュールの設定**

#### **スケジュールの作成**
1. EventBridgeコンソールで「スケジュール」を選択し、新規スケジュールを作成する。  
   名前は「rds-s3-snapshot-schedule」など、目的が分かりやすいものにする。

#### **スケジュールの設定**
- **スケジュールの種類**: cron式を選択。  
- **cron式の例**:  
  `0 18 * * ? *`（毎日日本時間の午前3時に実行）。  
  - cron式はUTCで記述する必要がある。日本時間を指定する場合、-9時間した時刻を設定する（例: 午前3時はUTCで18時）。

#### **ターゲットの指定**
1. ターゲットに「AWS Lambda」を選択し、Step 4で作成したLambda関数を指定する。
2. ペイロードは空欄のままで問題ない。

#### **動作確認**
1. **次回トリガー日時を確認**  
   EventBridgeの「スケジュール詳細」ページで、次回実行日時を確認する。
2. **CloudWatch Logsで実行結果を確認**  
   スケジュールが実行された際にLambda関数が正常に動作したかをCloudWatch Logsで確認する。

### **補足**

- **cron式の詳細例**  
  - `0 0 * * ? *` : 毎日午前0時（UTC）。  
  - `0 9 * * ? *` : 毎日午前9時（UTC）。  
  - `0 18 * * ? *` : 毎日日本時間午前3時（UTCの18時）。

- **トリガーがLambdaコンソールに表示されない場合**  
  EventBridgeで正しくスケジュールが作成されていても、Lambdaコンソールのトリガー一覧に反映されない場合がある。  
  この場合でもスケジュールは正常に動作するため、EventBridgeのスケジュール画面で次回実行日時を確認すること。

- **スケジュール実行の再試行設定**  
  EventBridgeにはデフォルトで再試行ポリシーが設定されており、一時的なエラー発生時に自動的に再試行が行われる。必要に応じて再試行回数や期間を調整することで、より安定した実行環境を構築できる。

## **4. 確認手順**

1. **CloudWatch Logs**でLambda関数のログを確認。
2. **S3バケット**にバックアップファイルが保存されていることを確認。
3. **RDS スナップショット**で古いスナップショットが削除されていることを確認。