AWS Analytics/Data Lake本番運用Vol1|Glue・Athena・Redshift

目次

1. なぜAnalytics/Data Lake本番運用 Vol1 か — 5本柱選定の現実解

S3 Data Lakeを構築し終えた段階で、多くの現場チームが「次は何から手をつけるか」で立ち止まる。Glue・Athena・Redshift Serverless・Lake Formation・QuickSightという5つのサービスはいずれも機能が豊富で、組み合わせ次第でアーキテクチャが大きく変わる。本記事はStorage基盤(S3)× Analytics層(Glue/Athena/Redshift/QuickSight)× アクセス制御(Lake Formation)の三位一体を本番視点で横断網羅し、選定マトリクスとTerraform実装を提供する。

1-1. 読者像と前提

対象読者はS3 Data Lakeを構築済みで、Analytics層の本番判断で迷う運用エンジニアだ。Glue JobのDPU試算、AthenaのPartition設計、Redshift ServerlessのRPU上限、Lake FormationのCross-Account共有、QuickSightのSPICE管理——これらのいずれか1つでも「どう設定すべきか分からず経験則で進めた」という経験があれば、本記事が参照に値する。

前提知識はAWS IAM基礎(Role/Policy)とS3基本操作(バケット・プレフィックス・ライフサイクル)。TerraformはHCL構文が読める程度でよい。

1-2. 差別化軸 — Storage基盤×Analytics層×アクセス制御の三位一体

本記事の差別化軸 3点

  • S3 → Glue → Athena → Redshift Serverless のETLパイプラインを一本の文脈で解説。「どこでGlue Crawlerを使い、どこでPartition Projectionに切り替えるか」まで判断できる。
  • Lake Formation × IAM の役割分担 を明示。リソースベースポリシーとLF-Grantの競合を避ける設計パターンを提示。
  • Terraform実装+コスト試算をセットで提供。DPU/RPU/SPICE/スキャン料金を数字で比較しながら選定できる。

1-3. 5本柱の選定マトリクス

サービス主用途課金軸代替検討のトリガー
Glue (Spark Job)大規模バッチETLDPU × 秒処理量が少ない → Python Shell Job
Glue (Python Shell)軽量ETL・データ変換1/16 DPU × 秒処理量が多い → Spark Job
AthenaS3上のアドホックSQL$5/TB scanned常時クエリ → Redshift Serverless
Redshift Serverless高頻度・BI連携DWHRPU-hour低頻度 → Athena
Lake FormationData Lakeアクセス制御無料(IAM/RAM料金のみ)単一アカウント限定 → IAM Policyのみ
QuickSightBI・ダッシュボードSPICE GB/月 + ユーザー数組込BI → Embedded Analytics API

1-4. 本番現場の痛点5選

Analytics/Data Lake本番運用 痛点5選

  • Crawler設定ミスでData Catalog汚染: S3プレフィックスをまたいでCrawlerを走らせると想定外のテーブルが量産される。Exclude patternとTable prefix設定を必ず確認せよ。
  • Athenaスキャン量爆発 ($/TB scanned): CSV形式のまま全走査すると1クエリで数十ドルのコストが発生する。Parquet + Partition設計は構築初日から必須。
  • Redshift Serverless RPU爆発: Base RPUを高く設定すると空き時間も課金される。8 RPUから始めてメトリクスを見ながら調整する。
  • Lake Formation Cross-Account共有失敗: RAM Resource ShareとLF Grantの両方を設定しないとConsumer AccountからのアクセスがDeniedになる。手順の抜け漏れが起きやすい。
  • QuickSight SPICE上限超過: データセットがSPICE容量上限を超えると自動更新が静かに失敗する。容量試算と自動アラートを事前設定せよ。

1-5. 本記事のナビゲーション(§2〜§8)

本記事の構成一覧 (AWS本番運用 第17軸)

  • §2 Glue本番運用: Job種別選定(Spark/Python Shell/Streaming ETL)・Crawler vs Partition Projection・Data Catalog・DataBrew・Glue Studio・Terraform実装
  • §3 Athena本番運用: Parquet/Snappy必須・Partition Projection・CTAS・Federated Query・Workgroupコスト制御・Terraform実装
  • §4 Redshift Serverless本番運用: Base RPU設計・Namespace/Workgroup分離・Concurrency Scaling・Spectrum・Terraform実装
  • §5 Lake Formation本番運用: LF-Tag-Based Access Control・Cross-Account Sharing・Data Filter(Row/Column)・Terraform実装
  • §6 QuickSight本番運用: SPICE管理・Embedded Analytics・Q for Natural Language
  • §7 詰まりポイント7選 + アンチパターン演習5問: 現場で繰り返される失敗と正解パターン
  • §8 まとめ: Vol2予告(EMR/OpenSearch/Kinesis/DataZone)+ 落とし穴10選 + 全17軸クロスリンク

関連 (前作 第16軸): Edge/CDN本番運用 Vol1

関連 (Storage基盤): Storage本番運用 Vol1

Analytics/Data Lake 5本柱 全体アーキテクチャ


2. Glue本番運用 — Job × Crawler × Data Catalog × DataBrew × Studio × Streaming ETL

GlueはETLサービスとして紹介されることが多いが、本番環境ではJob実行エンジン・メタデータ管理(Data Catalog)・ノーコードETL(DataBrew/Studio)・ストリーミング処理(Streaming ETL)と複数の顔を持つ。それぞれの使い分けを理解せずに導入すると、コストが想定の3〜5倍になったり、Data Catalogが混乱したりする。

2-1. Glue Job 種別選定 — Spark / Python Shell / Streaming ETL

2-1-1. Spark Job (G.1X / G.2X / G.025X)

Spark Jobは大規模バッチETLの主力だ。Worker Typeによってリソースが変わる。

Worker TypevCPUメモリDPU換算用途
G.1X4 vCPU16 GB1 DPU標準バッチ処理
G.2X8 vCPU32 GB2 DPU大規模データ・Join重視
G.025X2 vCPU4 GB0.25 DPU小規模・テスト用

DPU課金は $0.44/DPU-hour(東京リージョン)。最低課金は10分。

# DPU試算例: G.1X × 10 Workers × 1時間
# 10 DPU × $0.44 × 1h = $4.40 / 実行
# 月100回実行 → 約$440/月

Job Bookmarkを必ず有効化すること。 増分処理に対応し、再実行時の重複処理を防ぐ。無効のままだと再実行のたびに全件処理となり、コストが線形増加する。

2-1-2. Python Shell Job (1/16 DPU固定)

Python Shell JobはSpark環境を使わない軽量ETLだ。DPUは1/16固定で、課金単価はSpark Jobより大幅に低い。

適しているケース:
– CSVからParquetへの変換(小〜中規模)
– 外部API連携・HTTP Requestを含むETL
– pandas/boto3を使った単純変換処理

適さないケース:
– 1GB超のデータを高速にJoinする処理(Spark Jobへ切り替える)
– 並列分散処理が必要な変換(Spark Jobへ切り替える)

2-1-3. Streaming ETL — Kinesis / Kafka 統合

Streaming ETLはKinesis Data StreamsやApache Kafkaからのリアルタイム取り込みに使う。Spark Structured Streamingをベースに動作し、常時稼働のため24時間365日DPU課金が発生する点に注意が必要だ。

設定項目推奨値説明
Checkpoint場所S3バケット指定障害時の再開位置を保持
DLQ (Dead Letter Queue)SQS指定パース失敗レコードを退避
Window size100秒(デフォルト)マイクロバッチの集約ウィンドウ
Flex execution有効化推奨スポット容量活用でコスト削減(優先度低ジョブ向け)

2-2. Glue Crawler vs Partition Projection — 選定判断

2-2-1. Glue Crawler の動作と設定

Glue CrawlerはS3のデータをスキャンしてData Catalogにテーブル定義を自動登録するツールだ。設定のポイントは以下の通り。

