Cross-Region SNS の落とし穴と AWS Budgets — CloudFront アラームの罠

CloudFront 4xx アラームを作ろうとして、半日くらい悩んだ罠について書きます。「provider alias を正しく書いてるのに、なぜか us-east-1 で alarm が作れない」現象の正体は、SNS Topic の region 不一致 でした。

事象

CloudFront 4xx アラームは us-east-1 必須なので、Terraform でこう書きました:

resource "aws_cloudwatch_metric_alarm" "cf_4xx_rate" {
  provider = aws.us_east_1                          # us-east-1 alias

  alarm_name          = "iigtn-lab-prod-cf-4xx-rate"
  metric_name         = "4xxErrorRate"
  namespace           = "AWS/CloudFront"
  ...

  dimensions = {
    DistributionId = var.cloudfront_distribution_id
    Region         = "Global"
  }

  alarm_actions = [aws_sns_topic.alarms.arn]        # ← 東京の SNS topic
}

この terraform apply がエラーで落ちる:

Error: creating CloudWatch Metric Alarm: ValidationError:
Invalid region ap-northeast-1 specified. Only us-east-1 is supported.

「provider が ap-northeast-1 になってる?でも provider = aws.us_east_1 と書いてるのに…」と混乱。

原因の特定

plan 出力をよく見ると:

# aws_cloudwatch_metric_alarm.cf_4xx_rate will be created
+ resource "aws_cloudwatch_metric_alarm" "cf_4xx_rate" {
    + alarm_actions = [
        + "arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:iigtn-lab-prod-alarms",  # ← 東京の SNS!
      ]
    ...
  }

ここで気付きました。alarm_actions に渡している SNS Topic ARN が ap-northeast-1。CloudFront アラームは us-east-1 で作る必要があるが、AWS の制約として 「アラーム本体と alarm_actions の SNS は同一 region 必須」 という規則がある。

エラーメッセージの「Invalid region ap-northeast-1」は、provider の region ではなく、alarm_actions に含まれる ARN の region が us-east-1 と整合してないことを示していた、というのが正解。

解決策: us-east-1 用 SNS Topic を別途作る

SNS Topic も us-east-1 に作って、CloudFront アラームはそちらを参照する設計に変更:

# envs/prod/main.tf に追加

# us-east-1 用 SNS Topic
resource "aws_sns_topic" "alarms_us_east_1" {
  provider = aws.us_east_1
  name     = "iigtn-lab-prod-alarms-use1"
}

resource "aws_sns_topic_subscription" "alarms_us_east_1_email" {
  count    = var.alarm_email == "" ? 0 : 1
  provider = aws.us_east_1

  topic_arn = aws_sns_topic.alarms_us_east_1.arn
  protocol  = "email"
  endpoint  = var.alarm_email
}

# CloudFront 4xx アラーム
resource "aws_cloudwatch_metric_alarm" "cf_4xx_rate" {
  provider = aws.us_east_1

  alarm_name          = "iigtn-lab-prod-cf-4xx-rate"
  metric_name         = "4xxErrorRate"
  namespace           = "AWS/CloudFront"
  ...

  alarm_actions = [aws_sns_topic.alarms_us_east_1.arn]   # ← us-east-1 SNS
}

これで apply が通り、CloudFront 4xx アラームが us-east-1 で動き始めました。

結果として 2 つの SNS Topic を持つ構成

SNS TopicRegion用途
iigtn-lab-prod-alarmsap-northeast-1(東京)Lambda / API Gateway アラーム
iigtn-lab-prod-alarms-use1us-east-1(バージニア北部)CloudFront アラーム

email subscription も両方に設定。同じメアドが両方に登録される設計。Confirm メールが 2 通来ます。

クロスリージョン制約の他の例

AWS でリージョン跨ぎがダメなパターンは結構あります:

パターン制約
CloudWatch Alarm ↔ SNS Topic同一 region 必須
CloudWatch Logs ↔ Subscription Filter同一 region 必須
CloudFront ↔ ACM 証明書ACM が us-east-1 必須
CloudFront メトリクスus-east-1 でのみ取得可能
S3 Bucket Policy ↔ Bucket同一(バケットの region)

CloudFront 系は特に「グローバルだけど中身は us-east-1」という設計が多く、初学者の混乱の元。

今回のハマりで得た教訓

  1. エラーメッセージの「Invalid region xxx」を読み違えない。provider の region ではなく、参照している ARN の region を疑う
  2. plan 出力をよく読む。「期待しない値」が混入してないか確認
  3. クロスリージョン制約は AWS の至るところに散在。CloudFront / WAF / Global Accelerator などグローバル系は要注意
  4. 同じ責務のリソース(SNS Topic)が region ごとに 2 個ある状態は許容。料金も微々たるもの

