ECSのチュートリアル - コンテナ運用を現実のものにする

皆さんこんにちは

以前に[コンテナの何がいいのかを説明する資料](http://qiita.com/niisan-tokyo/items/88a53a1b4aa7ad60723e)を作りました。
「やっぱりコンテナ運用しか無いな」
「コンテナを使わざるを得ない」
「とりあえずdocker入れとけばいいや」
という声が聞こえてきそうです(幻聴)

一方で、コンテナで運用するとなると、新たなる監視体制が必要となります。
例えば、Dockerのコンテナが突然停止した場合、その状況を検知し、新しくコンテナを立ち上げる必要があります。
また、そもそもコンテナが乗っている環境をスケールする必要が出たりして、その場合は新しくスケールされた環境でもコンテナを立ち上げなければならないし、その設定をサーバに書かなければならないけど、そうなると結局コンテナが乗っているサーバがブラックボックス化したりするわけで、なんとかうまく回そうとすると、それなりに気を配ることが多くなってきます。

最近はlaradockを使用する開発者が増えてきた(ような気がする)ので、コンテナ運用で幸せになるためにECSを使ってみた記録をチュートリアル形式で再現しておきます。

え？備忘録？ははは

# ECSとは
ECSはAWSが提供しているコンテナ運用サービスです。
ECSの概念のちょっとしたものは以前に書いた[ECSで困ったときに読むための俺的Q&A](http://qiita.com/niisan-tokyo/items/d53afccaff1bf0914f47)に書いてありますが、今回はECSでたてるWEBサーバに絞って超かんたんなポンチ絵を作ってみました。

![ecs.png](https://qiita-image-store.s3.amazonaws.com/0/102182/42392c8e-31de-07a5-6bda-c3dccbebbd4a.png)


あまりややこしいところはなさそうですが、簡単に見ていきましょう。
## クラスター、コンテナインスタンス
コンテナインスタンスはdockerのコンテナのホストになるインスタンスで、クラスターはこのコンテナインスタンスの群れ( オートスケーリンググループ )です。
コンテナインスタンスにはdockerとコンテナの状態を監視してこちらに伝えてくれるエージェントが入っています。
コンテナ運用時には、個々のインスタンスには興味が無いので、「クラスター」と一括りにしてしまいます。

## タスク、ECR
タスクはアプリケーションのまとまりを一つ以上のコンテナ動作で定義します。
今回の例ではnginxを動かすコンテナとphp-fpmを動かすコンテナのふたつで、webサーバのタスクを定義していて、nginxが外部からのリクエストを受け付け、php-fpmに渡しています。
コンテナを定義するイメージはECRというAWSが提供しているプライベートなdockerイメージのレジストリから取得しています。
## サービス
サービスは3つの仕事をします。
1つ目はタスクが現在必要とされている個数を保っているかを監視し、その数を下回っていた場合、タスクを立ち上げて必要数を保つことです。
2つ目は現在の負荷状況を鑑みて、タスクを増減させます。この設定はCloudWatchのアラーム機能と連携させます。
3つ目はtarget groupと立ち上がったタスクを紐付けることです。

また、タスクの配置を各AZに均等に配置したりと、よく気配りのきくやつです。
## ALB、target group
ALBはロードバランサーです。昔のクラシックなELBと違って、間にtarget groupと言うものを設置して、細かくルーティングできるようになっています。
target groupは実際のサーバへの振り分けを実施し、ヘルスチェックもします。

今回はこれらを利用して簡単なWEBサーバを運用できる状況に持っていきましょう

# コンテナの準備
## とりあえずコンテナを作る
運用したくとも、そのもととなるコンテナイメージがないと何も始まりません。
そこで、例によってLaraDockを利用してlaravelアプリを作ってみましょう。
[プロジェクトの作成はここを見てもらう](http://qiita.com/niisan-tokyo/items/d4485b8bb4fbfcfcc6be)として(ダイマ)、LaraDockとほぼ同じ動作をしてもらうために、Dockerfileをコピーしちゃいましょう。

```
$ cp -r ../laradock/nginx ./
$ cp -r ../laradock/php-fpm ./
```
これでDockerfileと設定ファイルをコピーできます。

今回、コンテナの中は完全に固定化...つまり、スクリプトを内部に含ませてしまうため、各Dockerfileの後部に

```
COPY ./ /var/www/
```
これを追加しておきます
また、nginxのdefaultのコンフィグファイルも必要なので、nginxのDockerfileには

```
COPY nginx/sites/default.conf /etc/nginx/conf.d/
```
も追加しておきます。
なにはともあれ、イメージを作成してみましょう。

```
$ cp nginx/Dockerfile ./ && docker build -t niisan/nginx .
$ cp php-fpm/Dockerfile-70 ./Dockerfile && docker build -t niisan/php-fpm .
```
これでイメージが作られているので、試しにサーバを立ててみましょう。

```
$ docker run -d --name php-fpm niisan/php-fpm
$ docker run -d --link php-fpm:php-fpm -p 80:80  niisan/nginx
$ curl 127.0.0.1
<!DOCTYPE html>
<html lang="en">
    <head>
...
```
うまく動いているようです。

## コンテナをアップロードする
次にECRにコンテナをアップロードするのですが、初めての場合、ちょっとビビります。
けばけばしいタイトルとともに「今すぐ始める」ボタンが登場し、その次のページで妙な選択肢を迫ってきます。

![ECSを始めることを。。。強いられているんだ！](https://qiita-image-store.s3.amazonaws.com/0/102182/30bc66cc-660a-1b0e-8f4b-a3ed5d989ed1.png)

(こういうところ、あまり好きじゃないです)
ここではとりあえず、**「Amazon ECR によりコンテナイメージをセキュアに保存する」**だけを選択しておきます。
すると、リポジトリの作成用のウイザードが迫ってきます。

![ECR](https://qiita-image-store.s3.amazonaws.com/0/102182/22bde4c9-3154-7118-9248-bddbb1774227.png)

とりあえずリポジトリ名にnginxと入れて次のステップへ
今度はECRにイメージをアップロードする手順が出てきますので、この通りにやってみます。
前回の記事でも言及したように、ここだけはAWS CLIを使用する必要がありますので、頑張って入れておきましょう。

```
$ aws ecr get-login
```
これで出てくるログインコマンドをコマンドラインにコピーして実行すればログイン完了です。
既にイメージは作ってあるので、タグを張り替え、アップロードしましょう。

```
$ docker tag niisan/nginx:latest **************.ecr.ap-northeast-1.amazonaws.com/nginx:latest
$ docker push *****************.ecr.ap-northeast-1.amazonaws.com/nginx:latest
```

これで完了です。
手順ページの完了ボタンを押すと、いまアップロードしたイメージのレジストリができていることを確認できます。

そのまま、左のバーにあるリポジトリを選択し、同じようにphp-fpmもアップロードしてみましょう。
1分もかからず終わるでしょう。

![ECR登録済み](https://qiita-image-store.s3.amazonaws.com/0/102182/28ef535f-4f3e-907a-e79f-c0073a406d8d.png)

# デプロイ
## タスクを定義する
コンテナのイメージもアップロードできたことですし、早速タスクを定義していきましょう。
と言っても、今回の場合、内容は異様なほど簡単ですんで、すぐに終わるでしょう。

タスクの名前は適当に決めていいと思います。(今回は webtest という名前にしました。)
早速コンテナを入れていきましょう。まずはphp-fpmから行きます。「コンテナを追加」ボタンを押すと次のモーダルが出てきます。

![タスクを設定](https://qiita-image-store.s3.amazonaws.com/0/102182/1c6ed5f9-6b38-dc59-1667-e67667c655f9.png)

php-fpmの設定はこれだけで十分でしょう。
次にnginxを設定します。基本的にはphp-fpmと同様にイメージと名前を設定するわけですが、二つほど、違う設定があります。

![ポートフォワード](https://qiita-image-store.s3.amazonaws.com/0/102182/4b847f9d-7550-180a-ad5c-7c8aab179660.png)

まずはポートの設定です。
nginxは外部から接続できなければならないので、ホストからポートフォワードするように設定しました。

![内部リンク](https://qiita-image-store.s3.amazonaws.com/0/102182/8b1dbcba-2e15-f9d0-15b5-608cba7e3be2.png)

もう一つはphp-fpmへのリンクです。
これで、タスクの定義は完了しました。

## クラスターを作る
タスクを作ったので、こいつを動かしてみましょう。
こいつを動かすためには、動作環境が必要となりますが、それがクラスターです。
早速クラスターを作っていきましょう。

![クラスターの作成](https://qiita-image-store.s3.amazonaws.com/0/102182/a37b7a6a-0421-85c2-9724-75d6a52660f0.png)

せこいですが、コンテナが動作するインスタンスは一番ちっちゃいやつを使います。
次にネットワーキングの設定ですが、これはデフォルトのvpcを使ってお茶を濁しておきます。

![商用環境では独自のVPC使おう](https://qiita-image-store.s3.amazonaws.com/0/102182/4ac5b6c9-a80c-0001-3c4f-7e1433030f02.png)

では作成しましょう。

![クラスター作成完了](https://qiita-image-store.s3.amazonaws.com/0/102182/6c07d492-dd66-079a-39fc-c7a44d555768.png)

クラスターが出来上がりました。

作ったクラスターを選択して、早速タスクを動かしてみましょう。
クラスター名を選択し、タスクタブからタスクの開始ボタンを押すと、動作するタスクを選択する画面になります。

![タスクの実行](https://qiita-image-store.s3.amazonaws.com/0/102182/229989ad-6152-4528-69b5-41b484383aa8.png)

では動作してみましょう。
しばらくすると、次のような状態になっていると思います。

![動いているっぽい](https://qiita-image-store.s3.amazonaws.com/0/102182/ff35ec28-a8ef-576c-95ce-6b98ad84d41f.png)

どうやら動いているようです。
ECSインスタンスからコンテナのホストになっているインスタンスを参照して、public dnsを調べましょう。
それをブラウザにはっつけると、Laravelのページが表示されます。
![Webアプリのデプロイ完了](https://qiita-image-store.s3.amazonaws.com/0/102182/1f984fcf-a983-9492-b264-9b7b4afb6733.png)
うまくいきました！

# サービスを使う

とりあえずデプロイできて、動作も確認できました。
とはいえ、これを運用するのはまだまだ厳しいところです。

- タスクが死んだ場合の復旧をどうすればいいのか
- コンテナを更新したときのデプロイ方法はどうなるのか
- コンテナのスケーリングはどうするのか

この辺をサービスに解決してもらうとしましょう。

## タスクを停止してみる
とりあえず、タスクを停止します。クラスターのタスクタブで、先ほど動かしたタスクを落とします。
当然ですが、落としたタスクは復活したりしないので、この時点で先に動かしたWEBサーバはダウン状態となります。

## イメージを更新する
これからロードバランサーを使用するに当たり、ヘルスチェック用のルーティングをします。
今回はlaravel5.4を使う(使っちゃってる)ので次のようなroutingを用意します。

```routes/web.php
Route::get('/healthcheck', function () {
    return ['health'=>'ok'];
});
```
この状態で、Dockerのイメージビルドを再実行します。
コンテナを再度動かすと次のような挙動を示します。

```
$ docker run -d --name php-fpm niisan/php-fpm
$ docker run -d --link php-fpm:php-fpm -p 80:80  niisan/nginx
curl 127.0.0.1/healthcheck
{"health":"ok"}
```
これをECRに再アップロードしておきます。

## タスクを更新する
次に、タスクを更新していきます。
タスクはバージョン管理されていて、タスクの更新は正確には新しいリビジョンを作るということになります。

例えば、今動いているリビジョンのタスクを新しいリビジョンのタスクに置き換えることで、サーバを更新したことと同じ状況にできますし、いざ新しいリビジョンが障害を起こすようなものであった場合は古いリビジョンに戻してロールバックすることもできます。

`タスク定義->webtest`でタスク定義のリビジョン一覧が現れます。今は一個しか無いと思うので、それを選択し、新しいリビジョンの作成ボタンを押しましょう。
今回の変更はnginxのポートの部分になります。

![ダイナミックポート](https://qiita-image-store.s3.amazonaws.com/0/102182/4591162a-f5fb-e827-01ef-5b3dfab3bd4e.png)

なんとホストポートが空欄になっていますが、これがダイナミックポートという仕組みになります。
これはコンテナインスタンスで開いているポートを選んで、nginxコンテナの80ポートに繋げる仕組みで、ALB + target groupと組み合わせるときに使用します。

## ALBを用意する
ひとまずコンテナの調整はこの辺にしておいて、ロードバランサーの設定を始めましょう。
まず、サービス -> EC2 -> サイドバーのロードバランサーを選択し、ロードバランサーを作成ボタンを押しましょう。
![ALBの設定](https://qiita-image-store.s3.amazonaws.com/0/102182/40b81d60-5034-b375-f7dd-ba3931ccea48.png)
Application load balancer を選択して、次へを押すとロードバランサーの設定に移ります。
今回はデフォルトのVPCを使うので、名前以外は全部デフォルトで問題なしです。
サブネットも二つあると思うので、両方共チェックしておきましょう。
一応、プロトコルはHTTPにしておきましょう。
セキュリティの警告は、今回は別に秘密通信でもないので、必要ないです。

![セキュリティブループの設定](https://qiita-image-store.s3.amazonaws.com/0/102182/5f28a070-c48a-9346-6b61-58ccbaa5bed5.png)

セキュリティグループはとにかく設定しておきましょう。
覚えやすい名前にしておくと、後で便利かもしれません。

![空のターゲットグループ](https://qiita-image-store.s3.amazonaws.com/0/102182/54e28507-ba91-1d5b-21b5-5cff35782c9e.png)

ターゲットの登録画面になりますが、今はターゲットがないので、そのまま次へで、作成を完了させます。
これで、とりあえずロードバランサーの作成が完了しました。

最後に、ロードバランサーのセキュリティグループからコンテナインスタンスのセキュリティグループへの通信経路を確保しておきましょう。
コンテナインスタンスのセキュリティグループ( EC2ContainerService-*** )のinboundを編集しておきましょう。

![セキュリティグループの設定](https://qiita-image-store.s3.amazonaws.com/0/102182/83188abe-959a-a0d2-4bc1-8defcb76715f.png)

ロードバランサーからのinboundで全てのtcpを選択しておきましょう。
ダイナミックポートを使うと、開いているポートを使うのでどのポートが割り当てられるかわかりません。

## サービスの作成
ここまで来てようやくサービスの作成が初められます。
再びEC2 Container Serviceから先程作成したクラスターを選択し、サービスを選択します。
作成ボタンがあるので、これを押しましょう。
![サービス作成](https://qiita-image-store.s3.amazonaws.com/0/102182/fea72f65-b79f-0c86-204a-70c92e7cbe49.png)
まず、タスクはWebtestを選ぶわけですが、ここではリビジョンを含めて指定することになります。
サービス名は・・・とりあえずタスクのファミリー名と同じにしています。
画面下部に行くとELBを設定するボタンがあるので押してみましょう。
![スクリーンショット 2017-03-18 22.39.45.png](https://qiita-image-store.s3.amazonaws.com/0/102182/221906ef-0d89-c6ce-dbce-76b41a13f4e6.png)
するとこんな画面になるので、さっき作ったALBを選択し、負荷分散用のコンテナがnginxで、ポートが0:80になっていることを確認したら、ELBへの追加ボタンを押しましょう
![スクリーンショット 2017-03-18 22.41.36.png](https://qiita-image-store.s3.amazonaws.com/0/102182/a682d1e6-f0be-75da-e769-dfcd2aaefb85.png)
するとこんな画面になるのでターゲットグループを先程追加したものに変更して保存を押しましょう。

すると、サービスにALBの情報が登録されるので、この状態でサービスを作成しましょう。

![スクリーンショット 2017-03-18 23.41.08.png](https://qiita-image-store.s3.amazonaws.com/0/102182/da16a1e2-76b2-fc00-e739-d941f467555f.png)

クラスターの状態がこのようになれば、成功です。
あとは、ロードバランサーのDNSにアクセスして、
![Webアプリのデプロイ完了](https://qiita-image-store.s3.amazonaws.com/0/102182/1f984fcf-a983-9492-b264-9b7b4afb6733.png)
これが出れば成功です。

# まとめ
実はこれ書き始めたのって随分と前なのですが、途中でコンテナ運用の何がいいのかを説明するための資料作りしていたら、時間が立ってしまったのです。
まあ、ECSって使ってみるとすごく楽で、更にスケーリングしたり、他のサービスと相乗りさせたりマイクロサービス化させたりがすごく簡単にできるのですが、Webアプリを動かすに当たっては、わりとハマりどころが多いので、一貫したチュートリアル形式のものを作ってみました。
特にハマりどころなのは、ALB -> コンテナインスタンスにおけるセキュリティグループの設定だと思います。
考えてみれば当たり前なのですが、まあ、忘れることが多いです。
とはいえ、そんなところを超えてしまえば、後はコンテナで簡単に運用できる環境を量産できるので、皆さん是非とも使ってみてください。

今回はそんなところです。

# 参考
[コンテナに挫折したあなたへ](https://speakerdeck.com/toricls/a-letter-to-uncontainerized-apps) - 超わかりやすい発表資料です！師匠と呼びたい