設定項目推奨値注意点
Crawler sourcesS3プレフィックス単位で設定広すぎると不要テーブルが量産される
Recrawl policyCRAWL_NEW_FOLDERS_ONLY変更なし部分の再スキャンを省略
Exclude patterns**/_SUCCESS, **/*.tmpHadoopの制御ファイルを除外
Table prefix環境(dev_/prod_)で分けるテーブル名衝突防止
Scheduler日次バッチに合わせて設定Partition追加タイミングに合わせる

Crawlerのコスト: DPU課金ではないが、実行時間 × データ量に比例して処理時間が伸びる。Crawlerを毎時実行すると月数百ドルの無駄コストになるケースもある。

2-2-2. Partition Projection — Crawler不要化の選択肢

Partition Projectionは Crawlerを使わずにAthenaがパーティション情報を動的計算するメカニズムだ。S3のパーティション構造が規則的な場合に有効で、Crawler実行コストをゼロにできる

-- Partition Projection テーブル定義例
CREATE EXTERNAL TABLE access_logs (
  request_id  STRING,
  status_code INT,
  bytes BIGINT
)
PARTITIONED BY (
  year  STRING,
  month STRING,
  daySTRING
)
STORED AS PARQUET
LOCATION 's3://my-data-lake/access-logs/'
TBLPROPERTIES (
  'projection.enabled'  = 'true',
  'projection.year.type'= 'integer',
  'projection.year.range'  = '2023,2030',
  'projection.month.type'  = 'integer',
  'projection.month.range' = '1,12',
  'projection.month.digits'= '2',
  'projection.day.type' = 'integer',
  'projection.day.range'= '1,31',
  'projection.day.digits'  = '2',
  'storage.location.template' = 's3://my-data-lake/access-logs/year=${year}/month=${month}/day=${day}/'
);

Crawler vs Partition Projection 選定基準:

条件推奨
パーティションが日付/整数など規則的Partition Projection
スキーマが頻繁に変わるGlue Crawler
Cross-Accountでスキーマを共有Glue Crawler + Data Catalog
コスト最優先Partition Projection

2-3. Data Catalog — Database / Table / Partition / Schema Evolution

Data CatalogはGlueが管理するメタデータストアで、Athena・Redshift Spectrum・EMRが共通して参照できる。組織横断で活用するにはいくつかの設計原則がある。

Database(スキーマ)設計:
– 環境(dev/stg/prod)ごとにDatabaseを分ける
– ドメイン(sales/hr/ops)ごとにDatabaseを分ける
– 1 Database内のテーブル数が1000を超えると検索が遅くなるため分割を検討する

Schema Evolution対応:

Glue Schema RegistryはAvro・JSON Schema・Protobuf形式のスキーマをバージョン管理する。Kinesis Data Streamからのデータスキーマが変わった際に後方互換性を保ちながら更新できる。

互換性モードの選択基準:
BACKWARD: 古いスキーマで書かれたデータを新スキーマで読める(フィールド追加が主)
FORWARD: 新スキーマで書かれたデータを古いスキーマで読める(フィールド削除が主)
FULL: 双方向互換(最も安全だが制約が厳しい)

Partition管理:
パーティションが多い(100万件超)場合、MSCK REPAIR TABLEの代わりにALTER TABLE ADD PARTITIONを個別実行する。前者は全スキャンで遅く、後者はピンポイント追加で高速だ。

-- 新規パーティション追加(高速)
ALTER TABLE access_logs ADD
  PARTITION (year='2026', month='05', day='16')
  LOCATION 's3://my-data-lake/access-logs/year=2026/month=05/day=16/';

2-4. DataBrew — ノーコードデータ準備

DataBrewはSQLやPythonを書かずにGUIでデータ変換を定義できるツールだ。SQLアナリストやデータサイエンティストが前処理を自分で行える環境を作る際に使う。

主要機能:

機能内容
DatasetS3・Glue Catalog・RDS・Redshiftに接続
Profile Jobデータ品質スコア・外れ値・NULL率を自動分析
Recipe変換ステップを定義・バージョン管理(最大100ステップ)
Recipe JobRecipeをS3に適用してParquetなどで出力

DataBrew vs Glue Studio 選定:

シナリオ推奨
SQLアナリストが前処理を担当DataBrew
複雑なJoin・多段ETLGlue Studio
データ品質プロファイリングが必要DataBrew (Profile Job)
Spark Jobの可視化・デバッグGlue Studio

2-5. Glue Studio — ビジュアルETL と Job Monitoring

Glue StudioはGlue JobをGUIで設計・実行監視できるコンソール機能だ。ノードを接続してETLフローを組み立てると、裏でSpark Jobコードが生成される。

活用ポイント:
Visual ETL: S3 Source → Transform → S3 Sink をドラッグアンドドロップで設計
Job Monitoring: 実行履歴・DPU使用量・エラーログを一画面で確認
Script View: 生成されたPySpark/Scalaコードを直接編集可能
Auto-scale: Job実行中にWorker数を自動調整(G.1X以上で有効)

Job失敗時のアラート設定:

Glue JobのCloudWatch MetricsはデフォルトでOFFだ。本番運用では以下を有効化する:
glue.driver.aggregate.numFailedTasks → 失敗タスク数の監視
glue.driver.aggregate.numCompletedStages → 処理進捗の確認
– CloudWatch Alarm → SNS Topic → メール/Slack通知

2-6. Streaming ETL 詳細 — Kinesis/Kafka統合とCheckpoint/DLQ

Streaming ETLの本番設定で特に重要な3点を解説する。

① Checkpoint(再開ポイント):

Checkpointが設定されていないと、Jobが再起動したときに処理済みのデータを重複処理する。S3バケットはJob専用のパスで分離する。

# Streaming ETL Job Parameter 設定例
# --job-bookmark-option: job-bookmark-enable
# --checkpoint-s3-bucket: s3://my-glue-checkpoints/streaming-job-name/
# --window-size: 100 seconds

② Dead Letter Queue (DLQ):

パース失敗レコード(スキーマ不一致・文字化け等)をSQSに退避することで、正常レコードの処理を継続できる。DLQ未設定だとパースエラーでJob全体が停止することがある。

DLQのメッセージ保持期間は14日(最大)に設定し、定期的にDLQを監視してアラートを設定する。

③ Flex Execution:

優先度の低いStreaming ETLはFlex Executionを有効にすることで、スポット容量を活用しコストを削減できる。ただし遅延許容が必要で、SLAが厳しいリアルタイム処理には不向きだ。

2-7. Terraform 実装 — Glue Job × 2例 (Spark / Python Shell) + Crawler

# -------------------------------------------------------
# Glue Spark Job (G.1X × 5 Workers)
# -------------------------------------------------------
resource "aws_glue_job" "spark_etl" {
  name  = "${var.project}-spark-etl"
  role_arn = aws_iam_role.glue_service_role.arn
  glue_version= "4.0"
  number_of_workers = 5
  worker_type = "G.1X"

  command {
 name= "glueetl"
 script_location = "s3://${aws_s3_bucket.glue_scripts.bucket}/scripts/spark_etl.py"
 python_version  = "3"
  }

  default_arguments = {
 "--job-bookmark-option"  = "job-bookmark-enable"
 "--enable-metrics" = "true"
 "--enable-spark-ui"= "true"
 "--spark-event-logs-path"= "s3://${aws_s3_bucket.glue_logs.bucket}/spark-ui/"
 "--enable-continuous-cloudwatch-log" = "true"
 "--TempDir"  = "s3://${aws_s3_bucket.glue_temp.bucket}/temp/"
  }

  execution_property {
 max_concurrent_runs = 2
  }

  timeout = 60  # minutes

  tags = var.tags
}

# -------------------------------------------------------
# Glue Python Shell Job (1/16 DPU固定)
# -------------------------------------------------------
resource "aws_glue_job" "python_shell_etl" {
  name= "${var.project}-python-shell-etl"
  role_arn  = aws_iam_role.glue_service_role.arn
  glue_version = "3.0"
  max_capacity = 0.0625  # 1/16 DPU

  command {
 name= "pythonshell"
 script_location = "s3://${aws_s3_bucket.glue_scripts.bucket}/scripts/python_shell_etl.py"
 python_version  = "3.9"
  }

  default_arguments = {
 "--job-bookmark-option" = "job-bookmark-enable"
 "--enable-metrics"= "true"
 "--TempDir" = "s3://${aws_s3_bucket.glue_temp.bucket}/temp-shell/"
 "--extra-py-files"= "s3://${aws_s3_bucket.glue_scripts.bucket}/libs/helpers.zip"
  }

  execution_property {
 max_concurrent_runs = 5
  }

  timeout = 30  # minutes

  tags = var.tags
}

# -------------------------------------------------------
# Glue Service Role
# -------------------------------------------------------
resource "aws_iam_role" "glue_service_role" {
  name = "${var.project}-glue-service-role"

  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Principal = { Service = "glue.amazonaws.com" }
Action = "sts:AssumeRole"
 }]
  })
}

resource "aws_iam_role_policy_attachment" "glue_service" {
  role = aws_iam_role.glue_service_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole"
}

resource "aws_iam_role_policy" "glue_s3_access" {
  name = "${var.project}-glue-s3-access"
  role = aws_iam_role.glue_service_role.id

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [
{
  Effect = "Allow"
  Action = [
 "s3:GetObject",
 "s3:PutObject",
 "s3:DeleteObject",
 "s3:ListBucket"
  ]
  Resource = [
 "arn:aws:s3:::${var.data_lake_bucket}",
 "arn:aws:s3:::${var.data_lake_bucket}/*",
 "arn:aws:s3:::${aws_s3_bucket.glue_temp.bucket}/*",
 "arn:aws:s3:::${aws_s3_bucket.glue_scripts.bucket}/*",
 "arn:aws:s3:::${aws_s3_bucket.glue_logs.bucket}/*"
  ]
}
 ]
  })
}

# -------------------------------------------------------
# Glue Crawler (S3 → Data Catalog)
# -------------------------------------------------------
resource "aws_glue_crawler" "data_lake_crawler" {
  name = "${var.project}-data-lake-crawler"
  role = aws_iam_role.glue_service_role.arn
  database_name = aws_glue_catalog_database.data_lake.name

  s3_target {
 path = "s3://${var.data_lake_bucket}/processed/"
 exclusions = ["**/_SUCCESS", "**/*.tmp", "**/_temporary/**"]
  }

  schema_change_policy {
 delete_behavior = "LOG"
 update_behavior = "UPDATE_IN_DATABASE"
  }

  recrawl_policy {
 recrawl_behavior = "CRAWL_NEW_FOLDERS_ONLY"
  }

  configuration = jsonencode({
 Version = 1.0
 CrawlerOutput = {
Partitions = { AddOrUpdateBehavior = "InheritFromTable" }
Tables  = { AddOrUpdateBehavior = "MergeNewColumns" }
 }
 Grouping = {
TableGroupingPolicy = "CombineCompatibleSchemas"
 }
  })

  schedule = "cron(0 2 * * ? *)"

  tags = var.tags
}

resource "aws_glue_catalog_database" "data_lake" {
  name  = "${var.project}_data_lake"
  description = "Data Lake メタデータ管理"
}
Glue 本番運用ベストプラクティス

  • Job Bookmark必須: 増分処理に対応し、再実行時の全件処理コストを防ぐ。有効化は --job-bookmark-option: job-bookmark-enable の一行だけ。Bookmarkなしで再実行すると全件重複処理が発生する。
  • DPU サイズは小さく始める: G.1X × 5 Workersから開始し、CloudWatch MetricsのSpill Ratioが20%を超えたらG.2Xへ移行する。最初からG.2Xを選ぶとコストが2倍になる。
  • Glue Catalog Cross-Account共有: Resource Policyに Consumer AccountのIDを追加し、Lake FormationのGrant-on-catalog設定とセットで行う。片方だけでは機能しない。
  • Spark UI有効化: --enable-spark-ui + S3パス設定でSpark History Serverが使える。ボトルネックのStageを特定できる。
  • Crawler Recrawl Policy設定: CRAWL_NEW_FOLDERS_ONLY で新規パーティションのみスキャンし、Crawlerの実行時間とコストを削減する。デフォルト(全件再スキャン)のまま運用しているケースが多いため、必ず確認する。
Glue コスト管理 — DPU試算の注意

  • 最低課金10分: Jobが3分で完了しても10分分のDPUが課金される。軽量処理にSpark Jobを使うと割高になる。Python Shell Job (1/16 DPU) または Lambda を検討せよ。
  • 常時稼働コスト (Streaming ETL): G.1X × 2 Workers で 24時間稼働すると 2 DPU × $0.44 × 24h × 30日 = 約$633/月。利用頻度が低い場合はスケジュールバッチに切り替える。
  • Development Endpoint禁止: 開発用エンドポイント(Glue Dev Endpoint)はDPUが常時稼働し月数百ドルかかる。代わりにGlue ETL Libraryをローカルにインストールして開発する。
  • G.025X活用: テスト・検証用途のJobはG.025X(0.25 DPU)を使う。本番と同じコードを低コストで検証できる。1/4 DPUのため本番と比べてメモリ制約があることを考慮する。
§2 クロスリンク

Glue Job種別 + Crawler + Data Catalog 統合フロー図


3. Athena本番運用 — SQL最適化 × Partition Projection × CTAS × Federated Query × Workgroup

flowchart TD
 A[S3データ到着] --> B{Partition Projection設定済?}
 B -->|Yes| C[Athena直接クエリ\nCrawler不要]
 B -->|No| D[Glue Crawler実行\n別途コスト発生]
 D --> E[Glue Data Catalog更新\n数分〜数十分待機]
 E --> C
 C --> F{データ形式}
 F -->|CSV/JSON| G[Full Table Scan\n$/TB scanned 課金大]
 F -->|Parquet/ORC| H[Columnar Read\nスキャン量1/10以下]
 H --> I[クエリ完了\n低コスト]
 G --> J[コスト爆発リスク\nWorkgroupで上限設定必須]
§3 ナビゲーション (Athena本番運用 関連記事)

3-1. Athena SQL 最適化 — Columnar Format (Parquet/ORC) 必須

Athena の課金は スキャンデータ量 $5/TB で決まる。CSV のまま運用すると 1TB スキャンごとに $5 が発生し続けるが、Parquet または ORC に変換するだけでスキャン量を 1/5〜1/10 に削減できる。

フォーマット選定指針:

フォーマット圧縮率推奨圧縮コーデックAthena適性
CSVgzip非推奨 (スキャン量大)
JSON低〜中gzipログ取込のみ許容
ParquetSnappy本番デフォルト推奨
ORCSnappy/zlibHive連携時

Predicate Pushdown の活用: Parquet/ORC はカラム単位でデータを保持するため、WHERE year = '2024' AND region = 'ap-northeast-1' のような絞込みクエリでは該当カラムのストライプのみ読込み、それ以外を完全スキップする。

SQL最適化 チェックポイント:
SELECT * を禁止 → 必要カラムのみ明示指定
LIMIT を開発クエリに必ず付与 (誤って全件スキャン防止)
– 日付パーティションを WHERE 句に必ず含める
– 集計前に不要行を WHERE で絞り込む

3-2. Partition Projection — Glue Crawler不要化の本命設定

Glue Crawler は S3 をスキャンしてパーティション情報を Data Catalog に書き込む。しかし Crawler 実行には DPU課金 + 実行待機時間 (数分〜) が発生する。Partition Projection を使えば Crawler を一切使わず Athena がパーティション情報を動的に計算する。

Partition Projection 設定例 (日付パーティション):

CREATE EXTERNAL TABLE access_logs (
  request_id STRING,
  method  STRING,
  status_code INT,
  bytes_sent  BIGINT
)
PARTITIONED BY (
  year  STRING,
  month STRING,
  daySTRING
)
STORED AS PARQUET
LOCATION 's3://my-data-lake/access-logs/'
TBLPROPERTIES (
  'projection.enabled' = 'true',
  'projection.year.type'  = 'integer',
  'projection.year.range' = '2022,2030',
  'projection.month.type' = 'integer',
  'projection.month.range'= '1,12',
  'projection.month.digits'  = '2',
  'projection.day.type'= 'integer',
  'projection.day.range'  = '1,31',
  'projection.day.digits' = '2',
  'storage.location.template'= 's3://my-data-lake/access-logs/${year}/${month}/${day}/'
);

動的パーティションタイプ一覧:

タイプ用途設定例
integer年月日、連番range=2022,2030
date日付型パーティションrange=2022-01-01,NOW, format=yyyy-MM-dd
enum固定値リストvalues=ap-northeast-1,us-east-1,eu-west-1
injectedアプリからパーティション値を直接注入クライアント側で指定

コスト試算 (月間比較):
– Glue Crawler 毎時実行: 0.44 DPU-hour × $0.44 × 720h = 月額 約$140
– Partition Projection: $0 (クエリ時に動的計算)

3-3. CTAS — Create Table As Select でスキャン量を事前削減

CTAS は SELECT クエリの結果を S3 に Parquet 形式で書き出し、新しい外部テーブルとして登録する。繰り返し実行する集計クエリを CTAS で事前計算しておくことで、後続クエリのスキャン量を大幅に削減できる。

CTAS 実装例 (日次集計テーブル生成):

CREATE TABLE daily_summary
WITH (
  format  = 'PARQUET',
  parquet_compression = 'SNAPPY',
  external_location= 's3://my-data-lake/aggregated/daily-summary/',
  partitioned_by= ARRAY['year', 'month', 'day']
)
AS
SELECT
  date_format(from_unixtime(timestamp), '%Y') AS year,
  date_format(from_unixtime(timestamp), '%m') AS month,
  date_format(from_unixtime(timestamp), '%d') AS day,
  region,
  COUNT(*) AS request_count,
  SUM(bytes_sent) AS total_bytes,
  AVG(response_time_ms) AS avg_response_ms
FROM access_logs
WHERE year = '2024'
GROUP BY 1, 2, 3, 4;

CTAS 運用ルール:
– 既存テーブルへの上書きは不可 → 実行前に DROP TABLE IF EXISTS を実行
external_location のプレフィックスが重複すると TABLE_ALREADY_EXISTS エラー → 日付サフィックスで一意化
– Workgroup の出力先と CTAS の external_location を別パスに設定する (混在を防ぐ)

3-4. Federated Query — Lambda Connector でクロスデータソース統合

Federated Query は Lambda Data Source Connector 経由で RDS/Aurora/DynamoDB/CloudWatch Logs/Redshift などを Athena から直接クエリできる機能。S3 と RDB データを JOIN するような分析シナリオで有効。

主要コネクター一覧:

コネクター対象Lambda関数名 (AWS公式)
JDBCRDS (MySQL/PostgreSQL/Aurora)AthenaJdbcConnector
DynamoDBDynamoDBAthenaDynamoDBConnector
CloudWatchCloudWatch LogsAthenaCloudWatchConnector
RedshiftRedshiftAthenaRedshiftConnector
HBaseEMR HBaseAthenaHBaseConnector

セットアップ手順:
1. AWS Serverless Application Repository から対象コネクターをデプロイ
2. Athena コンソール → Data Sources → Create data source → Lambda connector 選択
3. AWSDataCatalog と並列に lambda:connector-name データソースが追加される
4. クエリ内でデータソースを指定: SELECT * FROM "lambda:my-rds-connector"."mydb"."users" LIMIT 100

注意: Federated Query はネットワーク経路が Lambda VPC → RDS VPC となるため、VPC Endpoint 設計が必要。Network Vol1 の VPC Endpoint for Athena と組み合わせて設計する。

3-5. Workgroup — コストガバナンス + 結果 KMS暗号化 + 出力先分離

Workgroup は チーム・用途別のクエリ実行環境を分離し、スキャン量上限・出力先・暗号化設定を強制できる。Workgroup を設定しないと全ユーザーが同一設定で実行され、誤ったクエリがコストを青天井にする。

Workgroup の制御項目:

設定内容
bytes_scanned_cutoff_per_queryクエリごとのデータスキャン上限 (超過でクエリ中止)
result_configuration.output_locationクエリ結果の S3 出力先 (チーム別バケットに強制)
result_configuration.encryption_configuration結果の SSE-KMS 暗号化 (キー指定)
enforce_workgroup_configurationユーザーによる上書きを禁止
publish_cloudwatch_metrics_enabledCloudWatch Metrics への統計送信

3-6. Named Query / Prepared Statement 活用

Named Query: よく使うクエリをコンソールや API で保存し、名前で呼び出す。Terraform で管理可能。

Prepared Statement: クエリのパラメータをバインド変数化し、実行時に値を差し込む。SQL インジェクション対策 + 実行計画の再利用でパフォーマンスが向上する。

-- Prepared Statement の登録
PREPARE daily_report FROM
  SELECT * FROM access_logs
  WHERE year = ? AND month = ? AND region = ?;

-- 実行 (値を注入)
EXECUTE daily_report USING '2024', '05', 'ap-northeast-1';

-- 削除
DEALLOCATE PREPARE daily_report;

3-7. Terraform 実装 — aws_athena_workgroup + aws_athena_database

resource "aws_athena_database" "analytics" {
  name= "analytics_prod"
  bucket = aws_s3_bucket.athena_results.id

  encryption_configuration {
 encryption_option = "SSE_KMS"
 kms_key  = aws_kms_key.athena.arn
  }
}

resource "aws_athena_workgroup" "prod" {
  name  = "prod-workgroup"
  description = "本番クエリ用 Workgroup (コストガバナンス + KMS暗号化)"
  state = "ENABLED"

  configuration {
 enforce_workgroup_configuration = true
 publish_cloudwatch_metrics_enabled = true

 result_configuration {
output_location = "s3://${aws_s3_bucket.athena_results.bucket}/prod/"

encryption_configuration {
  encryption_option = "SSE_KMS"
  kms_key  = aws_kms_key.athena.arn
}
 }

 bytes_scanned_cutoff_per_query = 1073741824  # 1 GB 上限
  }

  tags = {
 Environment = "production"
 Team  = "data-platform"
  }
}

resource "aws_athena_workgroup" "dev" {
  name  = "dev-workgroup"
  state = "ENABLED"

  configuration {
 enforce_workgroup_configuration = true

 result_configuration {
output_location = "s3://${aws_s3_bucket.athena_results.bucket}/dev/"
 }

 bytes_scanned_cutoff_per_query = 107374182  # 100 MB 上限 (開発環境)
  }
}

resource "aws_athena_named_query" "daily_scan" {
  name= "daily-access-log-scan"
  workgroup = aws_athena_workgroup.prod.id
  database  = aws_athena_database.analytics.name
  query  = <<-EOT
 SELECT year, month, day, region, COUNT(*) AS requests
 FROM access_logs
 WHERE year = '2024' AND month = '05'
 GROUP BY 1, 2, 3, 4
 ORDER BY day DESC;
  EOT
}

resource "aws_kms_key" "athena" {
  description = "Athena クエリ結果暗号化キー"
  deletion_window_in_days = 7
  enable_key_rotation  = true
}

resource "aws_s3_bucket" "athena_results" {
  bucket = "my-athena-query-results-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_public_access_block" "athena_results" {
  bucket = aws_s3_bucket.athena_results.id

  block_public_acls = true
  block_public_policy  = true
  ignore_public_acls= true
  restrict_public_buckets = true
}
Athena 本番 落とし穴 — Full Table Scan / Partition Pruning 失敗

  • CSV直接クエリで$/TB課金爆発: 1TB CSVを毎日クエリすると月$150超。Parquet変換が最優先タスク
  • Partition Pruning 失敗パターン: WHERE CAST(year AS INT) = 2024 のように型変換すると Partition Pruning が無効化 → WHERE year = '2024' と文字列で統一する
  • Glue Crawler と Partition Projection の混在: 同一テーブルに両方設定すると Projection が無効化される。どちらかに統一すること
  • Workgroup 未設定の誤クエリ: SELECT * FROM huge_table が上限なし実行 → 月末に予期せぬ高額請求。開発用 Workgroup は100MB上限を推奨
  • CTAS の external_location 重複: 同じパスに CTAS を再実行すると TABLE_ALREADY_EXISTS → 必ず DROP TABLE してから再実行
  • Federated Query の VPC経路設計漏れ: Lambda Connector が VPC外に配置されると RDS に到達できない。Security Group + VPC Endpoint を先に設計する
Athena 本番運用ベストプラクティス

  • フォーマット: 全テーブルを Parquet + Snappy 圧縮に統一。スキャン量を1/5〜1/10に削減し月額コストを大幅削減
  • Partition Projection: Glue Crawler を廃止し Partition Projection に移行。月$100〜$200 の Crawler コストを $0 に
  • Workgroup 設計: prod/dev/analyst の3 Workgroup を最低限用意。prod は 1GB 上限 + KMS暗号化、dev は 100MB 上限
  • CTAS 活用: 繰り返す集計クエリを CTAS で事前計算。後続クエリのスキャン量をMB単位まで削減
  • Named Query 管理: よく使うクエリを Terraform で Named Query として管理。属人化を防ぎ再現性を担保
  • VPC Endpoint: Athena + S3 の通信を VPC 内に閉じる。Network Vol1 の VPC Endpoint 設定と組み合わせ

fig03: Athena Partition Projection 仕組み図 — S3 prefix から動的マッピング

4. Redshift Serverless本番運用 — RPU × Namespace × Workgroup × Concurrency Scaling × Spectrum

Redshift Serverless 本番運用 落とし穴3選

  • RPU爆発コスト: Base RPU を 64 以上に設定したまま放置 → 月額 $16,589 超。max_capacity 未設定が最大の原因。週次 RPU 使用率レビューが必須
  • Concurrency Scaling 二重課金: Base RPU × 稼働時間 + Concurrency Scaling 秒単位課金が加算。Max RPU 上限設定をしないと突発負荷で請求額が数倍になる
  • Spectrum スキャン爆発: CSV ファイルを External Table で直接クエリすると $5/TB scanned が加算。Parquet + Snappy 変換を先行実施しないまま本番投入すると高額請求が発生

4-1. Redshift Serverless vs Provisioned — 使い分けマトリクス

Redshift Serverless は「ワークロードが不定期かつ予測困難」な場合に最適。Provisioned (ra3系) は「大規模固定ワークロード」で月額コストが安定する。

比較軸Redshift ServerlessRedshift Provisioned
課金モデルRPU × 実行時間 (秒単位)ノード × 時間 (固定)
スケーリング自動 (RPU範囲内)手動 / Elastic Resize
最小コスト使用時のみ課金 (アイドル課金あり)常時課金 ($0.25〜/node-hr)
同時クエリ対応Concurrency Scaling 自動Concurrency Scaling + WLM
管理コスト低 (ノード管理不要)高 (ノードタイプ選定/パッチ対応)
適用ワークロード開発/分析/非定常バッチ大規模DWH/定常クエリ
Spectrum 対応
Data API 対応

本番環境での選定指針: 月次クエリ実行が 500 RPU-hour 未満なら Serverless が有利。それを超える場合は Provisioned (ra3.4xlarge 2ノード: $3,648/月) と比較試算してから選定する。データウェアハウスの規模が数十TB・クエリが常時発生するなら Provisioned の方がコスト安定する。

4-2. Namespace + Workgroup — 本番/開発環境分離設計

Redshift Serverless の設計単位は Namespace (データ格納層)Workgroup (コンピューティング層) の2層構造。環境ごとに完全分離する。

本番環境:
  Namespace: prod-analytics-ns
 ├─ Database: analytics_db, mart_db
 ├─ 管理者 IAM Role: arn:aws:iam::123456789012:role/RedshiftServerlessAdmin
 └─ KMS Key: arn:aws:kms:ap-northeast-1:123456789012:key/xxx
  Workgroup: prod-analytics-wg
 ├─ Base RPU: 32 ($8,294/月)
 ├─ Max RPU: 64 (Concurrency Scaling 上限)
 └─ Subnet: private-subnet-a, private-subnet-b, private-subnet-c

開発環境:
  Namespace: dev-analytics-ns
 └─ Database: analytics_db (本番と同一スキーマ)
  Workgroup: dev-analytics-wg
 ├─ Base RPU: 8 ($2,073/月)
 └─ Max RPU: 16 (コスト上限)

設計原則:
– Namespace はアカウント単位でデータ保護境界を形成。本番/開発で Namespace を完全分離し IAM Role も別管理
– Workgroup の Base RPU は環境別に設定。開発は 8 RPU 固定でコスト抑制
publicly_accessible = false を徹底。VPC Endpoint 経由アクセスのみ許可
– セキュリティグループは Workgroup 単位で設計。本番は踏み台 EC2 + Glue Job からのみ 5439 ポート許可

4-3. Base RPU 設計と月額試算

Base RPU は 8〜512 RPU の範囲 (8単位) で設定。課金単価は $0.36/RPU-hour

RPU 月額試算表 (24h × 30日 = 720h 稼働想定):

Base RPU稼働時間/月月額概算
8 RPU720h~$2,073
16 RPU720h~$4,147
32 RPU720h~$8,294
64 RPU720h~$16,589
128 RPU720h~$33,178

Base RPU 決定フロー:
1. 8 RPU で開始 (最小構成)
2. CloudWatch QueryDuration / QueriesCompletedPerSecond を 2 週間観測
3. QueryDuration が許容値を超えた場合に 16 → 32 と段階的増加
4. Concurrency Scaling が頻発する場合は Base RPU 引き上げを検討

補足: Storage は RPU 課金とは別途 $24/TB-month が発生。中間テーブルや履歴データが増えると Storage コストが重要になる。

RPU 試算の注意点 — コスト超過の典型パターン

  • 試算式: 月額 = Base RPU × 720h × $0.36/RPU-hour。Base RPU 32 → $8,294/月、64 → $16,589/月
  • Serverless は「停止」概念がない。アイドル中も Base RPU に応じたウォームアップ待機コストが発生する
  • max_capacity (Max RPU) を設定しないとデフォルト上限まで Concurrency Scaling が自動拡張 → 突発負荷で請求額が数倍になる
  • Concurrency Scaling 課金も $0.36/RPU-hour (秒単位)。ただし 1日あたり 1 RPU-hour 分は無料クレジット (月 30 RPU-hour 無料)
  • Storage 課金 ($24/TB-month) は別途発生。不要な中間テーブルは定期的に DROP して Storage コストを抑制する

4-4. Concurrency Scaling — 同時クエリ集中対応

Concurrency Scaling は Base RPU のキャパシティを超えた同時クエリ発生時に自動でスケールアウトする機能。

動作フロー:
1. Base RPU のキャパシティ消費が閾値を超過
2. Concurrency Scaling クラスタが自動起動 (数秒〜数十秒)
3. 追加クエリを Concurrency Scaling クラスタにルーティング
4. クエリ完了後、Concurrency Scaling クラスタは自動停止

Terraform での上限設定 (コスト制御必須):

resource "aws_redshiftserverless_workgroup" "prod" {
  namespace_name = aws_redshiftserverless_namespace.prod.namespace_name
  workgroup_name = "prod-analytics-wg"
  base_capacity  = 32
  max_capacity= 64  # Concurrency Scaling 上限: Base RPU の2倍に抑制
}

max_capacity を設定しない場合、デフォルト上限 (512 RPU) まで自動スケールするリスクがある。ピーク負荷見積もりから max_capacity = Base RPU × 2 程度を基本設定とする。

4-5. Redshift Spectrum — S3データ直接クエリ

Redshift Spectrum は Glue Data Catalog の External Schema を経由して S3 の Parquet/ORC ファイルを Redshift SQL で直接クエリする機能。コールドデータの分析に最適。

External Schema 作成:

-- Glue Data Catalog の analytics_db を External Schema として登録
CREATE EXTERNAL SCHEMA glue_analytics
FROM DATA CATALOG
DATABASE 'analytics_db'
IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftSpectrumRole'
CREATE EXTERNAL DATABASE IF NOT EXISTS;

External Table クエリ例:

-- S3 の Parquet ファイルを Spectrum 経由でクエリ
SELECT
  event_date,
  user_id,
  COUNT(*) AS event_count
FROM glue_analytics.clickstream_events
WHERE event_date >= '2025-01-01'
  AND event_type = 'purchase'
GROUP BY 1, 2
ORDER BY event_count DESC
LIMIT 100;

Spectrum コスト: $5/TB scanned (Athena と同等)。Parquet + Snappy 圧縮でスキャン量を 1/10〜1/20 に削減可能。

Redshift 内部テーブル vs Spectrum 使い分け:

用途推奨
頻繁アクセス Hot データ (直近30日)Redshift 内部テーブル
低頻度・大容量 Cold データSpectrum (S3 直接)
Athena / Glue との共有テーブルSpectrum (Glue Catalog 共有)
リアルタイム集計 / Materialized ViewRedshift 内部テーブル

4-6. Snapshot / Recovery Point / Cross-Region Snapshot

Recovery Point: 約 30 分ごとに自動生成。24時間以内の任意の時点に無料でリストア可能。追加設定不要。

Snapshot (手動/自動):
手動 Snapshot: 明示的に作成。保持期限なし (削除まで保持)。$0.024/GB-month (圧縮後)
自動 Snapshot: 1〜35日の保持期間を設定。Serverless では Terraform または Console から明示的に有効化が必要

Cross-Region Snapshot 設定 (DR対応):

resource "aws_redshiftserverless_namespace" "prod" {
  namespace_name= "prod-analytics-ns"
  db_name = "analytics_db"
  admin_username= var.redshift_admin_user
  admin_user_password = var.redshift_admin_password
  kms_key_id = aws_kms_key.redshift.arn

  snapshot_copy_configurations {
 destination_region  = "ap-northeast-3"  # 東京 → 大阪
 retention_period = 7
 destination_kms_key_id = aws_kms_key.snapshot_osaka.arn
  }
}

DR 戦略:
– RTO: Recovery Point から新 Namespace にリストア → 数十分〜1時間
– RPO: 直近 Recovery Point (30分以内)
– Cross-Region: 大阪リージョンへの Snapshot コピーで地域障害対応 (別途 KMS キーが大阪に必要)

4-7. Redshift Data API — Lambda/Step Functions連携

Redshift Data API は VPC Endpoint 不要で HTTP API 経由の SQL 実行を可能にする。Lambda / Step Functions からの定期バッチや ETL 実行に最適。

Lambda 連携 (Python)with 構文を使わないシンプルな実装:

import boto3
import time

redshift = boto3.client('redshift-data', region_name='ap-northeast-1')

def execute_redshift_query(workgroup_name: str, database: str, sql: str) -> str:
 response = redshift.execute_statement(
  WorkgroupName=workgroup_name,
  Database=database,
  Sql=sql
 )
 return response['Id']

def wait_for_completion(statement_id: str, timeout_sec: int = 300) -> dict:
 elapsed = 0
 while elapsed < timeout_sec:
  desc = redshift.describe_statement(Id=statement_id)
  status = desc['Status']
  if status in ('FINISHED', 'FAILED', 'ABORTED'):
return desc
  time.sleep(5)
  elapsed += 5
 raise TimeoutError(f"Query timed out after {timeout_sec}s")

def lambda_handler(event, context):
 stmt_id = execute_redshift_query(
  workgroup_name='prod-analytics-wg',
  database='analytics_db',
  sql='REFRESH MATERIALIZED VIEW mv_daily_summary;'
 )
 result = wait_for_completion(stmt_id)
 return {'status': result['Status'], 'duration_ms': result.get('Duration', 0) // 1000000}

Step Functions 連携 (非同期実行パターン):

{
  "Comment": "Redshift Data API 非同期クエリ実行",
  "StartAt": "ExecuteStatement",
  "States": {
 "ExecuteStatement": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:redshiftdata:executeStatement",
"Parameters": {
  "WorkgroupName": "prod-analytics-wg",
  "Database": "analytics_db",
  "Sql": "REFRESH MATERIALIZED VIEW mv_daily_summary;"
},
"ResultPath": "$.statement",
"Next": "WaitForCompletion"
 },
 "WaitForCompletion": {
"Type": "Wait",
"Seconds": 30,
"Next": "CheckStatus"
 },
 "CheckStatus": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:redshiftdata:describeStatement",
"Parameters": {
  "Id.$": "$.statement.Id"
},
"Next": "IsFinished"
 },
 "IsFinished": {
"Type": "Choice",
"Choices": [
  { "Variable": "$.Status", "StringEquals": "FINISHED", "Next": "Success" },
  { "Variable": "$.Status", "StringEquals": "FAILED", "Next": "Fail" }
],
"Default": "WaitForCompletion"
 },
 "Success": { "Type": "Succeed" },
 "Fail": { "Type": "Fail" }
  }
}

Data API 制約:
– クエリ実行結果は 24 時間のみ保持 (get_statement_result で取得可能期間)
– レスポンス上限: 100MB。大量データは UNLOAD コマンドで S3 出力を組み合わせる
– 同時実行数: 上限 200 ステートメント/ワークグループ

4-8. Terraform 実装 — Namespace + Workgroup 本番構成

# KMS Key for Redshift Serverless
resource "aws_kms_key" "redshift" {
  description = "Redshift Serverless encryption key"
  deletion_window_in_days = 30
  enable_key_rotation  = true
}

resource "aws_kms_alias" "redshift" {
  name = "alias/redshift-serverless-prod"
  target_key_id = aws_kms_key.redshift.key_id
}

# Redshift Serverless Namespace
resource "aws_redshiftserverless_namespace" "prod" {
  namespace_name= "prod-analytics-ns"
  db_name = "analytics_db"
  admin_username= var.redshift_admin_user
  admin_user_password = var.redshift_admin_password
  kms_key_id = aws_kms_key.redshift.arn

  iam_roles = [
 aws_iam_role.redshift_spectrum.arn,
  ]

  log_exports = ["userlog", "connectionlog", "useractivitylog"]

  tags = {
 Environment = "production"
 Service  = "analytics"
  }
}

# Redshift Serverless Workgroup (本番)
resource "aws_redshiftserverless_workgroup" "prod" {
  namespace_name = aws_redshiftserverless_namespace.prod.namespace_name
  workgroup_name = "prod-analytics-wg"

  base_capacity = 32  # $8,294/月 (720h想定)
  max_capacity  = 64  # Concurrency Scaling 上限 — RPU爆発防止

  subnet_ids = [
 aws_subnet.private_a.id,
 aws_subnet.private_b.id,
 aws_subnet.private_c.id,
  ]

  security_group_ids  = [aws_security_group.redshift.id]
  publicly_accessible = false

  config_parameter {
 parameter_key= "enable_user_activity_logging"
 parameter_value = "true"
  }

  config_parameter {
 parameter_key= "require_ssl"
 parameter_value = "true"
  }

  tags = {
 Environment = "production"
 BaseRPU  = "32"
 MaxRPU= "64"
  }
}

# Redshift Serverless Workgroup (開発: 低コスト構成)
resource "aws_redshiftserverless_workgroup" "dev" {
  namespace_name = aws_redshiftserverless_namespace.dev.namespace_name
  workgroup_name = "dev-analytics-wg"

  base_capacity = 8# $2,073/月
  max_capacity  = 16  # 開発上限: Base RPU × 2
  publicly_accessible = false
  subnet_ids = [aws_subnet.private_a.id]
  security_group_ids  = [aws_security_group.redshift_dev.id]
}

# Spectrum 用 IAM Role
resource "aws_iam_role" "redshift_spectrum" {
  name = "RedshiftSpectrumRole"

  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "redshift.amazonaws.com" }
 }]
  })
}

resource "aws_iam_role_policy" "spectrum_glue_s3" {
  name = "SpectrumGlueS3Access"
  role = aws_iam_role.redshift_spectrum.id

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [
{
  Effect= "Allow"
  Action= ["s3:GetObject", "s3:ListBucket"]
  Resource = [
 "arn:aws:s3:::${var.data_lake_bucket}",
 "arn:aws:s3:::${var.data_lake_bucket}/*"
  ]
},
{
  Effect= "Allow"
  Action= ["glue:GetDatabase", "glue:GetTable", "glue:GetPartitions"]
  Resource = ["*"]
}
 ]
  })
}

# CloudWatch Alarm: RPU使用率監視
resource "aws_cloudwatch_metric_alarm" "redshift_rpu_high" {
  alarm_name = "redshift-serverless-rpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name= "ComputeSeconds"
  namespace  = "AWS/Redshift-Serverless"
  period  = 300
  statistic  = "Sum"
  threshold  = 72000  # 20 RPU相当を超えたらアラート
  alarm_description= "Redshift Serverless RPU使用量が高水準"

  dimensions = {
 WorkgroupName = aws_redshiftserverless_workgroup.prod.workgroup_name
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
}
Redshift Serverless コスト爆発パターンと回避策

  • max_capacity 未設定: Concurrency Scaling が Base RPU × 8 まで自動スケール。突発負荷で請求額が数倍になる。必ず max_capacity = Base RPU × 2 程度に制限
  • Base RPU の過剰設定: 64 RPU 設定でアイドル中も $16,589/月発生。開発環境で 8 RPU 固定、本番は CloudWatch で実使用率を観測してから増加
  • Spectrum の CSV クエリ: CSV ファイルを External Table で直接クエリすると $5/TB scanned。Parquet + Snappy 変換で 1/10 以下に削減してから本番投入
  • Data API の結果バッファ超過: 100MB を超えるクエリ結果は途中打ち切り。大量データ取得は UNLOAD TO 's3://...' コマンドで S3 出力してから取得する
  • Storage 過剰蓄積: 中間テーブル・一時テーブルを放置すると $24/TB-month が累積。定期 DROP ジョブを Data API 経由で実行
Redshift Serverless 本番運用ベストプラクティス

  • Base RPU 段階的拡大: 8 RPU から開始 → CloudWatch QueryDuration / QueriesCompletedPerSecond を 2 週間観測 → 許容値超過時に 16 → 32 と段階的増加
  • Namespace/Workgroup 環境分離: 本番/開発/分析チーム別に Workgroup を分離。RPU 上限をチーム別に設定してコスト配賦を可能にする
  • Spectrum + 内部テーブルの二層設計: S3 のコールドデータは Spectrum、直近 30 日のホットデータは Redshift 内部テーブル + Materialized View で高速化
  • Data API 活用: Lambda/Step Functions からの定期 ETL は Data API 経由 (VPC 不要)。Glue Job との役割を整理し二重コストを防止
  • Cross-Region Snapshot: 大阪リージョンへの Snapshot コピーを設定し、東京リージョン障害時の DR を確保。Recovery Point は 30 分以内なので RPO は許容できる水準
関連記事 — Redshift 導入・コスト管理

fig04: Redshift Serverless 全体構成図 — Namespace/Workgroup/Spectrum/Data API


5. Lake Formation本番運用 — LF-Tag-Based Access Control × Cross-Account Sharing × Data Filter × Governed Tables

5-1. LF-Tag-Based Access Control (LF-TBAC)

LF-TBAC は AWS Lake Formation が提供するタグベースのアクセス制御機構で、従来の Resource-Level 権限管理(テーブル・カラム個別に IAM/Lake Formation 権限を付与)を大幅に簡素化する。Tag(LF-Tag)をデータリソース(データベース・テーブル・カラム)に付与し、IAM ロールに対してはタグ条件で権限を委譲する仕組みだ。

LF-Tag 階層設計の原則

タグは キー:値 のペアで表現される。本番環境では以下の 3 階層構造が定番となる。

階層LF-Tag キー値の例
環境envprod / staging / dev
データ機密区分sensitivityconfidential / internal / public
ドメインdomainsales / finance / marketing

設計の鉄則: LF-Tag は最初にキー体系を確定させる。後からキーを追加・変更すると全リソースへの再タグ付けが発生し、権限漏れのリスクが生じる。

LF-Tag の付与と Permission 設定

# LF-Tag 定義
resource "aws_lakeformation_lf_tag" "sensitivity" {
  key = "sensitivity"
  values = ["confidential", "internal", "public"]
}

resource "aws_lakeformation_lf_tag" "domain" {
  key = "sensitivity"
  values = ["sales", "finance", "marketing"]
}

# テーブルへの LF-Tag 付与
resource "aws_lakeformation_resource_lf_tags" "sales_table" {
  table {
 database_name = "sales_db"
 name = "transactions"
  }

  lf_tag {
 key= "sensitivity"
 value = "confidential"
  }

  lf_tag {
 key= "domain"
 value = "sales"
  }
}

LF-TBAC Permission の委譲

# IAM ロールに LF-Tag ベースで権限付与
resource "aws_lakeformation_permissions" "analyst_access" {
  principal = "arn:aws:iam::123456789012:role/DataAnalystRole"

  lf_tag_policy {
 resource_type = "TABLE"
 expression {
key = "sensitivity"
values = ["internal", "public"]
 }
 expression {
key = "domain"
values = ["sales"]
 }
  }

  permissions = ["SELECT", "DESCRIBE"]
}

この設定で DataAnalystRolesensitivity=internal/public かつ domain=sales のタグを持つ全テーブルに自動的に SELECT 権限が付与される。新しいテーブルを追加した際も、タグさえ正しく付与すれば権限管理は自動的に適用される。


5-2. Resource-Level vs LF-TBAC — 選定フロー

観点Resource-LevelLF-TBAC
管理粒度テーブル・カラム個別タグ条件で一括
スケーラビリティテーブル増加で管理爆発タグ体系が一定なら追加コスト低
監査性権限一覧が長大タグ条件で可読性高
移行コスト既存 IAM からの移行が容易初期タグ設計コストが必要
推奨場面テーブル数 < 20 の小規模テーブル数 50+ / 複数ドメイン

選定フロー:
1. テーブル数 ≥ 50 → LF-TBAC を採用
2. 複数 AWS アカウント間でデータ共有 → LF-TBAC + Cross-Account が必須
3. 既存の IAM Policy のみ管理で十分な小規模 → Resource-Level でも可(Lake Formation の段階的導入)


5-3. Cross-Account Sharing — Producer/Consumer 手順

Lake Formation Cross-Account Sharing は、Producer アカウントが所有するデータカタログ・S3 データを Consumer アカウントに共有する機能だ。AWS Resource Access Manager (RAM) を経由して Glue Data Catalog エントリを共有する。

sequenceDiagram
  participant Producer as Producer Account
  participant LF as Lake Formation
  participant RAM as Resource Access Manager
  participant Consumer as Consumer Account
  Producer->>LF: Grant Lake Formation Permission (Cross-Account)
  LF->>RAM: Create Resource Share (Glue Catalog ARN)
  RAM->>Consumer: Accept Resource Share
  Consumer->>LF: Register Data Lake Location (S3 URI)
  Consumer->>LF: Athena/Glue からクエリ実行

Producer 側設定手順

Step 1: Lake Formation のデータレイク設定で「Cross-Account version settings」を v3 に更新する。

resource "aws_lakeformation_data_lake_settings" "producer" {
  admins = ["arn:aws:iam::PRODUCER_ACCOUNT:role/LakeFormationAdmin"]

  create_table_default_permissions {
 principal = "IAM_ALLOWED_PRINCIPALS"
 permissions  = []
  }
}

Step 2: Consumer アカウントの IAM プリンシパルに Lake Formation 権限を付与する。

resource "aws_lakeformation_permissions" "cross_account_share" {
  principal= "CONSUMER_ACCOUNT_ID"
  permissions = ["SELECT", "DESCRIBE"]
  permissions_with_grant_option = ["SELECT", "DESCRIBE"]

  table {
 database_name = "sales_db"
 name = "transactions"
  }
}

Consumer 側設定手順

Step 1: RAM Resource Share を承諾する(AWS Organizations 利用時は自動承諾可能)。

Step 2: Consumer アカウントで外部アカウントの Glue Catalog データベースを参照するリソースリンクを作成する。

resource "aws_glue_catalog_database" "resource_link" {
  name = "sales_db_link"

  target_database {
 catalog_id = "PRODUCER_ACCOUNT_ID"
 database_name = "sales_db"
  }
}

Step 3: Consumer の Athena から Resource Link 経由でクエリを実行する。S3 への直接アクセスは不要で、Lake Formation が権限を仲介する。


5-4. Data Filter — Cell-Level Security × Row-Level Filter

Lake Formation Data Filter を使うと、IAM ロールごとにアクセス可能な行・列を動的に制御できる。

Row-Level Filter

resource "aws_lakeformation_data_cells_filter" "region_filter" {
  table_data {
 database_name = "sales_db"
 name = "region_sales_filter"
 table_name = "transactions"
 row_filter {
filter_expression = "region = 'ap-northeast-1'"
 }
 column_wildcard {}
  }
}

特定のロールは region = 'ap-northeast-1' のデータのみ参照できる。WHERE 句の条件式は標準 SQL で記述可能で、複合条件(region = 'ap-northeast-1' AND year >= 2024)も対応している。

Column-Level Filter

resource "aws_lakeformation_data_cells_filter" "pii_exclude_filter" {
  table_data {
 database_name = "sales_db"
 name = "pii_excluded_filter"
 table_name = "customers"
 row_filter {
all_rows_wildcard {}
 }
 column_names = ["customer_id", "region", "purchase_amount"]
  }
}

emailphone_number などの PII カラムを除外した列セットのみをアナリストに公開する。


5-5. Governed Tables — ACID × Time-Travel (Iceberg 互換)

Lake Formation Governed Tables は、Amazon S3 上に ACID トランザクション対応のテーブルを構築する機能で、Apache Iceberg と互換性を持つ。

機能概要
ACID トランザクションS3 上のデータへの Commit/Rollback 対応
Time-TravelFOR TIMESTAMP AS OF / FOR VERSION AS OF クエリ
Automatic Compaction小さなファイルを自動マージしてクエリ性能を維持
Iceberg 互換Athena・EMR・Spark から直接参照可能

Time-Travel クエリの例(Athena SQL):

-- 7日前時点のデータを参照
SELECT *
FROM sales_db.transactions
FOR TIMESTAMP AS OF (CURRENT_TIMESTAMP - INTERVAL '7' DAY)
WHERE region = 'ap-northeast-1';

Governed Tables は S3 上に _lake_formation_storage/ プレフィックスでメタデータを管理する。Iceberg カタログとして Glue Data Catalog を使用するため、既存の Athena ワークフローとの統合が容易だ。


5-6. Terraform 実装 — aws_lakeformation_permissions + aws_lakeformation_data_lake_settings

# データレイク管理者の設定
resource "aws_lakeformation_data_lake_settings" "main" {
  admins = [
 "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/LakeFormationAdmin"
  ]

  create_table_default_permissions {
 principal= "IAM_ALLOWED_PRINCIPALS"
 permissions = []
  }

  create_database_default_permissions {
 principal= "IAM_ALLOWED_PRINCIPALS"
 permissions = []
  }
}

# S3 Data Lake 場所の登録
resource "aws_lakeformation_resource" "data_lake_s3" {
  arn= aws_s3_bucket.data_lake.arn
  role_arn = aws_iam_role.lakeformation_service_role.arn
}

# データベース単位の権限付与
resource "aws_lakeformation_permissions" "etl_role_db" {
  principal = aws_iam_role.glue_etl.arn

  database {
 name = aws_glue_catalog_database.sales.name
  }

  permissions = ["CREATE_TABLE", "ALTER", "DROP", "DESCRIBE"]
}

# テーブル単位の権限付与 (読取専用アナリスト)
resource "aws_lakeformation_permissions" "analyst_select" {
  principal = aws_iam_role.data_analyst.arn

  table {
 database_name = aws_glue_catalog_database.sales.name
 wildcard= true
  }

  permissions = ["SELECT", "DESCRIBE"]
}
Lake Formation 設計ミス 落とし穴 5選

  • LF-Tag 階層設計の先送り: テーブル追加後に LF-Tag を変更すると全リソースへの再タグ付けが必要。初期設計で確定させる
  • IAM_ALLOWED_PRINCIPALS を残したまま Lake Formation 移行: Lake Formation 移行後も IAM_ALLOWED_PRINCIPALS が有効だと Lake Formation の権限制御が機能しない。data_lake_settings で明示的に無効化する
  • Cross-Account 共有で RAM 承諾漏れ: Consumer アカウントで RAM Resource Share を承諾しないと Glue Catalog が見えない。Organizations 自動承諾を設定しておく
  • Data Filter 適用後の Athena 権限エラー: Data Filter は Lake Formation Admin でも適用される。テスト時はフィルタ未適用のロールで確認する
  • Governed Tables の S3 バケットポリシー設定漏れ: Lake Formation Service-Linked Role へのバケット書き込み権限がないと ACID コミットが失敗する
LF-TBAC ベストプラクティス 5選

  • タグ体系は 3〜5 キーに絞る: キーが多すぎると権限の組み合わせが複雑化し監査性が落ちる。env / sensitivity / domain の 3 軸が現場の定番
  • LF-TBAC への段階的移行: まず新規テーブルのみ LF-TBAC を適用し、既存 Resource-Level 権限と並行運用して移行リスクを最小化する
  • Cross-Account は Organizations + RAM 自動承諾: 手動承諾フローは運用ボトルネックになる。Organizations の信頼ポリシーで自動承諾を設定する
  • Data Filter はステージング環境で先行テスト: Row-Level Filter の条件式ミスは本番データアクセス遮断につながる。Athena クエリで事前検証を徹底する
  • CloudTrail + Lake Formation 監査ログの統合: GetTemporaryGlueTableCredentials イベントで誰がいつどのテーブルにアクセスしたかを追跡できる。CloudWatch Logs Insights でダッシュボード化しておく
Lake Formation クロスリンク


6. QuickSight本番運用 — SPICE × Embedded Analytics × Q for Natural Language

6-1. SPICE — Super-fast Parallel In-memory Calculation Engine

SPICE は QuickSight の In-memory 分析エンジンで、Athena・Redshift・RDS などのデータソースからデータを取り込んでメモリ上にキャッシュすることで、インタラクティブなダッシュボードの高速レスポンスを実現する。

SPICE サイズ設計

プランSPICE 容量 (Author 1人当たり)追加購入単価
Enterprise250 GB$0.25/GB/月
Standard10 GB$0.25/GB/月

サイズ試算例:
– データセット: 100 万行 × 50 列(平均 100 bytes/行)= 約 5 GB
– 履歴 12 ヶ月分保持 → 12 × 5 GB = 60 GB
– バッファ(30%)込み = 80 GB
– 追加購入コスト = (80 GB – 250 GB/Author数) × $0.25

Author 数が少ない小規模組織ではデフォルトの 250 GB/Author で不足する場合があるため、初期段階でデータセットのサイズ見積もりを行う。

Direct Query vs SPICE 選定基準

観点Direct QuerySPICE
データ鮮度リアルタイムバッチ更新(最短 15 分)
クエリ性能データソース依存高速(メモリ)
コストAthena $5/TB / Redshift RPU 課金SPICE 容量料金
データ量上限なし500 行未満は Direct Query 不要
推奨場面リアルタイム要件あり定期レポート / 大人数閲覧

判断ルール: ダッシュボードの閲覧者数が多く、データ更新が 1 時間以上の間隔で許容される場合は SPICE が有利。Athena Direct Query はスキャン量に応じた課金のため、ヘビーな利用パターンではコストが青天井になる。


6-2. Dataset — Join × Custom SQL × 増分更新

QuickSight Dataset は、データソースから取り込んだデータを変換・結合してダッシュボードで使える形式に整える。

Custom SQL でのデータ取得

SELECT
  t.transaction_id,
  t.amount,
  t.region,
  t.created_at,
  c.customer_name,
  c.segment
FROM transactions t
INNER JOIN customers c ON t.customer_id = c.customer_id
WHERE t.created_at >= DATEADD('day', -90, GETDATE())
  AND t.status = 'completed'

Athena データソースの場合は DATEADD ではなく date_add('day', -90, current_date) を使用する(方言の違いに注意)。

Dataset の Join 設計注意点

  • Left Join 優先: Inner Join でファクトテーブルに存在しない顧客データが抜け落ちる場合は Left Join を使う
  • Filter before Join: WHERE 句で先にデータを絞り込んでから Join することで SPICE 取り込み量を削減できる
  • 計算フィールドの活用: 複雑な変換ロジックは QuickSight の計算フィールド(ifelsedateDiff 等)で実装し、SQL をシンプルに保つ

6-3. Refresh Schedule — Full / Incremental Refresh

SPICE データセットの更新スケジュールは 2 種類から選択できる。

方式仕組み適用場面
Full Refresh全データを再取り込みデータセットサイズ < 1 GB / 変更行の特定困難
Incremental Refresh差分のみ追加追記のみのログ系テーブル / タイムスタンプで差分特定可能

Incremental Refresh の設定条件

Incremental Refresh を使用するには、データセットに以下の条件が必要だ。

  1. タイムスタンプ型のカラムが存在する(created_atevent_time 等)
  2. 過去分のデータが更新されないこと(追記専用テーブル)
  3. SPICE データセットが Full Refresh で初期構築済みであること
# QuickSight Refresh Schedule(Terraform は aws_quicksight_refresh_schedule を使用)
resource "aws_quicksight_refresh_schedule" "hourly" {
  aws_account_id = data.aws_caller_identity.current.account_id
  data_set_id = aws_quicksight_data_set.sales.id
  schedule_id = "hourly-incremental"

  schedule {
 refresh_type = "INCREMENTAL_REFRESH"
 schedule_frequency {
interval  = "HOURLY"
time_of_the_day = "00:00"
 }
  }
}

コスト最適化: Full Refresh は SPICE 書き込みコスト(無料枠超過分)が発生するため、大規模データセットは Incremental Refresh で月額コストを抑制する。


6-4. Embedded Analytics — Webアプリへのダッシュボード埋め込み

QuickSight Embedded Analytics を使うと、QuickSight のダッシュボードを自社 Web アプリに埋め込むことができる。

認証方式の選択

方式仕組み用途
Anonymous EmbeddingQuickSight ユーザー不要 / URL トークンのみ外部公開ダッシュボード
IAM FederationSTS Assume Role 経由社内システム / AWS 認証利用組織
Cognito IntegrationCognito User Pool と連携BtoC アプリ / ソーシャルログイン

Embedded URL 生成の流れ(IAM Federation)

import boto3

client = boto3.client('quicksight', region_name='ap-northeast-1')

# Registered User の埋め込み URL を生成
response = client.generate_embed_url_for_registered_user(
 AwsAccountId='123456789012',
 SessionLifetimeInMinutes=60,
 UserArn='arn:aws:quicksight:ap-northeast-1:123456789012:user/default/analyst-user',
 ExperienceConfiguration={
  'Dashboard': {
'InitialDashboardId': 'dashboard-id-xxx'
  }
 }
)
embed_url = response['EmbedUrl']

(注: Python の with 構文は使用せず、明示的なクライアント管理を採用している)

Anonymous Embedding の注意点

Anonymous Embedding は QuickSight Enterprise エディション以上で使用可能。セッションあたりの課金($0.30/セッション)が発生するため、アクセス数が多い外部公開ダッシュボードではコスト試算が重要だ。IAM ロールによる Row-Level Security との組み合わせで、外部ユーザーへの閲覧制限も実装できる。


6-5. Q for Natural Language — ML ベース自然言語クエリ

QuickSight Q は自然言語(日本語・英語)でデータにクエリできる機能で、データアナリスト以外のビジネスユーザーがデータを自ら探索できる環境を提供する。

Topic 設定の手順

Q の設定は「Topic(トピック)」単位で行う。Topic は 1 つ以上の Dataset をまとめた論理的な質問領域だ。

  1. Dataset 選択: Q の対象となる Dataset を指定する
  2. フィールド設定: 各カラムに「フレンドリーな名前」と「説明」を付与する(自然言語の認識精度に直結)
  3. Synonym(同義語)定義: 「売上」と「Amount」を同義語として登録することで、業務用語でのクエリが正確に解釈される
  4. Date フィールドの指定: 「先月の売上を教えて」のような時間軸クエリのために、Date/DateTime 型カラムを明示する

自然言語クエリの例

  • 「先月の地域別売上を教えて」→ created_at で直近 1 ヶ月、region 別に amount を集計
  • 「トップ 10 の顧客は?」→ customer_nametotal_amount 降順でソートして上位 10 件表示
  • 「昨年対比の成長率は?」→ 前年同期比の計算を自動生成

日本語対応: Q は日本語クエリに対応しているが、フィールド名・説明文を日本語で記述することで認識精度が向上する。英語のカラム名(cust_idamt_jpy)のまま運用するよりも、日本語フレンドリー名称を付与したほうが Q の回答品質が高い。

Q × Bedrock Agents 連携(将来展望)

現時点では Q は独立した ML モデルを使用しているが、Bedrock Agents との統合により、より複雑な分析・推論クエリへの対応が拡張予定とされている。自社の分析ニーズが Q の回答精度を超える場合は、Athena + Bedrock Agents の組み合わせを検討する(AI Vol1 参照)。


6-6. Terraform 実装 — QuickSight データソース × Dataset

QuickSight の Terraform リソースは比較的新しく、全機能を Terraform でカバーするよりも AWS CDK(TypeScript)での管理が進んでいる。以下は主要リソースの実装例だ。

# Athena データソースの登録
resource "aws_quicksight_data_source" "athena" {
  aws_account_id = data.aws_caller_identity.current.account_id
  data_source_id = "athena-sales-datasource"
  name  = "Athena Sales DataSource"
  type  = "ATHENA"

  parameters {
 athena {
work_group = aws_athena_workgroup.analytics.name
 }
  }

  ssl_properties {
 disable_ssl = false
  }

  permission {
 actions= ["quicksight:UpdateDataSourcePermissions", "quicksight:DescribeDataSource", "quicksight:DescribeDataSourcePermissions", "quicksight:PassDataSource", "quicksight:UpdateDataSource", "quicksight:DeleteDataSource"]
 principal = "arn:aws:quicksight:ap-northeast-1:${data.aws_caller_identity.current.account_id}:user/default/${var.quicksight_admin_user}"
  }
}

# Dataset の定義(Custom SQL)
resource "aws_quicksight_data_set" "sales_monthly" {
  aws_account_id = data.aws_caller_identity.current.account_id
  data_set_id = "sales-monthly-dataset"
  name  = "Sales Monthly Summary"
  import_mode = "SPICE"

  physical_table_map {
 physical_table_map_id = "sales-table"
 custom_sql {
data_source_arn = aws_quicksight_data_source.athena.arn
name= "sales_query"
sql_query = <<-SQL
  SELECT
 DATE_TRUNC('month', created_at) AS month,
 region,
 SUM(amount) AS total_amount,
 COUNT(*) AS transaction_count
  FROM transactions
  WHERE created_at >= date_add('month', -12, current_date)
  GROUP BY 1, 2
SQL
 }
  }
}

AWS CDK を補完的に使用する場合: QuickSight の Dashboard・Analysis・Theme は Terraform のリソース定義が限定的なため、AWS CDK(TypeScript)で QuickSightDashboard を定義し、Terraform の null_resource + CLI 呼び出しで連携する構成が現場では多い。

QuickSight 本番運用ベストプラクティス 5選

  • SPICE 容量は Author 数 × 月間データ量で試算: データセットが増えると容量が急増する。月次でコンソールの SPICE 使用率を CloudWatch カスタムメトリクスに出力し、80% 超過でアラートを設定する
  • Incremental Refresh でコスト・時間を削減: 大規模ログ系テーブルは Full Refresh だと SPICE 書き込みに数十分かかる。追記専用テーブルは Incremental Refresh を必ず設定する
  • Row-Level Security (RLS) を Dataset に設定: 閲覧ユーザーごとにアクセス可能な行を制限するには、RLS Dataset(ユーザー → 許可する行のルール)を先に作成し、本 Dataset に適用する。Lake Formation Data Filter と役割が重複するため、どちらかに統一する
  • Embedded は Session Manager ベースのログ管理: 埋め込み URL は時間制限(最大 10 時間)があるため、バックエンドで URL を動的生成する。CloudFront + Lambda@Edge で埋め込み URL を署名・発行する構成が標準だ
  • Q Topic は部門ごとに分離: 全社共通の 1 Topic に全データセットを詰め込むと、フィールド名の衝突・同義語の混在で精度が落ちる。営業・財務・マーケティングで Topic を分けて管理する
QuickSight クロスリンク

  • AI Vol1 ← QuickSight Q for Natural Language × Bedrock Agents 連携展望
  • コスト Vol1 ← SPICE 容量費用試算 / Athena Direct Query vs SPICE コスト比較

7. 詰まりポイント7選 + アンチパターン演習5問

7-1. 詰まり7選 — “なぜ詰まるか” → “どう解くか”

7-1-1. Glue Job OOM: Worker Type/DPU不足

なぜ詰まるか

Glue Spark JobでデータセットサイズがWorker TypeのメモリキャパシティGlue を超えた場合、java.lang.OutOfMemoryError: GC overhead limit exceeded が発生してJobが失敗する。デフォルト設定の NumberOfWorkers: 2 + G.1X (4 vCPU/16GB RAM) は小規模テスト向けであり、Parquet変換・大量JOIN・Spark Streamingで本番データ量を処理しようとすると必ず問題になる。また max_concurrent_runs を未設定のまま複数Trigger設定すると、並走したJobが互いにリソースを奪い合いOOMが連鎖する。

どう解くか

  • データ量10GB超: worker_type = "G.2X" (8 vCPU/32GB) に変更
  • execution_property.max_concurrent_runs = 1 で並走を防ぐ
  • default_arguments--spark.executor.memory を明示指定
  • CloudWatch Metrics の glue.driver.aggregate.bytesRead でDPU妥当性を継続監視
resource "aws_glue_job" "etl" {
  name= "prod-etl-job"
  role_arn  = aws_iam_role.glue.arn
  glue_version = "4.0"

  command {
 name= "glueetl"
 script_location = "s3://my-bucket/scripts/etl.py"
 python_version  = "3"
  }

  worker_type = "G.2X"
  number_of_workers = 10

  execution_property {
 max_concurrent_runs = 1
  }

  default_arguments = {
 "--spark.executor.memory"  = "24g"
 "--job-bookmark-option" = "job-bookmark-enable"
 "--enable-job-insights" = "true"
  }
}

7-1-2. Athena Partition Pruning失敗: WHERE句でパーティション列を使わない

なぜ詰まるか

S3データを dt=2024-01-01/ のようにパーティション管理していても、クエリで WHERE event_date = '2024-01-01' と非パーティション列を指定すると全パーティションスキャンになり、コスト ($5/TB scanned) が爆発する。カラム名の微妙なズレ (dt vs event_date) が本番環境でこの罠を引き起こす。Partition Projectionを設定済みでも projection.enabled = 'false' のまま放置した場合も同様の症状が発生する。

どう解くか

パーティション列名 (dt) とWHERE句のカラムを一致させ、Partition Projectionを有効化する。

-- NG: パーティション列と不一致
SELECT * FROM events WHERE event_date = '2024-01-01';

-- OK: パーティション列を直接指定
SELECT * FROM events WHERE dt = '2024-01-01';

Partition Projection 確認手順:
1. SHOW TBLPROPERTIES my_tableprojection.enabled = true であることを確認
2. projection.dt.type = date / projection.dt.format = yyyy-MM-dd が設定されているか確認
3. Athena Query Editor で EXPLAIN を実行してパーティション絞り込み件数を確認
4. CloudWatch Metrics DataScannedInBytes でクエリごとのスキャン量を監視


7-1-3. Redshift Serverless RPU爆発: Base RPUの見積もり甘さ

なぜ詰まるか

Redshift Serverlessは「使用中のRPU量 × 稼働秒数」で課金される。base_capacity = 128 RPU を固定にしたまま本番運用すると、軽量クエリでも128 RPU分 ($0.36/RPU-hour × 128 = 約$46/h) が発生し続け、月額で数十万円規模になる。開発フェーズで決めたBase RPU設定を本番にそのまま適用するケースで頻発する。

どう解くか

  • 本番起動時は base_capacity = 8 RPU から開始し、実測値から増分チューニング
  • CloudWatch の ServerlessComputeCapacity メトリクスでRPU使用量を監視
  • max_capacity を設定してコスト上限をガード
  • バッチ実行後はScheduled Actionで base_capacity を最小値に戻す
resource "aws_redshiftserverless_workgroup" "prod" {
  workgroup_name = "prod-workgroup"
  namespace_name = aws_redshiftserverless_namespace.main.namespace_name
  base_capacity  = 8
  max_capacity= 128

  config_parameter {
 parameter_key= "max_query_execution_time"
 parameter_value = "14400"
  }
}

resource "aws_cloudwatch_metric_alarm" "rpu_high" {
  alarm_name = "redshift-serverless-rpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name= "ServerlessComputeCapacity"
  namespace  = "AWS/Redshift-Serverless"
  period  = 300
  statistic  = "Maximum"
  threshold  = 64
  alarm_actions = [aws_sns_topic.alerts.arn]
}

7-1-4. Lake Formation Cross-Account設定詰まり: RAM Accept前にGrantしてしまう

なぜ詰まるか

Producer Account (データ提供側) でLake Formation の Grant permissions を先に実行し、その後に RAM (Resource Access Manager) の Resource Share をConsumer Account にAcceptさせるという逆順が詰まりの原因。Consumer側からは ResourceNotFoundExceptionAccess Denied エラーが発生し続け、原因が分かりにくい。

どう解くか

RAM Accept → LF Grant の順序を厳守する。

正しい手順フロー:
1. Producer Account: Glue Catalog → RAM Resource Share を作成してConsumer Accountを追加
2. Consumer Account: AWS Console → Resource Access Manager → Shared with me → RAM Accept
3. Consumer Account: Glue Console → Create a database link でProducer ARNを指定
4. ここでProducer側にGrant Producer Account: Lake Formation → Grant permissions → Consumer IAM Role ARN に SELECT権限を付与
5. Consumer Account: Athena から linked-db.table_name でクエリ実行して疎通確認


7-1-5. QuickSight SPICE再ロード失敗: メモリ上限/タイムアウト

なぜ詰まるか

SPICEのデフォルト容量は10GB (追加購入可能)。データセットが大きいままFull Refreshを毎時実行すると、SPICE_REFRESH_CAPACITY_EXCEEDED またはタイムアウトエラー (REFRESH_TIMEOUT) が発生する。特にAthenaをデータソースにして大量ログを直接クエリする構成で頻発する。エラーはQuickSight上で無音失敗することがあり、古いデータが表示され続ける問題も発生する。

どう解くか

  • Incremental Refresh設計: Created dateupdated_at カラムを活用してデルタ取り込みに変更
  • フィルタ設定: 直近N日分のみ取り込むWHERE条件をデータセットクエリに追加
  • SPICE容量追加: QuickSight コンソール → Admin → SPICE capacity で増量購入
  • Athena Workgroupのデータスキャン上限を設定してSPICE取り込みクエリのコスト制御

7-1-6. Glue Crawler無限ループ: S3 Event Trigger + Crawl All Sub-Folders

なぜ詰まるか

S3 EventBridge TriggerでGlue Crawlerを起動し、かつCrawler設定で Crawl all sub-folders を有効にしている場合、Crawlerの出力 (Parquet一時ファイルの書き込み) がS3 PUTイベントを発生させ、再度Crawlerがトリガーされる無限ループが発生する。CloudWatch Logsに Maximum concurrent runs エラーが積み重なり、Crawlerの課金も際限なく発生する。

どう解くか

  • Crawler設定: Exclude patterns_temporary/**_spark*/** などの一時ファイルパスを追加
  • Crawl mode を Crawl new sub-folders only に変更して差分のみクロール
  • S3 Event TriggerはRaw入力パス専用のPrefix (raw/) のみにスコープする
  • 根本解決: Glue CrawlerをやめてPartition Projectionに移行 (Crawlerコスト自体がゼロになる)

