Fargate + cloudwatch eventでcronシステム構築
FOLIOでFargate + cloudwatch eventを使ったcron(マイクロバッチ)システムを設計し、実際に本番で動いてるので紹介します。
(ロギング、モニタリングは別記事で紹介したいと思います)
構成図
技術
- fargate: アプリケーション
- cloudwatch event schedule: cron形式でFargateをさせる発火
- cloudwatch logs: awslogsドライバーでアプリのログを出力
- ecscli: Go製の社内cliツール。ECSタスク登録、cloudwatch eventのschedule put(update)をする
詳細
terraformでcluster / serviceの2つのmoduleがあります。
/* cluster生成 */ module "cron" { source = "modules/aws-ecs/fargate_batch_cron_cluster" cluster_name = "cron" vpc_id = "${data.aws_vpc.sandbox.id}" dd_tag_env = "sandbox" # monitor sandbox slack_webhook_url = "${var.SLACK_WEBHOOK_MONITOR_SANDBOX}" } /* koyama_testサービス生成 */ module "koyama_test" { source = "modules/aws-ecs/fargate-cron" cluster_name = "cron" task_name = "koyama_test" subnets = [ "${data.aws_subnet.private-subnet-1a.id}", "${data.aws_subnet.private-subnet-1c.id}", ] }
clusterは下記を行ってます。
- ecs cluster作成
- デフォルトで使える、iam作成
- non exit 0 の場合、slackに通知するlambda
serviceは下記を行ってます。
- 対象ecs fargate taskをターゲットとした、cloudwatch event作成
- cloudwatch eventからecsが操作できる + ecsで使うiam作成
下記のコマンドでdeploy, disable, enable が実行できます。
### deploy $ ecscli schedule update --env sandbox -t ecs.yml ### 停止 $ aws --profile sandbox events disable-rule --name cron-koyama_test ### 再開 $ aws --profile sandbox events enable-rule --name cron-koyama_test
こだわったところ
applyの負担軽減
$ tree ├── cluster │ ├── ecs.tf ├── koyama_test │ ├── ecs.tf └── logs ├── main.tf
のようにサービス毎のディレクトリに分割することで、apply時に他サービスが影響しないようにしました。
SREと開発者の責務分け
taskDefinitionはSRE。containerDefinitionは開発者に設定してもらうようにしました。
理由は、例えばFargateのネットワークはaws-vpcモードなのでSecurityGroupとSubnetIDがタスク定義に必要ですが、そこの設定を開発者にさせたくなかった(というか開発者は分からない)からです。
SRE管轄はecscliのGoコードにハードコーディング。 開発者には下記yamlを書いてもらいdeployしてもらってます。
$ vim ecs.yml type: "FARGATE" cluster: "cron" name: "cron-koyama_test" containers: - name: app cpu: 256 memory: 512 image: sioncojp/docker-slack:latest environment: WEBHOOK_URL: "https://hooks.slack.com/services/xxxxxxxxx" schedule: scheduleExpression: "rate(1 minute)" taskCount: 1
deploy = updateであり、開発者にcreate権限は与えてません。
理由はSREが把握できないcronを自由にdeployされてしまうのはよくないため、事前にSREが作った枠に対し、開発者がupdateする仕組みにしてます。
Fargateのtask CPU, memoryの自動選択
開発者にはcontainerDefinitionを設定してもらってますが、Fargateだと全体のCPU, memoryを決めないといけません。
開発者にはコンテナのCPU, memoryだけ決めさせたかった + 良いことにFargateの全体値は決め打ちだったので、全てのコンテナのCPU, memoryを加算した値から、適切な値を自動で選択させるようにしました。
$ vim internal/fargate/fargate.go package fargate import ( "strconv" ) // NewTaskCpuMemoryValue ... Fargateのタスク全体のCPU, Memoryを算出 func NewTaskCpuMemoryValue(cpus, mems []int64) (string, string) { var c, m int64 for _, v := range cpus { c += v } for _, v := range mems { m += v } taskCpu, taskMemory := convertTaskValueToString(c, supportMemoryValue(m)) return taskCpu, taskMemory } // supportMemoryValue ... 512 or 1024, 2048 ... のように1024の倍数を算出 func supportMemoryValue(m int64) int64 { if m <= 512 { return 512 } return (m/1024 + 1) * 1024 } // convertTaskValueToString ... CPUの範囲をベースに、Taskがサポートしてる値を算出 // https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-cpu-memory-error.html func convertTaskValueToString(c, m int64) (string, string) { memoryStr := strconv.FormatInt(m, 10) switch { case c == 256: if 512 <= m && m <= 2048 { return "256", memoryStr } fallthrough case 256 < c && c <= 512: if 1024 <= m && m <= 4096 { return "512", memoryStr } fallthrough case 512 < c && c <= 1024: if 2048 <= m && m <= 8192 { return "1024", memoryStr } fallthrough case 1024 < c && c <= 2048: if 4096 <= m && m <= 16384 { return "2048", memoryStr } fallthrough case 2048 < c && c <= 4096: if 8192 <= m && m <= 30720 { return "4096", memoryStr } fallthrough default: return "", "" } }