はじめに
皆さんの環境には定期バッチのために起動させっぱなし、又は都度手動で起動/停止させているEC2はありますか?
私は作ろうとしていました。「起動させっぱなしでcron実行はコストがかかる。」「都度起動はSREの工数を奪い、自分自身も常に意識していなければならない。」と悩んでいたところ、職場の先輩に「ECS Scheduled Tasksを使えば?」とお勧めされました。
使ってみるとかなり手軽に構築でき、コスト面含めメリットも多かったので、今回はECS Schedule Tasksを構築する際に使用したTerraformを共有したいと思います。
ECS Scheduled Tasksとは?
EventBridgeを使ってECSタスク(≒コンテナ)をスケジュール実行できるシステムのことです。cronでの定期実行だけでなく、GitHubと連携させてPRがマージされた時やCloudWatchがアラートを検知した時など様々なタイミングで実行することができます。
Lambdaでも同様のことができますが、Lambdaの最大実行時間15分を超える場合はECSが選択肢となります。
構成
サンプルコード
2023/12/22現在、terraform v1.5.7
、aws provider v5.31.0
の環境ではほぼコピペで動きますので、気になる方は試してみてください!
※サブネットとセキュリティグループは別に用意してください
ECS
resource "aws_ecs_cluster" "sample" {
name = "cluster-sample"
}
resource "aws_ecs_task_definition" "sample" {
family = "sample"
execution_role_arn = aws_iam_role.task_execution_role.arn
task_role_arn = aws_iam_role.task_role.arn
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512
memory = 1024
container_definitions = jsonencode([
{
name = "sample"
image = "${data.aws_caller_identity.current.account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/sample"
cpu = 512
memory = 1024
essential = true
"logConfiguration" : {
"logDriver" : "awslogs",
"options" : {
"awslogs-region" : "ap-northeast-1",
"awslogs-group" : "/ecs/scheduled-task/sample"
"awslogs-stream-prefix" : "ecs",
}
},
"environment" : [
{
"name" : "S3_BUCKET_NAME",
"value" : "bucket"
}
],
"secrets" : [
{
"name" : "DB_HOST",
"valueFrom" : data.aws_ssm_parameter.db_host.arn
}
]
}
])
}
// シークレットの管理にはSSM パラメータストアを使っています
data "aws_ssm_parameter" "db_host" {
name = "/ecs/sample/db-host"
}
data "aws_caller_identity" "current" {}
コンテナでシークレットを使う場合は下記コマンドを使うなどして事前にAWS上に置いておく必要があります。
aws ssm put-parameter \
--name "/ecs/sample/db-host" \
--value "db" \
--type SecureString
EventBridge
resource "aws_cloudwatch_event_rule" "sample" {
name = "sample"
schedule_expression = "cron(*/2 * * * ? *)" // 2分おき
}
resource "aws_cloudwatch_event_target" "sample" {
target_id = "sample"
rule = aws_cloudwatch_event_rule.sample.name
arn = aws_ecs_cluster.sample.arn
role_arn = aws_iam_role.scheduled_task_execute.arn
ecs_target {
task_definition_arn = aws_ecs_task_definition.sample.arn
task_count = 1
launch_type = "FARGATE"
platform_version = "1.4.0"
enable_execute_command = true
network_configuration {
assign_public_ip = false
// セキュリティグループ・サブネットは通信する外部システムや他サービスに合わせてください。
security_groups = [ "sg-xxx" ]
subnets = [ "subnet-xxxx" ]
}
}
}
cronの記述ルールがlinuxのものと微妙に違ってハマりました。cron 式のリファレンス
例) 毎週月曜16時(UTC)
- aws ->
0 16 ? * 1 *
- linux ->
0 16 * * 1 *
サブネットはFargateを使う場合VPCエンドポイントを設定しないといけないので注意が必要です。詳しい解説はリンクの記事にお任せします。
https://dev.classmethod.jp/articles/fargate_pv14_vpc_endpoint/
IAM
resource "aws_iam_role" "scheduled_task_execute" {
name = "scheduled-task-execute"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : "",
"Effect" : "Allow",
"Principal" : {
"Service" : "events.amazonaws.com"
},
"Action" : "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "scheduled_task_execute" {
role = aws_iam_role.scheduled_task_execute.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole"
}
// タスク実行ロール
resource "aws_iam_role" "task_execution_role" {
name = "task-execution-role"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : ""
"Effect" : "Allow",
"Principal" : {
"Service" : "ecs-tasks.amazonaws.com"
},
"Action" : "sts:AssumeRole",
}
]
})
}
resource "aws_iam_role_policy_attachment" "task_execution_role" {
role = aws_iam_role.task_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
// シークレットをssmで管理している場合は必要になります
resource "aws_iam_policy" "task_execution_role_ssm_get_parameter" {
name = "task-execution-role-ssm-get-parameter"
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : "",
"Effect" : "Allow",
"Action" : "ssm:GetParameters",
"Resource" : "arn:aws:ssm:ap-northeast-1:${data.aws_caller_identity.current.account_id}:parameter/ecs/sample/*",
}
]
})
}
resource "aws_iam_role_policy_attachment" "task_execution_role_ssm_get_parameter" {
role = aws_iam_role.task_execution_role.name
policy_arn = aws_iam_policy.task_execution_role_ssm_get_parameter.arn
}
// タスクロール
resource "aws_iam_role" "task_role" {
name = "task-role"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : ""
"Effect" : "Allow",
"Principal" : {
"Service" : "ecs-tasks.amazonaws.com"
},
"Action" : "sts:AssumeRole",
}
]
})
}
resource "aws_iam_policy" "task_role" {
name = "task-role"
// アプリでs3からファイルを取得している場合は下記権限を与えています。環境に合わせて設定してください。
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Resource" : [
"arn:aws:s3:::sample/*",
],
"Action" : [
"s3:GetObject",
]
}
]
})
}
resource "aws_iam_role_policy_attachment" "task_role" {
role = aws_iam_role.task_role.name
policy_arn = aws_iam_policy.task_role.arn
}
// ECSのログをCloudWatch Logsに送信する場合に必要になります。
resource "aws_iam_policy" "task_role_cwl" {
name = "task-role-put-logs"
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Resource" : [
"arn:aws:logs:ap-northeast-1:${data.aws_caller_identity.current.account_id}:log-group:/ecs/scheduled-task/sample/*",
],
"Action" : [
"logs:DescribeLogGroups",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
}
]
})
}
resource "aws_iam_role_policy_attachment" "task_role_cwl" {
role = aws_iam_role.task_role.name
policy_arn = aws_iam_policy.task_role_cwl.arn
}
EventBridge Targetsに渡すIAMにAmazonEC2ContainerServiceEventsRole
ポリシーをアタッチする必要があります。Amazon ECS CloudWatch Events IAM ロール
ECR
resource "aws_ecr_repository" "sample" {
name = "sample"
}
今回は動作確認に公式のhello worldイメージを使用します。
// ログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
// hello-worldイメージのpull
docker pull hello-world
// タグ付け
docker tag sample:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/sample:latest
// ECRへpush
docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/sample:latest
CloudWatch Logs
resource "aws_cloudwatch_log_group" "sample" {
name = "/ecs/scheduled-task/sample"
retention_in_days = 5 // ログの保持期間
}
その他
全くterraform環境が無い状態からコピペで動かす際は下記のようなterraformの初期設定も必要になります。
provider "aws" {
region = "ap-northeast-1"
}
terraform {
backend "local" {
path = "terraform.tfstate"
}
}
ECS Scheduled Taskを採用するメリット
- 手動から自動へ。時間の節約も小さな積み重ねが大事ですね!
- バッチ実行時のみEC2を起動する場合と比較しても大差ないコストになります。
- 例) バッチ実行時間が30時間だった場合、
- ECS:$0.932
- = (0.05056 / 2 + 0.00553) * 30 + (0.76 + 0.033) * 0.01
- EC2(都度起動):$0.816
- = 0.0272 * 30
- 参考: EC2(起動したまま):$19.584
- = 0.0272 * 24 * 30
- ECS:$0.932
- ※EC2はt3.small
- ※ECSはt3.smallのベースラインパフォーマンスを考慮した際、同等程度のパフォーマンスになる0.5vCPU/memory 500MB
- ※ログ転送量は10MB
- ※S3からファイル取得している場合など別途追加でコストがかかります
- 例) バッチ実行時間が30時間だった場合、
- 手動実行の場合、工数も取られるため月一実行としていましたが、自動になったことで短いスパンで実行でき、データの鮮度を高められました。
- 前回実行時から現在時刻までにS3にアップロードされた画像の解像度を取得するバッチであったため実行間隔の短縮がデータ鮮度の向上につながりました。
- 実行ログを残すのが簡単でした。 EC2で動かす場合はマシン内に入ってFTPするか、agentを仕込む必要があります。
最後に
いかがでしょうか?EC2を操作する手間やコストを考えると、このくらいのコードを書く手間は安いものではないでしょうか?
dockerイメージ化が難しかったり、DBや外部サービスとのネットワークが複雑であったりする場合はそう簡単でもないかもしれませんが一向の余地はあるかもしれません。
あとがき
記事を書きながら調べ直していたら、2023年現在はEventBridge Schedule RuleではなくEventBridge Schedulerを推奨していることに気づきました。。今後Scheduled Tasksを作成される方はこちらをご利用ください。