DynamoDB → Amazon SageMaker Lakehouse のクロスアカウント環境におけるZero-ETL統合を試してみた

DynamoDB → Amazon SageMaker Lakehouse のクロスアカウント環境におけるZero-ETL統合を試してみた

2025.09.09

はじめに

データ事業本部のkasamaです。今回はDynamoDB → Amazon SageMaker Lakehouse のクロスアカウント環境におけるZero-ETL統合を試してみたいと思います。

前提

DynamoDB-To-S3

DynamoDBとGlue Data Catalogが異なるアカウントにある状態でZero-ETL統合によりデータを連携し、S3に出力されたデータをAthenaを通して参照する構成です。

Zero-ETL統合のターゲットは、AWS Glue Databaseを指定します。このDatabaseはSageMaker Lakehouseの文脈では「S3をストレージとするマネージドカタログ」として機能し、データはApache Iceberg形式でS3に保存され、Athenaから参照可能になります。
https://docs.aws.amazon.com/sagemaker-unified-studio/latest/userguide/lakehouse-how.html
https://docs.aws.amazon.com/sagemaker-unified-studio/latest/userguide/lakehouse-components.html
Zero-ETL統合において、デフォルトではIAM/AWS Glueポリシーで管理され、Lake Formationはオプションなので、デフォルトのまま使用します。
https://docs.aws.amazon.com/glue/latest/dg/zero-etl-limitations.html

設定方法は以下のドキュメントを参考にします。
https://docs.aws.amazon.com/ja_jp/glue/latest/dg/zero-etl-target.html
https://aws.amazon.com/jp/blogs/aws/new-amazon-dynamodb-zero-etl-integration-with-amazon-sagemaker-lakehouse/

CloudFormationデプロイ

実装方針として、IaCで実装できるものはIaCで、できないものはAWS CLIを使用します。

まずはデータソース側のDynamoDBを定義します。Zero-ETL統合では、AWS Glueサービスが以下の操作を実行する必要があります。

  • DynamoDBテーブルの構造を読み取る
  • Point-in-Time Recovery機能を使ってデータをエクスポートする

これらの権限は、DynamoDBテーブルのリソースベースポリシーで直接設定できます。
https://docs.aws.amazon.com/glue/latest/dg/zero-etl-sources.html#zero-etl-config-source-dynamodb

cm-kasama-dynamodb.yml
			
			AWSTemplateFormatVersion: "2010-09-09"
Description: Source account resources for Glue Zero-ETL (DynamoDB table with PITR).

Parameters:
  TableName:
    Type: String
    Default: cm-kasama-test-transactions
    Description: DynamoDB table name (source)

Resources:
  SourceTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Ref TableName
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: transaction_id
          AttributeType: S
        - AttributeName: timestamp
          AttributeType: N
      KeySchema:
        - AttributeName: transaction_id
          KeyType: HASH
        - AttributeName: timestamp
          KeyType: RANGE
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      ResourcePolicy:
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Sid: AllowGlueZeroETLFromIntegration
              Effect: Allow
              Principal:
                Service: glue.amazonaws.com
              Action:
                - dynamodb:ExportTableToPointInTime
                - dynamodb:DescribeTable
                - dynamodb:DescribeExport
              Resource: "*"
              Condition:
                StringEquals:
                  aws:SourceAccount: !Ref AWS::AccountId
                StringLike:
                  aws:SourceArn: !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:integration:*

		

CloudFormationからデプロイしました。
Screenshot 2025-09-08 at 22.48.13

次にターゲット側のS3、IAM Role、Glue Databaseを定義します。
IAM Roleのpolicyは以下ドキュメントを元に設定しました。
https://docs.aws.amazon.com/glue/latest/dg/zero-etl-prerequisites.html#zero-etl-setup-target-resources-target-iam-role

cm-kasama-zero-etl-target.yml
			
			AWSTemplateFormatVersion: "2010-09-09"
Description: Target account resources for Glue Zero-ETL (S3, Glue DB, IAM Role). 
Parameters:
  GlueDatabaseName:
    Type: String
    Default: cm_kasama_zero_etl_db
    Description: Glue database name to receive data
  S3BucketName:
    Type: String
    Description: S3 bucket to store data
  TargetRoleName:
    Type: String
    Description: IAM role used by Glue on target side

