AWSの請求書確定時に特定メールへ自動送付する仕組みをTerraformで構築する
はじめに
AWSの請求書(Invoice)は毎月確定しますが、標準のマネジメントコンソールには「請求書確定時に特定メールアドレスへPDFを自動送付する」機能はありません。 本記事では、以下のAWSサービスを組み合わせて請求書PDF自動送付の仕組みをTerraformで構築する方法を解説します。 メール送信にはSES(Simple Email Service)を使用します。送信元・送信先ともにSESへの検証登録が必要ですが、サンドボックスの解除は不要です。 Terraformを利用しているのはあくまで私自身の好みです(お勉強中のため)。
構成するAWSサービス
| サービス | 役割 |
|---|---|
| EventBridge(Scheduler) | 毎月スケジュールトリガー |
| Lambda | 請求書PDF取得・SESでのメール送信 |
| SES | PDF添付メールの送信 |
| IAM | LambdaへのAWS API実行権限付与 |
| CloudWatch Logs | Lambda実行ログの保管 |
全体アーキテクチャ
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の請求アカウント、または単独アカウント
invoicingAPIは請求アカウントでのみ全請求書を取得可能です(他アカウントでは全請求を確認できません)
- 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をマネジメントコンソールから手動実行して確認します。
- AWSコンソール → Lambda →
aws-invoice-senderを開く - 「テスト」タブ → 空のJSONイベント
{}で実行 - CloudWatch Logsでエラーがないか確認
- 指定したメールアドレスへ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で検証登録するだけで運用できます。
参考資料
- Amazon SES サンドボックスからの移行(本稼働アクセスのリクエスト)
- Amazon SES 送信制限の管理
- ListInvoiceSummaries API リファレンス
- GetInvoicePDF API リファレンス
- list_invoice_summaries – boto3 ドキュメント
- get_invoice_pdf – boto3 ドキュメント
- Lock and upgrade provider versions – Terraform チュートリアル
- terraform-provider-aws リリース一覧
- Terraform AWS Provider 6.0 リリースブログ
