0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自宅HDFS上のApache IcebergをAmazon S3へ複製しAthenaから分析してみる【構築編】

0
Last updated at Posted at 2026-08-19

はじめに

前回の設計編では、自宅 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_icebergauthlog_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 ユーザーでも使いたい場合は、~/.awsspark ユーザーの 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-name12345678901220260819home-awsyour-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.confglue_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=2write.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_countdest_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 scannedExecution 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 は主に参照に使います。

参考:


Iceberg maintenance

AWS 側 Iceberg も、運用が続くと metadata や小さい file が増えます。

今後の運用課題として、次を検討します。

  • expire_snapshots
  • remove_orphan_files
  • rewrite_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 公式料金ページを確認してください。

参考:


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 で可視化する発展編はこちらです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?