docker-composeで開発環境構築、一部コンテナをECS Fargate起動タイプで定期実行するまで



最近社内向けwebアプリの開発にdocker-composeを使ったんですが、一部のコンテナだけをAWS ECSにデプロイするような構成にしたので、その辺をざっくりメモしておきます。
今回、抽象化したsampleアプリを作成してgithubにあげておきました。
※sampleなので中身はスタブ


[sampleアプリ](https://github.com/kentaro-a/docker-compose-sample)

### sampleアプリの構成
VueとGoを使ったWebアプリ。WebのUIにて登録されたデータをバッチで処理していく感じのアプリ。
- frontend: Vue.js
- backend: Go
- batch: Go



# 環境
- OSX 10.11.6
- [Docker Toolbox](https://docs.dr.com/toolbox/toolbox_install_mac/)
- [aws cli](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/installing.html)


# docker-composeで開発環境を構築してみる
docker composeは複数のdockerコンテナをまとめて管理できるツールで、port forwardingやコンテナ間通信をサービス名で名前解決、volumeの設定などdocker-compose.yml１つで管理できるのがいいところです。



### まずはdocker machine作成
docker machineは複数のDockerホストを構築・管理するためのツールで、docker machine上でコンテナを起動します。プロジェクトごとにmachineを分ける感じで使っています。

以下でsampleアプリ用のホスト環境を作成
`$ docker-machine create -d virtualbox docker-compose-sample`
`docker-machine ls`で確認すると**docker-compose-sample**という名前のホスト環境(machine)が作成されます。
  
  
`$ eval $(docker-machine env docker-compose-sample)`
docker-compose-sampleをアクティブにする。以後、docker-compose-sampleホスト環境上でコンテナを作れるようになります。


`$ docker-machine ip docker-compose-sample`
docker-compose-sampleのIPを取得。今回は192.168.99.101でした。
コンテナに対してこのIP + portでアクセスできます。
  
  
  
  
### docker-compose.yml作成
必要なコンテナ群の作成方法をまとめた設定ファイル。docker-composeコマンドで特別ファイル名を指定しなければ、カレントディレクトリ配下のdocker-compose.ymlを参照するようになっています。重要なラベルは以下の通りです。
[docker-compose.yml version3](https://docs.docker.com/compose/compose-file/)
  

#### version
docker-compose.ymlのバージョン。バージョンによって書き方が若干変わります。

#### services
コンテナとして起動される。sampleではbackend、batch、frontendの３つがあり、
これらをサービス名としてそれぞれコンテナになります。

### build
コンテナをビルドするためのコンテキストを指定する。
sampleアプリのように直接ディレクトリを指定すると、その直下のDockerfileをもとにコンテナを作成する。


#### ports
ホスト:コンテナのように指定。
例えばsampleアプリでは、ブラウザで[http://192.168.99.101:20002](http://192.168.99.101:20002)にアクセスすると、backendからレスポンスが返ってきます。

#### links
サービス名を指定すると、そのコンテナ内においてサービス名で他コンテナにアクセスできます。
sampleではfrontend -> backendへの参照のみ必要だと仮定し、frontendコンテナ内で[http://backend](http://backend)でbackendコンテナのサーバーサイドにアクセスできるようになります。
本来であればmysqlなどのdbコンテナも入れて、backend,batch -> dbといったアクセスも必要だが、sampleなので割愛します。

#### volumes
コンテナ内でソースコードなどのデータを保持するだけだと、コンテナを削除するとデータが消えます。なので普通はvolumeを設定し、そのvolumeをコンテナのあるディレクトリにマウントすることで、データの永続化とホスト-コンテナ間でのデータの共有を実現します。
例えば、mac上にgithubからソースコードをcloneしてきて、そのディレクトリとコンテナの中のディレクトリにマウントすると、mac上でソースをいじればコンテナの中にも反映される -> コンテナの中で最新のソースコードをビルドできる、といった感じです。
sampleアプリでは、frontendだけFONTEND_DOCKER_SYNC_VOLUMEというvolume名でexternal=trueとなっているのは、docker-sync用のvolumeを使うという意味で、docker-sync.ymlに設定を書いています。


### docker-sync
サンプルの場合、vuejsのホットリロードがめちゃくちゃ遅く、原因はjsのビルド等ではなくホストとコンテナの同期の遅さでした。（体感ではvueのソースを編集してからブラウザのホットリロードがかかるまで数分とか、耐えられないレベル。。） 

この遅さを改善するツールが[docker-sync](http://docker-sync.io/)です。 
 
docker-syncでは、docker-sync専用のvolumeを作ってホストのディレクトリを同期、docker-sync専用のvolumeからコンテナに対していくつかの転送方法で同期するという仕組みらしい。

[転送方法とベンチマークについてはこちら](https://github.com/EugenMayer/docker-sync/wiki/4.-Performance)


docker-syncを使ったところ同期がかなり速くなりましたw
（vueのソースを編集してからブラウザのホットリロードがかかるまで5~10秒程度）


#### docker-syncのインストール
[@pocariさんの記事](https://qiita.com/pocari/items/0340049742927f3a94b7)を参考にさせていただきました。



### docker-syncしつつコンテナ起動
`$ docker-sync start`
`docker volume ls`で確認するとFONTEND_DOCKER_SYNC_VOLUMEというボリュームが作られています。
 
 
 docker-compose.yml通りにコンテナを起動します。
`$ docker-compose up -d`
`docker-compose ps`で確認するとbackend, batch frontendという3つのコンテナが立ち上がる


### アプリを動かしてみる
#### backend
backendコンテナに入る
`$ docker-compose exec backend bash`  

goのパッケージマネージャを使ってパッケージを入れる
`$ dep init`

godoでサーバーサイドのアプリを起動
`$ godo server -w`
[http://192.168.99.101:20002/jobs](http://192.168.99.101:20002/jobs)にアクセスするとjsonが返ってくると思います.



#### frontend
frontendコンテナに入る
`$ docker-compose exec frontend ash`  
  

npmでパッケージを入れる
`$ npm install`  

webpack-dev-serverをたててアプリを起動
`$ npm run dev`
[http://192.168.99.101:20003](http://192.168.99.101:20003)にアクセスするとbackendからデータをとってきたfrontendのviewが見れます。　　
　　
frontendはdocker-syncの対象にしているので、例えば`/frontend/vue/src/app/src/components/JobList.vue`などのソースをhost側で修正すると、数秒たってからブラウザのリロードが走って修正が反映されます。(docker-sync + webpack-dev-serverのhot reload)



### batch
batchコンテナに入る
`$ docker-compose exec batch bash`
  

goのパッケージマネージャを使ってパッケージを入れる
`$ dep init`

実行
`$ go run main.go`
Batch executed.の文字列が標準出力される

  
  
  

ここまでがdocker-composeを使った開発環境の構築になります。
続いて、batchコンテナをECSで動かす手順を見ていきましょう。
  


# AWS ECS
AWS ECS(Elastic Container Service)は、DockerHubやAWS ECR(docker imageのAWSリポジトリ的なやつ)にpushしたdocker imageからコンテナ起動・管理するサービスです。ECSには2つの起動タイプがあり、Fargate起動タイプ・EC2起動タイプから選ぶことができます。EC2起動タイプはもともとあって、後発でFargate起動タイプが追加されたという経緯があり、EC2起動タイプではEC2インスタンスをベースにコンテナを起動するが、Fargate起動タイプではEC2インスタンスを必要としません。つまりFargate起動タイプではEC2の管理を必要とせずにサーバーレスでコンテナを実行できるというイメージです。
  
料金はEC2起動タイプではEC2やEBSなどのAWSリソースに対して、Fargate起動タイプはCPUおよびメモリに対して従量課金されます。  

EC2起動タイプはEC2インスタンスにSSHで入っていろいろできますが、***Fargate起動タイプではSSHでログインできません。***また、Fargate起動タイプでは基本的に処理終了後にコンテナがkillされるます。
コンテナを立てるEC2インスタンスに対して色々やりたい場合はEC2起動タイプ、
input/outputがDBやS3など外部サービスに集約できるのであればFargate起動タイプを選ぶといいかも。。


### タスク定義
コンテナやボリューム、およびメモリ、CPUなどを設定する。
タスク定義にしたがってタスク(コンテナ)が生成されて処理が行われる。


### タスク
タスク定義を元に作られたコンテナ


### クラスター
タスクまたはサービスのまとまり。


### サービス
Webサーバー用コンテナなど、アプティブな状態で常駐してほしい場合に、コンテナを１くくりにして死活監視/必要タスク数まで復旧、およびポートマッピングなどを行ってくれるもの。


## 実際に動かしてみる
今回のsampleアプリではbatchコンテナのみをECSで動かしてみる。
batchはDB fetch -> 処理 -> DB updateのようにI/OがDBに完結する処理を想定しており、コンテナは実行後破棄でいいので、ECSのサービスではなくタスクのスケジュール実行を使うことにする。


### ECRにbatchコンテナのimageをpushする
前もってAWSマネジメントコンソール上でECRにsampleアプリ用のリポジトリを作成しておきましょう。

batchコンテナの中でアプリをビルドする(コンテナ内のroot直下にバイナリを出力)
`$ GOOS=linux GOARCH=amd64 go build -o /app`
batchコンテナからexitする  
  

aws cliでECRのログインセッションを取得
`$ $(aws ecr get-login --no-include-email --region ap-northeast-1)`

  
batchコンテナからイメージを作成
`$ docker commit batch docker-compose-sample:batch`
  
ECRにプッシュするためのタグ付け
`$ docker tag docker-compose-sample:batch [リポジトリURI]:batch`
  
ECRにイメージをプッシュ
`$ docker push [リポジトリURI]:batch`

これでECSがECR上のリポジトリからbatchコンテナイメージを取得できるようになりました。


### ECSクラスターを作成する
「クラスターの作成」から 「ネットワーキングのみ　AWS Fargate を使用」を選択し、「次のステップへ」
クラスター名を設定して作成。

![create_cluster_1.png](https://qiita-image-store.s3.amazonaws.com/0/72810/479dc054-c0df-5523-f2dd-23fe13917c08.png)



### タスク定義を作成する
「新しいタスク定義を作成」で「FARGATE」を選択し「次のステップへ」

タスク定義名、タスクメモリ、タスクvCPU等を設定し、「コンテナの追加」をクリック


イメージは先ほどECRにプッシュした際の[リポジトリURI]:batchを指定する。
「エントリポイント」にbatchアプリの実行ファイルパス「/app」を指定する。

![entrypoint.png](https://qiita-image-store.s3.amazonaws.com/0/72810/854a9785-a039-6b0d-abee-9a1dd6b0a3fb.png)


コンテナに対する環境変数が必要であれば、この画面でセットしておく。
このタスクでは、batchコンテナはECRからイメージをpull、コンテナをたてて/appを実行、終わったらkillされる。


### タスクスケジューリングにより定期実行する
先ほど作成したクラスターを選択し「タスクのスケジューリング」タブから「作成」をクリック

![schedule_0.png](https://qiita-image-store.s3.amazonaws.com/0/72810/87959e43-6b45-05a9-3ca5-29746afa9f91.png)



実行間隔や対象となるタスク定義を指定して作成。

![schedule_1.png](https://qiita-image-store.s3.amazonaws.com/0/72810/808013a7-dd62-0ba0-efe5-423af3044c7d.png)




sampleアプリだと、1分間隔でcloudwatchにBatch executed.の文字列が標準出力されたログが出ている。

![cloudwatch.png](https://qiita-image-store.s3.amazonaws.com/0/72810/66c482e1-b619-8137-b4e4-2f26c82ae7e9.png)




# おわりに
docker-composeについて、ほぼほぼ言うことなくかなり快適です。ただ、webpack-dev-server + Vue.jsでhot reload開発する場合、docker-syncないと地獄です。。　　

ECSについて、dockerで開発しておけばECSへのデプロイ・運用はめちゃくちゃ楽だなぁという印象です。今回の記事のきっかけとなった社内向けのwebアプリの中でECSに乗せたのはbatch部分だけでしたが、デプロイの楽さとデプロイフローの統一という意味では全部ECSにしてもよかったと思いました。




[弊社「仏のCTO」はこちら](https://qiita.com/jakkrokk)