🍣

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

に公開

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

運用・監視・権限・dbtまわりの“効く型”を、手順とコードでまとめます。

1. はじめに(後編のねらい)

前編では、最小構成のデータ基盤を Terraform で立ち上げ、再利用しやすい設計に整えました。
後編はその「運用フェーズ」を深掘りします。回し始めて見えた“ハマりどころ”を、実例ベースでサクッと共有していきます。

後編で扱うテーマ

  • 監視MVPの作り方:CloudWatch アラーム(Errors / Throttles)
  • IaC運用のコツ:S3ロックファイル、プロファイル分割での適用
  • 権限の現実解:Permission set を段階付与して詰みを回避
  • Athena×dbt:結果S3/KMS 権限と Projection 制約への対応
  • バケットポリシー&KMS:安全運用のテンプレ化
  • トラブル実録:症状 → 原因 → 解決策のパターン
  • Runbook:日次点検と障害時の初動チェックリスト

2. 監視MVPのつくり方(CloudWatchアラーム)

Lambda をまず「見える化」します。最小で ErrorsThrottles にアラームを置き、通知は SNS に流す構成です。運用に合わせて有効/無効をフラグで切り替えられるようにしておくと楽でした。

2.1 設計のポイント

  • 失敗の“気づき”を最速にするため Errors≥1 を 1 分粒度で検知。
  • スロットリングも Throttles≥1 を即検知(バースト時の兆し)。
  • treat_missing_data = notBreaching で無駄な通知を抑制。
  • 通知は SNS トピックを可変に(環境やチームで切替え)。
  • いつでも止められるよう フラグ(enable_alarms) を用意。

2.2 variables.tf(抜粋)

variable "project"               { type = string }
variable "env"                   { type = string }
variable "lambda_function_name"  { type = string }

variable "enable_alarms"         { type = bool   default = true }
variable "alarm_sns_topic_arn"   { type = string default = "" } # 未設定なら通知なし

2.3 main.tf(CloudWatch アラーム)

Errors アラーム

resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
  count               = var.enable_alarms ? 1 : 0
  alarm_name          = "${var.project}-${var.env}-lambda-errors"
  alarm_description   = "Lambda Errors detected"
  namespace           = "AWS/Lambda"
  metric_name         = "Errors"
  statistic           = "Sum"
  period              = 60
  evaluation_periods  = 1
  threshold           = 1
  comparison_operator = "GreaterThanOrEqualToThreshold"
  treat_missing_data  = "notBreaching"

  dimensions = {
    FunctionName = var.lambda_function_name
  }

  alarm_actions = length(var.alarm_sns_topic_arn) > 0 ? [var.alarm_sns_topic_arn] : []
  ok_actions    = length(var.alarm_sns_topic_arn) > 0 ? [var.alarm_sns_topic_arn] : []
}

Throttles アラーム

resource "aws_cloudwatch_metric_alarm" "lambda_throttles" {
  count               = var.enable_alarms ? 1 : 0
  alarm_name          = "${var.project}-${var.env}-lambda-throttles"
  alarm_description   = "Lambda Throttles detected"
  namespace           = "AWS/Lambda"
  metric_name         = "Throttles"
  statistic           = "Sum"
  period              = 60
  evaluation_periods  = 1
  threshold           = 1
  comparison_operator = "GreaterThanOrEqualToThreshold"
  treat_missing_data  = "notBreaching"

  dimensions = {
    FunctionName = var.lambda_function_name
  }

  alarm_actions = length(var.alarm_sns_topic_arn) > 0 ? [var.alarm_sns_topic_arn] : []
  ok_actions    = length(var.alarm_sns_topic_arn) > 0 ? [var.alarm_sns_topic_arn] : []
}

2.4 terraform.tfvars(例)

project                 = "new-project"
env                     = "dev"
lambda_function_name    = "new-project-dev-rss-ingestion"

enable_alarms           = true
alarm_sns_topic_arn     = "arn:aws:sns:ap-northeast-1:<account_id>:<topic_name>"

2.5 運用 Tips(ピンポイント適用)

AWS_PROFILE=<profile_for_cloudwatch> terraform -chdir=<repo>/env/dev plan \
  -target=aws_cloudwatch_metric_alarm.lambda_errors \
  -target=aws_cloudwatch_metric_alarm.lambda_throttles

