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

GoでSlackに定常アラートが出たら、スレッドで自動コメントする

github.com

Slackに定常アラートが出たら、スレッドで自動コメントするGoのプログラムです。

今はアラート撲滅に着手出来ないけど、それまで周りに分かりやすいようにコメントを自動でしたい!という時に便利に使えるなぁと思って作りました。

動作

例えばこんなtomlを設定したら

$ vim examples/monitoring.toml
[[action]]
channel = "xxxxx"
in      = "test"
out     = "hoge"

testというワードに反応してスレッドにhogeを書いてくれます。

https://github.com/sioncojp/go-slack-auto-comment/blob/master/docs/go-slack-auto-comment01.png?raw=true


https://github.com/sioncojp/go-slack-auto-comment/blob/master/docs/go-slack-auto-comment02.png?raw=true

https://github.com/sioncojp/go-slack-auto-comment/blob/master/examples/monitoring-channel.toml

にサンプルがあります。

inは正規表現が使えたり、outは改行した書き方が出来ます。

またディレクトリ配下のtomlを全て読み込むので、チャンネルごとにtomlファイルを分離すると良いでしょう。

Usage

事前準備

Customize Slack -> API -> Your Apps -> Create New App

でAppを作り、

  • Bot UsersをONにし、Web版SlackからBOT ID
  • OAuth & PermissionsでBot User OAuth Access Token
  • BasicInformationにあるVerification Token

を取得してください。

動かしてみる

https://github.com/sioncojp/go-slack-auto-comment/tree/master/examples

を参考にconfigを設定してください。

Parameter Storeを使ってる場合は GitHub - sioncojp/tomlssm を使ってるので、 ssm://SSMのAlias名 と書けばIAM, KMS権限があればDecodeしてくれます。

### build
$ make build

### help
$ ./bin/go-slack-auto-comment help
NAME:
   go-slack-auto-comment - A new cli application

USAGE:
   go-slack-auto-comment [global options] command [command options] [arguments...]

VERSION:
   0.0.0

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --config-dir value, -c value  Load configuration *.toml in target dir
   --region value, -r value      Setting AWS region for tomlssm (default: "ap-northeast-1")
   --help, -h                    show help
   --version, -v                 print the version

### run。configが入ってるdirectoryを指定
$ ./bin/go-slack-auto-comment -c examples/
{"level":"info","ts":1562750380.530795,"caller":"go-slack-auto-comment/main.go:64","msg":"start..."}

実際にどう利用してる?

datadogからの通知、slowQueryの通知などに対して、設定してたりします f:id:sion_cojp:20190719145235p:plain

[[action]]
channel = "xxxxx"
in      = "^(Re-Triggered|Triggered|Warn):(.*)ECS cluster CPU reservation high.*"
out     = "autoscaleでCPUが下がらなければ、Terraformからcluster台数を増やしましょう"

課題

正規表現に癖があるので、そこが少し難しいです。

例えば

RDS Slow Log [test]

[IP: xxx.xxx.xxx.xxx]

'''
DELETE FROM `xxxx`
'''

にヒットさせようとしたら下記を書きます。

in = "^RDS Slow Log \\[test\\](?s)(.*)FROM `xxx`"

# (?s)(.*) ... 改行含む全ての文字

デバッグは容易ですが、さくっとやるにはサンプル数を増やしたり慣れる必要があります。

最後に

定常アラートをなくすことが大事なので、忘れずに。。

FOLIOに入社して1年でやったこと

やったことをまとめてみます。

terraformのコマンドラッパー(Makefile)作成

  • terraform周りの自動セットアップ
  • terraformのバージョンが固定
  • terraform providerが自動更新
  • 実行ログをs3に保管するようにしたりなどなど。

コンテナ本番導入

コンテナイメージの脆弱性診断チャレンジ

ECS用イメージをcodebuild + packer + bashで生成

batch/cronのコンテナ設計

  • batch: fargate runTask
  • cron: fargate + cloudwatch event
  • でterraform module作成し、検証まで。
  • これから移行していきたいなぁというお気持ちです。

slack deploy

社内ラジオ、映像サイト構築

landing pageサイト構築

  • s3 hosting + cloudfront + waf
  • deployツールがtypescriptだったので、IAM認証系周りのPRを投げたり。

CodeBuild + PackerでAMIを焼く

CodeBuild + Packerを使って、AmazonLinuxベースのimageを焼きます。

完成品こちらです

github.com

構成図

f:id:sion_cojp:20190128200331p:plain

CodeBuildの準備

Terraformでセットアップします。

https://github.com/sioncojp/codebuild-packer/tree/master/terraform/codebuild/packer を参考にして terraform apply してください。

やってることは、

  • CodeBuild作成
  • CodeBuildで使うファイル群を保管するs3作成
  • CodeBuildのIAM作成
  • Packerで使うEC2のIAM instance profile作成
  • Packerで使うEC2のSG作成

