AWSの請求書確定時に特定メールへ自動送付する仕組みをTerraformで構築する

はじめに

AWSの請求書(Invoice)は毎月確定しますが、標準のマネジメントコンソールには「請求書確定時に特定メールアドレスへPDFを自動送付する」機能はありません。 本記事では、以下のAWSサービスを組み合わせて請求書PDF自動送付の仕組みをTerraformで構築する方法を解説します。 メール送信にはSES(Simple Email Service)を使用します。送信元・送信先ともにSESへの検証登録が必要ですが、サンドボックスの解除は不要です。 Terraformを利用しているのはあくまで私自身の好みです(お勉強中のため)。

構成するAWSサービス

サービス役割
EventBridge(Scheduler)毎月スケジュールトリガー
Lambda請求書PDF取得・SESでのメール送信
SESPDF添付メールの送信
IAMLambdaへのAWS API実行権限付与
CloudWatch LogsLambda実行ログの保管

全体アーキテクチャ

EventBridge Scheduler(毎月15日 9:00 JST)
    ↓ invoke
Lambda(Python)
    ├─ Invoicing API  → 請求書PDF取得
    └─ SES            → PDF添付メール送信

                    受信者のメールアドレスへ届く
                  (PDFが直接添付されているため
                    AWSアカウントなしでダウンロード可能)

なぜ15日?
AWSの請求書は通常毎月頭(2〜10日頃)に確定します。 余裕を持って15日にスケジュールすることで、確定済みの請求書を確実に取得できます。

前提条件

  • 以下の環境で動作を確認しています
    • Terraform v1.14.8
    • aws-cli/2.34.19
    • MacOS(15.7.4)
  • AWSアカウントへのアクセス(us-east-1 リージョン)
  • AWSアカウントがOrganizationsの請求アカウント、または単独アカウント
    • invoicing APIは請求アカウントでのみ全請求書を取得可能です(他アカウントでは全請求を確認できません)
  • SESのサンドボックス解除は不要
    • サンドボックス状態でも、送信元・送信先メールアドレスをSESに検証登録すれば送信可能
    • 検証登録はTerraformで実行します(後述)

ディレクトリ構成

IAMリソースは通常、管理者が操作するリソースとなるため(iam:CreateRole 等)、管理者向けテンプレート開発者向けテンプレートに分離します。

├── iam/                        # 【管理者向け】IAMロール・ポリシーのみ
│   ├── main.tf
│   └── outputs.tf

└── app/                        # 【開発者向け】Lambda・EventBridge・SES等
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    ├── lambda/
    │   └── invoice_sender.py   # Lambda関数のソースコード
    └── terraform.tfvars        # 環境ごとの変数値(gitignore推奨)

分割のポイント

  • iam/ は管理者が一度だけ適用し、作成されたIAMロールのARNを app/ へ渡す
  • app/ は開発者が自由にデプロイ・更新できる
  • outputs.tf でIAMロールARNを出力し、app/variables.tf で受け取る

Step 1: SES メールアドレスの検証(事前作業)

SESのサンドボックス状態では、送信元・送信先ともにSESへの検証登録が必要です。検証はTerraformで自動化できます。

app/main.tf に以下のリソースを追加してください(Step 4で解説するファイルに含まれています)。

# ============================================================
# SES 送信先メールアドレスの検証
# - terraform apply 後に確認メールが届くので、リンクをクリックして有効化
# - 有効化しないとSESからメールが送信されません
# ============================================================
resource "aws_ses_email_identity" "recipient" {
  email = var.recipient_email
}

resource "aws_ses_email_identity" "sender" {
  email = var.sender_email
}

terraform apply 後に recipient_email と sender_email それぞれ宛へAWSから件名 Amazon Web Services – Email Address Verification Request in region US East (N. Virginia) というメールが届きます。 本文内の “Click here to verify…” リンクをクリックして検証を完了させてください。

検証を完了しないとSESからのメール送信が失敗します。必ずTerraform適用後に確認してください。

検証済みアドレスはAWSマネジメントコンソールの SES → Verified identities から確認できます。 リージョンを us-east-1(バージニア北部) に設定して開いてください。

Step 2: IAMテンプレート(管理者が適用)

iam/main.tf

# ============================================================
# IAMロール: Lambda実行用
# - Lambda が AssumeRole できるよう信頼ポリシーを設定
# ============================================================
resource "aws_iam_role" "lambda_invoice_sender" {
  name = "lambda-invoice-sender-role"

  # Lambdaサービスがこのロールを引き受けることを許可する信頼ポリシー
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "lambda.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })

  tags = {
    Name    = "lambda-invoice-sender-role"
    Project = "aws-invoice-auto-send"
  }
}