7-1-7. S3 Listing過多によるAthena遅延: 非Partition列のGlob使用

なぜ詰まるか

S3データが s3://bucket/data/year=2024/month=01/day=01/ のような深いパーティション構造を持つ場合、WHERE句に yearmonthday を全て指定しないとAthenaがS3 Listing APIを大量発行し、クエリ開始まで数秒〜数十秒の遅延が発生する。数百万オブジェクトを持つバケットで特に顕著になる。

どう解くか

Partition Projectionへの移行手順:

-- Step 1: テーブルにPartition Projection設定を追加
ALTER TABLE access_logs
SET TBLPROPERTIES (
  'projection.enabled'  = 'true',
  'projection.year.type'= 'integer',
  'projection.year.range'  = '2020,2030',
  'projection.month.type'  = 'integer',
  'projection.month.range' = '1,12',
  'projection.month.digits'= '2',
  'projection.day.type' = 'integer',
  'projection.day.range'= '1,31',
  'projection.day.digits'  = '2',
  'storage.location.template' =
 's3://bucket/data/year=${year}/month=${month}/day=${day}/'
);

-- Step 2: パーティション列を全て指定してクエリ
SELECT * FROM access_logs
WHERE year = 2024 AND month = 1 AND day = 15;

7-2. アンチパターン演習5問

