AWS DynamoDB 多表跨账户备份到S3方案

方案概述

本方案利用AWS EventBridge定时触发Lambda函数,通过Python脚本调用DynamoDB API将指定表格数据导出为DynamoDB JSON格式,并存储到目标账户的S3存储桶中。所有配置参数均通过Lambda环境变量设置,确保灵活性和安全性。

架构设计

graph TD A[EventBridge定时规则] --> B[触发Lambda函数] B --> C{备份类型判断} C -->|全量| D[调用DynamoDB全量导出API] C -->|增量| E[调用DynamoDB增量导出API] D --> F[数据导出到目标账户S3] E --> F F --> G[多表循环]

详细实现方案

1. Lambda环境变量配置

环境变量名称 描述 示例值 默认值 备注
SOURCE_TABLES 要备份的DynamoDB表名列表(JSON格式) ["table1", "table2"] 必需
TARGET_BUCKET_NAME 目标账户S3存储桶名称 my-backup-bucket 必需
TARGET_ACCOUNT_ID 目标AWS账户ID 123456789012 必需
ACCOUNT_ID 源账户ID 123456789012 必需
EXPORT_TYPE_CONFIG 导出类型配置:FULL(全量)或INCREMENTAL(增量) INCREMENTAL INCREMENTAL 可选
AWS_PARTITION AWS分区标识 aws-cn 自动从Lambda Context获取 可选

2. Lambda函数代码(支持多表+备份类型配置)

import boto3
import os
import json
from datetime import datetime, timezone
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    # Get environment variable configuration
    source_tables = json.loads(os.environ['SOURCE_TABLES'])
    region = os.environ['AWS_REGION']
    export_type_config = os.environ.get('EXPORT_TYPE_CONFIG', 'INCREMENTAL').upper()
    partition = os.environ.get('AWS_PARTITION', context.invoked_function_arn.split(':')[1])

    # Get account ID, use current account if not configured
    account_id = os.environ.get('ACCOUNT_ID', context.invoked_function_arn.split(':')[4])

    # Validate export type configuration
    if export_type_config not in ['FULL', 'INCREMENTAL']:
        error_msg = f"Invalid export type configuration: {export_type_config}. Must be 'FULL' or 'INCREMENTAL'"
        logger.error(error_msg)
        return {
            'statusCode': 400,
            'body': json.dumps({
                'error': error_msg
            })
        }

    logger.info(f"Export type configuration: {export_type_config}")

    # Create DynamoDB client
    dynamodb = boto3.client('dynamodb', region_name=region)

    # Process each table
    results = []
    for table_name in source_tables:
        table_arn = f"arn:{partition}:dynamodb:{region}:{account_id}:table/{table_name}"

        # Determine export type and build export parameters
        export_params = build_export_params(dynamodb, table_arn, table_name, export_type_config)

        # Start export task
        try:
            response = dynamodb.export_table_to_point_in_time(**export_params)

            export_arn = response['ExportDescription']['ExportArn']
            results.append({
                'table': table_name,
                'status': 'STARTED',
                'exportArn': export_arn,
                'exportType': export_params.get('ExportType', 'FULL_EXPORT')
            })
            logger.info(
                f"Table {table_name} export task started: {export_arn}, Type: {export_params.get('ExportType', 'FULL_EXPORT')}")

        except Exception as e:
            error_msg = f"Table {table_name} export failed: {str(e)}"
            logger.error(error_msg)
            results.append({
                'table': table_name,
                'status': 'FAILED',
                'error': str(e)
            })

    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'Export tasks started',
            'exportTypeConfig': export_type_config,
            'results': results
        })
    }


def build_export_params(dynamodb, table_arn, table_name, export_type_config):
    """Build export parameters, correctly handling incremental export time ranges"""

    base_params = {
        'TableArn': table_arn,
        'S3Bucket': os.environ['TARGET_BUCKET_NAME'],
        'S3BucketOwner': os.environ['TARGET_ACCOUNT_ID'],
        'S3Prefix': f"exports/{table_name}/",
        'ExportFormat': 'DYNAMODB_JSON',
        'S3SseAlgorithm': 'AES256'
    }

    # If configured for full export, return full export parameters directly
    if export_type_config == 'FULL':
        base_params['ExportType'] = 'FULL_EXPORT'
        base_params['ExportTime'] = datetime.now(timezone.utc)
        return base_params

    # If configured for incremental export, check if it's the first export
    try:
        # If configured for incremental export, try to get the most recent successful export
        last_successful_export = get_last_successful_export(dynamodb, table_arn)

        # If there's a successful export record, build incremental export parameters
        if last_successful_export:
            export_detail = dynamodb.describe_export(ExportArn=last_successful_export)
            last_export_time = export_detail['ExportDescription']['StartTime']

            # Set incremental export parameters
            base_params['ExportType'] = 'INCREMENTAL_EXPORT'
            base_params['IncrementalExportSpecification'] = {
                'ExportFromTime': last_export_time,
                'ExportToTime': datetime.now(timezone.utc),
                'ExportViewType': 'NEW_AND_OLD_IMAGES'
            }

            logger.info(f"Table {table_name} incremental export: from {last_export_time} to current time")
            return base_params

        # No successful history found, use full export
        logger.info(f"Table {table_name} no successful export history found, using full export")
        base_params['ExportType'] = 'FULL_EXPORT'
        base_params['ExportTime'] = datetime.now(timezone.utc)
        return base_params

    except Exception as e:
        logger.error(f"Error checking table {table_name} export history: {str(e)}")
        # Use full export as a safe choice when an error occurs
        base_params['ExportType'] = 'FULL_EXPORT'
        base_params['ExportTime'] = datetime.now(timezone.utc)
        return base_params


