"実務でつまずいたところ全部まとめました: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)
- バケットは
rawとathena-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 に
rootとLambda実行ロールを明示的に許可 - Grant を多用せず、ポリシーで完結できる範囲はポリシーで対応
- 出力値:
key_id,key_arn,alias_arn
3.5 S3モジュール(storage-s3)
-
rawとathena-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 = trueprojection.dt.type = dateprojection.source.type = injectedstorage.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)
-
Lambda を手動実行(コンソールでテスト
{}) - CloudWatch Logs に処理ログが出ることを確認
- S3 生成物(当日パーティション配下)を確認
- 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を許可
- Lambda が未作成 /
-
S3 Put/Get: AccessDenied- IAM の
Resourceがバケット直下になっていないか(オブジェクトキー配下に限定) -
ListBucketにprefix条件を付けているか
- IAM の
4.7 検証チェックリスト(使い回しOK)
-
AWS SSO ログイン済み(
aws sts get-caller-identityOK) - 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 の結果保存でエラー
症状
- クエリ実行時に
AccessDeniedやOutput 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