AWS Budgets の細かい設定

前記事で Budgets の基本を書きましたが、もう少し細かい設定について。

サービス別予算

「全体予算」ではなく「Lambda 予算」「S3 予算」など特定サービスに絞った予算も作れます:

resource "aws_budgets_budget" "lambda_only" {
  name              = "lambda-only-monthly"
  budget_type       = "COST"
  limit_amount      = "5"
  limit_unit        = "USD"
  time_unit         = "MONTHLY"

  cost_filter {
    name = "Service"
    values = ["Amazon Elastic Compute Cloud - Compute"]   # Lambda
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = [var.alarm_email]
  }
}

cost_filter で他にも絞れる

filter意味
Serviceサービス別(Lambda / S3 / EC2 等)
LinkedAccountサブアカウント別(Organizations 構成時)
OperationAPI オペレーション別
UsageType料金タイプ別(DataTransfer-Out 等)
TagKeyValueリソースタグ別(Project=iigtn 等)

本シリーズでは default_tags で全リソースに Project=iigtn を付けているので、後で「iigtn プロジェクトだけの予算」を Tag フィルタで作れます。

Cost Explorer との併用

Budgets はアラート、Cost Explorer は分析ツール。これで月末に「何にいくら使ったか」を可視化:

aws ce get-cost-and-usage \
  --time-period Start=2026-04-01,End=2026-05-01 \
  --granularity MONTHLY \
  --metrics "UnblendedCost" \
  --group-by Type=DIMENSION,Key=SERVICE

Cost Explorer は GUI だと Web コンソールから「Bills」「Cost Explorer」で確認可能。Budget アラートが鳴った時の犯人特定に必須。

本シリーズの実際の月額(参考)

サービス月額(円・概算)
S3 (state + web)~5
DynamoDB (lock + contacts)~2
CloudFront 転送量 (5 GB)~85
API Gateway HTTP API~0.2
Lambda 実行0(無料枠内)
Lambda CloudWatch Logs~10
CloudWatch Alarms (4 個)~60
SNS~0
合計約 170〜200 円

個人運用の最安ライン。月額 1,000 円超えたら何かおかしいと思って Cost Explorer で詳細見る、というのが運用ルール。

次の記事

監視・コスト管理が一通り揃ったところで、次は 「ドキュメントとアーキテクチャ図を作る」 話に入ります。本シリーズではアーキテクチャ図を Mermaid → drawio (AWS 公式アイコン) に進化させた経緯があり、その記録を書きます。

📚 用語集

クロスリージョン制約
AWS で「リソース A は region X、リソース B は region Y にある時、両者を直接参照できない」という制約。CloudWatch Alarm ↔ SNS / CloudFront ↔ ACM 等で発生。
4xxErrorRate (CloudFront メトリクス)
CloudFront のレスポンスが 4xx エラーだった割合(パーセント)。攻撃や Bot 流入の早期検知に使える。
Region dimension (CloudFront)
CloudFront メトリクスのディメンション。値は Global(全 Edge 合算)/ 個別 region コード(特定 Edge)が指定できる。
cost_filter (AWS Budgets)
予算を特定サービス・タグ・アカウント等に絞り込むフィルタ。「Lambda だけ月 $5」のような設定が可能。
UnblendedCost
AWS の請求データの基本となるコスト形式。Reserved Instance 等の割引適用前。
Cost Explorer
AWS の課金分析ツール。月別・サービス別・タグ別等で課金を可視化できる。Budget アラート発火時の原因調査に必須。
default_tags (Provider)
AWS provider のオプション。指定したタグがその provider で作られる全リソースに自動付与される。Cost Allocation Tag として活用可能。
Cost Allocation Tag
AWS の課金レポートで「タグ別の課金集計」を有効にしたタグ。Billing コンソールで有効化が必要。
Confirm subscription (SNS)
SNS Email Subscription が有効になるための確認ステップ。AWS から送られる確認メールのリンクをクリック。
region 跨ぎ参照
Terraform で別 region のリソースを参照する設計。provider alias を駆使する。本シリーズでは us-east-1 と ap-northeast-1 を併用。
provider alias 経由のリソース
Terraform で provider = aws.us_east_1 を付けたリソース。指定 region に作られる。
SNS Topic ARN の region 部分
ARN の 4 番目フィールド(arn:aws:sns:<REGION>:...)。これと CloudWatch Alarm の region が一致しないとアラーム作成失敗。
FORECASTED (Budgets)
AWS の機械学習による「月末予測値」。実績で気付くより早く異常を検知できる予算閾値タイプ。