はじめに
前回の設計編では、自宅 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 が完全に同じであるとは決め打ちしません。
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
確認します。
aws --version
credential は、aws configure だけでなく AWS credential provider chain で解決されます。
代表例です。
- 環境変数
~/.aws/credentials~/.aws/config- AWS profile
- EC2 / ECS / EKS などの Role
ローカルや自宅サーバー上で実行する場合は、作業ユーザーで AWS profile を設定しておきます。
aws configure --profile home-aws
設定後、AWS アカウントへアクセスできることを確認します。
aws sts get-caller-identity --profile home-aws
Spark 実行ユーザーを spark にする場合は、spark ユーザーから aws コマンドと credential が使える必要があります。
まず、spark ユーザーで AWS CLI が実行できることを確認します。
実行ユーザー: 通常ユーザー
sudo -u spark -H aws --version
AWS CLI は /usr/local/bin/aws に入ることが多いため、spark ユーザーの PATH から見えない場合はフルパスでも確認します。
sudo -u spark -H /usr/local/bin/aws --version
次に、spark ユーザー側にも AWS profile を設定します。
実行ユーザー: 通常ユーザー
sudo -u spark -H aws configure --profile home-aws
すでに通常ユーザー側で設定済みの profile を spark ユーザーでも使いたい場合は、~/.aws を spark ユーザーの home 配下へ配置します。
sudo install -d -m 700 -o spark -g spark /home/spark/.aws
sudo cp ~/.aws/config /home/spark/.aws/config
sudo cp ~/.aws/credentials /home/spark/.aws/credentials
sudo chown -R spark:spark /home/spark/.aws
sudo chmod 600 /home/spark/.aws/config /home/spark/.aws/credentials
設定後、spark ユーザーで AWS アカウントへアクセスできることを確認します。
sudo -u spark -H aws sts get-caller-identity --profile home-aws
spark ユーザーとして直接ログインして確認する場合は、以下でも確認できます。
実行ユーザー: spark
aws sts get-caller-identity --profile home-aws
変数
AWS CLI の準備ができたら、以降のコマンドで使う変数を設定します。
新規に作成する名前と、既存 AWS アカウントの情報を入れる値が混在しているため、先に整理します。
| 変数 | 指定する値 | 種別 |
|---|---|---|
AWS_REGION |
利用する AWS リージョン。例では東京リージョン ap-northeast-1
|
任意に指定 |
ICEBERG_BUCKET |
Iceberg warehouse 用に新規作成する S3 bucket 名 | 新規に任意で指定 |
ATHENA_RESULT_BUCKET |
Athena query result 用に新規作成する S3 bucket 名 | 新規に任意で指定 |
GLUE_DATABASE |
AWS Glue Data Catalog に作成する database 名 | 新規に任意で指定 |
AWS_PROFILE |
AWS CLI で使う profile 名 | 既存設定または新規設定 |
AWS_ACCOUNT_ID |
利用する AWS アカウント ID | 既存 AWS アカウント情報 |
IAM_USER_NAME |
policy を付与する IAM User 名 | 既存 AWS アカウント情報 |
AWS_PROFILE には、AWS 上のリソース名ではなく、AWS CLI を実行する端末に設定した profile 名を指定します。
例えば、AWS CLI 準備で次のように設定した場合は、
aws configure --profile home-aws
変数には次のように指定します。
export AWS_PROFILE="home-aws"
profile 名は、作業端末の以下のファイルで確認できます。
cat ~/.aws/config
cat ~/.aws/credentials
~/.aws/config では、通常は次のように表示されます。
[profile home-aws]
region = ap-northeast-1
output = json
この場合、AWS_PROFILE に指定するのは profile を除いた home-aws です。
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 GLUE_DATABASE=logs
export AWS_PROFILE="home-aws"
export AWS_ACCOUNT_ID="123456789012"
export IAM_USER_NAME="your-iam-user-name"
your-name、123456789012、20260819、home-aws、your-iam-user-name は自分の環境に合わせて置き換えてください。
AWS アカウント ID は以下で確認できます。
aws sts get-caller-identity --profile "${AWS_PROFILE}"
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
必要であれば versioning も有効化します。
実行ユーザー: 通常ユーザー
aws s3api put-bucket-versioning \
--bucket "${ICEBERG_BUCKET}" \
--versioning-configuration Status=Enabled \
--profile "${AWS_PROFILE}"
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}/*"
]
}
]
}
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 に付与します。ここでは user 例です。
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 を優先します。
AWS疎通確認
実行ユーザー: spark
aws sts get-caller-identity --profile "${AWS_PROFILE}"
aws s3 ls --profile "${AWS_PROFILE}"
aws s3 ls s3://${ICEBERG_BUCKET} \
--profile "${AWS_PROFILE}"
環境変数で profile を指定しておく場合です。
export AWS_PROFILE="home-aws"
export AWS_REGION=ap-northeast-1
aws sts get-caller-identity
aws s3 ls s3://${ICEBERG_BUCKET}
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 を追加します。
実行ユーザー: root
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/
既存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 を追加します。
実行ユーザー: root
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=ap-northeast-1
EOF
ICEBERG_BUCKET は実際の bucket 名に置き換えます。
既存の 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
または、既存ラッパーに --jars を追加する方式でも構いません。
実行ユーザー: root
sudo cp -a \
/usr/local/bin/spark-sql-iceberg \
/usr/local/bin/spark-sql-iceberg.$(date +%Y%m%d%H%M%S).bak
既存ラッパーを大きく変更したくない場合は、次のように AWS 用ラッパーを別名で作ります。
実行ユーザー: root
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}"
exec /opt/spark/current/bin/spark-sql \
--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.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 として使います。
Glue Namespace確認
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
/usr/local/bin/spark-sql-iceberg-aws <<'EOF'
SHOW NAMESPACES IN glue_prod;
CREATE NAMESPACE IF NOT EXISTS glue_prod.logs;
SHOW NAMESPACES IN glue_prod;
EOF
AWS CLI でも確認します。
実行ユーザー: spark
aws glue get-database \
--region ap-northeast-1 \
--name logs \
--profile "${AWS_PROFILE}"
自宅側schema確認
まず自宅側の2テーブルを確認します。
実行ユーザー: spark
sudo -u 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
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
/usr/local/bin/spark-sql-iceberg-aws <<EOF
CREATE TABLE IF NOT EXISTS glue_prod.logs.syslog_iceberg (
host string,
ts TIMESTAMP_NTZ,
severity int,
program string,
msg string,
dt date,
hr int
)
USING iceberg
LOCATION 's3://${ICEBERG_BUCKET}/warehouse/logs/syslog_iceberg'
PARTITIONED BY (dt, host)
TBLPROPERTIES (
'format-version'='2',
'write.distribution-mode'='hash',
'write.parquet.compression-codec'='zstd'
);
EOF
schema や partition spec は、直前に確認した自宅側の定義に合わせてください。自宅側が TIMESTAMP_NTZ ではない場合やカラムが増えている場合は、AWS 側も合わせます。
syslogを1日分コピー
ここでは 2026-08-18 を例にします。
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
/usr/local/bin/spark-sql-iceberg-aws <<'EOF'
DELETE FROM glue_prod.logs.syslog_iceberg
WHERE dt = DATE '2026-08-18';
INSERT INTO glue_prod.logs.syslog_iceberg
SELECT *
FROM hive_prod.logs.syslog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
DELETE してから INSERT することで、同じ日付を再実行しても二重登録されにくくなります。
syslog件数確認
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
/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.logs.syslog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
src_count と dest_count が一致すれば、1日分のコピーは成功です。
syslogのS3確認
実行ユーザー: spark
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/logs/syslog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
Iceberg テーブルとして、次のようなファイル群が存在することを確認します。
data/
metadata/
syslogのGlue確認
実行ユーザー: spark
aws glue get-table \
--region ap-northeast-1 \
--database-name logs \
--name syslog_iceberg \
--profile "${AWS_PROFILE}"
Glue 上に次の table が登録されていれば OK です。
Database: logs
Table: syslog_iceberg
Athenaからsyslogを検索
Athena の query result location に次を設定します。
s3://${ATHENA_RESULT_BUCKET}/athena-results/
Athena で実行します。
SELECT count(*)
FROM logs.syslog_iceberg;
日付と 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
sudo -u 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
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
/usr/local/bin/spark-sql-iceberg-aws <<EOF
CREATE TABLE IF NOT EXISTS glue_prod.logs.authlog_iceberg (
host string,
ts TIMESTAMP_NTZ,
severity int,
program string,
msg string,
dt date,
hr int
)
USING iceberg
LOCATION 's3://${ICEBERG_BUCKET}/warehouse/logs/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
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
/usr/local/bin/spark-sql-iceberg-aws <<'EOF'
DELETE FROM glue_prod.logs.authlog_iceberg
WHERE dt = DATE '2026-08-18';
INSERT INTO glue_prod.logs.authlog_iceberg
SELECT *
FROM hive_prod.logs.authlog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
authlog件数確認
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
/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.logs.authlog_iceberg
WHERE dt = DATE '2026-08-18';
EOF
authlogのS3確認
実行ユーザー: spark
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/logs/authlog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
data/ と metadata/ が作成されていることを確認します。
authlogのGlue確認
実行ユーザー: spark
aws glue get-table \
--region ap-northeast-1 \
--database-name logs \
--name authlog_iceberg \
--profile "${AWS_PROFILE}"
Glue 上に次の table が登録されていれば OK です。
Database: logs
Table: authlog_iceberg
Athenaからauthlogを検索
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 から実行可能 |
| データ転送 | 不要 | 日次同期が必要 |
共通同期スクリプト
syslog / authlog 共通で使えるスクリプトを作ります。
ファイル:
/opt/iceberg/bin/export_iceberg_to_s3.sh
実行ユーザー: root
sudo mkdir -p /opt/iceberg/bin
sudo tee /opt/iceberg/bin/export_iceberg_to_s3.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
SOURCE_CATALOG=${SOURCE_CATALOG:-hive_prod}
DEST_CATALOG=${DEST_CATALOG:-glue_prod}
DB=${DB:-logs}
TABLE=${TABLE:?TABLE is required}
DT=${DT:-$(date -d yesterday +%F)}
SPARK_SQL_BIN=${SPARK_SQL_BIN:-/usr/local/bin/spark-sql-iceberg}
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
}
main() {
local src_table="${SOURCE_CATALOG}.${DB}.${TABLE}"
local dest_table="${DEST_CATALOG}.${DB}.${TABLE}"
local src_count
local dest_count
log "start export"
log "TABLE=${TABLE}"
log "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: source=${src_count}, dest=${dest_count}"
exit 1
fi
log "completed successfully"
}
main "$@"
EOF
sudo chmod 755 /opt/iceberg/bin/export_iceberg_to_s3.sh
sudo chown spark:spark /opt/iceberg/bin/export_iceberg_to_s3.sh
実行例です。
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws \
TABLE=syslog_iceberg \
DT=2026-08-18 \
/opt/iceberg/bin/export_iceberg_to_s3.sh
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws \
TABLE=authlog_iceberg \
DT=2026-08-18 \
/opt/iceberg/bin/export_iceberg_to_s3.sh
日次親スクリプト
syslog / authlog を順番に同期する親スクリプトを作ります。
ファイル:
/opt/iceberg/bin/export_logs_to_s3_daily.sh
実行ユーザー: root
sudo tee /opt/iceberg/bin/export_logs_to_s3_daily.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DT=${DT:-$(date -d yesterday +%F)}
EXPORT_BIN=${EXPORT_BIN:-/opt/iceberg/bin/export_iceberg_to_s3.sh}
SPARK_SQL_BIN=${SPARK_SQL_BIN:-/usr/local/bin/spark-sql-iceberg}
TABLES=(
syslog_iceberg
authlog_iceberg
)
log() {
echo "[INFO] $(date '+%F %T') $*"
}
log "daily export start: DT=${DT}"
for TABLE in "${TABLES[@]}"; do
log "export start: TABLE=${TABLE}"
TABLE="${TABLE}" \
DT="${DT}" \
SPARK_SQL_BIN="${SPARK_SQL_BIN}" \
"${EXPORT_BIN}"
log "export finished: TABLE=${TABLE}"
done
log "daily export completed successfully: DT=${DT}"
EOF
sudo chmod 755 /opt/iceberg/bin/export_logs_to_s3_daily.sh
sudo chown spark:spark /opt/iceberg/bin/export_logs_to_s3_daily.sh
実行例です。
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws \
DT=2026-08-18 \
/opt/iceberg/bin/export_logs_to_s3_daily.sh
DT 未指定時は前日を対象にします。
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws \
/opt/iceberg/bin/export_logs_to_s3_daily.sh
冪等性確認
同じ日付で2回実行します。
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws \
TABLE=syslog_iceberg \
DT=2026-08-18 \
/opt/iceberg/bin/export_iceberg_to_s3.sh
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws \
TABLE=syslog_iceberg \
DT=2026-08-18 \
/opt/iceberg/bin/export_iceberg_to_s3.sh
再実行後も件数が増えていなければ OK です。
SELECT count(*)
FROM glue_prod.logs.syslog_iceberg
WHERE dt = DATE '2026-08-18';
authlog_iceberg も同じ方法で確認します。
失敗時の扱い
PoC では、各 table を独立して再実行できる構成にしています。
例えば次の状態になったとします。
syslog成功
authlog失敗
この場合は、失敗した authlog_iceberg だけを再実行します。
実行ユーザー: spark
sudo -u spark \
AWS_PROFILE="${AWS_PROFILE}" \
AWS_REGION=ap-northeast-1 \
SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws \
TABLE=authlog_iceberg \
DT=2026-08-18 \
/opt/iceberg/bin/export_iceberg_to_s3.sh
DELETE + INSERT 方式なので、同じ DT を指定して再実行できます。
自動実行
既存環境への影響が少ないように、systemd timer で日次実行します。
実行ユーザー: root
sudo tee /etc/systemd/system/export-logs-to-s3.service > /dev/null <<'EOF'
[Unit]
Description=Export Iceberg logs to S3
[Service]
Type=oneshot
User=spark
Group=spark
Environment=AWS_PROFILE="${AWS_PROFILE}"
Environment=AWS_REGION=ap-northeast-1
Environment=SPARK_SQL_BIN=/usr/local/bin/spark-sql-iceberg-aws
ExecStart=/opt/iceberg/bin/export_logs_to_s3_daily.sh
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
手動実行する場合です。
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 は主に参照に使います。
参考:
- 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 側の保存容量は次で確認できます。
実行ユーザー: spark
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/logs/syslog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
aws s3 ls \
s3://${ICEBERG_BUCKET}/warehouse/logs/authlog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
Glue:
aws glue get-table \
--region ap-northeast-1 \
--database-name logs \
--name syslog_iceberg \
--profile "${AWS_PROFILE}"
aws glue get-table \
--region ap-northeast-1 \
--database-name logs \
--name authlog_iceberg \
--profile "${AWS_PROFILE}"
Athena:
SELECT count(*) FROM logs.syslog_iceberg;
SELECT count(*) FROM logs.authlog_iceberg;
最終構成図
後片付け
検証終了後、不要な料金が継続しないように削除します。
誤削除防止のため、bucket 名と database 名を必ず確認してから実行してください。
Athena query results を削除します。
実行ユーザー: 通常ユーザー
aws s3 rm \
s3://${ATHENA_RESULT_BUCKET}/athena-results/ \
--recursive \
--profile "${AWS_PROFILE}"
Glue table を削除します。
aws glue delete-table \
--region ap-northeast-1 \
--database-name logs \
--name syslog_iceberg \
--profile "${AWS_PROFILE}"
aws glue delete-table \
--region ap-northeast-1 \
--database-name logs \
--name authlog_iceberg \
--profile "${AWS_PROFILE}"
Glue database が不要であれば削除します。
aws glue delete-database \
--region ap-northeast-1 \
--name logs \
--profile "${AWS_PROFILE}"
S3 Iceberg data を削除します。
aws s3 rm \
s3://${ICEBERG_BUCKET}/warehouse/logs/syslog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
aws s3 rm \
s3://${ICEBERG_BUCKET}/warehouse/logs/authlog_iceberg/ \
--recursive \
--profile "${AWS_PROFILE}"
bucket 自体が不要であれば、空にしてから削除します。
aws s3 rb s3://${ICEBERG_BUCKET} \
--force \
--profile "${AWS_PROFILE}"
aws s3 rb s3://${ATHENA_RESULT_BUCKET} \
--force \
--profile "${AWS_PROFILE}"
不要な IAM User / Access Key / IAM Policy も削除します。Access Key を削除する前に、他の用途で使っていないことを確認してください。
まとめ
これで、自宅 HDFS 上の Iceberg テーブルを AWS S3 側の Iceberg テーブルへ複製し、Glue Data Catalog と Athena から分析できるようになりました。
今回作った最終形です。
hive_prod.logs.syslog_iceberg
hive_prod.logs.authlog_iceberg
↓
Spark + Iceberg
↓
glue_prod.logs.syslog_iceberg
glue_prod.logs.authlog_iceberg
↓
Amazon S3
↓
AWS Glue Data Catalog
↓
Amazon Athena
次の検証テーマは、同じログを対象にした比較です。
自宅 HDFS + Iceberg + Trino
vs
AWS S3 + Iceberg + Athena
性能、運用負荷、コスト、検索開始までの手軽さを比較すると、自宅ラボとクラウド分析環境の使い分けが見えてきそうです。
AWS 側 Iceberg コピーを継続利用する場合は、snapshot、orphan file、compaction、S3 Versioning、Athena スキャン量の運用が必要になります。
Athena で分析できるようになったログを QuickSight / Amazon Quick や Amazon Managed Grafana で可視化する発展編はこちらです。