Q1. Glue Job設定のアンチパターン

Q1. 問題: 以下のGlue Job設定の何が問題か?

resource "aws_glue_job" "bad_job" {
  command {
 name  = "pythonshell"
 python_version = "3"
  }
  default_arguments = {
 "--enable-spark-ui" = "true"
  }
}

このJobのスクリプト内では SparkContext を初期化してS3 DataFrameを処理する実装になっている。デプロイは成功するが実行時にエラーになる。

A1. 正解パターン

  • 問題点: pythonshell Job TypeはSparkエンジンを起動しない。SparkContext / DataFrame API を使うには glueetl (Spark ETL) Job Typeが必須。
  • --enable-spark-uipythonshell では無効なパラメータ。
  • 修正: command.name = "glueetl" に変更し、worker_type = "G.1X"number_of_workers を明示指定する。
  • Python Shell Jobの用途: SparkなしでPython処理する軽量タスク (boto3 APIコール・小規模データ変換) のみ。

Q2. Athena Partition Projectionのアンチパターン

Q2. 問題: 以下のAthenaテーブル設定の何が問題か?

TBLPROPERTIES (
  'projection.enabled'  = 'false',
  'projection.dt.type'  = 'date',
  'projection.dt.format'= 'yyyy-MM-dd',
  'storage.location.template' =
 's3://bucket/logs/dt=${dt}/'
)