AWS_PROFILE=<profile_for_cloudwatch> terraform -chdir=<repo>/env/dev apply -auto-approve \
  -target=aws_cloudwatch_metric_alarm.lambda_errors \
  -target=aws_cloudwatch_metric_alarm.lambda_throttles

3. IaC運用のコツ(バックエンド/プロファイル分割)

運用フェーズで効いたのは「バックエンド簡素化」と「プロファイル分割」です。詰まりやすい認証まわりを先に整え、必要な差分だけ安全に適用していきます。

3.1 バックエンド:DynamoDBロック → S3ロックファイル(シンプル運用)

  • ロック用の DynamoDB をやめて、S3 側で単一キーの ロックファイル(= state 書き込み中の目印)に寄せてシンプル化しました。
  • チームで同時 apply をしない運用(適用担当プロファイルを1つに固定/時間帯をずらす)を前提にすれば、実務的には十分回ります。
  • 併用案:規模や並行適用が増えるなら DynamoDB ロック復帰を推奨。
# env/dev/backend.tf(例): S3 backend を最小設定に
terraform {
  backend "s3" {
    bucket = "<state_bucket>"
    key    = "env/dev/terraform.tfstate"  # チームで一意に
    region = "ap-northeast-1"
    # dynamodb_table は使わない(=簡素化)
    encrypt = true
  }
}

3.2 プロファイル分割:Permission set 別に apply を安全化

SSO の Permission set ごとにプロファイルを分け、用途別に plan/apply を走らせます。
例)terraform-bootstrap(基盤系)/terraform-cloudwatch(監視だけ)など。

例:CloudWatchアラームだけを監視用プロファイルで適用

AWS_PROFILE=terraform-cloudwatch terraform -chdir=<repo>/env/dev plan \
  -target=aws_cloudwatch_metric_alarm.lambda_errors \
  -target=aws_cloudwatch_metric_alarm.lambda_throttles

AWS_PROFILE=terraform-cloudwatch terraform -chdir=<repo>/env/dev apply -auto-approve \
  -target=aws_cloudwatch_metric_alarm.lambda_errors \
  -target=aws_cloudwatch_metric_alarm.lambda_throttles

3.3 フラグ駆動で「いつでも止められる」設計に

監視や外部連携は enable フラグ で on/off。最初は最小で、問題なければ段階的に有効化します。
既存 Lambda 参照や Invoke 許可の付与も トグルで吸収。

variables.tf(抜粋)

variable "enable_alarms"        { type = bool   default = true }
variable "enable_sns"           { type = bool   default = false }
variable "enable_ssm"           { type = bool   default = false }

variable "use_existing_lambda"  { type = bool   default = true }
variable "lambda_function_arn"  { type = string default = "" }
variable "add_lambda_permission"{ type = bool   default = false }

main.tf(イメージ)

module "scheduler" {
  source                = "../modules/scheduler-eventbridge"
  add_lambda_permission = var.add_lambda_permission
  lambda_function_arn   = var.lambda_function_arn
  # ...
}

resource "aws_sns_topic" "alerts" {
  count = var.enable_sns ? 1 : 0
  name  = "${var.project}-${var.env}-alerts"
}

3.4 ピンポイント適用と最小権限でのフェーズドリリース

まずは監視だけ

AWS_PROFILE=<profile> terraform -chdir=<repo>/env/dev plan  \
  -target=aws_cloudwatch_metric_alarm.lambda_errors
AWS_PROFILE=<profile> terraform -chdir=<repo>/env/dev apply -auto-approve \
  -target=aws_cloudwatch_metric_alarm.lambda_errors

4. 権限設計の現実解(Permission set と差分付与)

最初から“完璧な最小権限”を狙うと進みません。**段階付与(フェーズド)**で「読める → 作れる → 更新できる」の順に広げていくのが現実的でした。

4.1 ロール/Permission set の分割モデル

  • Observer(閲覧・計測)terraform plan やタグ参照、Athena/Glue の Read。
  • Operator(限定適用):CloudWatch アラームや EventBridge など運用系の Create/Update。
  • Owner(基盤変更):S3/KMS/Glue/Athena/IAM の本流変更。頻度は低いが強力。

まず Observer で plan が通る状態にしてから、Operator/Owner を小出しに付与するのが無難でした。


4.2 “読めなくて落ちる”典型 Read 権限(Observer)

