Amazon AuroraのS3エクスポート機能
AuroraのS3エクスポート機能は、DBクラスターの現在のデータやスナップショットのデータをS3にApache Parquet形式で出力する機能です。
Apache Parquet形式は、スキーマ情報を内包している・列志向で分析用途にも適している・高効率な圧縮が可能・複雑なデータ構造にも対応しているといった特徴を備えたデータ形式です。AthenaやRedshift、あるいはBigQueryへの取り込みに利用することができ、これらを使ったDB内のデータ分析が可能になります。
高効率さについての実験として、試しに手元の64GBのクラスタースナップショットをS3エクスポートしてみたところ、出力されたParquetファイルの合計サイズは約4GBと、なんと1/16にも圧縮されました。特に毎日何TBもの大容量データをリージョン外に転送するようなケースではネットワーク転送費が月百万円オーダーになるため、圧縮効率も重要になってきます。
そんなS3エクスポートにも欠点はあります。Aurora自身はParquet形式のデータインポート機能を備えておらず、エクスポートしたデータを読み込めません。*1
とはいえ書き出したデータをAuroraに復元できると有効活用できるシーンもあるため、これをAWS Glueを使うことで簡単に実現できないか検討してみました。
前提
(1) Aurora MySQL 3.0 (MySQL 8.0互換) で検証
(2) テーブル構造はAurora内にあるものとする
Parquet 形式はスキーマを保持していますが、MySQLのスキーマが完全に維持されるわけではないため、この前提を置いています。これはインポート前にマイグレーションを走らせるなどしてテーブルを作っておけば満たせます。
AWS Glueのセットアップ
Glue がエクスポート先のS3および復元先のAurora DBクラスターにアクセスできるようにします。基本的には下記Glueのドキュメント通りなのですが、注意が必要な点について触れておきます。
AWS Glue 用 JDBC データストアに接続するための VPC の設定 - AWS Glue
IAM Roleの設定
最初にGlueが利用するIAM Roleの設定が必要です。
AWS Glue Studio のセットアップ - AWS Glue Studio
Glue Studioのドキュメントを参考にセットアップを行います。コンソール上で設定を進めていくと、IAMロールが作成されます。S3バケットへのアクセス許可をGlueのGUIで行うこともできますが、RDSからエクスポートされたデータはKMS鍵で暗号化されているため、この鍵を使った復号も許可する必要があります。このため、Glue用のマネージドポリシーに加えて、以下のようなポリシーを追加する必要があります。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "KMSDecrypt", "Effect": "Allow", "Action": "kms:Decrypt", "Resource": "arn:aws:kms:<REGION>:<アカウントID>:key/<キーID>" }, { "Sid": "S3Access", "Effect": "Allow", "Action": ["s3:Get*","s3:List*"], "Resource": [ "arn:aws:s3:::<バケット名>", "arn:aws:s3:::<バケット名>/*" ] } ] }
Connectionsの設定
Auroraにアクセスするためには、GlueがVPC内にアクセスするための設定が必要です。
AWS GlueのサイドバーからData connectionsを開き、Connections セクションの Create connection ボタンを押します。Connection typeを開くとAuroraなど色々なDBの選択肢が出てきますが、後述する手順ではあくまでVPCアクセスのためだけにConnectionsを使用するため、ここではNetworkを選びます。そして、S3バケットにアクセス可能かつAuroraクラスターの書き込みエンドポイントに接続可能なVPC・サブネット・セキュリティグループを選択します。ここで選択したENI(ネットワークインターフェイス)を介してGlueのワーカーが通信をすることになります。
(S3については、具体的にはVPCエンドポイントかNAT Gatewayを介してS3にアクセスできる必要があります。)
Glueジョブの作成と実行を行うスクリプト
インポート処理は、データ変換の必要もないため、読み取ったParquetのデータをそのままJDBCコネクタに流すだけのシンプルなものです。この場合、複数のテーブルをインポートする場合は複数のジョブを作る必要があるため、ジョブ生成をスクリプト化しました。設定項目に必要な値を書き込んでスクリプトを実行すると、指定テーブル用のPythonスクリプトをS3上に配置して、それを実行するGlueジョブを作成し、起動します。これを必要なテーブル数分だけ繰り返します。
実行中にGlueのコンソールで作成されたジョブを選んでRunsタブを開くと、ログなどをリアルタイムに追うことができます。
#!/bin/bash -eu # S3 に Export されたAuroraのテーブルのParquetファイルをGlueを使用してRDSにロードする # 設定項目 aws_account_id=$(aws sts get-caller-identity --query Account --output text) region="ap-northeast-1" S3_SCRIPT_BASE=s3://aws-glue-assets-"$aws_account_id"-"$region"/scripts/ S3_TEMP_PATH=s3://aws-glue-assets-"$aws_account_id"-"$region"/temporary/ S3_UILOG_PATH=s3://aws-glue-assets-"$aws_account_id"-"$region"/sparkHistoryLogs/ GLUE_ROLE=AWSGlueServiceRole # Glue用のIAM Role名 GLUE_CONNECTION=VPCNetwork # Glue上で設定したVPC接続設定名 S3_DATA_PATH=s3://rds-export/export_name # Aurora Export先S3 S3_DB_NAME=database_name # エクスポート時のデータベース名 DB_ENDPOINT=database.cluster-xxxx."$region".rds.amazonaws.com DB_NAME=tmp # インポート先のデータベース名 DB_USER=write_user DB_PASSWORD=write_password TABLE=target_table_name # インポート対象のテーブル名 ### script_path="$S3_SCRIPT_BASE$DB_NAME-$TABLE.py" template=$(cat <<EOF import sys from awsglue.transforms import * from awsglue.utils import getResolvedOptions from pyspark.context import SparkContext from awsglue.context import GlueContext from awsglue.job import Job args = getResolvedOptions(sys.argv, ["JOB_NAME"]) sc = SparkContext() glueContext = GlueContext(sc) spark = glueContext.spark_session job = Job(glueContext) job.init(args["JOB_NAME"], args) S3bucket_node1 = glueContext.create_dynamic_frame.from_options( format_options={}, connection_type="s3", format="parquet", connection_options={ "paths": [ "$S3_DATA_PATH/$S3_DB_NAME/$S3_DB_NAME.$TABLE/" ], "recurse": True, }, transformation_ctx="S3bucket_node1", ) MySQLtable_node2 = glueContext.write_dynamic_frame.from_options( frame=S3bucket_node1, connection_type="mysql", connection_options={ "url": "jdbc:mysql://$DB_ENDPOINT:3306/$DB_NAME", "dbtable": "$TABLE", "user": "$DB_USER", "password": "$DB_PASSWORD", "bulkSize": 100 }, transformation_ctx="MySQLtable_node2", ) job.commit() EOF ) echo "Creating script on S3: $script_path" >&2 aws s3 cp - "$script_path" <<< "$template" echo "Creating job: $DB_NAME-$TABLE" >&2 aws glue create-job \ --name "$DB_NAME-$TABLE" \ --role "$GLUE_ROLE" \ --command "Name=glueetl,ScriptLocation=$script_path,PythonVersion=3" \ --glue-version "4.0" \ --connections Connections="$GLUE_CONNECTION" \ --default-arguments '{ "--TempDir": "'"$S3_TEMP_PATH"'", "--enable-metrics": "true", "--enable-continuous-cloudwatch-log": "true", "--enable-continuous-log-filter": "true", "--enable-spark-ui": "true", "--spark-event-logs-path": "'"$S3_UILOG_PATH"'", "--enable-glue-datacatalog": "true" }' \ --number-of-workers 4 echo "Starting job: $DB_NAME-$TABLE" >&2 aws glue start-job-run --job-name "$DB_NAME-$TABLE"
スクリプトなどはAWS Glue StudioのGUIで使用されるものと同一にしています。
--number-of-workers 4 は 4つのワーカーを起動するという意味です。大規模なデータ・Auroraインスタンスを使用する場合はこれを大きくして並列度を上げた方が良いでしょう。
また、MySQLのconnection_optionsの "bulkSize": 100 は、1回のINSERT文で100行分のデータを挿入するという意味で、所要時間に結構効いてきます。ちなみにJDBC接続のオプションのドキュメントは以下にありますが、本記事の執筆時点では英語版含めて bulkSize の大文字小文字が間違っており、そのまま指定しても無効になるという罠があってハマりました。。
AWS Glue での ETL の接続タイプとオプション - AWS Glue
実際、Glue Studio のGUIから生成したスクリプトでは bulkSizeは 1 相当(デフォルト)となっており、これをそのまま使うと上記スクリプトと比べて所要時間が2〜3倍くらいかかってしまいました。いかにbulkSize指定に効果があるかが見て取れますね。
試しにdb.r6g.4xlargeのインスタンスに64GBのデータを復元してみたところ、かかった時間は40分程度*2で、十分実用的かなと感じました。
まとめ
AuroraからS3エクスボートで書き出したParquet形式のデータを、再びAuroraデータベースにAWS Glueを使って書き戻す方法について紹介しました。
BIGINT, TEXT, JSON, BINARY, DATETIME など色々なデータ型で試してみましたが、今のところいずれもきちんと元通りに復元できていることを確認することができました。*3
Parquet形式は高効率でデータのやり取りが可能です。またRDSエクスポート機能も、スナップショットからでも既存クラスターからでも性能に影響を与えずに利用できて使い勝手が良いので、RDSへ再インポートできることでさらに活用の機会を広げられるのではないかと考えています。