instance profileを作っておくことで、後からAMIを焼くための権限をインスタンスに付与することができます。

s3にimageに必要なファイルをsyncし、codebuildにqueueを飛ばす

https://github.com/sioncojp/codebuild-packer/blob/master/Makefile

$ make build IMAGE=images/amazon-linux.json

やってることは、

  • images配下をs3にsync
  • codebuildにqueueを飛ばす

これでCodeBuildが起動し、s3にあるbuildspec.ymlを元に、AMIを焼いてくれます。

FargateでSSMのデータをコンテナ起動時に環境変数にセットする

はじめに

dev.classmethod.jp

こんな機能が提供されましたが、

(重要)Fargateは対応していない

本当かどうか実際に試したところ、やはり対応してませんでした。

なのでFargateの場合、SSMのデータを読み込む場合は下記2択が想定されます。

  1. SSMのデータをdecryptして、task definition(container definition)に埋め込む
  2. 起動時にSSMのデータをdecryptして環境変数にセットする

1. task definitionに埋め込む場合

弊社もこちらでやってましたが、タスクの中に復号キーが載り(ECSのdescribe-tasks権限さえあれば)閲覧出来てしまうのでセキュアではありません。

ちなみに下記のmakefileで、すでに用意されてるcontainer definitionに対して、decryptしたデータに置換する処理をしてました。

$ vim ci.mk
DATADOG_API_KEY    := $(shell docker run --rm sioncojp/awscli aws ssm get-parameters --region ap-northeast-1 --name datadog_api_key --with-decryption | jq -r -e .Parameters[].Value)

ecs/container_task_definition: ecs/clean
   @sed -i -e 's|@@API_KEY@@|$(DATADOG_API_KEY)|' container_task_definition.json

ecs/clean:
   -rm -f container_task_definition.json

2. 起動時に環境変数にセットする場合

github.com

chamberというSSMを管理するツールで解決させました。

ロジックとしては、entrypoint.shを用意し、コンテナ起動時にchamberを使ってdecryptし、環境変数にセットしてます。

これでtask definitionにも復号キーが載らず、よりセキュアになります。

設定例

Fargateで、datadog-agentを立ち上げて外形監視させてる例を出してみました。

chamberのexportにはformatがjson/java-properties/csv/tsv/dotenvと用意されてます。

dotenv: DD_API_KEY="xxxxxxxx"
java-properties: DD_API_KEY = xxxxxxx

今回は、java-propertiesで出力し、trコマンドで空白文字を削除して、 DD_API_KEY=xxxxxxx として出力させ、それをexportして環境変数にセット。

最後のexecで、ベースとなるイメージが /init を実行してるので、それを渡してます。

Dockerfile

$ vim Dockerfile
FROM datadog/agent:latest

ENV AWS_REGION ap-northeast-1

RUN curl -L -fsS --retry 2 -o /usr/local/bin/chamber https://github.com/segmentio/chamber/releases/download/v2.3.2/chamber-v2.3.2-linux-amd64 && \
    chmod 755 /usr/local/bin/chamber

COPY url.yaml /etc/datadog-agent/conf.d/http_check.d/url.yaml
COPY fargate.yaml /etc/datadog-agent/conf.d/ecs_fargate.d/fargate.yaml

COPY ./entrypoint.sh /
RUN ["chmod", "+x", "/entrypoint.sh"]

ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh

$ vim entrypoint.sh
#!/bin/sh -

export $(chamber export --format java-properties fargate-datadog | tr -d ' ')
exec /init

最後に

おそらくFargateのECSコンテナバージョンが1.22.0以上がサポートされれば、使えるかと思いますので、一時的な対応とはなりそうです。

ECSのタスク切り替りを通知させる

本記事は、FOLIOアドベントカレンダーの13日目の記事になります。



f:id:sion_cojp:20181211154702p:plain:w170

ことの始め

f:id:sion_cojp:20181211145831p:plain

なるほど。

つまりALBにぶら下がってるコンテナが、現在のコンテナがなくなって、新しいコンテナだけになったときに通知があれば嬉しいってことですね。

棄却した案: cloudwatchのevent * lambda

ECSからcloudwatch eventにtask変動のトリガー送信して、lambdaでslackに通知させることを考えました。

が、タスクが作成、削除されただけの通知で、どのタイミングで切り替わったかわかりづらかったです。

ecs-update-notify

github.com

ロジックは

  • 起動後、/tmp/ecs-update-notify.pid を生成(終了時に削除される)
  • intervalで設定してる秒数、ECSサービスで保持してるタスクを取得
  • 現在のタスクバージョンと、新しいバージョンのデータを保持
  • 新しいバージョンだけになったタイミングでSlackにPOST
  • 新しいバージョンは現在のバージョンとして保持される

