AWS App Runnerを自動で一時停止してコスト削減する

App Runnerはリクエストがない場合でも費用が発生します。
（プロビジョニングされたコンテナインスタンスは最低でも1つ必要。それに割り当てられたメモリに対して費用がかかる。）

App Runnerサービスを一時停止をすることで上記の費用は削減できますが、手動で一時停止・再開するのは面倒なので、夜間などの明らかに使わない時間だけ自動で一時停止にするような仕組みについて紹介します。

## EventBridge Schedulerを使って定期的に一時停止する

EventBridge Schedulerを使い、定期的にApp Runnerサービスを一時停止・再開する仕組みをつくることで実現できます。

https://docs.aws.amazon.com/ja_jp/scheduler/latest/UserGuide/what-is-scheduler.html

### やり方

まず、

- 一時停止
- 再開

を行うScheduleの作成をします。
ここでは仮に

- 一時停止: 毎日15:00（UTC）
- 再開: 毎日00:00（UTC）

にしています。

```terraform
# Event Scheduler
resource "aws_scheduler_schedule_group" "auto-pause-and-resume" {
  name = "auto-pause-and-resume"
}
resource "aws_scheduler_schedule" "auto-pause" {
  group_name                   = aws_scheduler_schedule_group.auto-pause-and-resume.name
  name                         = "auto-pause"
  schedule_expression          = "cron(0 15 * * ? *)"
  schedule_expression_timezone = "UTC"
  state                        = "ENABLED"
  flexible_time_window {
    mode = "OFF"
  }
  target {
    arn = "arn:aws:scheduler:::aws-sdk:apprunner:pauseService"
    input = jsonencode({
      ServiceArn = "App RunnerサービスのARN"
    })
    role_arn = "TODO"
    retry_policy {
      maximum_event_age_in_seconds = 86400
      maximum_retry_attempts       = 0
    }
  }
}
resource "aws_scheduler_schedule" "auto-resume" {
  group_name                   = aws_scheduler_schedule_group.auto-pause-and-resume.name
  name                         = "auto-resume"
  schedule_expression          = "cron(0 0 * * ? *)"
  schedule_expression_timezone = "UTC"
  state                        = "ENABLED"
  flexible_time_window {
    mode = "OFF"
  }
  target {
    arn = "arn:aws:scheduler:::aws-sdk:apprunner:resumeService"
    input = jsonencode({
      ServiceArn = "App RunnerサービスのARN"
    })
    role_arn = "TODO"
    retry_policy {
      maximum_event_age_in_seconds = 86400
      maximum_retry_attempts       = 0
    }
  }
}
```

上記のScheduleに加え、SchedulerがApp Runnerサービスを一時停止・再開できるようなAssumeRoleをアタッチしてあげる必要があります。
そのため、まず以下のIAM Roleを作成します。

```terraform
# IAM role
data "aws_iam_policy_document" "auto-pause-and-resume-assume-role-policy" {
  statement {
    effect = "Allow"
    actions = [
      "sts:AssumeRole"
    ]
    principals {
      type = "Service"
      identifiers = [
        "scheduler.amazonaws.com"
      ]
    }
  }
}
data "aws_iam_policy_document" "auto-pause-and-resume-inline-policy" {
  statement {
    effect = "Allow"
    actions = [
      "apprunner:PauseService",
      "apprunner:ResumeService"
    ]
    resources = [
      "App RunnerサービスのARN"
    ]
  }
}
resource "aws_iam_role" "auto-pause-and-resume" {
  name               = "auto-pause-and-resume"
  assume_role_policy = data.aws_iam_policy_document.auto-pause-and-resume-assume-role-policy.json
  inline_policy {
    name   = "auto-pause-and-resume-inline-policy"
    policy = data.aws_iam_policy_document.auto-pause-and-resume-inline-policy.json
  }
}
```

そして、作成したIAM RoleをScheduleにアタッチしてあげます。

```diff_terraform
resource "aws_scheduler_schedule" "auto-pause" {
  # ...
  target {
    # ...
-   role_arn = "TODO"
+   role_arn = aws_iam_role.auto-pause-and-resume.arn
  # ...
}
resource "aws_scheduler_schedule" "auto-resume" {
  # ...
  target {
    # ...
-   role_arn = "TODO"
+   role_arn = aws_iam_role.auto-pause-and-resume.arn
  # ...
}
```

これで完了です！

<details><summary>最終的なterraformのコード</summary>

```terraform
# Event Scheduler
resource "aws_scheduler_schedule_group" "auto-pause-and-resume" {
  name = "auto-pause-and-resume"
}
resource "aws_scheduler_schedule" "auto-pause" {
  group_name                   = aws_scheduler_schedule_group.auto-pause-and-resume.name
  name                         = "auto-pause"
  schedule_expression          = "cron(0 15 * * ? *)"
  schedule_expression_timezone = "UTC"
  state                        = "ENABLED"
  flexible_time_window {
    mode = "OFF"
  }
  target {
    arn = "arn:aws:scheduler:::aws-sdk:apprunner:pauseService"
    input = jsonencode({
      ServiceArn = "App RunnerサービスのARN"
    })
    role_arn = aws_iam_role.auto-pause-and-resume.arn
    retry_policy {
      maximum_event_age_in_seconds = 86400
      maximum_retry_attempts       = 0
    }
  }
}
resource "aws_scheduler_schedule" "auto-resume" {
  group_name                   = aws_scheduler_schedule_group.auto-pause-and-resume.name
  name                         = "auto-resume"
  schedule_expression          = "cron(0 0 * * ? *)"
  schedule_expression_timezone = "UTC"
  state                        = "ENABLED"
  flexible_time_window {
    mode = "OFF"
  }
  target {
    arn = "arn:aws:scheduler:::aws-sdk:apprunner:resumeService"
    input = jsonencode({
      ServiceArn = "App RunnerサービスのARN"
    })
    role_arn = aws_iam_role.auto-pause-and-resume.arn
    retry_policy {
      maximum_event_age_in_seconds = 86400
      maximum_retry_attempts       = 0
    }
  }
}

# IAM role
data "aws_iam_policy_document" "auto-pause-and-resume-assume-role-policy" {
  statement {
    effect = "Allow"
    actions = [
      "sts:AssumeRole"
    ]
    principals {
      type = "Service"
      identifiers = [
        "scheduler.amazonaws.com"
      ]
    }
  }
}
data "aws_iam_policy_document" "auto-pause-and-resume-inline-policy" {
  statement {
    effect = "Allow"
    actions = [
      "apprunner:PauseService",
      "apprunner:ResumeService"
    ]
    resources = [
      "App RunnerサービスのARN"
    ]
  }
}
resource "aws_iam_role" "auto-pause-and-resume" {
  name               = "auto-pause-and-resume"
  assume_role_policy = data.aws_iam_policy_document.auto-pause-and-resume-assume-role-policy.json
  inline_policy {
    name   = "auto-pause-and-resume-inline-policy"
    policy = data.aws_iam_policy_document.auto-pause-and-resume-inline-policy.json
  }
}
```

</details>

動作確認をしてると、定期的に実行されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352836/dc26e030-37d2-af4e-dffa-516daa7b8279.png)

また、コストも削減できていることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352836/d0bec628-174c-96a9-a9a8-f5e4a84f31ec.png)

## さいごに

EventBridge Schedulerを使うことで思ったよりも簡単に自動停止できるようになりました。
これでプライベートのインフラコストが少し削減できたので、よかったです。

もし少しでも参考になれば幸いです。