Sionの技術ブログ

SREとして日々の学習を書いて行きます。twitterは@sion_cojp

Fargate + cloudwatch eventでcronシステム構築

FOLIOでFargate + cloudwatch eventを使ったcron(マイクロバッチ)システムを設計し、実際に本番で動いてるので紹介します。

(ロギング、モニタリングは別記事で紹介したいと思います)

構成図

f:id:sion_cojp:20190903204310p:plain

技術

  • 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 "", ""
    }
}