# ============================================================
# IAMポリシー: Lambda が必要とする最小権限
# - CloudWatch Logs   : 実行ログの書き込み
# - invoicing:*       : 請求書一覧の取得・PDF取得
# - sts:GetCallerIdentity: アカウントID取得
# - ses:SendRawEmail  : PDF添付メールの送信
# ============================================================
resource "aws_iam_policy" "lambda_invoice_sender" {
  name        = "lambda-invoice-sender-policy"
  description = "AWS請求書自動送付Lambda用の最小権限ポリシー"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # CloudWatch Logs: ロググループ・ストリームの作成とログ書き込み
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:*"
      },
      # Invoicing API: 請求書の一覧取得とPDF取得
      {
        Effect = "Allow"
        Action = [
          "invoicing:ListInvoiceSummaries",
          "invoicing:GetInvoicePDF",
          "sts:GetCallerIdentity"
        ]
        Resource = "*"
      },
      # SES: 添付ファイル付きのRAWメール送信
      # SendRawEmail を使うことでPDFを添付できる
      {
        Effect   = "Allow"
        Action   = ["ses:SendRawEmail"]
        Resource = "*"
      }
    ]
  })
}

# ============================================================
# IAMロールへポリシーをアタッチ
# ============================================================
resource "aws_iam_role_policy_attachment" "lambda_invoice_sender" {
  role       = aws_iam_role.lambda_invoice_sender.name
  policy_arn = aws_iam_policy.lambda_invoice_sender.arn
}

# ============================================================
# EventBridge Scheduler 実行用 IAMロール
# - EventBridgeがLambdaをInvokeできるよう権限を付与
# ============================================================
resource "aws_iam_role" "eventbridge_scheduler" {
  name = "eventbridge-invoice-scheduler-role"

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

resource "aws_iam_role_policy" "eventbridge_scheduler_invoke_lambda" {
  name = "eventbridge-invoke-lambda-policy"
  role = aws_iam_role.eventbridge_scheduler.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["lambda:InvokeFunction"]
        Resource = "*"
      }
    ]
  })
}

iam/outputs.tf

# app/ テンプレートで使用するIAMロールARNを出力
output "lambda_role_arn" {
  description = "Lambda実行用IAMロールのARN"
  value       = aws_iam_role.lambda_invoice_sender.arn
}

output "eventbridge_role_arn" {
  description = "EventBridge Scheduler実行用IAMロールのARN"
  value       = aws_iam_role.eventbridge_scheduler.arn
}

IAMの適用コマンド

cd iam/
terraform init
terraform plan
terraform apply

適用後、出力されたARNをメモしておきます。

# 出力例
Outputs:
lambda_role_arn      = "arn:aws:iam::123456789012:role/lambda-invoice-sender-role"
eventbridge_role_arn = "arn:aws:iam::123456789012:role/eventbridge-invoice-scheduler-role"

Step 3: Lambda関数のソースコード

app/lambda/invoice_sender.py

"""
AWS請求書PDF自動送付 Lambda関数

処理の流れ:
  1. 前月分の請求書PDFをInvoicing APIで取得
  2. SES(SendRawEmail)でPDFを添付してメール送信

環境変数:
  SENDER_EMAIL    : 送信元メールアドレス(SESで検証済みであること)
  RECIPIENT_EMAIL : 送信先メールアドレス(SESで検証済みであること)
"""

import boto3
import os
import urllib.request
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders


def lambda_handler(event, context):
    # ── 対象月の計算(実行月の前月)────────────────────────────
    today = datetime.today()
    year  = today.year if today.month > 1 else today.year - 1
    month = today.month - 1 if today.month > 1 else 12

    print(f"対象請求月: {year}{month:02d}月")

    # ── アカウントIDを動的取得 ────────────────────────────────────
    sts        = boto3.client("sts")
    account_id = sts.get_caller_identity()["Account"]

    # ── Invoicing APIで対象月の請求書を取得 ──────────────────────
    # invoicing APIは us-east-1 のみ対応
    # Filter.BillingPeriod で対象月を直接指定
    invoicing = boto3.client("invoicing", region_name="us-east-1")

    response = invoicing.list_invoice_summaries(
        Selector={
            "ResourceType": "ACCOUNT_ID",
            "Value": account_id
        },
        Filter={
            "BillingPeriod": {
                "Month": month,
                "Year": year
            }
        }
    )

    # ── 結果チェック ─────────────────────────────────────────────
    invoices = response.get("InvoiceSummaries", [])
    if not invoices:
        print(f"⚠️  {year}{month:02d}月の請求書が見つかりませんでした")
        return {"statusCode": 404, "body": "Invoice not found"}

    invoice_id = invoices[0]["InvoiceId"]
    print(f"請求書ID: {invoice_id}")

    # ── 請求書PDFを取得 ──────────────────────────────────────────
    # get_invoice_pdf はダウンロード用URLを返すため urllib でバイナリをダウンロード
    pdf_response = invoicing.get_invoice_pdf(InvoiceId=invoice_id)
    billing_url  = pdf_response["InvoicePDF"]["DocumentUrl"]

    with urllib.request.urlopen(billing_url) as res:
        pdf_bytes = res.read()

    print(f"PDF取得完了: {len(pdf_bytes)} bytes")

    # ── SESでPDF添付メールを送信 ──────────────────────────────────
    sender    = os.environ["SENDER_EMAIL"]
    recipient = os.environ["RECIPIENT_EMAIL"]
    filename  = f"AWS_Invoice_{year}{month:02d}.pdf"

    # MIMEメッセージを構築
    # SendRawEmail を使うことでPDFを添付できる
    msg            = MIMEMultipart()
    msg["Subject"] = f"【AWS請求書】{year}{month:02d}月分"
    msg["From"]    = sender
    msg["To"]      = recipient

    # 本文テキスト
    body_text = (
        f"{year}{month:02d}月分のAWS請求書をお送りします。\n\n"
        f"請求書ID: {invoice_id}\n"
        f"添付ファイル: {filename}\n"
    )
    msg.attach(MIMEText(body_text, "plain", "utf-8"))

    # PDF添付
    attachment = MIMEBase("application", "pdf")
    attachment.set_payload(pdf_bytes)
    encoders.encode_base64(attachment)
    attachment.add_header(
        "Content-Disposition", "attachment", filename=filename
    )
    msg.attach(attachment)

    # SES送信(us-east-1 を使用)
    ses = boto3.client("ses", region_name="us-east-1")
    ses.send_raw_email(
        Source       = sender,
        Destinations = [recipient],
        RawMessage   = {"Data": msg.as_bytes()},
    )

    print(f"✅ メール送信完了: {recipient}")
    return {"statusCode": 200, "body": "Invoice sent successfully"}

Step 4: アプリテンプレート

app/variables.tf

variable "lambda_role_arn" {
  description = "IAMテンプレート(iam/)で作成したLambda実行用IAMロールのARN"
  type        = string
}

variable "eventbridge_role_arn" {
  description = "IAMテンプレート(iam/)で作成したEventBridge Scheduler用IAMロールのARN"
  type        = string
}

variable "recipient_email" {
  description = "請求書PDFの送信先メールアドレス"
  type        = string
}

variable "sender_email" {
  description = "SESで検証済みの送信元メールアドレス"
  type        = string
}

app/terraform.tfvars

※任意の値を入力してください。

# iam/ terraform apply 後に出力されたARNを記載する(以下は例)
lambda_role_arn      = "arn:aws:iam::123456789012:role/lambda-invoice-sender-role"
eventbridge_role_arn = "arn:aws:iam::123456789012:role/eventbridge-invoice-scheduler-role"

# 請求書の送信元・送信先メールアドレス
sender_email    = "From@yourcompany.com"
recipient_email = "To@yourcompany.com"

注意terraform.tfvars にはメールアドレスが含まれるため Git管理時には.gitignore への追加を推奨します。

app/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "6.38.0"
    }
    null = {
      source  = "hashicorp/null"
      version = "3.2.4"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

# ============================================================
# SES 送信先メールアドレスの検証
# - terraform apply 後に届く確認メールのリンクをクリックして有効化
# ============================================================
resource "aws_ses_email_identity" "recipient" {
  email = var.recipient_email
}

resource "aws_ses_email_identity" "sender" {
  email = var.sender_email
}

# ============================================================
# Lambdaデプロイパッケージの作成
# - lambda/ ディレクトリ内のPythonファイルをzip化する
# ============================================================
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda"
  output_path = "${path.module}/lambda.zip"
}

