はじめに
前回の設計編では、自宅 HDFS 上の Apache Iceberg を Source of Truth として残し、AWS 側に分析用 Iceberg コピーを作る方針を整理しました。
今回は構築編です。実際に次の流れで作ります。
syslogで疎通確認
↓
authlogへ展開
↓
共通スクリプト化
↓
syslog/authlogを日次同期
↓
Athenaで両方を分析
AWS へ全面移行するのではなく、あくまで次の構成です。
自宅 = メイン環境 / Source of Truth
AWS = 分析用コピー
ゴール
最終的に以下の状態にします。
前提
自宅側は構築済みとします。
| 項目 | 値 |
|---|---|
| OS | Ubuntu 24.04 |
| Hadoop | 3.3.6 |
| HDFS nameservice | cluster1 |
| Hive | 3.1.3 |
| Spark | 3.5.8 |
| Scala | 2.12 |
| Apache Iceberg | 1.10.1 |
| 既存 Iceberg runtime | iceberg-spark-runtime-3.5_2.12-1.10.1.jar |
| 自宅側 catalog | hive_prod |
| 自宅側 warehouse | hdfs://cluster1/warehouse/iceberg |
対象テーブルは次の2つです。
hive_prod.logs.syslog_iceberg
hive_prod.logs.authlog_iceberg
既存記事では次のような schema / partition の例を使っています。
CREATE TABLE hive_prod.logs.syslog_iceberg (
host string,
ts TIMESTAMP_NTZ,
severity int,
program string,
msg string,
dt date,
hr int
)
USING iceberg
PARTITIONED BY (dt, host);
ただし、構築時には必ず実環境で syslog_iceberg と authlog_iceberg の両方を確認します。2テーブルの schema が完全に同じであるとは決め打ちしません。
IAMユーザー作成
自宅サーバーから AWS CLI / Spark で S3、Glue、Athena にアクセスするため、IAM ユーザーを2つに分けます。
| IAMユーザー | profile | 用途 |
|---|---|---|
| home-aws-admin | home-aws-admin | S3 bucket、Glue database、Athena workgroup、IAM policy を作成する初期構築用 |
| home-iceberg-sync | home-aws | 日次同期で使う Spark / AWS CLI 用 |
home-aws-admin は初期構築だけに使います。
日次同期では使わず、後続で作成する最小権限の home-iceberg-sync を使います。
本番運用では IAM Identity Center や AssumeRole を使う方が望ましいですが、この記事では自宅検証で手順を追いやすくするため IAM User + Access Key の例にしています。
Access Key / Secret Access Key は記事、GitHub、共有メモに記載しません。
初期構築用IAMユーザーを作成する
まず、AWS 管理コンソールで初期構築用の IAM ユーザーを作成します。
この作業は、AWS アカウントのルートユーザー、または既に管理者権限を持つ IAM ユーザー / Role で実施します。
IAM
↓
ユーザー
↓
ユーザーを作成
作成時のパラメータです。
| 画面 | 項目 | 値 |
|---|---|---|
| ユーザーの詳細 | ユーザー名 | home-aws-admin |
| ユーザーの詳細 | AWS Management Console へのユーザーアクセスを提供する | 任意 |
| 許可を設定 | 許可のオプション | ポリシーを直接アタッチする |
| 許可を設定 | 許可ポリシー | AdministratorAccess |
| タグ | Project | home-iceberg-s3-athena |
| タグ | Env | lab |
作成後、home-aws-admin の Access Key を作成します。
IAM
↓
ユーザー
↓
home-aws-admin
↓
セキュリティ認証情報
↓
アクセスキーを作成
作成した Access Key ID / Secret Access Key は、後続の「AWS CLI準備」で OS ユーザーごとの profile に設定します。
同期用IAMユーザーを作成する
次に、日次同期用の IAM ユーザーを作成します。
このユーザーは AWS アカウント全体を管理する権限を持たせません。
IAM
↓
ユーザー
↓
ユーザーを作成
作成時のパラメータです。
| 画面 | 項目 | 値 |
|---|---|---|
| ユーザーの詳細 | ユーザー名 | home-iceberg-sync |
| ユーザーの詳細 | AWS Management Console へのユーザーアクセスを提供する | 無効 |
| 許可を設定 | 許可のオプション | 後でポリシーを直接アタッチする |
| タグ | Project | home-iceberg-s3-athena |
| タグ | Env | lab |
この時点では、まだ権限を付与しません。
後続の「IAM Policy」で、対象 S3 bucket、Glue database / table、Athena query result bucket に絞った policy を作成して、このユーザーに付与します。
AWS CLI準備
AWS 側リソースの作成や確認では aws コマンドを使います。
Ubuntu 24.04 で AWS CLI が未導入の場合は導入します。
AWSインポートシェルを実行するホスト(ope等)で実施することを推奨します。
実行ユーザー: 通常ユーザー
sudo apt-get update
sudo apt-get install -y unzip curl
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \
-o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
rm -rf aws
rm -f awscliv2.zip
確認します。
aws --version
credential は、aws configure だけでなく AWS credential provider chain で解決されます。
代表例です。
- 環境変数
~/.aws/credentials~/.aws/config- AWS profile
- EC2 / ECS / EKS などの Role
ローカルや自宅サーバー上で実行する場合は、まず初期構築用の管理者 profile を設定しておきます。
このコマンドは、AWS リソース作成作業を行う通常ユーザーで実行します。
実行ユーザー: 通常ユーザー
aws configure --profile home-aws-admin
設定する値です。
| 項目 | 値 |
|---|---|
| AWS Access Key ID |
home-aws-admin の Access Key ID |
| AWS Secret Access Key |
home-aws-admin の Secret Access Key |
| Default region name | ap-northeast-1 |
| Default output format | json |
設定後、AWS アカウントへアクセスできることを確認します。
aws sts get-caller-identity --profile home-aws-admin
Arn が以下のように home-aws-admin になっていれば OK です。
arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:user/home-aws-admin
今回の同期スクリプトは通常ユーザーで実行します。
そのため、同期用 profile も通常ユーザーの ~/.aws/ に作成します。
変数
AWS CLI の準備ができたら、以降のコマンドで使う変数を設定します。
新規に作成する名前と、既存 AWS アカウントの情報を入れる値が混在しているため、先に整理します。
| 変数 | 指定する値 | 種別 |
|---|---|---|
AWS_REGION |
利用する AWS リージョン。例では東京リージョン ap-northeast-1
|
任意に指定 |
ICEBERG_BUCKET |
Iceberg warehouse 用に新規作成する S3 bucket 名 | 新規に任意で指定 |
ATHENA_RESULT_BUCKET |
Athena query result 用に新規作成する S3 bucket 名 | 新規に任意で指定 |
ATHENA_WORKGROUP |
Athena 用に新規作成する workgroup 名 | 新規に任意で指定 |
GLUE_DATABASE |
AWS Glue Data Catalog に作成する database 名 | 新規に任意で指定 |
AWS_PROFILE |
AWS CLI で使う profile 名 | 既存設定または新規設定 |
AWS_SHARED_CREDENTIALS_FILE |
現在使う profile の credentials ファイル | 用途に応じて切り替え |
AWS_CONFIG_FILE |
現在使う profile の config ファイル | 用途に応じて切り替え |
AWS_ACCOUNT_ID |
利用する AWS アカウント ID | 既存 AWS アカウント情報 |
IAM_USER_NAME |
policy を付与する 同期用IAM User 名 | 既存 AWS アカウント情報 |
AWS_PROFILE には、AWS 上のリソース名ではなく、AWS CLI を実行する端末に設定した profile 名を指定します。
初期構築用 profile と同期用 profile は、どちらも通常ユーザーの ~/.aws/ を参照します。
profile 名は、次のコマンドで確認できます。
aws configure list-profiles
今回、初期構築用の AWS_PROFILE には home-aws-admin を指定します。
S3 bucket 名は AWS 全体で一意である必要があります。別ユーザーの bucket 名と被らないように、アカウント ID、日付、用途名などを含めます。
例です。
naritomo-123456789012-20260819-iceberg-warehouse
naritomo-123456789012-20260819-athena-result
実行ユーザー: 通常ユーザー
export AWS_REGION=ap-northeast-1
export ICEBERG_BUCKET="your-name-123456789012-20260819-iceberg-warehouse"
export ATHENA_RESULT_BUCKET="your-name-123456789012-20260819-athena-result"
export ATHENA_WORKGROUP="home-log-iceberg"
export GLUE_DATABASE=logs
export AWS_PROFILE="home-aws-admin"
export AWS_SHARED_CREDENTIALS_FILE="${HOME}/.aws/credentials"
export AWS_CONFIG_FILE="${HOME}/.aws/config"
export AWS_ACCOUNT_ID="123456789012"
export IAM_USER_NAME="home-iceberg-sync"
your-name、123456789012、20260819、home-log-iceberg、home-aws-admin は自分の環境に合わせて置き換えてください。
ここでは、S3 bucket、Glue database、Athena workgroup、IAM policy を作るため、手元の shell の AWS_PROFILE は初期構築用の home-aws-admin にしています。
systemd / Spark 用の /etc/iceberg/aws.env は、最初から日次同期用の home-aws を指すようにします。
AWS アカウント ID は以下で確認できます。
aws sts get-caller-identity --profile "${AWS_PROFILE}"
変数を毎回打たないようにする
上記の export は、現在の shell session だけで有効です。ログアウトしたり、別 terminal を開いたりすると再設定が必要になります。
毎回打たなくてよいように、共通の環境変数ファイルを作ります。
ファイル:
/etc/iceberg/aws.env
実行ユーザー: 通常ユーザー
sudo install -d -m 755 /etc/iceberg
sudo tee /etc/iceberg/aws.env > /dev/null <<EOF
AWS_REGION=${AWS_REGION}
ICEBERG_BUCKET=${ICEBERG_BUCKET}
ATHENA_RESULT_BUCKET=${ATHENA_RESULT_BUCKET}
ATHENA_WORKGROUP=${ATHENA_WORKGROUP}
GLUE_DATABASE=${GLUE_DATABASE}
AWS_ADMIN_PROFILE=home-aws-admin
AWS_SYNC_PROFILE=home-aws
AWS_PROFILE=home-aws
AWS_ADMIN_SHARED_CREDENTIALS_FILE=${HOME}/.aws/credentials
AWS_ADMIN_CONFIG_FILE=${HOME}/.aws/config
AWS_SYNC_SHARED_CREDENTIALS_FILE=/var/lib/spark/.aws/credentials
AWS_SYNC_CONFIG_FILE=/var/lib/spark/.aws/config
AWS_SHARED_CREDENTIALS_FILE=/var/lib/spark/.aws/credentials
AWS_CONFIG_FILE=/var/lib/spark/.aws/config
AWS_ACCOUNT_ID=${AWS_ACCOUNT_ID}
IAM_USER_NAME=${IAM_USER_NAME}
SPARK_RUN_USER=spark
SPARK_RUN_LOCAL_DIRS=/var/lib/spark/work
EOF
sudo chmod 644 /etc/iceberg/aws.env
sudo tee /etc/profile.d/iceberg-s3-athena.sh > /dev/null <<'EOF'
use_iceberg_aws_admin() {
set -a
. /etc/iceberg/aws.env
set +a
export AWS_PROFILE="${AWS_ADMIN_PROFILE}"
export AWS_SHARED_CREDENTIALS_FILE="${AWS_ADMIN_SHARED_CREDENTIALS_FILE}"
export AWS_CONFIG_FILE="${AWS_ADMIN_CONFIG_FILE}"
}
use_iceberg_aws_sync() {
set -a
. /etc/iceberg/aws.env
set +a
export AWS_PROFILE="${AWS_SYNC_PROFILE}"
export AWS_SHARED_CREDENTIALS_FILE="${AWS_SYNC_SHARED_CREDENTIALS_FILE}"
export AWS_CONFIG_FILE="${AWS_SYNC_CONFIG_FILE}"
}
EOF
sudo chmod 644 /etc/profile.d/iceberg-s3-athena.sh
このファイルには Access Key / Secret Access Key は書きません。credential は AWS profile、~/.aws/credentials、IAM Role など AWS credential provider chain に任せます。
/etc/iceberg/aws.env の既定値は、systemd / Spark 日次同期用です。
/etc/profile.d/iceberg-s3-athena.sh は、手動作業時に初期構築用 profile と同期用 profile を選ぶための関数だけを定義します。
手動実行するときは、最初に読み込みます。
実行ユーザー: 通常ユーザー
source /etc/profile.d/iceberg-s3-athena.sh
use_iceberg_aws_admin
通常ユーザーで AWS 側リソース作成、IAM policy 作成、S3 / Glue / Athena の管理確認を行う場合も、再ログイン後の shell では環境変数が未反映の状態から始まる前提にします。
そのため、通常ユーザーで初期構築作業を再開するたびに、同じログインセッション内で source /etc/profile.d/iceberg-s3-athena.sh と use_iceberg_aws_admin を実行します。
AWS_PROFILE=home-aws-admin、AWS_SHARED_CREDENTIALS_FILE=<通常ユーザーのHOME>/.aws/credentials になっていることを確認してから、AWS 管理系コマンドを実行します。
変数を変更して反映する
bucket 名、workgroup 名、database 名などを変えたい場合は、/etc/iceberg/aws.env を編集します。
実行ユーザー: 通常ユーザー
sudo vi /etc/iceberg/aws.env
編集後、手元の shell に反映します。
source /etc/profile.d/iceberg-s3-athena.sh
# 初期構築作業の場合
use_iceberg_aws_admin
# Spark 同期作業の場合
# use_iceberg_aws_sync
反映された値を確認します。
printf 'AWS_REGION=%s\n' "${AWS_REGION}"
printf 'AWS_PROFILE=%s\n' "${AWS_PROFILE}"
printf 'AWS_SHARED_CREDENTIALS_FILE=%s\n' "${AWS_SHARED_CREDENTIALS_FILE}"
printf 'AWS_CONFIG_FILE=%s\n' "${AWS_CONFIG_FILE}"
printf 'ICEBERG_BUCKET=%s\n' "${ICEBERG_BUCKET}"
printf 'ATHENA_RESULT_BUCKET=%s\n' "${ATHENA_RESULT_BUCKET}"
printf 'ATHENA_WORKGROUP=%s\n' "${ATHENA_WORKGROUP}"
printf 'GLUE_DATABASE=%s\n' "${GLUE_DATABASE}"
spark ユーザーで AWS / Spark / Iceberg の同期確認を行う前には、同じログインセッション内で必要に応じて次を実行します。
実行ユーザー: spark
source /etc/profile.d/iceberg-s3-athena.sh
use_iceberg_aws_sync
printf 'AWS_PROFILE=%s\n' "${AWS_PROFILE}"
printf 'AWS_SHARED_CREDENTIALS_FILE=%s\n' "${AWS_SHARED_CREDENTIALS_FILE}"
printf 'AWS_CONFIG_FILE=%s\n' "${AWS_CONFIG_FILE}"
AWS_PROFILE=home-aws、AWS_SHARED_CREDENTIALS_FILE=/var/lib/spark/.aws/credentials になっていることを確認してから、後続の spark-sql-iceberg-aws や同期スクリプトを実行します。
ログインし直した場合や、新しい terminal で作業する場合も、この初期化をやり直します。
使い分けは以下です。
| 作業 | 実行する関数 | 参照する credential |
|---|---|---|
| S3 bucket / Glue database / Athena workgroup / IAM policy などの管理系操作 | use_iceberg_aws_admin |
通常ユーザーの ~/.aws/credentials
|
spark ユーザーでの spark-sql-iceberg-aws や同期スクリプトの直接実行 |
use_iceberg_aws_sync |
/var/lib/spark/.aws/credentials |
/usr/local/bin/export-iceberg-to-s3 の実行 |
事前初期化は不要 | wrapper が /etc/iceberg/aws.env を読み込み、内部で spark ユーザーへ切り替える |
AWS側準備
S3 bucket作成
東京リージョン ap-northeast-1 では、create-bucket-configuration を指定します。
実行ユーザー: 通常ユーザー
aws s3api create-bucket \
--bucket "${ICEBERG_BUCKET}" \
--region "${AWS_REGION}" \
--create-bucket-configuration LocationConstraint="${AWS_REGION}" \
--profile "${AWS_PROFILE}"
aws s3api create-bucket \
--bucket "${ATHENA_RESULT_BUCKET}" \
--region "${AWS_REGION}" \
--create-bucket-configuration LocationConstraint="${AWS_REGION}" \
--profile "${AWS_PROFILE}"
Block Public Access を有効化します。
実行ユーザー: 通常ユーザー
for BUCKET in "${ICEBERG_BUCKET}" "${ATHENA_RESULT_BUCKET}"; do
aws s3api put-public-access-block \
--bucket "${BUCKET}" \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true \
--profile "${AWS_PROFILE}"
done
Server-side encryption を有効化します。
実行ユーザー: 通常ユーザー
for BUCKET in "${ICEBERG_BUCKET}" "${ATHENA_RESULT_BUCKET}"; do
aws s3api put-bucket-encryption \
--bucket "${BUCKET}" \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}' \
--profile "${AWS_PROFILE}"
done
HTTPS 以外のアクセスを bucket policy で拒否します。
s3://${ICEBERG_BUCKET}/... は S3 上の場所を表す URI であり、HTTP 通信を指定しているわけではありません。
ただし、誤設定や古いクライアントから平文 HTTP でアクセスされないように、S3 側でも aws:SecureTransport を使って拒否しておきます。
この policy は、通常の AWS CLI、Spark の Iceberg S3FileIO、Athena からの HTTPS アクセスは拒否しません。
一方、put-bucket-policy は既存の bucket policy を置き換えるため、既存 bucket を使う場合は現在の policy に以下の DenyInsecureTransport statement を追加する形にします。
本記事では新規 bucket を作る前提なので、そのまま設定します。
実行ユーザー: 通常ユーザー
for BUCKET in "${ICEBERG_BUCKET}" "${ATHENA_RESULT_BUCKET}"; do
cat > "deny-insecure-transport-${BUCKET}.json" <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::${BUCKET}",
"arn:aws:s3:::${BUCKET}/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
EOF
aws s3api put-bucket-policy \
--bucket "${BUCKET}" \
--policy "file://deny-insecure-transport-${BUCKET}.json" \
--profile "${AWS_PROFILE}"
done
設定後、bucket policy を確認します。
for BUCKET in "${ICEBERG_BUCKET}" "${ATHENA_RESULT_BUCKET}"; do
aws s3api get-bucket-policy \
--bucket "${BUCKET}" \
--query Policy \
--output text \
--profile "${AWS_PROFILE}"
done
S3 Versioning は削除や上書きに対する復旧余地を作れますが、noncurrent version の保存容量も増えます。
そのため、構築編では有効化を必須にせず、継続運用する場合に lifecycle とセットで検討します。
具体的な設定は運用編で扱います。
Athena Workgroup作成
Athena の query result location を毎回手動で指定しなくてよいように、今回の検証用 workgroup を作成します。
作成する workgroup です。
Workgroup:
${ATHENA_WORKGROUP}
Query result location:
s3://${ATHENA_RESULT_BUCKET}/athena-results/
実行ユーザー: 通常ユーザー
この操作を実行する AWS profile には、少なくとも Athena workgroup を作成・参照する権限が必要です。
athena:CreateWorkGroupathena:GetWorkGroup
aws athena create-work-group \
--region "${AWS_REGION}" \
--name "${ATHENA_WORKGROUP}" \
--configuration "ResultConfiguration={OutputLocation=s3://${ATHENA_RESULT_BUCKET}/athena-results/},EnforceWorkGroupConfiguration=true,PublishCloudWatchMetricsEnabled=true" \
--profile "${AWS_PROFILE}"
すでに存在する場合はエラーになります。その場合は確認だけで問題ありません。
aws athena get-work-group \
--region "${AWS_REGION}" \
--work-group "${ATHENA_WORKGROUP}" \
--profile "${AWS_PROFILE}"
この workgroup では EnforceWorkGroupConfiguration=true を指定しています。これにより、Athena 実行時の query result location は workgroup 側の設定を使います。
Athena コンソールから実行する場合も、画面上部の workgroup で ${ATHENA_WORKGROUP} を選択してから SQL を実行します。
既存の primary workgroup を使っても動作しますが、PoC 用に分けておくと以下を確認しやすくなります。
- query result location
- 実行履歴
- Data scanned
- CloudWatch metrics
- 後片付け対象
Glue Database作成
実行ユーザー: 通常ユーザー
aws glue create-database \
--region "${AWS_REGION}" \
--database-input "{\"Name\":\"${GLUE_DATABASE}\"}" \
--profile "${AWS_PROFILE}"
すでに存在する場合はエラーになります。その場合は確認だけで問題ありません。
aws glue get-database \
--region "${AWS_REGION}" \
--name "${GLUE_DATABASE}" \
--profile "${AWS_PROFILE}"
IAM Policy
Spark から S3 と Glue にアクセスするための policy 例です。
実行ユーザー: 通常ユーザー
cat > iceberg-s3-glue-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCallerIdentity",
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity"
],
"Resource": "*"
},
{
"Sid": "AllowIcebergBucketList",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::${ICEBERG_BUCKET}",
"arn:aws:s3:::${ATHENA_RESULT_BUCKET}"
]
},
{
"Sid": "AllowIcebergBucketObjects",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": [
"arn:aws:s3:::${ICEBERG_BUCKET}/*",
"arn:aws:s3:::${ATHENA_RESULT_BUCKET}/*"
]
},
{
"Sid": "AllowGlueCatalogForLogs",
"Effect": "Allow",
"Action": [
"glue:GetDatabase",
"glue:GetDatabases",
"glue:CreateDatabase",
"glue:GetTable",
"glue:GetTables",
"glue:CreateTable",
"glue:UpdateTable",
"glue:DeleteTable",
"glue:GetPartition",
"glue:GetPartitions",
"glue:CreatePartition",
"glue:UpdatePartition",
"glue:DeletePartition"
],
"Resource": [
"arn:aws:glue:${AWS_REGION}:${AWS_ACCOUNT_ID}:catalog",
"arn:aws:glue:${AWS_REGION}:${AWS_ACCOUNT_ID}:database/${GLUE_DATABASE}",
"arn:aws:glue:${AWS_REGION}:${AWS_ACCOUNT_ID}:table/${GLUE_DATABASE}/*"
]
},
{
"Sid": "AllowAthenaQueryExecution",
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults",
"athena:GetWorkGroup",
"athena:ListQueryExecutions",
"athena:StopQueryExecution"
],
"Resource": [
"arn:aws:athena:${AWS_REGION}:${AWS_ACCOUNT_ID}:workgroup/${ATHENA_WORKGROUP}"
]
}
]
}
EOF
生成された policy の中身を確認します。
cat iceberg-s3-glue-policy.json
policy を作成します。
実行ユーザー: 通常ユーザー
aws iam create-policy \
--policy-name IcebergS3GlueLogsPolicy \
--policy-document file://iceberg-s3-glue-policy.json \
--profile "${AWS_PROFILE}"
作成した policy を IAM User または Role に付与します。
aws iam attach-user-policy \
--user-name "${IAM_USER_NAME}" \
--policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/IcebergS3GlueLogsPolicy" \
--profile "${AWS_PROFILE}"
EC2 など AWS 上で Spark を実行する場合は、IAM User の Access Key ではなく IAM Role を優先します。
同期用IAMユーザー設定
ここまでの S3 bucket、Glue database、Athena workgroup、IAM policy 作成は、初期構築用の管理者 profile で実行しました。
以降の Spark による日次同期は、同期用IAMユーザー home-iceberg-sync で実行します。
AWS IAM コンソールで home-iceberg-sync のアクセスキーを作成します。
実行ユーザー: 通常ユーザー
aws iam create-access-key \
--user-name "${IAM_USER_NAME}" \
--profile "${AWS_PROFILE}"
このコマンドでは Access Key ID と Secret Access Key が表示されます。
Secret Access Key が表示されるのは作成時だけなので、安全な場所に保管します。
記事やリポジトリには貼り付けません。
作成した Access Key ID / Secret Access Key を、spark ユーザーの同期用 profile として設定します。
実行ユーザー: 通常ユーザー
sudo install -d -m 700 -o spark -g spark /var/lib/spark/.aws
sudo -u spark -H aws configure --profile home-aws
設定する値です。
| 項目 | 値 |
|---|---|
| AWS Access Key ID |
home-iceberg-sync の Access Key ID |
| AWS Secret Access Key |
home-iceberg-sync の Secret Access Key |
| Default region name | ap-northeast-1 |
| Default output format | json |
/etc/iceberg/aws.env は最初から systemd / Spark 用に home-aws と /var/lib/spark/.aws/ を指しているため、ここで書き換える必要はありません。
Spark 実行時に使う profile を spark ユーザーで確認します。
sudo -u spark -H env \
AWS_PROFILE=home-aws \
AWS_SHARED_CREDENTIALS_FILE=/var/lib/spark/.aws/credentials \
AWS_CONFIG_FILE=/var/lib/spark/.aws/config \
aws sts get-caller-identity --profile home-aws
Arn が以下のように home-iceberg-sync になっていれば OK です。
arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:user/home-iceberg-sync
AWS疎通確認
以降の Spark 手動作業では、spark ユーザーの shell で同期用 profile を有効にします。
実行ユーザー: 通常ユーザー
source /etc/profile.d/iceberg-s3-athena.sh
use_iceberg_aws_sync
S3 へアクセスできることを確認します。
aws s3 ls s3://${ICEBERG_BUCKET} --profile "${AWS_PROFILE}"
SparkへIceberg AWS関連JARを追加
既存環境では Iceberg Spark runtime を使っています。
iceberg-spark-runtime-3.5_2.12-1.10.1.jar
GlueCatalog と S3FileIO を使うには、Iceberg の AWS integration が必要です。Iceberg 1.10.1 では、AWS 関連の依存をまとめた artifact として次を利用します。
iceberg-aws-bundle-1.10.1.jar
不要な AWS SDK JAR を手当たり次第に追加せず、Iceberg の version に合わせた bundle を追加します。
実行ユーザー: 通常ユーザー
sudo mkdir -p /opt/spark/extra-jars
cd /opt/spark/extra-jars
sudo curl -LO \
https://repo1.maven.org/maven2/org/apache/iceberg/iceberg-aws-bundle/1.10.1/iceberg-aws-bundle-1.10.1.jar
sudo chmod 644 /opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar
既存の Iceberg runtime jar も同じディレクトリにあることを確認します。
ls -l /opt/spark/extra-jars/
AWS bundle の中に GlueCatalog が必要とする AWS SDK v2 の Glue class が含まれていることも確認します。
jar tf /opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar \
| grep 'software/amazon/awssdk/services/glue/model/EntityNotFoundException.class'
何も表示されない場合は、iceberg-aws-bundle ではない jar を置いている、ダウンロードに失敗して HTML などを保存している、または version が想定と違っている可能性があります。
jar として読めるかも確認します。
file /opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar
jar tf /opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar >/dev/null
既存Spark設定を壊さない
既存の hive_prod はそのまま残します。
追加するのは AWS 用の glue_prod です。
hive_prod = 自宅HDFS Iceberg
glue_prod = AWS S3 Iceberg
既存の /usr/local/bin/spark-sql-iceberg も可能な限りそのまま使います。
Spark Catalog設定
/etc/spark/conf/spark-defaults.conf に glue_prod を追加します。
実行ユーザー: 通常ユーザー
sudo cp -a \
/etc/spark/conf/spark-defaults.conf \
/etc/spark/conf/spark-defaults.conf.$(date +%Y%m%d%H%M%S).bak
sudo tee -a /etc/spark/conf/spark-defaults.conf > /dev/null <<EOF
# AWS Glue Data Catalog + S3 Iceberg
spark.sql.catalog.glue_prod=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.glue_prod.catalog-impl=org.apache.iceberg.aws.glue.GlueCatalog
spark.sql.catalog.glue_prod.io-impl=org.apache.iceberg.aws.s3.S3FileIO
spark.sql.catalog.glue_prod.warehouse=s3://${ICEBERG_BUCKET}/warehouse
spark.sql.catalog.glue_prod.client.region=${AWS_REGION}
EOF
ICEBERG_BUCKET と AWS_REGION は、先に設定した変数の値に展開されます。
既存の spark.driver.extraClassPath / spark.executor.extraClassPath が単一 jar だけを指定している場合は、AWS bundle も含めます。
例です。
spark.driver.extraClassPath=/opt/spark/extra-jars/iceberg-spark-runtime-3.5_2.12-1.10.1.jar:/opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar
spark.executor.extraClassPath=/opt/spark/extra-jars/iceberg-spark-runtime-3.5_2.12-1.10.1.jar:/opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar
本記事では、既存ラッパーは変更せず、AWS 用ラッパーを別名で作ります。
既存の自宅 HDFS Iceberg 用コマンドを壊さずに戻せるようにするためです。
AWS 用ラッパーは内部で spark ユーザーに切り替えてから Spark SQL を実行します。
そのため、Spark の local work directory は spark ユーザーが書き込める場所にします。
実行ユーザー: 通常ユーザー
sudo install -d -m 700 -o spark -g spark /var/lib/spark/work
sudo cp -a \
/etc/spark/conf/spark-env.sh \
/etc/spark/conf/spark-env.sh.$(date +%Y%m%d%H%M%S).bak
if grep -q '^export SPARK_LOCAL_DIRS=' /etc/spark/conf/spark-env.sh; then
sudo sed -i \
's#^export SPARK_LOCAL_DIRS=.*#export SPARK_LOCAL_DIRS="${SPARK_LOCAL_DIRS:-/var/lib/spark/work}"#' \
/etc/spark/conf/spark-env.sh
else
echo 'export SPARK_LOCAL_DIRS="${SPARK_LOCAL_DIRS:-/var/lib/spark/work}"' \
| sudo tee -a /etc/spark/conf/spark-env.sh > /dev/null
fi
grep -n 'SPARK_LOCAL_DIRS' /etc/spark/conf/spark-env.sh
実行ユーザー: 通常ユーザー
sudo tee /usr/local/bin/spark-sql-iceberg-aws > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH="${JAVA_HOME}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export SPARK_HOME=/opt/spark/current
export SPARK_CONF_DIR=/etc/spark/conf
export HADOOP_CONF_DIR=/etc/hadoop/conf
export HIVE_CONF_DIR=/etc/hive/conf
export AWS_REGION="${AWS_REGION:-ap-northeast-1}"
export AWS_PROFILE="${AWS_PROFILE:-home-aws}"
export AWS_SHARED_CREDENTIALS_FILE="${AWS_SHARED_CREDENTIALS_FILE:-${HOME}/.aws/credentials}"
export AWS_CONFIG_FILE="${AWS_CONFIG_FILE:-${HOME}/.aws/config}"
export SPARK_LOCAL_DIRS="${SPARK_LOCAL_DIRS:-/var/lib/spark/work}"
mkdir -p "${SPARK_LOCAL_DIRS}"
chmod 700 "${SPARK_LOCAL_DIRS}"
exec /opt/spark/current/bin/spark-sql \
--driver-class-path /opt/spark/extra-jars/iceberg-spark-runtime-3.5_2.12-1.10.1.jar:/opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar \
--jars /opt/spark/extra-jars/iceberg-spark-runtime-3.5_2.12-1.10.1.jar,/opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar \
--conf spark.driver.memory=1g \
--conf spark.executor.memory=512m \
--conf spark.executor.cores=1 \
--conf spark.cores.max=1 \
--conf spark.local.dir="${SPARK_LOCAL_DIRS}" \
--conf spark.driver.extraClassPath=/opt/spark/extra-jars/iceberg-spark-runtime-3.5_2.12-1.10.1.jar:/opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar \
--conf spark.executor.extraClassPath=/opt/spark/extra-jars/iceberg-spark-runtime-3.5_2.12-1.10.1.jar:/opt/spark/extra-jars/iceberg-aws-bundle-1.10.1.jar \
--conf "spark.executorEnv.AWS_REGION=${AWS_REGION}" \
--conf "spark.executorEnv.AWS_PROFILE=${AWS_PROFILE}" \
--conf "spark.executorEnv.AWS_SHARED_CREDENTIALS_FILE=${AWS_SHARED_CREDENTIALS_FILE}" \
--conf "spark.executorEnv.AWS_CONFIG_FILE=${AWS_CONFIG_FILE}" \
--conf "spark.driver.extraJavaOptions=-Daws.profile=${AWS_PROFILE} -Daws.region=${AWS_REGION}" \
--conf "spark.executor.extraJavaOptions=-Daws.profile=${AWS_PROFILE} -Daws.region=${AWS_REGION}" \
--conf spark.sql.shuffle.partitions=1 \
--conf spark.sql.parquet.enableDictionary=false \
"$@"
EOF
sudo chmod 755 /usr/local/bin/spark-sql-iceberg-aws
以降は SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws として使います。
作成した wrapper が Iceberg runtime と AWS bundle の両方を参照していることを確認します。
grep -n 'iceberg-.*1.10.1.jar' /usr/local/bin/spark-sql-iceberg-aws
ここで iceberg-spark-runtime-3.5_2.12-1.10.1.jar と iceberg-aws-bundle-1.10.1.jar の両方が表示される必要があります。
GlueCatalog の初期化は Spark SQL CLI の driver 側で行われるため、--jars だけでなく --driver-class-path と spark.driver.extraClassPath にも同じ jar を明示します。
software.amazon.awssdk.services.glue.model.EntityNotFoundException が見つからない場合は、AWS bundle の jar 自体が正常でも driver classpath に載っていない可能性があります。
S3 への実データ書き込みは executor 側で行われるため、AWS profile と credential file の場所も spark.executorEnv.* で executor に渡します。
Unable to load credentials ... ProfileCredentialsProvider(profileName=default) が出る場合は、driver ではなく executor 側に AWS_PROFILE=home-aws が伝わっていない可能性があります。
Glue Namespace確認
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg-aws <<EOF
SHOW NAMESPACES IN glue_prod;
CREATE NAMESPACE IF NOT EXISTS glue_prod.${GLUE_DATABASE};
SHOW NAMESPACES IN glue_prod;
EOF
AWS CLI でも確認します。
実行ユーザー: spark
aws glue get-database \
--region "${AWS_REGION}" \
--name "${GLUE_DATABASE}" \
--profile "${AWS_PROFILE}"
自宅側schema確認
まず自宅側の2テーブルを確認します。
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg <<'EOF'
DESCRIBE TABLE hive_prod.logs.syslog_iceberg;
DESCRIBE TABLE hive_prod.logs.authlog_iceberg;
SHOW CREATE TABLE hive_prod.logs.syslog_iceberg;
SHOW CREATE TABLE hive_prod.logs.authlog_iceberg;
EOF
ここで確認するポイントです。
- カラム名
- 型
-
dtの型 -
tsの型 - partition spec
- table properties
format-version
既存記事の例では、どちらも PARTITIONED BY (dt, host)、format-version=2、write.distribution-mode=hash です。
syslog_icebergをAWS側へ作成
まずは syslog_iceberg だけで疎通確認します。
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg-aws <<EOF
CREATE TABLE IF NOT EXISTS glue_prod.${GLUE_DATABASE}.syslog_iceberg (
host string,
ts TIMESTAMP_NTZ,
severity int,
program string,
msg string,
dt date,
hr int
)
USING iceberg
LOCATION 's3://${ICEBERG_BUCKET}/warehouse/${GLUE_DATABASE}/syslog_iceberg'
PARTITIONED BY (dt, host)
TBLPROPERTIES (
'format-version'='2',
'write.distribution-mode'='hash',
'write.parquet.compression-codec'='zstd'
);
EOF
ここから先の例は、直前に確認した自宅側の定義が上記と同じである前提で進めます。
自宅側が TIMESTAMP_NTZ ではない場合やカラムが増えている場合は、この時点で AWS 側の CREATE TABLE を自宅側に合わせて修正してから進めます。
syslogを1日分コピー
ここでは 2026-08-18 を例にします。
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg-aws <<EOF
DELETE FROM glue_prod.${GLUE_DATABASE}.syslog_iceberg
WHERE dt = DATE '2026-08-18';
INSERT INTO glue_prod.${GLUE_DATABASE}.syslog_iceberg
SELECT *
FROM hive_prod.logs.syslog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
DELETE してから INSERT することで、同じ日付を再実行しても二重登録されにくくなります。
syslog件数確認
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg-aws <<EOF
SELECT count(*) AS src_count
FROM hive_prod.logs.syslog_iceberg
WHERE dt = DATE '2026-08-18';
SELECT count(*) AS dest_count
FROM glue_prod.${GLUE_DATABASE}.syslog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
src_count と dest_count が一致すれば、1日分のコピーは成功です。
syslogのS3確認
実行ユーザー: spark
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/${GLUE_DATABASE}/syslog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
Iceberg テーブルとして、次のようなファイル群が存在することを確認します。
data/
metadata/
syslogのGlue確認
実行ユーザー: spark
aws glue get-table \
--region "${AWS_REGION}" \
--database-name "${GLUE_DATABASE}" \
--name syslog_iceberg \
--profile "${AWS_PROFILE}"
Glue 上に次の table が登録されていれば OK です。
Database: ${GLUE_DATABASE}
Table: syslog_iceberg
Athenaからsyslogを検索
Athena コンソールで ${ATHENA_WORKGROUP} を選択します。
この workgroup では、query result location として次を設定済みです。
s3://${ATHENA_RESULT_BUCKET}/athena-results/
Athena で実行します。
SELECT count(*)
FROM logs.syslog_iceberg;
以降の Athena SQL 例では GLUE_DATABASE=logs を前提にしています。GLUE_DATABASE を別名にした場合は、logs.syslog_iceberg や logs.authlog_iceberg の logs を実際の database 名に置き換えてください。
日付と host で集計します。
SELECT
host,
count(*) AS cnt
FROM logs.syslog_iceberg
WHERE dt = DATE '2026-08-18'
GROUP BY host
ORDER BY cnt DESC;
必要な列だけを選び、dt で絞り込むことでスキャン量を抑えます。
authlog_icebergをAWS側へ作成
syslog で確認できたら、次は authlog_iceberg です。
まず schema を確認します。
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg <<'EOF'
DESCRIBE TABLE hive_prod.logs.authlog_iceberg;
SHOW CREATE TABLE hive_prod.logs.authlog_iceberg;
EOF
AWS 側に table を作成します。
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg-aws <<EOF
CREATE TABLE IF NOT EXISTS glue_prod.${GLUE_DATABASE}.authlog_iceberg (
host string,
ts TIMESTAMP_NTZ,
severity int,
program string,
msg string,
dt date,
hr int
)
USING iceberg
LOCATION 's3://${ICEBERG_BUCKET}/warehouse/${GLUE_DATABASE}/authlog_iceberg'
PARTITIONED BY (dt, host)
TBLPROPERTIES (
'format-version'='2',
'write.distribution-mode'='hash',
'write.parquet.compression-codec'='zstd'
);
EOF
ここでも、自宅側の schema / partition spec と差分がある場合は AWS 側定義を合わせます。
authlogを1日分コピー
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg-aws <<EOF
DELETE FROM glue_prod.${GLUE_DATABASE}.authlog_iceberg
WHERE dt = DATE '2026-08-18';
INSERT INTO glue_prod.${GLUE_DATABASE}.authlog_iceberg
SELECT *
FROM hive_prod.logs.authlog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
authlog件数確認
実行ユーザー: spark
/usr/local/bin/spark-sql-iceberg-aws <<EOF
SELECT count(*) AS src_count
FROM hive_prod.logs.authlog_iceberg
WHERE dt = DATE '2026-08-18';
SELECT count(*) AS dest_count
FROM glue_prod.${GLUE_DATABASE}.authlog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
authlogのS3確認
実行ユーザー: spark
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/${GLUE_DATABASE}/authlog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
data/ と metadata/ が作成されていることを確認します。
authlogのGlue確認
実行ユーザー: spark
aws glue get-table \
--region "${AWS_REGION}" \
--database-name "${GLUE_DATABASE}" \
--name authlog_iceberg \
--profile "${AWS_PROFILE}"
Glue 上に次の table が登録されていれば OK です。
Database: ${GLUE_DATABASE}
Table: authlog_iceberg
Athenaからauthlogを検索
Athena コンソールで ${ATHENA_WORKGROUP} が選択されていることを確認します。
Athena で実行します。
SELECT count(*)
FROM logs.authlog_iceberg;
host 別に集計します。
SELECT
host,
count(*) AS cnt
FROM logs.authlog_iceberg
WHERE dt = DATE '2026-08-18'
GROUP BY host
ORDER BY cnt DESC;
program 別に集計します。
SELECT
program,
count(*) AS cnt
FROM logs.authlog_iceberg
WHERE dt = DATE '2026-08-18'
GROUP BY program
ORDER BY cnt DESC;
SSH ログが含まれる場合は、sshd を対象に確認します。
SELECT
host,
count(*) AS cnt
FROM logs.authlog_iceberg
WHERE dt = DATE '2026-08-18'
AND program = 'sshd'
GROUP BY host
ORDER BY cnt DESC;
Athenaスキャン量確認
Athena は query 実行後に Data scanned と Execution time を確認できます。
比較用に、まず広いクエリを実行します。
SELECT *
FROM logs.syslog_iceberg;
次に、列と日付を絞ります。
SELECT host
FROM logs.syslog_iceberg
WHERE dt = DATE '2026-08-18';
確認する観点です。
-
Data scannedがどれくらい減ったか -
Execution timeがどう変わったか -
dtによる partition pruning が効いているか - 必要な列だけ読むことで Parquet の列指向が効いているか
authlog_iceberg でも同じ傾向を確認します。
SELECT *
FROM logs.authlog_iceberg;
SELECT host
FROM logs.authlog_iceberg
WHERE dt = DATE '2026-08-18';
自宅Trinoとの比較
同じ日付で、自宅 Trino と Athena を比較します。
自宅 Trino:
SELECT
host,
count(*) AS cnt
FROM iceberg.logs.syslog_iceberg
WHERE dt = DATE '2026-08-18'
GROUP BY host
ORDER BY cnt DESC;
Athena:
SELECT
host,
count(*) AS cnt
FROM logs.syslog_iceberg
WHERE dt = DATE '2026-08-18'
GROUP BY host
ORDER BY cnt DESC;
比較表です。
| 項目 | 自宅 HDFS + Iceberg + Trino | AWS S3 + Iceberg + Athena |
|---|---|---|
| 実行時間 | 自宅サーバー性能に依存 | Athena の実行状況に依存 |
| スキャン量表示 | Trino 側の統計で確認 | Athena の Data scanned で確認 |
| CPU / I/O | 自宅側で消費 | AWS 側で消費 |
| サーバー管理 | Trino / JVM / OS 管理が必要 | Athena はサーバーレス |
| 料金 | 電気代 / 機材 / 運用負荷 | S3 / Athena / Glue 料金 |
| 検索開始まで | 自宅環境に接続が必要 | AWS Console / Athena から実行可能 |
| データ転送 | 不要 | 日次同期が必要 |
同期スクリプト
手動実行、日次実行、systemd timer からの実行を、1つの入口に統合します。
ファイル:
/usr/local/bin/export-iceberg-to-s3
実行ユーザー: 通常ユーザー
sudo tee /usr/local/bin/export-iceberg-to-s3 > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
ENV_FILE=${ENV_FILE:-/etc/iceberg/aws.env}
if [ -r "${ENV_FILE}" ]; then
set -a
. "${ENV_FILE}"
set +a
fi
SOURCE_CATALOG=${SOURCE_CATALOG:-hive_prod}
DEST_CATALOG=${DEST_CATALOG:-glue_prod}
SOURCE_DB=${SOURCE_DB:-logs}
DEST_DB=${DEST_DB:-${GLUE_DATABASE:-logs}}
DT=${DT:-$(date -d yesterday +%F)}
SPARK_SQL_BIN=${SPARK_SQL_BIN:-/usr/local/bin/spark-sql-iceberg-aws}
SPARK_RUN_USER=${SPARK_RUN_USER:-spark}
SPARK_RUN_LOCAL_DIRS=${SPARK_RUN_LOCAL_DIRS:-/var/lib/spark/work}
if [ "$(id -un)" != "${SPARK_RUN_USER}" ]; then
export ENV_FILE
export SOURCE_CATALOG
export DEST_CATALOG
export SOURCE_DB
export DEST_DB
export GLUE_DATABASE="${GLUE_DATABASE:-}"
export TABLE="${TABLE:-}"
export TABLES="${TABLES:-}"
export DT
export SPARK_SQL_BIN
export AWS_REGION
export AWS_PROFILE="${AWS_SYNC_PROFILE:-${AWS_PROFILE:-}}"
export AWS_SHARED_CREDENTIALS_FILE="${AWS_SYNC_SHARED_CREDENTIALS_FILE:-${AWS_SHARED_CREDENTIALS_FILE:-}}"
export AWS_CONFIG_FILE="${AWS_SYNC_CONFIG_FILE:-${AWS_CONFIG_FILE:-}}"
export SPARK_LOCAL_DIRS="${SPARK_RUN_LOCAL_DIRS}"
exec sudo -E -u "${SPARK_RUN_USER}" -H "$0" "$@"
fi
if [ -z "${AWS_PROFILE:-}" ] && [ -n "${AWS_SYNC_PROFILE:-}" ]; then
AWS_PROFILE="${AWS_SYNC_PROFILE}"
fi
if [ -z "${AWS_SHARED_CREDENTIALS_FILE:-}" ] && [ -n "${AWS_SYNC_SHARED_CREDENTIALS_FILE:-}" ]; then
AWS_SHARED_CREDENTIALS_FILE="${AWS_SYNC_SHARED_CREDENTIALS_FILE}"
fi
if [ -z "${AWS_CONFIG_FILE:-}" ] && [ -n "${AWS_SYNC_CONFIG_FILE:-}" ]; then
AWS_CONFIG_FILE="${AWS_SYNC_CONFIG_FILE}"
fi
export AWS_PROFILE
export AWS_SHARED_CREDENTIALS_FILE
export AWS_CONFIG_FILE
export AWS_REGION
: "${AWS_PROFILE:?AWS_PROFILE is required}"
: "${AWS_SHARED_CREDENTIALS_FILE:?AWS_SHARED_CREDENTIALS_FILE is required}"
: "${AWS_CONFIG_FILE:?AWS_CONFIG_FILE is required}"
: "${AWS_REGION:?AWS_REGION is required}"
if [ -n "${TABLE:-}" ]; then
TABLES=("${TABLE}")
else
read -r -a TABLES <<< "${TABLES:-syslog_iceberg authlog_iceberg}"
fi
log() {
echo "[INFO] $(date '+%F %T') $*"
}
err() {
echo "[ERROR] $(date '+%F %T') $*" >&2
}
extract_last_integer() {
awk '
{
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
if ($0 ~ /^[0-9]+$/) val=$0
}
END {
if (val == "") exit 1
print val
}
'
}
run_count() {
local sql="$1"
local out
out="$("${SPARK_SQL_BIN}" -S -e "${sql}")"
printf '%s\n' "${out}" | extract_last_integer
}
export_table() {
local table="$1"
local src_table="${SOURCE_CATALOG}.${SOURCE_DB}.${table}"
local dest_table="${DEST_CATALOG}.${DEST_DB}.${table}"
local src_count
local dest_count
log "start export: TABLE=${table}, DT=${DT}"
log "SOURCE=${src_table}"
log "DEST=${dest_table}"
src_count="$(run_count "SELECT count(*) FROM ${src_table} WHERE dt = DATE '${DT}'")"
log "source count=${src_count}"
"${SPARK_SQL_BIN}" <<SQL
DELETE FROM ${dest_table}
WHERE dt = DATE '${DT}';
INSERT INTO ${dest_table}
SELECT *
FROM ${src_table}
WHERE dt = DATE '${DT}';
SQL
dest_count="$(run_count "SELECT count(*) FROM ${dest_table} WHERE dt = DATE '${DT}'")"
log "dest count=${dest_count}"
if [ "${src_count}" != "${dest_count}" ]; then
err "count mismatch: TABLE=${table}, source=${src_count}, dest=${dest_count}"
exit 1
fi
log "completed successfully: TABLE=${table}"
}
main() {
local table
log "daily export start: DT=${DT}"
for table in "${TABLES[@]}"; do
export_table "${table}"
done
log "daily export completed successfully: DT=${DT}"
}
main "$@"
EOF
sudo chmod 755 /usr/local/bin/export-iceberg-to-s3
この wrapper は通常ユーザーから起動できますが、内部で spark ユーザーとして自分自身を再実行します。
そのため、通常ユーザーからパスワードなしでこの wrapper だけを spark ユーザーとして実行できるように sudoers を設定します。
実行ユーザー: 通常ユーザー
sudo visudo -f /etc/sudoers.d/iceberg-export
your-user ALL=(spark) NOPASSWD: SETENV: /usr/local/bin/export-iceberg-to-s3
your-user は実際に手動実行する OS ユーザー名に置き換えます。
SETENV は wrapper から AWS_PROFILE や DT などを spark ユーザー側へ渡すために必要です。
systemd service から通常ユーザーで起動する場合も、この sudoers 設定を使います。
このスクリプトでは、エクスポート前に自宅側 Iceberg の対象日付件数を取得し、DELETE + INSERT 後に AWS 側 Iceberg の対象日付件数を取得します。
件数が一致しない場合は count mismatch として終了コード 1 で失敗させます。
対象テーブルは日次または手動バッチでのみ更新する前提です。
同じ DT に対する通常同期、手動再実行、復旧用リストアを同時に動かさないことで、DELETE + INSERT の競合を避けます。
確認している内容です。
source count = hive_prod.logs.<table> の対象 DT 件数
dest count = glue_prod.<GLUE_DATABASE>.<table> の対象 DT 件数
実行ログでは次のように確認できます。
[INFO] source count=12345
[INFO] dest count=12345
[INFO] completed successfully
実行例です。
実行ユーザー: 通常ユーザー
DT=2026-08-18 export-iceberg-to-s3
DT 未指定時は前日を対象にします。
export-iceberg-to-s3
通常は syslog_iceberg と authlog_iceberg を順番に同期します。
片方だけ再実行したい場合は TABLE を指定します。
TABLE=authlog_iceberg DT=2026-08-18 export-iceberg-to-s3
手動実行は foreground の Spark SQL job として動くため、SSH セッションをログオフすると途中で停止する可能性があります。
長時間実行する場合やログオフ後も継続したい場合は、後続の systemd service / timer から実行します。
冪等性確認
同じ日付で2回実行します。
実行ユーザー: 通常ユーザー
TABLE=syslog_iceberg \
DT=2026-08-18 \
export-iceberg-to-s3
TABLE=syslog_iceberg \
DT=2026-08-18 \
export-iceberg-to-s3
再実行後も件数が増えていなければ OK です。
SELECT count(*)
FROM glue_prod.${GLUE_DATABASE}.syslog_iceberg
WHERE dt = DATE '2026-08-18';
authlog_iceberg も同じ方法で確認します。Spark SQL に直接貼り付ける場合は、${GLUE_DATABASE} を実際の database 名に置き換えてください。
失敗時の扱い
PoC では、各 table を独立して再実行できる構成にしています。
例えば次の状態になったとします。
syslog成功
authlog失敗
この場合は、失敗した authlog_iceberg だけを再実行します。
実行ユーザー: 通常ユーザー
TABLE=authlog_iceberg \
DT=2026-08-18 \
export-iceberg-to-s3
DELETE + INSERT 方式なので、同じ DT を指定して再実行できます。
ただし、同じ DT の同期 job を並列実行しないことが前提です。
手動再実行する場合は、systemd timer の実行中でないこと、また復旧用リストアを同時に実行していないことを確認します。
自動実行
既存環境への影響が少ないように、systemd timer で日次実行します。
実行ユーザー: 通常ユーザー
SERVICE_USER="$(id -un)"
SERVICE_GROUP="$(id -gn)"
sudo tee /etc/systemd/system/export-logs-to-s3.service > /dev/null <<EOF
[Unit]
Description=Export Iceberg logs to S3
[Service]
Type=oneshot
User=${SERVICE_USER}
Group=${SERVICE_GROUP}
EnvironmentFile=/etc/iceberg/aws.env
Environment=SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws
ExecStart=/usr/local/bin/export-iceberg-to-s3
EOF
sudo tee /etc/systemd/system/export-logs-to-s3.timer > /dev/null <<'EOF'
[Unit]
Description=Daily export Iceberg logs to S3
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now export-logs-to-s3.timer
実行時刻 03:30 は例です。自宅側の curated / Iceberg 取り込みが完了した後の時刻に変更します。
確認します。
systemctl list-timers export-logs-to-s3.timer
sudo systemctl status export-logs-to-s3.timer --no-pager
systemd から起動した job はログインセッションに紐づかないため、SSH をログオフしても継続します。
また、User=${SERVICE_USER} として登録しているため、root や spark ユーザーではなく通常ユーザーの systemd job として実行されます。
一方で、export-iceberg-to-s3 を SSH 上で直接実行した場合は、その SSH セッションに紐づく foreground process になるため、ログオフで停止する可能性があります。
手動実行する場合です。
sudo systemctl start export-logs-to-s3.service
journalctl -u export-logs-to-s3.service -n 100 --no-pager
Athena / Iceberg特有の注意点
Athena の Iceberg 対応状況は、必ず AWS 公式ドキュメントで確認します。
記事執筆時点で確認したい観点です。
- Athena が対応している Iceberg table format version
- Athena で利用できる Iceberg の DML
- Athena で利用できない Iceberg 機能
- timestamp 型の扱い
-
TIMESTAMP_NTZとの互換性 - Glue Catalog と Iceberg metadata の役割分担
記事執筆時点の AWS 公式ドキュメントでは、主に次の点に注意します。
| 項目 | 確認ポイント |
|---|---|
| Iceberg version support | Athena 側でサポートされる Iceberg バージョンが明記されているため、Spark 側 Iceberg 1.10.1 で作成したテーブルを Athena で実際に読めるか確認する |
| Iceberg v2 | Athena は Iceberg v2 テーブルを作成・操作するため、AWS 側テーブルは format-version=2 を基本にする |
| Catalog | Athena から利用する Iceberg テーブルは Glue Catalog に登録されている必要がある |
| File format | Athena engine version 3 では Iceberg の Parquet / ORC / Avro が対象。今回の data file は Parquet |
| timestamp | Athena の timestamp はミリ秒精度の扱いに注意する |
| timestamp without time zone | time / timestamp without time zone の表示やフィルタ条件で UTC として扱われる場面があるため、実データで時刻ずれを確認する |
| Unsupported operation |
ALTER TABLE SET LOCATION など、Athena で未対応の Iceberg 操作がある |
Spark 側では TIMESTAMP_NTZ を使っていても、Athena 側では timestamp without time zone としてどう見えるかを確認します。
Athena から確認する SQL 例です。
SELECT
ts,
CAST(ts AS varchar) AS ts_text,
dt,
hr
FROM logs.syslog_iceberg
WHERE dt = DATE '2026-08-18'
ORDER BY ts DESC
LIMIT 20;
自宅側 Spark / Trino で見た時刻と Athena で見た時刻に差がないか確認します。
過去に Hive Parquet の TIMESTAMP と Spark の spark.sql.session.timeZone の組み合わせで時刻差異が出たことがあるため、ここは推測で済ませず、実データで確認します。
Iceberg の DELETE / UPDATE / MERGE は engine 側の対応状況にも依存します。Spark で書き込み、Athena で分析する構成では、更新系処理をどちらの engine が担当するかを明確にします。
今回の PoC では、AWS 側 Iceberg への書き込みは Spark が担当し、Athena は主に参照に使います。
また、syslog_iceberg / authlog_iceberg は日次または手動バッチ以外では更新しない前提にします。
Flink や別の常駐 Spark job などが同じテーブルへ継続的に書き込む構成にする場合は、DELETE + INSERT の同時実行制御や競合時の扱いを別途設計します。
参考:
- Amazon Athena Iceberg tables: https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg.html
- Apache Iceberg AWS integration: https://iceberg.apache.org/docs/latest/aws/
- Apache Iceberg Spark writes: https://iceberg.apache.org/docs/latest/spark-writes/
Iceberg maintenance
AWS 側 Iceberg も、運用が続くと metadata や小さい file が増えます。
今後の運用課題として、次を検討します。
expire_snapshotsremove_orphan_filesrewrite_data_files- compaction
構築編では必須実装にしませんが、日次同期が安定した後に追加します。
コスト確認
Athena 実行後に Data scanned を確認します。
概算式です。
Athenaクエリ料金 = スキャン量 × Athenaの1TBあたり料金
料金単価は変わる可能性があるため、必ず AWS 公式料金ページを確認します。
S3 側の保存容量は次で確認できます。
実行ユーザー: 通常ユーザー
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/ \
--recursive \
--summarize \
--profile "${AWS_PROFILE}"
確認する料金項目です。
- S3 保存料金
- S3 API request
- Athena Data scanned
- Athena query result 保存容量
- Glue Data Catalog
最新料金は AWS 公式料金ページを確認してください。
参考:
- Amazon Athena Pricing: https://aws.amazon.com/athena/pricing/
- Amazon S3 Pricing: https://aws.amazon.com/s3/pricing/
- AWS Glue Pricing: https://aws.amazon.com/glue/pricing/
AWS側の最終確認
S3:
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/${GLUE_DATABASE}/syslog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/${GLUE_DATABASE}/authlog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
Glue:
aws glue get-table \
--region "${AWS_REGION}" \
--database-name "${GLUE_DATABASE}" \
--name syslog_iceberg \
--profile "${AWS_PROFILE}"
aws glue get-table \
--region "${AWS_REGION}" \
--database-name "${GLUE_DATABASE}" \
--name authlog_iceberg \
--profile "${AWS_PROFILE}"
Athena:
SELECT count(*) FROM logs.syslog_iceberg;
SELECT count(*) FROM logs.authlog_iceberg;
Workgroup:
aws athena get-work-group \
--region "${AWS_REGION}" \
--work-group "${ATHENA_WORKGROUP}" \
--profile "${AWS_PROFILE}"
最終構成図
後片付け
検証終了後、不要な料金が継続しないように削除します。
以下の記事のAWS環境を削除する参考に不要なものを削除してください。
まとめ
これで、自宅 HDFS 上の Iceberg テーブルを AWS S3 側の Iceberg テーブルへ複製し、Glue Data Catalog と Athena から分析できるようになりました。
今回作った最終形です。
hive_prod.logs.syslog_iceberg
hive_prod.logs.authlog_iceberg
↓
Spark + Iceberg
↓
glue_prod.${GLUE_DATABASE}.syslog_iceberg
glue_prod.${GLUE_DATABASE}.authlog_iceberg
↓
Amazon S3
↓
AWS Glue Data Catalog
↓
Amazon Athena
↓
Dedicated Athena Workgroup
次の検証テーマは、同じログを対象にした比較です。
自宅 HDFS + Iceberg + Trino
vs
AWS S3 + Iceberg + Athena
性能、運用負荷、コスト、検索開始までの手軽さを比較すると、自宅ラボとクラウド分析環境の使い分けが見えてきそうです。
AWS 側 Iceberg コピーを継続利用する場合は、snapshot、orphan file、compaction、S3 Versioning、Athena スキャン量の運用が必要になります。
Athena で分析できるようになったログを QuickSight / Amazon Quick で可視化する発展編はこちらです。
AWS 側 Iceberg コピーから自宅 HDFS Iceberg へ特定日データを戻す復旧手順はこちらです。