「Partition Projectionを設定した」として運用しているが、Athenaクエリで WHERE dt = '2024-01-15' を指定してもパーティションがプルーニングされず、全データをスキャンしてコストが高止まりしている。

A2. 正解パターン

  • 問題点: projection.enabled = 'false' のまま。Partition Projectionのパラメータを定義しても、enabledfalse では機能しない。
  • 修正: 'projection.enabled' = 'true' に変更する。
  • 設定変更後は SHOW TBLPROPERTIES my_table を実行して projection.enabled = true を確認する。
  • Glue CrawlerによるパーティションMSCK管理を廃止できるためCrawlerコストも削減できる。

Q3. Redshift WorkgroupのアンチパターンQ

Q3. 問題: 以下のRedshift Serverless構成の何が問題か?

本番ETLと開発用アドホッククエリを同一Workgroupで運用
Namespace: prod-namespace
Workgroup: prod-workgroup (Base RPU: 64)
本番バッチ: 毎時0分に大量集計処理
開発チーム: 日中随時アドホッククエリを実行
A3. 正解パターン

  • 問題点: 本番バッチと開発クエリが同一Workgroupで実行されるとピーク時に競合し、相互にパフォーマンスが劣化する。コスト配賦も不可能。
  • 修正: WorkgroupをProd/Devで分離する。
  • 本番Workgroup: base_capacity = 8、バッチ時間帯のみ max_capacity を高めに設定
  • 開発Workgroup: base_capacity = 8、コスト上限でアドホック分析を制御
  • Namespace (データ) は共有しつつWorkgroup (コンピュート) を分離できる設計を活用する。