# ============================================================
# Lambda Layer: 最新boto3を使用するためのレイヤー
# - LambdaランタイムのデフォルトSDKは古い場合があるため
#   invoicing APIに対応した最新版を明示的に指定する
# ============================================================
resource "null_resource" "install_boto3" {
  triggers = {
    always_run = timestamp()
  }

  provisioner "local-exec" {
    command = <<EOT
      mkdir -p ${path.module}/layer/python
      pip install boto3 --upgrade -t ${path.module}/layer/python --quiet
    EOT
  }
}

data "archive_file" "boto3_layer_zip" {
  type        = "zip"
  source_dir  = "${path.module}/layer"
  output_path = "${path.module}/boto3_layer.zip"

  depends_on = [null_resource.install_boto3]
}

resource "aws_lambda_layer_version" "boto3_latest" {
  layer_name          = "boto3-latest"
  filename            = data.archive_file.boto3_layer_zip.output_path
  source_code_hash    = data.archive_file.boto3_layer_zip.output_base64sha256
  compatible_runtimes = ["python3.14"]
}

# ============================================================
# Lambda関数
# - Python 3.14ランタイムで実行
# - 環境変数で送信元・送信先メールアドレスを受け取る
# - タイムアウト: 60秒(PDF取得・メール送信を考慮)
# ============================================================
resource "aws_lambda_function" "invoice_sender" {
  function_name    = "aws-invoice-sender"
  role             = var.lambda_role_arn
  handler          = "invoice_sender.lambda_handler"
  runtime          = "python3.14"
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  layers           = [aws_lambda_layer_version.boto3_latest.arn]

  timeout     = 60
  memory_size = 256

  environment {
    variables = {
      SENDER_EMAIL    = var.sender_email
      RECIPIENT_EMAIL = var.recipient_email
    }
  }

  tags = {
    Name    = "aws-invoice-sender"
    Project = "aws-invoice-auto-send"
  }
}

# ============================================================
# CloudWatch Logs: Lambdaのロググループ
# - 保持期間: 30日(コスト最適化のため適宜調整)
# ============================================================
resource "aws_cloudwatch_log_group" "invoice_sender" {
  name              = "/aws/lambda/${aws_lambda_function.invoice_sender.function_name}"
  retention_in_days = 30
}

# ============================================================
# EventBridge Scheduler: 毎月15日 9:00 JST にLambdaを起動
# - schedule_expression_timezone で JST を直接指定できる
# - flexible_time_window: OFF = 指定時刻に厳密に実行
# ============================================================
resource "aws_scheduler_schedule" "invoice_sender" {
  name       = "monthly-invoice-sender"
  group_name = "default"

  schedule_expression          = "cron(0 9 15 * ? *)"
  schedule_expression_timezone = "Asia/Tokyo"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = aws_lambda_function.invoice_sender.arn
    role_arn = var.eventbridge_role_arn

    retry_policy {
      maximum_retry_attempts = 2
    }
  }
}

app/outputs.tf

output "lambda_function_name" {
  description = "Lambda関数名"
  value       = aws_lambda_function.invoice_sender.function_name
}

output "lambda_function_arn" {
  description = "Lambda関数のARN"
  value       = aws_lambda_function.invoice_sender.arn
}

output "scheduler_name" {
  description = "EventBridge Schedulerのスケジュール名"
  value       = aws_scheduler_schedule.invoice_sender.name
}

Step 5: アプリの適用コマンド

cd app/
terraform init
terraform plan
terraform apply

動作確認

Lambdaをマネジメントコンソールから手動実行して確認します。

  1. AWSコンソール → Lambda → aws-invoice-sender を開く
  2. 「テスト」タブ → 空のJSONイベント {} で実行
  3. CloudWatch Logsでエラーがないか確認
  4. 指定したメールアドレスへPDF添付メールが届いているか確認

注意事項まとめ

項目内容
Invoicing APIのリージョンus-east-1 のみ対応
Organizations利用時請求アカウントで実行する必要あり
SES サンドボックス解除不要。送信元・送信先ともにSES検証登録が必要
SES メールアドレス検証terraform apply 後に届く確認メール(送信元・送信先それぞれ)のリンクをクリックすること
terraform.tfvarsメールアドレスが含まれるため .gitignore 推奨
IAMの適用順序iam/ → app/ の順で適用すること

おわりに

本記事では EventBridge Scheduler + Lambda + SES を組み合わせた請求書PDF自動送付の仕組みをTerraformで構築しました。 SESのサンドボックス解除は不要で、送信元・送信先メールアドレスをTerraformで検証登録するだけで運用できます。

参考資料