古い(現在の)タスクがECSサービスから存在しなくなれば、ALBからも確実に外されてるので、これで解決します。

このようなconfig.tomlを設定して

$ vim config.toml
interval = 30

[[monitor]]
name = "FooCluster"
aws_profile = "foo"
aws_region = "ap-northeast-1"
incoming_webhook = "https://hooks.slack.com/services/....."

[[monitor]]
name = "BarCluster"
aws_profile = "bar"
aws_region = "ap-northeast-1"
incoming_webhook = "https://hooks.slack.com/services/....."

クラスタ名やawsのprofile, regionを入れます。

incoming_webhookはslack apiなどで生成してください。

./ecs-update-notify -c config.toml

とすると、下記のような通知がされます。

f:id:sion_cojp:20181211153940p:plain

導入した後

f:id:sion_cojp:20181211154252p:plain

解決した!

ps: datadogでこういうの通知してくれると最高なんですけどね

プロゲーマーのマネージメントとチームワーク

本記事は、FOLIOアドベントカレンダーの11日目の記事になります。

はじめに

ゲーマー時代に培ったマネージメントやチームワークは仕事にも当てはまることが多いと感じます。

私は様々なプレイヤーを指導したりリーダとして率いることが多かったので、ゲーマー時代に自分がどう行動してたか + それを仕事に照らし合わせて雑多に書いてみたいと思います。

自己紹介

私はCS1.6 (Counter Strike 1.6)の2012年アジアチャンピオンになった者です。

このゲームは私が高2のときくらいからやってました。

いろんなチームを渡り歩き、大半はリーダーとしてチームを指揮したりマネージメントする役目をやってました。

otya-milk.blog.jp

このCLARING STYLEが私本人です。

CS 1.6とは?

f:id:sion_cojp:20181210204710p:plain

現在だとCS:GO(Counter Strike Global Offensive)が後継のバージョンになります。

FPSというジャンルで、5 vs 5の銃で打ち合う戦い。

1:45sの間に、テロリストがC4(プラスチック爆弾)を所定の場所に爆破するか、どちらか壊滅させたら1round終了。

合計16ラウンド先に取ったら勝利です。

この1:45sという時間が恐ろしく短く、めまぐるしく考えを働かさないと一瞬で負けてしまうのがこのゲームの楽しさです。

本題

メンバーを適したポジションに配置する

f:id:sion_cojp:20181210215555p:plain:w220

スナイパー、仲間をフォローする、最後まで生き残るc4を運んで設置する....など様々なポジションがありました。

まずはチームメンバーにやりたいポジションを聞き、できる限り反映させます。

理由は、やりたいポジションを任せた方が責任感が生まれるからです。

その上で、個人の特徴を生かして「こういう役割、動き方をして欲しい」と伝えたり、ポジショニングの指導をします。

仕事に対してもやりたいことをさせたほうが責任感が生まれます。

その上で「こういうことをして欲しい」と伝えると本人もやる気になってくれるでしょう。

最高は「言わなくてもそのポジションを理解して勝手に行動してくれる」です。

報告はできる限りシンプルかつ明確に

f:id:sion_cojp:20181210220848p:plain:w220

よくある初心者の場合だと、「撃たれてる!いてて!なんか遠くで撃って来てる」とかですかね。

この報告を聞いて味方はどうすればいいか分からないですよね。

例えば、「階段に2人いた」という報告は重要なのか?という議論をよくしました。

結論は、「重要ではない、があると嬉しいかも」です。

敵がここから攻めてくるか引くのか分からない報告だからです。

ベストは「1人カバーに来て欲しい」。これなら敵がどうしようが、仲間がどうして欲しいかわかるので行動しやすいです。

このように指示と行動がシンプルかつ明確になってるほど、お互いが連携しやすいです。

これは仕事や特に障害対応、ドキュメント(手順書)で言えることですね。


ちなみに、最初の問いですが、さらにベストの「何も言わなくても100%伝わる」を私たちは目指してました。

報告に感情は不要

f:id:sion_cojp:20181210220210p:plain:w220

あるあるだと「くっそ!あいつむかつくわ!うぜー!」とかですかね。

この報告を聞いて味方は....ry

「報告に感情は不要だ」と私はよく指導してました。

怒らない

f:id:sion_cojp:20181210215839p:plain:w220

怒っても何も解決はしません。

私自身若い時はよく怒ってました。が、この結論に至りました。

(精神を保つために、「自分が5人倒さなかったから負けた」と言い聞かせたりしてました)

よくあるよねーってスルーしたり、「なぜそうなったのか?」「そういう行動をしてしまったか?」というのを議論しあい解決に導くのが最善です。

「なんで言ったのに理解できないの?」 -> 難しいよね〜、また教えるね

