🐷

"実務でつまずいたところ全部まとめました:Terraform×AWSデータ基盤ハンドブック(前編)"

に公開

1. はじめに

外資コンサルのデータチームにジョインしました。直近のプロジェクトで弱いところを補うテーマで実装してみることになり、最初のタスクが 「RSS → S3 → Glue/Athena基盤をTerraformで立ち上げること」でした。

やってみたら、予想以上にハマりどころだらけ(笑)。
そこで、同じような基盤をつくる人向けに、実務でつまずいたポイントを全部まとめて記事化しました。

今回の記事は 前編(設計・Terraform適用編) です。
後編では 監視・運用・権限管理のベストプラクティス を掘り下げていく予定です。


2. アーキテクチャ概要(今回の採用技術)

今回つくった基盤の全体像はこんなイメージです 👇

[RSS] --fetch--> [Lambda]
|
v (CSV, SSE-KMS)
s3://raw/rss/dt=YYYY-MM-DD/source=<name>/part-*.csv
|
v
[Glue Catalog: rss table]
|
v
[Athena WorkGroup]
- s3://athena-results (SSE-KMS)
- 結果保存先と暗号化を強制

ストレージ(S3)

  • バケットは rawathena-results の2種類を用意
  • Public Access Block 有効化
  • Versioning 有効化
  • デフォルト暗号化は SSE-KMS

暗号鍵(KMS)

  • alias 運用で管理
  • Key Policy に 運用ロールLambda実行ロール を明示的に許可

メタデータ(Glue Catalog)

  • Database + Table を定義
  • rss テーブルは Partition Projection を採用
    • パーティションキー: dt(日付), source(取得元)

クエリエンジン(Athena)

  • WorkGroup を固定
  • クエリ結果は 専用S3 + SSE-KMS で保存

取得処理(Lambda)

  • 言語は Python
  • 処理フロー: RSS → 正規化 → CSV → S3
  • 保存先: raw/rss/dt=YYYY-MM-DD/source=<name>/part-*.csv
  • 環境変数 KMS_KEY_ARN で暗号鍵を指定

スケジュール(EventBridge)

  • ルール: rate(15 minutes)
  • Lambda Invoke の許可は 関数側のリソースベースポリシーで付与

監視(CloudWatch Logs)

  • Lambda の実行ログを CloudWatch Logs に送信
  • Athena結果S3の暗号化状態も合わせてチェック

3. Terraform設計パターン

3.1 ディレクトリ構成

Terraform のリポジトリ構成はこんな感じにしました。

new-project-terraform-aws/
├─ env/
│ ├─ dev/
│ │ ├─ backend.tf
│ │ ├─ main.tf
│ │ ├─ variables.tf
│ │ └─ terraform.tfvars.example
│ └─ bootstrap/ # state 用 S3 や DynamoDB ロック用
└─ modules/
├─ security-kms/
├─ storage-s3/
├─ analytics-athena-glue/
├─ iam-lambda/
├─ lambda-function/
└─ scheduler-eventbridge/

  • env/dev : 開発環境用
  • env/bootstrap : state 管理用(S3 バケットや DynamoDB ロックテーブル)
  • modules : 論理コンポーネントごとに分割して再利用できるようにした。

3.2 Backend(state 管理)

state は S3 + DynamoDB Lock を採用しました。

terraform {
backend "s3" {
bucket = "<state-bucket>"
key = "env/<env>/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "<lock-table>"
encrypt = true
}
}

  • 初回注意点:S3 バケットと DynamoDB テーブルが存在しないと terraform init が失敗します
  • そのため bootstrap 環境を用意し、まずはそこから state 管理用のリソースを作るのがおすすめです
  • 既存の S3 がある場合は terraform import で取り込みます。

3.3 変数・出力・タグの扱い

  • 共通変数: project, env, region
  • 論理名: kms_alias, raw_bucket_name, athena_results_bucket_name, glue_database_name
  • すべてのモジュールに Project, Env タグを付与して課金や運用を追跡しやすくします。

3.4 KMSモジュール(security-kms)

  • Key Policy に rootLambda実行ロール を明示的に許可
  • Grant を多用せず、ポリシーで完結できる範囲はポリシーで対応
  • 出力値: key_id, key_arn, alias_arn