Q4. LF-Tag PolicyのアンチパターンQ

Q4. 問題: 以下のLake Formation設定の何が問題か?

設定A: LF-Tag "env=prod" → IAM Role A に SELECT権限付与
設定B: Resource-Level (テーブル直接指定) → IAM Role A に SELECT権限付与
→ 両設定が同一テーブルに重なっている
症状: IAM Role Aからのクエリが断続的に Access Denied になる
A4. 正解パターン

  • 問題点: LF-Tag-Basedポリシーと Resource-Levelポリシーが同一リソースに混在するとLake FormationのPolicyエバリュエーションで競合が発生し、予期せぬDenyになる。
  • 修正: アクセス制御方式をLF-Tag-Based (LF-TBAC) に統一する。Resource-Level設定を全て削除する。
  • 移行中は aws lakeformation get-effective-permissions-for-path で実効権限を確認する。
  • LF-TBACに一本化することで大規模な組織でもタグ単位で権限管理できる。

Q5. QuickSight DatasetのアンチパターンQ

Q5. 問題: 以下のQuickSight構成の何が問題か?

データソース: Athena (ログテーブル 500GB/月)
インポートモード: Direct Query
ダッシュボード: 20種類のビジュアル、1日50回参照
症状: ダッシュボード表示に毎回30〜60秒。月次Athenaコストが予算超過。
A5. 正解パターン

  • 問題点: Direct QueryモードではダッシュボードアクセスのたびにAthenaクエリが実行され、コスト ($5/TB scanned) と待ち時間が発生し続ける。参照頻度が高いほど悪化する。
  • 修正: SPICE (インメモリ) モードに移行し、Incremental Refresh設定を追加する。
  • 直近30日分のみSPICEに取り込み、それ以前はDirect Queryで補完するハイブリッド設計も有効。
  • SPICE Auto-refresh を日次1回に設定してAthenaクエリ発行を最小化する。
  • $0.25/GB/月 のSPICEストレージコストはAthenaクエリ削減効果で十分回収できる。
