はじめに
個人開発しているNBAニュースサイト NBA ISO FLOW で、定期バッチ処理にAWS MWAA(Managed Apache Airflow)を使っていました。しかし月額$350+のコストが重く、EventBridge Schedulerに移行して年間約$4,200のコスト削減に成功しました。
📌 この記事で扱うこと:
- MWAA → EventBridge Scheduler の移行で何が変わるか
- Terraformによる実装コード(コピペで使えるレベル)
- 移行時にハマったポイントと対処法
環境
| 項目 | バージョン |
|---|---|
| Terraform | >= 1.10.0 |
| AWS Provider | ~> 5.0 |
| Terramate | 最新 |
| 旧環境 | MWAA 2.8.1 (mw1.small) |
| 新環境 | EventBridge Scheduler |
旧構成: MWAA(Apache Airflow)
何をしていたか
3つのAirflow DAGが動いていました:
- nba_rss_scraper — 5分ごとにRSSフィードをスクレイピング
- nba_scraping_dag — 毎時ECSタスクでニュース取得
- nba_translation — スクレイピング後に日本語翻訳
Terraformの構成(抜粋)
# MWAA環境 — これだけで月$350+
resource "aws_mwaa_environment" "main" {
name = "iso-flow-prod"
airflow_version = "2.8.1"
environment_class = "mw1.small" # 最小でもこの価格...
min_workers = 1
max_workers = 5
schedulers = 2
network_configuration {
security_group_ids = [aws_security_group.mwaa.id]
subnet_ids = slice(var.private_subnets, 0, 2)
}
# 大量のAirflow設定が必要
airflow_configuration_options = {
"core.default_timezone" = "Asia/Tokyo"
"core.parallelism" = "32"
"core.dag_concurrency" = "16"
"core.max_active_runs_per_dag" = "1"
"celery.worker_prefetch_multiplier" = "1"
# ... 他にも20項目以上
}
}
さらに必要だったリソース:
- IAMロール + 巨大なポリシー(S3, CloudWatch, SQS, KMS, ECS, EC2, SecretsManager)
- セキュリティグループ
- S3バケット × 2(DAGs用、Results用)
- SSMパラメータ × 3
- VPCエンドポイント
- CloudWatch Logグループ × 5
合計14リソース、モジュール488行。
新構成: EventBridge Scheduler
実装
# EventBridge Scheduler IAM Role — シンプル
resource "aws_iam_role" "scheduler" {
name = "${local.name}-scheduler-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "scheduler.amazonaws.com" }
}]
})
}
# 必要な権限はecs:RunTaskとiam:PassRoleだけ
resource "aws_iam_role_policy" "scheduler" {
name = "${local.name}-scheduler-policy"
role = aws_iam_role.scheduler.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["ecs:RunTask"]
Resource = var.ecs_task_definition_arn
},
{
Effect = "Allow"
Action = ["iam:PassRole"]
Resource = [var.ecs_task_role_arn, var.ecs_execution_role_arn]
}
]
})
}
# RSSスクレイピング — 毎時ECSタスクを起動
resource "aws_scheduler_schedule" "rss_scraping" {
name = "${local.name}-rss-scraping"
schedule_expression = "rate(1 hour)"
flexible_time_window { mode = "OFF" }
target {
arn = var.ecs_cluster_arn
role_arn = aws_iam_role.scheduler.arn
ecs_parameters {
task_definition_arn = var.ecs_task_definition_arn
launch_type = "FARGATE"
platform_version = "LATEST"
network_configuration {
subnets = var.private_subnets
security_groups = [var.ecs_security_group_id]
assign_public_ip = false
}
task_count = 1
}
# コンテナコマンドを上書きしてスクレイピングモードで起動
input = jsonencode({
containerOverrides = [{
name = "backend"
command = ["/usr/local/bin/scrape"]
}]
})
retry_policy {
maximum_event_age_in_seconds = 86400
maximum_retry_attempts = 3
}
}
state = var.enable_scheduler ? "ENABLED" : "DISABLED"
}
# 翻訳 — スクレイピング15分後にGraphQL mutationを実行
resource "aws_scheduler_schedule" "translation" {
name = "${local.name}-translation"
schedule_expression = "cron(15 * * * ? *)"
flexible_time_window { mode = "OFF" }
target {
arn = var.ecs_cluster_arn
role_arn = aws_iam_role.scheduler.arn
ecs_parameters {
task_definition_arn = var.ecs_task_definition_arn
launch_type = "FARGATE"
platform_version = "LATEST"
network_configuration {
subnets = var.private_subnets
security_groups = [var.ecs_security_group_id]
assign_public_ip = false
}
task_count = 1
}
input = jsonencode({
containerOverrides = [{
name = "backend"
command = [
"sh", "-c",
"curl -sf -X POST $BACKEND_URL/graphql -H 'Content-Type: application/json' -d '{\"query\": \"mutation { translatePendingNews { translatedCount } }\"}'"
]
}]
})
retry_policy {
maximum_event_age_in_seconds = 86400
maximum_retry_attempts = 3
}
}
state = var.enable_scheduler ? "ENABLED" : "DISABLED"
}
合計5リソース、モジュール175行。
ハマったポイント
1. MWAA環境の削除に54分かかる
terraform destroy を実行したら、MWAA環境の削除だけで54分かかりました。MWAAは内部でVPCのENI、セキュリティグループ、ECSクラスタ等を作るため、クリーンアップに非常に時間がかかります。
module.mwaa.aws_mwaa_environment.main: Still destroying... [54m51s elapsed]
module.mwaa.aws_mwaa_environment.main: Destruction complete after 54m52s
対処: CIのタイムアウトに注意。手動実行推奨。
2. バージョニング有効S3バケットの削除
MWAAのDAGs用S3バケットはバージョニングが有効だったため、terraform destroy で BucketNotEmpty エラーが発生。
Error: deleting S3 Bucket: BucketNotEmpty: The bucket you tried to delete
is not empty. You must delete all versions in the bucket.
対処: オブジェクトのバージョンとデリートマーカーを全て削除してからバケット削除。
# バージョン付きオブジェクトを全削除
aws s3api list-object-versions --bucket $BUCKET \
--query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' \
--output json | \
aws s3api delete-objects --bucket $BUCKET --delete file:///dev/stdin
# デリートマーカーも削除
aws s3api list-object-versions --bucket $BUCKET \
--query '{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}' \
--output json | \
aws s3api delete-objects --bucket $BUCKET --delete file:///dev/stdin
# これでバケット削除可能
aws s3 rb s3://$BUCKET
3. Terramate CIでのTerraform validate失敗
Terramate(Terraform state分割ツール)を使ったCIで、terraform init がlock fileを更新してしまい、Terramateの「uncommittedファイルチェック」に引っかかりました。
Error: repository has uncommitted files
対処: --disable-safeguards=git-uncommitted フラグを追加。
# .github/workflows/code-quality.yml
- name: Terraform Validate
run: |
cd terraform
terramate generate
terramate run --disable-safeguards=git-uncommitted -- terraform init -backend=false
terramate run --disable-safeguards=git-uncommitted -- terraform validate
コスト比較
| 項目 | MWAA | EventBridge Scheduler |
|---|---|---|
| 月額コスト | ~$350+ | $0(無料枠内) |
| 年間コスト | ~$4,200+ | $0 |
| Terraformコード | 488行 | 175行 |
| リソース数 | 14 | 5 |
| 環境構築 | 20〜30分 | 数秒 |
| 環境削除 | 54分 | 数秒 |
EventBridge Schedulerの無料枠は月14,000,000回。このプロジェクトの呼び出しは月約1,440回で、無料枠の0.01%以下です。
まとめ
- 個人開発でMWAAは避けるべき。最小構成でも月$350+は痛い
- やりたいことが「定期的にECSタスクを起動する」だけならEventBridge Scheduler一択
- 「将来複雑になるかも」で高コストツールを維持するより、今の要件にフィットするシンプルな解を選ぶ方が健全
- 本当にDAG依存関係やワークフロー分岐が必要になったら、Step Functionsを検討すればいい
TerraformのコードはGitHubリポジトリで公開しています。
🏀 NBA ISO FLOW: https://www.nba-iso-flow.com/ — NBAの最新ニュースをリアルタイムでお届け