def get_last_successful_export(dynamodb, table_arn):
    """Get the most recent successful export record"""
    try:
        # Get export history
        response = dynamodb.list_exports(
            TableArn=table_arn,
            MaxResults=10  # Get more records to improve the probability of finding a valid record
        )

        # Find the most recent successful export
        last_successful_export_arn = None
        if response.get('ExportSummaries'):
            for export in response['ExportSummaries']:
                # Only consider exports with status COMPLETED
                if export.get('ExportStatus') == 'COMPLETED':
                    last_successful_export_arn = export['ExportArn']
                    break

        return last_successful_export_arn
    except Exception as e:
        logger.error(f"Error getting export history: {str(e)}")
        return None

3. 使用场景

  1. 首次部署
    • 设置EXPORT_TYPE_CONFIG=INCREMENTAL
    • 首次运行会自动检测到没有历史导出记录,使用全量导出
    • 后续运行会自动使用增量导出
  2. 定期全量备份
    • 设置EXPORT_TYPE_CONFIG=FULL
    • 每次运行都会执行全量导出
  3. 日常增量备份
    • 设置EXPORT_TYPE_CONFIG=INCREMENTAL
    • 系统会自动判断导出类型(首次全量,后续增量)

4. Lambda执行角色权限

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowDynamoDBExport",
      "Effect": "Allow",
      "Action": [
        "dynamodb:ExportTableToPointInTime",
        "dynamodb:ListExports",
        "dynamodb:DescribeExport"
      ],
      "Resource": [
        "arn:aws-cn:dynamodb:REGION:SOURCE_ACCOUNT_ID:table/table1",
        "arn:aws-cn:dynamodb:REGION:SOURCE_ACCOUNT_ID:table/table2"
      ]
    },
    {
      "Sid": "AllowS3Export",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws-cn:s3:::TARGET_BUCKET_NAME/*"
      ]
    },
    {
      "Sid": "AllowCloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws-cn:logs:*:*:*"
    }
  ]
}

5. 目标账户S3存储桶策略

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCrossAccountBackupAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::SOURCE_ACCOUNT_ID:role/LambdaDynamoDBExportRole"
      },
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl",
        "s3:ListBucket",
        "s3:GetObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": [
        "arn:aws:s3:::TARGET_BUCKET_NAME/*"
      ]
    }
  ]
}

部署与测试建议

  1. 部署步骤

    1. 在源账户创建导出角色
      • 创建IAM角色LambdaDynamoDBExportRole
      • 配置权限策略
    2. 在目标账户配置S3桶
      • 创建S3存储桶(如果不存在)
      • 配置桶策略允许源账户的导出角色访问
    3. 在源账户创建Lambda函数
      • 使用提供的Python代码
      • 配置环境变量
      • 设置适当的执行超时时间(建议5分钟)
      • 配置Lambda执行角色为LambdaDynamoDBExportRole
    4. 设置EventBridge定时触发
      • 创建EventBridge规则定时触发Lambda函数
  2. 测试建议

    • 验证多表备份是否正常工作
    • 修改数据后测试增量备份功能
    • 检查S3存储桶中的备份文件结构和内容

方案特点与优势

  1. 灵活的导出类型配置
    • 通过环境变量EXPORT_TYPE_CONFIG控制导出类型
    • 支持FULL(全量导出)和INCREMENTAL(增量导出)两种模式
  2. 智能的首次导出处理
    • 当配置为增量导出时,自动检测是否为首次导出
    • 首次导出使用全量导出,后续使用增量导出
  3. 真正的跨账户备份
    • 使用DynamoDB原生导出功能
    • 通过TARGET_ACCOUNT_ID 参数指定目标账户
  4. 完善的错误处理
    • 验证环境变量配置
    • 处理检查导出历史时的异常
    • 详细的日志记录
  5. 简化权限管理
    • 使用专门的导出角色
    • 清晰的跨账户权限配置
    • 最小权限原则

常见问题排查

  1. 导出任务失败
    • 检查DynamoDB表是否启用PITR
    • 验证导出角色权限
    • 检查S3桶策略
  2. 增量导出未生效
    • 确认有成功的历史导出记录
    • 检查时间范围是否在PITR保留期内
  3. 控制台不显示导出记录
    • 确认使用原生导出API(非Scan方式)
    • 检查导出任务状态是否为COMPLETED

注意事项

  1. PITR要求
    • 增量导出需要表启用PITR(时间点恢复)
    • 确保所有要导出的表都已启用PITR
  2. 导出限制
    • 每个账户同时最多有10个导出任务
    • 每个表每24小时最多导出4次
  3. 时间范围限制
    • 增量导出的时间范围必须在PITR保留期内(默认35天)
    • 如果上次导出时间超过35天,增量导出会失败
  4. 监控和告警
    • 建议配置CloudWatch告警监控导出失败情况
    • 监控Lambda函数的执行日志

此方案提供了灵活可靠的DynamoDB跨账户备份解决方案,支持多表备份和备份策略配置,满足中国区AWS环境下的特殊需求。

posted @ 2025-09-24 17:38  Donaver  阅读(7)  评论(0)    收藏  举报