不足しがちだった参照系(全部ではないが要点):

  • IAMiam:GetRole, iam:ListRolePolicies
  • EventBridgeevents:DescribeRule, events:ListTagsForResource
  • S3s3:GetBucketLocation, s3:GetEncryptionConfiguration, s3:GetBucketVersioning, s3:GetBucketPolicyStatus, s3:GetAccelerateConfiguration, s3:ListBucket(※ prefix 条件)
  • KMSkms:DescribeKey
  • Glueglue:GetDataCatalog, glue:GetDatabase, glue:GetTables, glue:GetTableVersions
  • Athenaathena:GetWorkGroup, athena:GetQueryExecution

4.3 代表ポリシー断片(JSON, ダミー)

4.3.1 S3(結果バケット / RAW 参照系+最小書込)

{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "BucketReadMeta",
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation","s3:GetEncryptionConfiguration","s3:GetBucketVersioning",
        "s3:GetBucketPolicyStatus","s3:GetAccelerateConfiguration","s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::<results_bucket>",
      "Condition": { "StringLike": { "s3:prefix": ["results/*","athena/*","raw/rss/*"] } }
    },
    { "Sid": "ObjectRWResults",
      "Effect": "Allow",
      "Action": ["s3:GetObject","s3:PutObject"],
      "Resource": "arn:aws:s3:::<results_bucket>/results/*"
    },
    { "Sid": "ObjectRWRawRss",
      "Effect": "Allow",
      "Action": ["s3:GetObject","s3:PutObject"],
      "Resource": "arn:aws:s3:::<raw_bucket>/raw/rss/*"
    }
  ]
}

4.3.2 KMS(Athena 結果・S3 デフォルト暗号化キー)

{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "UseKmsForResults",
      "Effect": "Allow",
      "Action": [
        "kms:Encrypt","kms:Decrypt","kms:GenerateDataKey","kms:GenerateDataKeyWithoutPlaintext","kms:DescribeKey"
      ],
      "Resource": "<kms_key_arn>"
    }
  ]
}

4.3.3 Glue/Athena(実行に必要な最小セット)

{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "GlueRead",
      "Effect": "Allow",
      "Action": [
        "glue:GetDataCatalog","glue:GetDatabase","glue:GetTable","glue:GetTables","glue:GetTableVersions"
      ],
      "Resource": "*"
    },
    { "Sid": "AthenaExec",
      "Effect": "Allow",
      "Action": [
        "athena:StartQueryExecution","athena:GetQueryExecution","athena:GetQueryResults","athena:GetWorkGroup"
      ],
      "Resource": [
        "arn:aws:athena:ap-northeast-1:<account_id>:workgroup/<workgroup_name>",
        "arn:aws:athena:ap-northeast-1:<account_id>:query-execution/*"
      ]
    }
  ]
}

4.3.4 EventBridge→Lambda(関数側リソースベースポリシー)

{
  "Sid": "AllowExecutionFromEventBridge",
  "Effect": "Allow",
  "Principal": { "Service": "events.amazonaws.com" },
  "Action": "lambda:InvokeFunction",
  "Resource": "arn:aws:lambda:ap-northeast-1:<account_id>:function:<lambda_name>",
  "Condition": {
    "ArnLike": {
      "AWS:SourceArn": "arn:aws:events:ap-northeast-1:<account_id>:rule/<rule_name>"
    }
  }
}

5. Athena×dbtの実務ノウハウ

Athena を dbt で回すときの「効く型」をメモっておきます。結果S3/KMS 権限と、Projection テーブル特有の制約(dt / source静的等価条件が必要)でハマりやすかったので、最初から仕込んでおくとスムーズでした。

5.1 セットアップ(パッケージとプロファイル)

packages.yml(抜粋)

packages:
  - package: dbt-labs/dbt_utils
    version: "~1.3"
  - package: dbt-athena-community/athena
    version: "~1.8"

profiles.yml(dev の例・プレースホルダ)

<profile_name>:
  target: dev
  outputs:
    dev:
      type: athena
      region_name: ap-northeast-1
      work_group: <workgroup_name>           # ★WG固定(結果S3/KMSはWG側で強制)
      database: <glue_db>                    # 例: new_project_dev
      schema: <glue_db>                      # AthenaではDatabase/Schema同義扱い
      threads: 4
      aws_profile_name: <aws_profile>        # 例: terraform-bootstrap
      s3_staging_dir: s3://<results_bucket>/staging/   # WG強制時は実効限定的

