CloudWatchEvents + Lambdaを使ったfluentdインスタンスのローリングアップデート

<div class="alert alert-success text-center">
この記事は、<a href="http://www.adventar.org/calendars/1617">Recruit Engineers Advent Calendar 2016</a>の16日目の記事です。
</div>

## はじめに

[リクルートライフスタイル Advent Calendar 2016](http://qiita.com/advent-calendar/2016/recruitlifestyle)の2日目の記事では[ELB経由のfluentd Aggregatorを安定化するためにやった3つの対策](http://qiita.com/mia_0032/items/241e611ef2180ad444ff)について書きました。

前記事で書いたようにAggregatorクラスタの数が増えて手動で行っていたデプロイが大変になってきました。また開発メンバーが増えたときに、デプロイ方法を共有してミスなく行えるようにするのも大変です。
そこでfluentd Aggregatorのローリングアップデートを自動化したくなり、CloudWatchEvents + Lambda + SQSで仕組みを作りました。

## fluentdのデプロイ時に気をつけること

fluentdはローカル上にバッファを持っているため、それを出力し終える前にインスタンスを落としてしまうと、ログのロストが発生します。
それを防ぐためにはバッファがなくなるまで待って、その後にインスタンスを落とすことが必要になります。
それを実現するために今回の仕組みを構築しました。

## 作った仕組み

![20161216_fluentd_cluster_deploy.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/59581/cf4aa91d-402e-8ec7-e131-a74710f8e685.jpeg)

図の通り5つのステップから構成されています。

1. AMIを作成する
2. オートスケーリンググループの起動設定を更新する
3. オートスケーリンググループの更新をトリガーにして、古いインスタンスをSQSに積む
4. SQSを定期的にポーリングして、オートスケーリンググループからインスタンスをデタッチする
5. デタッチしたインスタンスをSQSに積み、定期的にポーリングし、バッファがなくなったら削除する

各ステップについて順番に解説します。

### 1. AMIの作成

![20161216_fluentd_cluster_deploy_002.png](https://qiita-image-store.s3.amazonaws.com/0/59581/15979081-4ccc-f0bb-5306-970cdd418c1b.png)

gh:eでリリースタグを作成するとCI/CDサーバー(OSS版Drone)から、Packer + AnsibleでAMIが作成されます。特に特別なことはしていません。

### 2. 起動設定更新

![20161216_fluentd_cluster_deploy_003.png](https://qiita-image-store.s3.amazonaws.com/0/59581/c18a3a78-2846-395d-95ae-f51a2c9e7a52.png)

新しいAMIを指定した起動設定を作成します。
起動設定の作成およびオートスケーリンググループへの適用はAnsibleを使っています。
Terraformだと過去の起動設定を消してしまいますが、Ansibleではそういったことがなく、切り戻ししやすいため、ここではAnsibleを使っています[^1]。

ここからのフローをDrone上で行うこともできなくはないものの、タスクが長くなってしまうと他のビルドタスクがスタックしてしまうため、ここから先はLambdaなどに任せます。

[^1]: 他のインフラ部分は基本的にTerraformで管理しています。

### 3. デタッチ対象をキューに積む

![20161216_fluentd_cluster_deploy_004.png](https://qiita-image-store.s3.amazonaws.com/0/59581/4454fe52-2cf8-7944-f402-c8d0858401e4.png)

オートスケーリンググループの起動設定を更新するイベントをCloudWatchEventsでフックし、Lambdaを実行します。
Lambdaでオートスケーリンググループに含まれるインスタンスIDとオートスケーリンググループ名を、各インスタンスごとにSQSへ積みます。

### 4. キューに積まれたインスタンスを1つずつデタッチ

![20161216_fluentd_cluster_deploy_005.png](https://qiita-image-store.s3.amazonaws.com/0/59581/20d145a1-4c22-7f11-282e-a3b602204b89.png)

キューに積まれたインスタンスを1つずつオートスケーリンググループからデタッチしていきます。
このときにオートスケーリンググループ内のデタッチ中のインスタンスが一定以上にならないようにしています。
もしデタッチ中のインスタンスが一定数以上ある場合はスキップして次の実行まで待ちます。

デタッチイベントを更にCloudWatchEventsでフックしキューに積みます。

### 5. バッファを出力し終えたインスタンスを削除

![20161216_fluentd_cluster_deploy_006.png](https://qiita-image-store.s3.amazonaws.com/0/59581/d78175da-0dca-bfb6-ab11-899676dc0e65.png)

デタッチしたインスタンスの一覧が入っているキューをポーリングし、バッファーを出力し終えたfluentdのインスタンスを削除します。
将来的にはバッファーのチェックと強制flush処理も入れようと思っていますが、今は一定時間経過したインスタンスを削除するという方法でやっています。

## 障害時の切り戻し

オートスケーリンググループの起動設定を戻すだけです。
正常に動作していたときの起動設定およびAMIは残っているので問題なく、また起動設定の更新のイベントは同じように発生するため、残りのフローは同じように実行されます。

デプロイスクリプトでエラーが出た場合はキューが削除されないため、修正したバージョンをデプロイすることで解決できます。

## おわりに

ピタゴラスイッチっぽくデプロイ・後片付けまでやってくれる仕組みを作ることができました。
うまくCloudWatchEventsとSQSを使うことで、並列化やエラー時のリトライなどの処理を簡潔にすることができます。

この仕組みも最初から全部一気に作ったわけではなく、1, 2を先にやり、次に5、最後に3, 4の部分を作成しました。
やることを切り分けることで、順次、必要なところから自動化できたのは良かったと思います。

この仕組みはAggregator以外にもfluentdを使っているインスタンスであれば、応用できる構成になっています。
ただ思っていた以上に複雑な仕組みになってしまったので、もう少しシンプルな形にできないか、今後試してみようと思います。