DynamoDB → Amazon SageMaker Lakehouse のクロスアカウント環境におけるZero-ETL統合を試してみた
はじめに
データ事業本部のkasamaです。今回はDynamoDB → Amazon SageMaker Lakehouse のクロスアカウント環境におけるZero-ETL統合を試してみたいと思います。
前提
DynamoDBとGlue Data Catalogが異なるアカウントにある状態でZero-ETL統合によりデータを連携し、S3に出力されたデータをAthenaを通して参照する構成です。
Zero-ETL統合のターゲットは、AWS Glue Databaseを指定します。このDatabaseはSageMaker Lakehouseの文脈では「S3をストレージとするマネージドカタログ」として機能し、データはApache Iceberg形式でS3に保存され、Athenaから参照可能になります。
Zero-ETL統合において、デフォルトではIAM/AWS Glueポリシーで管理され、Lake Formationはオプションなので、デフォルトのまま使用します。設定方法は以下のドキュメントを参考にします。
CloudFormationデプロイ
実装方針として、IaCで実装できるものはIaCで、できないものはAWS CLIを使用します。
まずはデータソース側のDynamoDBを定義します。Zero-ETL統合では、AWS Glueサービスが以下の操作を実行する必要があります。
- DynamoDBテーブルの構造を読み取る
- Point-in-Time Recovery機能を使ってデータをエクスポートする
これらの権限は、DynamoDBテーブルのリソースベースポリシーで直接設定できます。
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からデプロイしました。
次にターゲット側のS3、IAM Role、Glue Databaseを定義します。
IAM Roleのpolicyは以下ドキュメントを元に設定しました。
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からデプロイしました。
ターゲット側 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コマンドで実行します。
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の設定
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の設定
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-config
のRefreshInterval
を指定することでCDCの間隔をデフォルトの15分から変更することができます。
実行結果
データソースアカウントから問題なく作成されていることを確認しました。
ターゲットアカウントからも確認できます。
S3では二つのフォルダが作成されていました。
cm_kasama_test_transactions
は実際のDynamoDB tableのデータが格納されています。zetl_integration_table_state
はZero-ETL統合のステータスが格納されています。
Athenaで参照するとcm_kasama_test_transactions
にはDynamoDB tableからの実際の値が確認できます。
zetl_integration_table_state
では、Zero-ETL統合の詳細なステータスが確認できます。
差分連携確認
追加で差分連携の確認もしておきます。先ほどと同様にデータソースアカウントの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上でデータが更新されていることを確認しました。
2025/9/9 9:53頃にCloudWatchとS3の更新を確認しました。
Athenaでもcm_kasama_test_transactions
tableの更新を確認できました。配列もそのままの状態で格納されています。
zetl_integration_table_state
tableも更新されています。
最後に
比較的新しいサービスのため、今後も機能追加や仕様変更の可能性があります。そのため、2025年9月段階での参考程度にしていただければと思います。