3.5 S3モジュール(storage-s3)

  • rawathena-results の2つのバケットを作成
  • SSE-KMS を強制
  • Public Access Block 有効化
  • Versioning 有効化

3.6 Glue / Athenaモジュール(analytics-athena-glue)

  • Glue Database と rss テーブルを作成
  • Athena WorkGroup を専用に作成し、結果出力先と暗号化を固定化

rssテーブル定義のイメージ:

  • パーティションキー: dt (string), source (string)
  • パラメータ(Partition Projection)
    • projection.enabled = true
    • projection.dt.type = date
    • projection.source.type = injected
    • storage.location.template = s3://<raw>/raw/rss/dt=$${dt}/source=$${source}/
  • カラム: title, link, published_at, source_name

3.7 IAM(Lambda実行ロール)

  • Logs: logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents
  • S3:
    • ListBucket は prefix 条件つき(raw/rss/*
    • PutObject/GetObject はオブジェクトキーに限定
  • KMS: 対象キーのみに限定して Encrypt/Decrypt/GenerateDataKey*/DescribeKey

💡 よくある失敗

  • ListBucket に prefix 条件を付け忘れて過剰権限になる
  • Resource をバケット直下に指定してしまい、オブジェクトキーが限定されない

3.8 Lambdaデプロイ(lambda-function)

  • 組織によっては lambda:CreateFunction が SCP や Permissions boundary で禁止されている
  • 初回はコンソールで手動作成し、以降は UpdateFunctionCode 運用
  • Terraformでは use_existing_lambda フラグを用意して既存関数を参照できるようにした

3.9 EventBridge(scheduler-eventbridge)

  • aws_cloudwatch_event_rule + aws_cloudwatch_event_target を利用
  • Lambda Invoke の許可は 関数のリソースベースポリシーで制御
  • Terraformでは add_lambda_permission フラグで付与の有無を切り替え可能

4. デプロイ手順(dev想定)

Terraform で dev 環境を立ち上げるまでの手順を、前提 → 認証 → 適用 → 検証 の順でまとめます。

4.1 前提(ディレクトリ & tfvars)

  • この記事の前提ディレクトリ構成:
    new-project-terraform-aws/
    ├─ env/
    │ ├─ dev/
    │ │ ├─ backend.tf
    │ │ ├─ main.tf
    │ │ ├─ variables.tf
    │ │ └─ terraform.tfvars # ★ 実値を入れる
    │ └─ bootstrap/
    └─ modules/
    ├─ ...

  • env/dev/terraform.tfvars に一意な名前等を設定します(例):

project = "new-project"
env = "dev"
region = "ap-northeast-1"

raw_bucket_name = "new-project-dev-raw-123456789013"
athena_results_bucket_name = "new-project-dev-athena-results-123456789013"
glue_database_name = "new_project_dev"
kms_alias = "alias/new-project-dev"

use_existing_lambda = true
lambda_function_arn = "arn:aws:lambda:ap-northeast-1:123456789013:function:new-project-dev-rss-ingestion"
add_lambda_permission = false

4.2 認証(AWS SSO / プロファイル)

  • 例:terraform-bootstrap という SSO プロファイルを使う前提です。

ログイン

AWS_PROFILE=terraform-bootstrap aws sso login

認証確認

AWS_PROFILE=terraform-bootstrap aws sts get-caller-identity --region ap-northeast-1

4.3 初期化と適用(dev 環境)

  • env/dev 配下で init → apply の順に実行します。

cd new-project-terraform-aws/env/dev

初期化(backend.tf の S3/DynamoDB が存在する前提)

terraform init -input=false

差分確認(任意)

terraform plan

適用

terraform apply -auto-approve

📝 メモ:

  • もし terraform init で backend 用の S3/DynamoDB が無くて失敗する場合は、
    先に env/bootstrap で state リソースを作成するか、既存リソースを import して整合を取ってください。

4.4 既存 Lambda 参照パターン(CreateFunction 禁止対応)

  • 組織の SCP/Permissions boundary で lambda:CreateFunction が禁止されているケース向けの回避策です。
  • 初回は コンソールで関数を手動作成 → Terraform は 既存関数を参照

変数フラグで既存参照に切り替え(tfvars 側で設定している場合は不要)

terraform apply
-var="use_existing_lambda=true"
-var="lambda_function_arn=arn:aws:lambda:ap-northeast-1:123456789013:function:new-project-dev-rss-ingestion"
-var="add_lambda_permission=false"
-auto-approve

  • EventBridge → Lambda の Invoke 許可は 関数側のリソースベースポリシー で制御します(必要に応じて手動付与)。

4.5 動作確認(Lambda → S3 → Athena)

  1. Lambda を手動実行(コンソールでテスト {}
  2. CloudWatch Logs に処理ログが出ることを確認
  3. S3 生成物(当日パーティション配下)を確認
  4. Athena(WorkGroup 固定)で SELECT が通ることを確認

直近1時間のログを tail(関数名は環境に合わせて変更)

AWS_PROFILE=terraform-bootstrap aws logs tail /aws/lambda/new-project-dev-rss-ingestion --since 1h

-- Athena(WorkGroup は専用WGに切り替え)
-- 当日分をサッと確認
SELECT *
FROM new_project_dev.rss
WHERE dt = 'YYYY-MM-DD'
LIMIT 10;

-- 日別件数の確認
SELECT dt, count(*) AS cnt
FROM new_project_dev.rss
GROUP BY dt
ORDER BY dt DESC;

4.6 よくあるエラーと素早い対処

  • AccessDenied: athena:StartQueryExecution

    • WorkGroup で結果保存先や KMS を固定しているか確認
    • 対象バケット/KMS の権限がロールに付与されているか確認
  • CreateFunction: 403

    • 組織の SCP/Permissions boundary が原因
    • 初回は手動作成 → 以降は既存参照で回避
  • AddPermission: 404/403

    • Lambda が未作成 / GetPolicy 不足
    • 先に関数を作成し、関数側のリソースベースポリシーで events.amazonaws.com を許可
  • S3 Put/Get: AccessDenied

    • IAM の Resource がバケット直下になっていないか(オブジェクトキー配下に限定)
    • ListBucketprefix 条件を付けているか

4.7 検証チェックリスト(使い回しOK)

  • AWS SSO ログイン済み(aws sts get-caller-identity OK)
  • backend の S3/DynamoDB が存在、または import 完了
  • terraform init / plan / apply が成功
  • Lambda 手動実行で CloudWatch Logs が更新
  • S3(raw/rss/dt=YYYY-MM-DD/source=<name>/)に CSV が生成
  • Athena(専用 WorkGroup)で SELECT が成功
  • EventBridge ルール(15分間隔)が動作

5. セキュリティ・ベストプラクティス

5.1 IAM ポリシーは「最小権限」

  • ListBucket は必ず prefix 条件を付ける
    • ✅ 良い例: arn:aws:s3:::<raw-bucket>/raw/rss/*
    • ❌ 悪い例: arn:aws:s3:::<raw-bucket>(バケット直下だと過剰権限)
  • PutObject/GetObject はオブジェクトキー配下だけに限定
  • KMS は対象キーのみに制限(Encrypt/Decrypt/GenerateDataKey/DescribeKey)

5.2 暗号化は SSE-KMS をデフォルトで強制

  • S3 バケットは デフォルト SSE-KMS を設定
  • Athena WorkGroup の出力先も SSE-KMS で強制
  • 「デフォルト暗号化なし」だとレビューで落とされることが多い

5.3 リソースベースポリシーでの制御

  • EventBridge → Lambda の Invoke は 関数側のリソースベースポリシー で許可
  • IAM ロールに余計な権限を付けずに済むのでセキュア
  • Terraform 側では add_lambda_permission をフラグ化し、環境ごとに制御

5.4 組織ポリシー(SCP/Boundary)との付き合い方

  • 多くの組織では lambda:CreateFunction が禁止されている
  • 回避策:
    • 初回はコンソールで Lambda を作成
    • Terraform 側は use_existing_lambda=true で既存参照
  • 長期的には「運用専用ロール」を定義して CreateFunction を部分的に許可するのが理想

5.5 ログと監査

  • Lambda 実行ログは必ず CloudWatch Logs に出力
  • Athena クエリログも WorkGroup 経由で監査可能
  • S3 のアクセスログや CloudTrail 連携で「誰がいつ何をしたか」を可視化できるとさらに安心

6. トラブルシュート(実録)

ここでは、実際にハマったポイントとその解決策をまとめます。

6.1 Lambda CreateFunction が 403 になる

症状

  • terraform apply 時に lambda:CreateFunction が 403 で失敗

原因

  • 組織の SCP や Permissions boundary で CreateFunction が禁止されている

解決策

  • 初回だけ コンソールで関数を手動作成
  • Terraform は use_existing_lambda=true で既存関数を参照するように変更

6.2 Lambda AddPermission が 404 / 403 になる

症状

  • EventBridge → Lambda の Invoke 許可を Terraform で付与しようとすると失敗

原因

  • 関数がまだ存在しない
  • または GetPolicy 権限不足

解決策

  • コンソールから関数にリソースベースポリシーを手動追加
  • その後、権限が整えば IaC 化に戻す

6.3 RSS 取得が失敗する

症状

  • Lambda 実行時にタイムアウト / 接続エラー

原因

  • VPC 接続ありで NAT が無いケース
  • タイムアウト値が短すぎるケース

解決策

  • 基本は VPC 接続なし で動かす
  • やむを得ず VPC 内で実行するなら NAT Gateway を用意する
  • Lambda のタイムアウトを 60〜90秒 に調整する

6.4 Athena の結果保存でエラー

症状

  • クエリ実行時に AccessDeniedOutput location not found が発生

原因

  • WorkGroup 側で出力先 S3 や SSE-KMS 設定がされていない
  • 実行ロールに権限が不足している

解決策

  • WorkGroup を 専用に作成し、S3 出力先と KMS を固定化する
  • S3/KMS 権限をロールに付与する

6.5 S3 バケットの import 周り

症状

  • terraform apply が S3 でハング / 403 / 409 エラーになる

原因

  • 既に同名バケットが存在していた
  • 読み取り系権限不足(GetBucketVersioning など)

解決策

  • terraform import を活用して既存バケットを state に取り込む
  • IAM 権限に s3:GetBucketVersioning, s3:GetEncryptionConfiguration などを追加

7. 他プロジェクトへの展開ポイント

今回つくった基盤は、そのまま別の案件でも再利用しやすいように設計しました。
横展開する際に気をつけたポイントをまとめます。

7.1 変数と命名規約を外出し

  • project, env, region を共通変数として定義
  • バケット名 / Glue DB 名 / KMS alias などは terraform.tfvars 側で差し替えられるようにした
  • 環境ごとに env/dev, env/stage, env/prod を切って使い回し

7.2 モジュールの入出力IFを固定化

  • modules/security-kms, modules/storage-s3, modules/analytics-athena-glue などの IF を固定化
  • 共通化すると、別プロジェクトでも「env/ 配下をコピーして tfvars を書き換えるだけ」で利用可能

7.3 Lambda 運用の標準化

  • 多くの組織では CreateFunction が禁止されているので「既存関数参照フラグ」を標準パターン化
  • IaC 側では EventBridge のスケジュールや IAM だけを管理
  • 関数の初期作成は人間がやって、以降のコード更新は CI/CD or UpdateFunctionCode で回す

7.4 EventBridge → Lambda 権限の統一

  • EventBridge → Lambda の Invoke は 関数側のリソースベースポリシーに統一
  • プロジェクトごとにバラバラにしないことで、セキュリティレビューがスムーズに通る

7.5 S3 のデフォルト設定をテンプレ化

  • SSE-KMS / Versioning / Public Access Block を強制するのをデフォルトにした
  • 「特別な理由が無い限り、この設定で」というルールを組織内で共有すると安心

8. まとめと次回予告

今回は Terraform × AWS データ基盤の設計と適用編(前編) をまとめました 🎉

  • ディレクトリ構成は env/modules/ を分けて再利用性を確保
  • state は S3 + DynamoDB Lock で安定運用
  • IAM は prefix 条件つきで最小権限を徹底
  • S3 / Athena は SSE-KMS を強制
  • Lambda は「既存関数参照」を標準化して CreateFunction 禁止にも対応
  • EventBridge → Lambda の権限は関数側リソースベースポリシーに集約

要するに、実務でハマったところを整理してハンドブック化した、という感じです。


次回(後編)では、もっと運用寄りのテーマにフォーカスします👇

  • 監視・通知のベストプラクティス(失敗回数 / レイテンシ / メトリクス監視)
  • Runbook(手順書)のサンプル
  • SCP / Permissions boundary と権限管理のリアルなTips

後編も、実務で役立つ内容をガッツリ書いていく予定です。

Discussion