「なんで何回も同じ場所でミスるの?」 -> 何か原因ある?違う方法だとミスらない?

といった感じ。

あえて失敗させる

f:id:sion_cojp:20181210220017p:plain:w220

上の議論しあってるときに、意見がまとまらない場合があります。

例えばあるポイントに投げるグレネードが絶対必要だ、必要じゃないなど。

その場合は全部やるのが筋です。そこで失敗を経験してなぜ失敗したのかを理解します。

そうしないと実践で応用ができないからです。

またあえて失敗させる誘導をすることもあります。

そうしないと理解ができないからです。

(今の子たちはペヤングを一度もこぼすことはないでしょうし、その事実を知らないでしょう)

失敗は大事な糧なので、出来る限り失敗出来る時にいっぱい失敗させるチームにしてました。

讃え合う

f:id:sion_cojp:20181210215924p:plain:w220

先ほど報告に感情は...と言いましたが、1killとった時の「ナイス」などの賞賛は大事です。

理由はその場の雰囲気、士気がよくなるからですね。人間味ありますね。

slackやPRでいう :thumbsup: だと思ってます。

ルールはシンプルに最小に

f:id:sion_cojp:20181210223534p:plain:w220

日本人はガチガチの作戦で固めてくるチームが多かったのですが、それが4人以上限定とかグレネードの個数限定だったりしました。

敵の奇襲で3 vs 3とかになってしまうと、誰も次の行動を考えれなくなって時間切れになったりします。

私たちのチームは、作戦を設けず、自分たちで考えて行動させました。

その方が敵のアクションに柔軟に対応できます。

その場で、「こうしてここを攻めよう」と誰かが作戦を発言して行動を起こします。

またそこでのルールとして、「指揮官のいうことが絶対。それ以外は早めに言ったやつを優先」というルールでした。

「で、誰のを従うの?」っていうコミュニケーションコストの発生を避けたかったからです。


状況は一番メンバーがわかってます。仕事もそうですが、ルールを最小限にし、メンバーに権限を委ねた方が、スピーディーにスムーズに事が進みます。

自分たちで考えさせる

f:id:sion_cojp:20181210220059p:plain:w220

最高は「言わなくてもそのポジションを理解して勝手に行動してくれる」です。

少数精鋭の場合、マネージメントなんてない方が最高です。

彼ら自身が言葉を交わし合い、共通の目標に向かって行く方がベストです。


仕事でも少数精鋭の場合はマネージャーがいかにセルフマネージメント出来るようになるか、ポイントを指摘していくのが良いかと思います。

全体的に見渡すレベルを目指す

f:id:sion_cojp:20181210221044p:plain:w220

まずメンバーより下の実力だとダメです。「おれより弱いやつが」と言われたら終わりです。

(それ以上の何かしら尊敬の念があれば問題はない)

仕事でいうと、エンジニアなのに勉強することをやめたり、プログラム書く事を諦めてる人は部下に舐められる可能性が高いです。


次に、メンバーと同じ目線だと、いつまでも実力が水平です。

これではさらなる高みは目指せません。


私は全体を見回して、このチームがどうなったら強くなるかを考えてました。

例えばBサイドが抜かれやすかったらBサイドの研究をします。

それはBサイドのメンバーも研究してると思いますが、彼らは「連携や練習」という私より余分な時間が発生します。

その間に海外の動きをチェックし、良いものがあれば「このチームこういう動きをしてたから取り入れてもいいかもよ」と伝えることができます。

あとはプロフェッショナルな彼らに任せればいいです。そして存分に高みを目指してもらいましょう。

信頼する、言い過ぎない

f:id:sion_cojp:20181210221535p:plain:w220

例えば、「Flash Bomb欲しい」と言って、くれなかったとき。仲間は敵と交戦してるかもしれないです。

状況は仲間にしかわかりません。

Flash Bomb欲しい」を5回くらい連呼されたら相手も「わかってるようっせーな!交戦してんだよ!」ってなるでしょう。

フラストレーション貯めるのは色々と支障が出ます。

出来る限りフラストレーションためないよう言い過ぎないように、相手を信頼して待ちましょう。

待つのは不安になったり、精神辛かったりしますが、我慢ステータスを身につけましょう。

待った上でチームが議論し、それでもうまくいかない場合はリーダーが一言指摘しましょう。

時間を決めて練習する

f:id:sion_cojp:20181210231825p:plain:w220

時間を決めないとダラダラとやります。また長時間やらないように気をつけます。

事前に伸ばしたいポイントを決めておいて、短い時間で、サクッと練習する方が身になります。

仕事でいう、残業はもちろん、短時間で集中したほうがいいよね。というお話です。

最後に

リーダーはいいチームを作るための一つです。

もちろんメンバーもいてこそです。

お互いが尊敬しあい同じ目標に行くのはゲームの世界も仕事の世界も同じだなと思います。