本番デプロイ前 Analytics/Data Lake チェックリスト

  • S3 Data Lake バケット Block Public Access 4設定 全有効
  • Glue Job Type が glueetl であることを確認 (pythonshell 誤用禁止)
  • Glue Job DPU 試算 + Job Bookmark 有効化 + max_concurrent_runs 設定
  • Athena Workgroup でデータスキャン上限設定 (BytesScanned上限)
  • Redshift Serverless Base RPU = 8 スタート + CloudWatch Alarm 設定
  • Lake Formation アクセス制御を LF-TBAC に統一 (Resource-Level混在禁止)
  • QuickSight SPICE容量試算 + Incremental Refresh 設定完了

8. まとめ — Vol2予告 + 落とし穴10選 + 全17軸クロスリンク

Analytics/Data Lake 本番運用コスト警戒5項目

  • Glue DPU: G.2X (8 vCPU/32GB) は G.1X の2倍課金 — 最初は G.1X で計測してから昇格
  • Athena: CSV直接クエリは $/TB scanned 爆発 — Parquet+Snappy圧縮を本番デフォルト
  • Redshift Serverless: Base RPU 8 × $0.36/RPU-hour × 24h × 30日 ≈ 月額$2,074 + Concurrency Scaling別課金
  • QuickSight SPICE: $0.25/GB/月 × データセット容量 — 履歴保持期間も加味した試算必須
  • S3 Storage Class: Standard固定でなく Lifecycle Policy で Intelligent-Tiering へ自動移行