5.2 結果S3/KMS 権限(最初に通す)

  • ロールに s3://<results_bucket>/results/* の Get/Put を付与。
  • 併せて <kms_key_arn> の Encrypt/Decrypt/GenerateDataKey/DescribeKey を付与。
  • これが抜けると dbt run/test が AccessDenied で止まります(まずここから)。

5.3 Projection テーブル対策(静的等価条件を強制)

Partition Projection(dt=date, source=injected)では、WHERE で静的等価条件を入れないとフルスキャンっぽくなったり失敗します。
テストや検証用の vars を用意して、必ず dt / source を固定するのが楽でした。

dbt_project.yml(vars 例)

name: new_project_dbt
version: 1.0.0
config-version: 2

vars:
  test_dt: "2025-09-07"          # 検証用日付(本番相当はCIで上書き)
  test_source: "gemini"          # 検証用ソース
  run_data_tests: true           # 権限が整うまで false にしてスキップも可

#### モデル例(staging / 必要なら WHERE を条件付与)

-- models/staging/stg_rss_clean.sql
{{ config(materialized='view') }}

with src as (
  select
    title,
    link,
    published_at,
    source_name,
    dt,
    source
  from {{ source('raw', 'rss') }}
  {% if var('test_dt', none) and var('test_source', none) %}
  where dt = '{{ var("test_dt") }}'
    and source = '{{ var("test_source") }}'
  {% endif %}
)

select
  title,
  link,
  published_at,
  source_name,
  dt,
  source
from src

5.4 data test の設計(generic を外して custom へ)

  • rojection と相性が悪い generic test(等価条件を付与できない)は外し、custom data - test に寄せると安定しました。

tests/custom/not_null_stg_rss_clean_link.sql

-- 0件で合格

select count(*) as failures
from {{ ref('stg_rss_clean') }}
where dt = '{{ var("test_dt") }}'
  and source = '{{ var("test_source") }}'
  and link is null;

tests/schema.yml(必要最低限に)

version: 2
models:
  - name: stg_rss_clean
    columns:
      - name: link
        tests: []   # genericは使わず customに寄せる

5.5 実行コマンド(dev)

# 依存取得
AWS_PROFILE=<aws_profile> dbt deps --profiles-dir .

# 実行(WG固定/権限OK前提)
AWS_PROFILE=<aws_profile> dbt run  --profiles-dir . --vars 'test_dt=2025-09-07 test_source=gemini'
AWS_PROFILE=<aws_profile> dbt test --profiles-dir . --vars 'test_dt=2025-09-07 test_source=gemini'

