AmazonECS / Fargate 本番運用のための構築とデプロイ方法まとめ

<img width="600" alt="Screen Shot 2018-10-21 at 13.55.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/747abdae-24a0-99a7-c62e-d769d156e1af.png">


[Amazon ECS](https://aws.amazon.com/ecs/)（以下ECS）と[Fargate](https://aws.amazon.com/fargate/)を用いて、今までEC2で運用されていたサービスをコンテナ化してECS上で稼働させるプロジェクトをしました。

特に、Fargateは2018年7月3日にTokyoリージョンで使えるようになったばかりなので、情報がまだまだ少なく手探りの状況でした。

> 
Posted On: Jul 3, 2018
AWS Fargate is now available in Asia Pacific (Tokyo) region.

> https://aws.amazon.com/about-aws/whats-new/2018/07/aws-fargate-now-available-in-tokyo-region/

ECS/Fargateで実現したアーキテクチャ・デプロイ方法の全容とその実装方法を記していきます。以下のスライドにもまとめているのでぜひご覧ください。

[内定者バイトが1ヶ月ちょいでDSPをAmazonECSに乗っけた話](https://speakerdeck.com/hiromumasuda/nei-ding-zhe-haitoka1keyue-tiyoitedspwoamazonecsnicheng-tuketahua)

# ECSとは

ECSは、AWSが提供するコンテナオーケストレーションサービスです。

> Amazon Elastic Container Service (ECS)は、Dockerコンテナをサポートする拡張性とパフォーマンスに優れたコンテナオーケストレーションサービスです。これにより、コンテナ化されたアプリケーションをAWSで簡単に実行およびスケールできます。

ECSを理解するには、Task(TaskDefinition)/Service/Clusterという3つのレイヤーについての理解が必須なので、続けて解説をします。

## Task/TaskDefinition
**Taskはコンテナの集合体です。そして、TaskDefinitionはどのようなTaskを生成するか定義しているもの**です。

**TaskDefinitionはdocker-composeのようなもの**と考えるとわかりやすいと思います。taskDefinitionにどのようなイメージを使ってどのようなコンテナを立てるのか、CPU/メモリの割り当てをどうするか、環境変数・ポートの設定などをしておくと、taskという形で定義されたコンテナの集合体を生成できます。

<img width="500" alt="Screen Shot 2018-10-20 at 17.14.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/8bd2a2b4-4132-0a97-8870-8861fcc20706.png">

> [Amazon ECS Task Definitions](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html)

## Service
Serviceの役目は、**指定したtaskDefinitionからtaskを生成する**という作業をいい感じにやることです。taskの希望台数・最大数・最低数、オートスケールのルールを設定することで、CPU/メモリの使用量やタスクのステータスを監視しながら自動でタスクの台数を調節してくれます。

まさに、下の絵のようなイメージです。TaskDefinitionから毎回手動でTaskを生成するわけにもいかないため、Serviceに「このようなルールでTaskを生成して管理してくださいね」と指示してあげることで、コンテナオーケストレーションを実現します。

<img width="500" alt="Screen Shot 2018-10-20 at 17.20.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/4d7667ec-0489-4b09-4956-706617ca249c.png">

> [Amazon ECS Services](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html)

## Cluster

Clusterは**TaskとServiceをグルーピングする概念**です。アプリケーションごと、環境ごとに用意します。例えばHogeというサービスとFugaというサービスがあり、それぞれにstg環境・prd環境があるならば、clusterはHoge-prd/Hoge-stg/Fuga-prd/Fuga-stgの4つが用意されていると良いです。

<img width="500" alt="Screen Shot 2018-10-20 at 17.22.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/1518e255-cfe9-2de1-e369-0e3e4589e348.png">

> [Amazon ECS Clusters](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_clusters.html)

# Fargateとは

ECSで使うことができる、**サーバレスでコンテナが実行できる技術**であり、タスク(コンテナの実行単位)に割り当てたvCPUとメモリの量に応じて秒単位での課金でコンテナを実行できます。Fargateを用いればEC2インスタンスの管理が不要になり、オートスケールに柔軟に対応できます。

最初に述べた通り、今まではECSではコンテナ用のインスタンスとしてEC2しか用いることができませんでしたが、2018年7月よりFargateを選択することが可能になりました。このFargateがコンテナオーケストレーションの柔軟性を高くし、インスタンスの管理コストを大きく下げました。

> [AWS Fargate on Amazon ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html)

# ECSの構築

次に、ECSの構築についてです。全体像とそれぞれの設定方法を記していきます。

## アーキテクチャ全体像
<img width="800" alt="Screen Shot 2018-10-20 at 17.50.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/8f765b9a-a704-2afa-b0b3-9bbdc84e90cd.png">

今回はDSPをECS上に構築しましたが、アドテクに詳しくない方はDSPをアプリケーション（例えばRails）、SSPをページをリクエストするユーザーに置き換えると把握しやすいと思います。

DSPはDatadogとFlumeをデータ転送のために周辺サービスとして持って動いているという設定です。もともとEC2では、同一サーバー上に3つのプロセスが動いていましたが、それぞれが独立したコンテナとして作成され、連携して動いています。その集合体がTaskだと思ってください。

Serviceは指定したTaskDefinitionからTaskを生成しています。上の図では3つのTaskをCPU/メモリ使用量などから必要と判断して動かしています。

## 1. Clusterの構築

SampleAppというアプリケーションのPrd環境を作成したいと思います。まずは、Clusterを作成します。Fargateを指定して、RepositoryNameは`saple_app_prd`にします。

<img width="800" alt="Screen Shot 2018-10-20 at 18.17.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/9cefafeb-6a19-a637-c7e7-6d7832198f35.png">

## 2. ECRにDockerImageをpush

次に、ECRにDockerイメージをpushします。以下のコマンドをDockerfileの置かれているディレクトリで順番に実行します。

```
$(aws ecr get-login --no-include-email --region ap-northeast-1) \
&& docker build -t sample_app . \
&& docker tag sample_app:latest <YOUR ECR URL>/sample_app:latest \
&& docker push <YOUR ECR URL>/sample_app:latest
```

すると、以下のように`latest`というタグがついたイメージがpushされているのが確認できます。

<img width="800" alt="Screen Shot 2018-10-20 at 18.55.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/37f96464-3d11-f60d-c2a8-907a694e8cb2.png">


## 3. TaskDefinitionの構築

次にTaskDefinitionを構築します。まず、ローンチタイプでFargateを指定します。

<img width="800" alt="Screen Shot 2018-10-20 at 18.33.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/f6e3171c-61b5-86e1-1bea-b6f3d2c76ae6.png">

次に、TaskDefinitionの名前、Roleの指定、Taskのサイズ（CPU/メモリの割り当て）、コンテナの定義をします。コンテナの定義では、コンテナ名と参照するイメージのURLを指定します。先ほどECRにpushしたので該当のURLをコピーしてきます。

最初の図のような構成にするためには、DSP/Datadog/Flumeそれぞれのイメージをpushした上で、3つのコンテナの定義を作成します。このGUIで設定した項目はJSONとして管理されます。JSONを直接構築してもいいのですが、GUIで項目ごとに指定するのが一番簡単です。

<img width="800" alt="Screen Shot 2018-10-20 at 19.14.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/64cce8a2-c2be-f847-74fb-23c13a103b94.png">
<img width="800" alt="Screen Shot 2018-10-20 at 19.14.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/3da3f764-ca36-e537-a53b-69c9a452f300.png">

## 4. Serviceの構築と実行

最後に、serviceを作成します。ローンチタイプでFargeteを指定した上で、先ほど作成したTaskDefinition名とそのリビジョンを指定します。その上で、taskの数や最大数・最低数、ロードバランサの定義、オートスケーリングのルールなどを指定していきます。

指定の内容からわかる通り、serviceがTaskDefinitionを元にtaskを作成・管理します。ロードバランサの管理も行うため、serviceを作成しないことには本番運用ができないとても大切な作業です。

<img width="1440" alt="Screen Shot 2018-10-20 at 18.43.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/6c4cdc1f-8cef-2dcd-b319-55817331cc56.png">

serviceの作成が完了すると、設定したルールに従ってtaskが作成されているのが確認できます。statusが`RUNNING`になっていることを確認してください。

<img width="800" alt="Screen Shot 2018-10-20 at 19.24.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/c2284304-e97a-a052-6270-857d8f787dba.png">


# デプロイフローの構築

今までの作業で手動でTaskをデプロイすることが可能になりました。しかし、このままでは今後新しいバージョンのアプリケーションをデプロイしたい場合、イメージをpushしてTaskDefinitionの新しいRevisionを作成して、それを用いるためにServiceをUpdateしてとかなり煩雑な手順を踏む必要があります。

そうならないためにも、デプロイ用のスクリプトを構築してJenkinsを用いて自動でデプロイできるような設計を目指します。

また、今回はさらにカナリアデプロイを実現したため、それも合わせて紹介します。また、デプロイのためのスクリプトでカナリアデプロイを実現しているものがなかったため、自作・公開しています。

> [HiromuMasuda/ecs-deploy](https://github.com/HiromuMasuda/ecs-deploy)

## カナリアデプロイ：1台だけ最新バージョンのTaskをデプロイする

まずは、カナリアデプロイをします。

>
1. 新しいバージョンのイメージをpushする。この時timestampをタグとして登録する。
2. pushしたイメージを参照する新しいリビジョンのTaskDefinitionを作成する。
3. 作成したTaskDefinitionを元に1台だけTaskを生成する。
4. 生成したTaskのPrivateIPをロードバランサのターゲットグループに追加する。


<img width="800" alt="Screen Shot 2018-10-20 at 19.41.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/b2f8ae15-79d5-0ed1-2b23-a023901b5782.png">


この手順を踏むことで、複数台の旧バージョンのTaskが動きながらも1台だけ最新のバージョンのTaskが動いている状態になります。上の図であれば、33%配信ができている状態です。実際に一部だけデプロイをしてみることで、障害に対する被害を最小限にすることができます。

スクリプトは以下の通りです。今回デプロイしたTaskを識別するために、`STARTED_BY`というタグ情報に`canary`と指定しています。後々削除する時に、そのタグがついたTaskを削除すればいいため、処理がシンプルになります。

```rb
# ruby path/to/ecs-deploy.rb deploy --canary
def canary_deploy
  puts "-----> START canary deploy"
  image_name = push_latest_image
  task_definition = update_task_definition(image_name)
  new_task = run_task(task_definition, CANARY_STARTED_BY_TAG)
  task_arn = new_task["tasks"][0]["taskArn"]
  puts "-----> task ARN: #{task_arn}"

  # take several seconds to get IP
  while true
    private_ip = get_task_private_ip(task_arn)
    if !private_ip.nil?
      puts "-----> private IP: #{private_ip}"
      break
    end
    sleep(1)
  end

  add_task_to_target_group(private_ip)
  puts "-----> END canary deploy"
end
```

## カナリアロールバック：先ほどデプロイしたTaskを削除して元の状態に戻す

カナリアデプロイの結果、最新バージョンに問題があることが判明した場合はロールバックを行います。手順は以下の通りです。

>
1. 最新バージョンのTaskを削除
2. ロードバランサのターゲットグループから該当TaskのIPを削除

```rb
# ruby path/to/ecs-deploy.rb rollback --canary
def canary_rollback
  puts "-----> START canary rollback"
  task_definition_arn = remove_canary_task
  deregister_task_definition(task_definition_arn)
  puts "-----> END canary rollback"
end
```

## 全デプロイ：カナリアデプロイによって問題がないことが確認できたので100%配信をする

1台だけデプロイしたTaskに問題がない場合は、全Taskが最新のTaskDefinitionを参照している状態にします。手順は以下の通りです。

>
1. 最新のTaskDefinitionを元にSarviceをUpdateする
2. 1台だけデプロイしたTaskを削除

<img width="800" alt="Screen Shot 2018-10-20 at 19.51.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/d5c7b1e3-8b61-5545-a353-32d2d33a3e08.png">


```rb
# ruby path/to/ecs-deploy.rb deploy
def deploy
  puts "-----> START deploy"
  task_arn = get_task_arn(CANARY_STARTED_BY_TAG)
  task_definition_arn = get_task_definition_arn(task_arn)
  update_service(task_definition_arn)
  remove_canary_task
  puts "-----> END deploy"
end
```

# 実装してみた感想
## デプロイ・ロールバックがとても楽になった
これはFargateの目的そのままですが、サーバーのリソース管理や起動・停止などをブラックボックス化して裏でうまくやってくれるために、本当に何も考えずにサーバーの起動・台数調整などができます。素直に楽だなと感じました。

## Fargateに関する最新情報を取ってくる必要がある

公式ドキュメントの英語版と日本語版の情報量が異なる。下の英語版で書かれている`Docker volumes are only supported when using the EC2 launch type.`という内容が日本語では書かれていません。私はこここで情報不足状態になり数日詰まったので、情報は英語で取ってくるのがベターだと感じました。

<img width="600" alt="Screen Shot 2018-10-21 at 14.12.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/108729/bf7a8564-e6c0-e3c9-5d0e-91389562d015.png">

# 最後に
datadog-agent側にFargateを使うとエラーがでるバグがあったり、デプロイ用のスクリプトがFargate対応が間に合ってなかったりと、まだまだ不便な点も多いです。ただ、とにかく楽になるということと、このように本番環境で使えるようになったよという実例があるので、ぜひどんどん使い倒してもらいたいです。

また、Twitterでは常に技術系・筋トレ系のアウトプットをしているのでぜひフォローしてください！👉 https://twitter.com/hiromu_bdy