Resources:
  DataBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName

  TargetRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref TargetRoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: glue.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: glue-target-inline
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: GlueCatalogAccess
                Effect: Allow
                Action:
                  - glue:GetDatabase
                  - glue:GetDatabases
                  - glue:GetTable
                  - glue:GetTables
                  - glue:CreateTable
                  - glue:UpdateTable
                  - glue:DeleteTable
                  - glue:CreatePartition
                  - glue:BatchCreatePartition
                  - glue:UpdatePartition
                  - glue:GetPartition
                  - glue:GetPartitions
                Resource:
                  - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:catalog
                  - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:database/${GlueDatabaseName}
                  - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:table/${GlueDatabaseName}/*
              - Sid: S3Write
                Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetBucketLocation
                  - s3:PutObject
                  - s3:GetObject
                  - s3:DeleteObject
                Resource:
                  - !Sub arn:aws:s3:::${S3BucketName}
                  - !Sub arn:aws:s3:::${S3BucketName}/*
              - Sid: LogsAndMetrics
                Effect: Allow
                Action:
                  - cloudwatch:PutMetricData
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"
  GlueDatabase:
    Type: AWS::Glue::Database
    Properties:
      CatalogId: !Sub ${AWS::AccountId}
      DatabaseInput:
        Name: !Ref GlueDatabaseName
        Description: Zero-ETL target database
        LocationUri: !Sub s3://${S3BucketName}/

		

こちらもCloudFormationからデプロイしました。
Screenshot 2025-09-08 at 22.49.07

ターゲット側 AWS CLIコマンドでのセットアップ

ここからはCloudShell上からAWS CLIコマンドを実行してセットアップしていきます。
まずはターゲット側のセットアップです。
以下を CloudShell に貼り付けて実行してください。

			
			export AWS_REGION=ap-northeast-1

# === ターゲット:Glueの CloudShell で実行する場合 ===
# 自アカウントIDをTARGET_ACCOUNT_IDに、ソース(SOURCE_ACCOUNT_ID)を手入力
export TARGET_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export SOURCE_ACCOUNT_ID=<SOURCE_AWS_ACCOUNT_ID>

# リソース名(両アカウントで共通の命名を使う想定。異なる場合は適宜変更)
export DYNAMODB_TABLE=cm-kasama-test-transactions
export GLUE_DB_NAME=cm_kasama_zero_etl_db
export TARGET_ROLE=<IAM_ROLE_NAME>
export S3_BUCKET=<S3_BUCKET>
export INTEGRATION_NAME=cm-kasama-cross-account-dynamodb-glue

echo "A=${TARGET_ACCOUNT_ID} B=${SOURCE_ACCOUNT_ID} REGION=${AWS_REGION}"

		

Glue Resource-based policyの設定

Glue Resource-based policyはCloudFormation未対応なので、AWS CLIコマンドで実行します。
https://docs.aws.amazon.com/glue/latest/dg/security_iam_resource-based-policy-examples.html

			
			
cat > catalog-resource-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCreateInboundFromSourceAccount",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::${SOURCE_ACCOUNT_ID}:root" },
      "Action": "glue:CreateInboundIntegration",
      "Resource": [
        "arn:aws:glue:${AWS_REGION}:${TARGET_ACCOUNT_ID}:catalog",
        "arn:aws:glue:${AWS_REGION}:${TARGET_ACCOUNT_ID}:database/${GLUE_DB_NAME}"
      ]
    },
    {
      "Sid": "AllowGlueServiceAuthorize",
      "Effect": "Allow",
      "Principal": { "Service": "glue.amazonaws.com" },
      "Action": "glue:AuthorizeInboundIntegration",
      "Resource": [
        "arn:aws:glue:${AWS_REGION}:${TARGET_ACCOUNT_ID}:catalog",
        "arn:aws:glue:${AWS_REGION}:${TARGET_ACCOUNT_ID}:database/${GLUE_DB_NAME}"
      ]
    }
  ]
}
EOF

aws glue put-resource-policy \
  --region "${AWS_REGION}" \
  --policy-in-json file://catalog-resource-policy.json

aws glue get-resource-policy --region "${AWS_REGION}"

		

AllowCreateInboundFromSourceAccountはデータソース側のZero-ETL統合作成者がターゲットのGlueリソースに対して統合を作成する権限を受け入れるためのものです。今回はrootを指定していますが、より制限する場合はデータソース側でaws glue create-integrationコマンドを実行するIAMユーザーもしくはロールを指定します。
AllowGlueServiceAuthorizeはAWS Glueサービス自体(glue.amazonaws.com)がターゲットアカウントの代わりに統合を承認する権限です。

create-integration-resource-propertyの設定

https://docs.aws.amazon.com/cli/latest/reference/glue/create-integration-resource-property.html

			
			aws glue create-integration-resource-property \
  --resource-arn arn:aws:glue:${AWS_REGION}:${TARGET_ACCOUNT_ID}:database/${GLUE_DB_NAME} \
  --target-processing-properties RoleArn=arn:aws:iam::${TARGET_ACCOUNT_ID}:role/${TARGET_ROLE}

aws glue get-integration-resource-property \
  --resource-arn arn:aws:glue:${AWS_REGION}:${TARGET_ACCOUNT_ID}:database/${GLUE_DB_NAME}

		

create-integration-resource-propertyコマンドは、Zero-ETL統合のターゲットリソース(この場合はGlue Database)に対して、統合に必要な設定を行います。このコマンドにより、統合サービスがターゲットのGlue Databaseにアクセスし、S3へのデータ書き込みを行うための権限設定が完了します。

データソースアカウントのマネコン上でZero-ETL統合を作成しようとした際にターゲットのIAM Roleの紐付け設定が現時点ではできなかったため、AWS CLIで操作しています。

データソース側 AWS CLIコマンドでのセットアップ

以下を CloudShell に貼り付けて実行してください。

			
			export AWS_REGION=ap-northeast-1

# === ソース:DynamoDBの CloudShell で実行する場合 ===
# 自アカウントIDをSOURCE_ACCOUNT_IDに、ターゲット(TARGET_ACCOUNT_ID)を手入力
export SOURCE_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export TARGET_ACCOUNT_ID=<TARGET_ACCOUNT_ID>   # ← ターゲット(Glue)アカウントID

# リソース名(両アカウントで共通の命名を使う想定。異なる場合は適宜変更)
export DYNAMODB_TABLE=cm-kasama-test-transactions
export GLUE_DB_NAME=cm_kasama_zero_etl_db
export TARGET_ROLE=<IAM_ROLE_NAME>
export S3_BUCKET=<S3_BUCKET>
export INTEGRATION_NAME=cm-kasama-cross-account-dynamodb-glue

echo "TARGET_ACCOUNT_ID=${TARGET_ACCOUNT_ID} SOURCE_ACCOUNT_ID=${SOURCE_ACCOUNT_ID} REGION=${AWS_REGION}"

		

DynamoDBデータ挿入

3レコードのテストデータをCLIで挿入します。

			
			cat > seed-items.json <<'EOF'
{
  "cm-kasama-test-transactions": [
    {"PutRequest": {"Item": {
      "transaction_id": {"S": "txn-001"},
      "timestamp": {"N": "1735300800"},
      "user_id": {"S": "user_1"},
      "amount": {"N": "5000"},
      "currency": {"S": "USD"},
      "status": {"S": "completed"},
      "merchant": {"S": "merchant_1"},
      "category": {"S": "shopping"},
      "location": {"M": {"country": {"S": "US"}, "city": {"S": "New York"}}}
    } }},
    {"PutRequest": {"Item": {
      "transaction_id": {"S": "txn-002"},
      "timestamp": {"N": "1735301400"},
      "user_id": {"S": "user_2"},
      "amount": {"N": "3500"},
      "currency": {"S": "EUR"},
      "status": {"S": "pending"},
      "merchant": {"S": "merchant_2"},
      "category": {"S": "food"},
      "location": {"M": {"country": {"S": "UK"}, "city": {"S": "London"}}}
    } }},
    {"PutRequest": {"Item": {
      "transaction_id": {"S": "txn-003"},
      "timestamp": {"N": "1735302000"},
      "user_id": {"S": "user_3"},
      "amount": {"N": "8000"},
      "currency": {"S": "JPY"},
      "status": {"S": "completed"},
      "merchant": {"S": "merchant_3"},
      "category": {"S": "transport"},
      "location": {"M": {"country": {"S": "JP"}, "city": {"S": "Tokyo"}}}
    } }}
  ]
}
EOF

aws dynamodb batch-write-item \
  --region "${AWS_REGION}" \
  --request-items file://seed-items.json

		

glue create-integrationの設定

https://docs.aws.amazon.com/cli/latest/reference/glue/create-integration.html

			
			aws glue create-integration \
  --region "${AWS_REGION}" \
  --integration-name "${INTEGRATION_NAME}" \
  --source-arn arn:aws:dynamodb:${AWS_REGION}:${SOURCE_ACCOUNT_ID}:table/${DYNAMODB_TABLE} \
  --target-arn arn:aws:glue:${AWS_REGION}:${TARGET_ACCOUNT_ID}:database/${GLUE_DB_NAME}

		

実際にcreate-integrationコマンドでDynamoDBとGlueのZero-ETL統合を作成します。今回は細かい設定を行いませんが、例えば--integration-configRefreshIntervalを指定することでCDCの間隔をデフォルトの15分から変更することができます。

実行結果

データソースアカウントから問題なく作成されていることを確認しました。
Screenshot 2025-09-09 at 8.58.46

ターゲットアカウントからも確認できます。
Screenshot 2025-09-09 at 8.55.29
Screenshot 2025-09-09 at 8.57.05

S3では二つのフォルダが作成されていました。
Screenshot 2025-09-09 at 9.00.58

cm_kasama_test_transactionsは実際のDynamoDB tableのデータが格納されています。zetl_integration_table_stateはZero-ETL統合のステータスが格納されています。
Screenshot 2025-09-09 at 9.08.16

Athenaで参照するとcm_kasama_test_transactionsにはDynamoDB tableからの実際の値が確認できます。
Screenshot 2025-09-09 at 9.20.25

zetl_integration_table_stateでは、Zero-ETL統合の詳細なステータスが確認できます。
Screenshot 2025-09-09 at 9.20.05

差分連携確認

追加で差分連携の確認もしておきます。先ほどと同様にデータソースアカウントのCloudShellで、データを操作します。

			
			export AWS_REGION=ap-northeast-1
export DYNAMODB_TABLE=cm-kasama-test-transactions

		

既存項目の更新(txn-001 の払い戻し・金額変更)

			
			NOW=$(date +%s)
cat > update_txn_001.json <<EOF
{
  "TableName": "${DYNAMODB_TABLE}",
  "Key": { "transaction_id": {"S": "txn-001"}, "timestamp": {"N": "1735300800"} },
  "UpdateExpression": "SET #status = :s, #amount = :a, #updated_at = :u",
  "ExpressionAttributeNames": { "#status": "status", "#amount": "amount", "#updated_at": "updated_at" },
  "ExpressionAttributeValues": { ":s": {"S": "refunded"}, ":a": {"N": "5500"}, ":u": {"N": "${NOW}"} },
  "ReturnValues": "ALL_NEW"
}
EOF
aws dynamodb update-item --region "${AWS_REGION}" --cli-input-json file://update_txn_001.json

		

配列を含む新規項目を挿入(txn-004)

			
			cat > put_txn_004.json <<EOF
{
  "TableName": "${DYNAMODB_TABLE}",
  "Item": {
    "transaction_id": {"S": "txn-004"},
    "timestamp": {"N": "1735302600"},
    "user_id": {"S": "user_1"},
    "amount": {"N": "1200"},
    "currency": {"S": "USD"},
    "status": {"S": "completed"},
    "merchant": {"S": "merchant_4"},
    "category": {"S": "entertainment"},
    "location": {"M": {"country": {"S": "US"}, "city": {"S": "Seattle"}}},
    "payment_methods": {"L": [
      {"S": "credit_card"},
      {"S": "apple_pay"}
    ]},
    "tags": {"SS": ["movie", "imax", "weekend"]}
  }
}
EOF
aws dynamodb put-item --region "${AWS_REGION}" --cli-input-json file://put_txn_004.json

		

既存項目を削除(txn-002 を削除)

			
			cat > delete_txn_002.json <<EOF
{
  "TableName": "${DYNAMODB_TABLE}",
  "Key": { "transaction_id": {"S": "txn-002"}, "timestamp": {"N": "1735301400"} }
}
EOF
aws dynamodb delete-item --region "${AWS_REGION}" --cli-input-json file://delete_txn_002.json

		

2025/9/9 9:44頃にDynamoDB上でデータが更新されていることを確認しました。
Screenshot 2025-09-09 at 9.46.10

2025/9/9 9:53頃にCloudWatchとS3の更新を確認しました。
Screenshot 2025-09-09 at 10.18.53
Screenshot 2025-09-09 at 10.19.50

Athenaでもcm_kasama_test_transactionstableの更新を確認できました。配列もそのままの状態で格納されています。
Screenshot 2025-09-09 at 10.22.38
zetl_integration_table_statetableも更新されています。
Screenshot 2025-09-09 at 10.23.05

最後に

比較的新しいサービスのため、今後も機能追加や仕様変更の可能性があります。そのため、2025年9月段階での参考程度にしていただければと思います。

この記事をシェアする

FacebookHatena blogX

関連記事