5.6 ハマりどころと対処メモ

  • AccessDenied(結果S3/KMS):ロールに s3:Get/PutObject(/results/*)と KMS の
  • Encrypt/Decrypt/GenerateDataKey を付与。WG の結果先が想定と一致しているか再確認。
  • Projection で失敗/重い:必ず dt / source を 等価条件で固定。テストは custom SQL で。
  • GetDataCatalog 系不足:Glue/Athena の Read を追加(glue:GetTables/GetTableVersions など)。
  • dbt run は通るが test が失敗:generic test を外して custom に寄せる/
  • run_data_tests=false で段階導入。

6. バケットポリシー&KMS運用の型

S3 と KMS は “最初から安全デフォルト” に寄せておくと、後の事故をだいぶ避けられます。ここでは 結果S3(Athena)/ RAW S3 のバケットポリシーと、KMS Key Policy の最小セットをテンプレ化しておきます。すべてプレースホルダ表記です。

6.1 S3 バケットの基本設定(まずはここから)

  • Public Access Block: 4項目すべて true
  • Versioning: Enabled
  • デフォルト暗号化: SSE-KMS(既定キーは <kms_key_arn>

バケット設定で締めたうえで、ポリシー側で“例外を認めない” を重ねがけします。


6.2 結果S3(Athena)のバケットポリシー(テンプレ)

  • TLS 強制aws:SecureTransport
  • 指定 KMS キー以外のアップロード拒否
  • 暗号化ヘッダ未指定のアップロード拒否
  • 許可は必要最小限の IAM ロールに限定
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::<results_bucket>",
        "arn:aws:s3:::<results_bucket>/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    },
    {
      "Sid": "DenyWrongKmsKey",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::<results_bucket>/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "<kms_key_arn>"
        }
      }
    },
    {
      "Sid": "DenyWithoutKMSEncryption",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::<results_bucket>/*",
      "Condition": {
        "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" }
      }
    },
    {
      "Sid": "AllowAthenaAndAppRoles",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::<account_id>:role/<athena_exec_role>",
          "arn:aws:iam::<account_id>:role/<app_exec_role>"
        ]
      },
      "Action": [
        "s3:PutObject","s3:GetObject","s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::<results_bucket>",
        "arn:aws:s3:::<results_bucket>/results/*",
        "arn:aws:s3:::<results_bucket>/staging/*"
      ],
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms",
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "<kms_key_arn>"
        }
      }
    }
  ]
}

6.3 RAW S3(取り込み先)ポリシーのポイント

  • 書き込みは Lambda 実行ロールだけに許可。
  • ListBucket は prefix 条件で絞る(raw/rss/*)。
  • アップロードは SSE-KMS & 指定キー必須。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowLambdaPutGetWithinPrefix",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::<account_id>:role/<lambda_exec_role>" },
      "Action": ["s3:PutObject","s3:GetObject"],
      "Resource": "arn:aws:s3:::<raw_bucket>/raw/rss/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms",
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "<kms_key_arn>"
        }
      }
    },
    {
      "Sid": "AllowLambdaListPrefix",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::<account_id>:role/<lambda_exec_role>" },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::<raw_bucket>",
      "Condition": { "StringLike": { "s3:prefix": [ "raw/rss/*" ] } }
    },
    {
      "Sid": "DenyWithoutTLSOrWrongKey",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::<raw_bucket>/*",
      "Condition": {
        "Bool":   { "aws:SecureTransport": "false" },
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms",
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "<kms_key_arn>"
        }
      }
    }
  ]
}

6.4 KMS Key Policy(最小構成の型)

  • 管理者(Key admin) と 利用者(Key user) を分ける。
  • Grant は極力使わず、Key Policy と IAM で完結させる方針。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableRootAccount",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::<account_id>:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "KeyAdministrators",
      "Effect": "Allow",
      "Principal": { "AWS": [ "arn:aws:iam::<account_id>:role/<kms_admin_role>" ] },
      "Action": [
        "kms:Create*","kms:Describe*","kms:Enable*","kms:List*","kms:Put*","kms:Update*","kms:Revoke*",
        "kms:Disable*","kms:Get*","kms:Delete*","kms:TagResource","kms:UntagResource","kms:ScheduleKeyDeletion",
        "kms:CancelKeyDeletion"
      ],
      "Resource": "*"
    },
    {
      "Sid": "KeyUsers",
      "Effect": "Allow",
      "Principal": { "AWS": [
        "arn:aws:iam::<account_id>:role/<lambda_exec_role>",
        "arn:aws:iam::<account_id>:role/<athena_exec_role>"
      ]},
      "Action": [
        "kms:Encrypt","kms:Decrypt","kms:ReEncrypt*","kms:GenerateDataKey*","kms:DescribeKey"
      ],
      "Resource": "*"
    }
  ]
}

6.5 運用チェックリスト(デプロイ前にサクッと)

  • WorkGroup 固定(結果 S3 / SSE-KMS が強制になっている)
  • 結果S3:/results/* に Put/Get、KMS 利用可
  • RAW S3:/raw/rss/* に Put/Get、List は prefix 条件あり
  • バケットポリシー:TLS 強制、KMS キー強制、不要 Principal が含まれていない
  • Key Policy:Key user に Lambda / Athena 実行ロールが入っている

7. まとめ&次回予告

後編では、実運用で効いた型を最小構成から段階導入の順で固めました。要点だけサクッと振り返ります。

7.1 学びのハイライト

  • 監視MVP:まずは Errors / Throttles のアラームで“気づける”状態に。-target 適用で安全に先出し。
  • IaC運用の安定化:S3 バックエンド+プロファイル分割で認証の詰まりを回避。フラグ駆動で「いつでも止められる」設計に。
  • 権限の現実解:Observer→Operator→Owner の順に段階付与。典型 Read 不足(iam:ListRolePolicies など)は観察者権限に束ねて恒久化。
  • Athena×dbt:WorkGroup 固定(結果S3/KMS をWGで強制)。Projection は dt / source を等価条件で固定。generic test は外し custom data test へ。
  • S3/KMS の型:TLS 強制・KMS 強制の バケットポリシー と、Key admin / Key user 分離の Key Policy をテンプレ化。
  • Runbook:日次点検→障害初動→週次メンテ→リリース手順をチェックリスト化。迷わず動ける“地図”を用意。

Discussion