0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS MWAAを廃止してEventBridge Schedulerに移行したら年間$4,200削減できた【Terraform実装付き】

0
Last updated at Posted at 2026-04-01

はじめに

個人開発している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が動いていました:

  1. nba_rss_scraper — 5分ごとにRSSフィードをスクレイピング
  2. nba_scraping_dag — 毎時ECSタスクでニュース取得
  3. 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 destroyBucketNotEmpty エラーが発生。

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の最新ニュースをリアルタイムでお届け

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?