8-1. Vol1 まとめ: 5本柱統合の3ポイント

Analytics/Data Lake本番運用 Vol1 設計3原則

  • ① データレイク設計の三位一体: S3 (Storage) × Glue/Athena (Processing) × Lake Formation (Governance) を一体設計する。Storage側でパーティション戦略を固め、Athena側でPartition Projectionを合わせることでGlue Crawlerコストを排除できる。S3 Parquet + Snappy圧縮をデフォルトにしてスキャンコストを1/10に削減する。
  • ② アクセス制御の一本化: IAM Policy単独でなくLake Formation LF-Tag-Based Access Control (LF-TBAC) に統一する。Resource-Level設定との混在は断続的なAccess Deniedの元凶。Cross-Account共有はRAM Accept → LF Grant の順序を厳守する。
  • ③ 可視化コストの最適化: QuickSight は Direct Queryのまま放置しない。SPICE + Incremental Refresh でAthenaクエリ回数を大幅削減し、ダッシュボード応答速度とコストを同時に改善する。$0.25/GB/月のSPICEコストはクエリ削減効果で回収できる。

8-2. Vol2 予告: ストリーミング × オープンテーブルフォーマット

Analytics/Data Lake本番運用 Vol2 予告トピック

  • Kinesis Data Firehose × S3: リアルタイムデータレイク構築とPartitioning設定
  • Apache Iceberg on S3: タイムトラベルクエリ・ACID トランザクション・スキーマ進化
  • Delta Lake on S3: CDC (Change Data Capture) 設計とGlue Native Delta対応
  • Apache Hudi on S3: アップサート (Upsert) 対応とGlue Spark統合パターン
  • AWS Glue Data Quality: DQルール定義・自動アラート・品質スコアダッシュボード

Vol2 (Kinesis/Iceberg/Delta Lake/Hudi) を待つ

8-3. Analytics/Data Lake本番運用 落とし穴10選

  1. Glue Job Typeミス: Python Shell でSparkコードを動かそうとして実行エラー → glueetl 必須
  2. Partition Pruning未達: Athena WHERE句のカラム名がパーティション列名と不一致 → S3全スキャン
  3. Projection enabled見落し: projection.enabled=false のまま設定完了と誤認 → 全スキャン継続
  4. Redshift Base RPU高止まり: 開発時の128 RPU設定を本番流用 → 月数十万円の過剰課金
  5. LF-Tag×Resource-Level混在: 同一リソースに複数のLF設定 → 断続的Access Denied
  6. Cross-Account Grant順序ミス: RAM Accept前にGrant → Consumer側でResourceNotFoundException
  7. QuickSight Direct Query乱用: 高頻度ダッシュボードにDirect Query → Athenaコスト爆発
  8. Glue Crawler無限ループ: S3 Event Trigger + Crawl all sub-folders → Crawler課金無限発生
  9. SPICE容量超過無音失敗: 容量上限でRefreshが失敗し古いデータを表示し続ける
  10. Workgroup未設定: Athena全ユーザーがデフォルトWorkgroupで無制限クエリ → コスト青天井

8-4. 全17軸 双方向ハブナビゲーション

AWS本番運用 全17軸 (Analytics/Data Lake Vol1 起点)


前記事: Edge/CDN本番運用 Vol1