Dockerコンテナで作ったアプリをECS+RDSでデプロイする

# この記事について
コンテナを使ったアプリをデプロイしたい！となると、1つ選択肢に上がるのがAWSのECSではないでしょうか。
この記事では、ごくごく一般的な3層アーキテクチャ(Web+AP+DB)のアプリケーションの本番環境を、ECSとRDSを用いていい具合に構築する手順を示します。

## 使用する環境サービス
今回使用(関連)するAWSのサービスは以下の通りです。

- ECS (Amazon Elastic Container Service)
- ECR (Amazon Elastic Container Registry)
- RDS (Amazon Relational Database Service)
- ELB (Elastic Load Balancing)
- VPC (Amazon Virtual Private Cloud)
- AWS Route 53
- AWS Cloud Map

## 前提条件
- Dockerを使用したローカルアプリがすでに開発済みであるとします。
- AWSのアカウントを持っていて、VPCが作成済みであるとします。
- 複数AZにパブリック・プライベートサブネットが作成済みであるとします。

## 読者に要求する前提知識
- Dockerの基本的な知識
- AWSのセキュリティグループがなんなのか・設定の意味がわかること
- ECSで何ができるか(オーケストレーション)を知っていること

# アプリの構造
今回デプロイするアプリは、ローカルでは以下の3つのサービス(コンテナ)で構成されています。
<img width="1000" alt="スクリーンショット 2020-06-02 14.14.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/ff884f3d-8511-56f4-f669-72a7657dff49.png">
それぞれの概要は以下の通りです。

|サービス名|内容|備考|
|:--|:--|:--|
|voting-app|ユーザーに対して投票を促すWebページを表示する。|Golangで実装|
|vote-api|DBへの読み書きをRESTful APIで提供。|Golangで実装|
|db|投票結果を保存するデータベース。|MySQL|

# アプリコードでの準備
アプリというのは、「ローカルで動いているコードをそのまま本番環境に持って来れば動く」というものではないことがほとんどです。
今回の場合、voting-appがvote-apiにアクセスするときのURLと、vote-apiがdbに接続するときの設定が、ローカルと本番環境で違うことが想定されます。

## (例)ローカル運用時のコード
例えば、ローカルで動かしているときは以下のようになっていました。

```docker-compose.yml
...(略)...
services:
    voting-app:
        ...(略)...

    vote-api:
        ...(略)...

    db:
        environment:
            MYSQL_ROOT_USER: root
            MYSQL_ROOT_PASSWORD: pass
            MYSQL_DATABASE: sampledb
```
`docker-compose.yml`でこのように設定すると、vote-apiのホストは`vote-api`に、データベースのURLホストは`db`になります。
また、MySQLのデータベースでは、以下のように初期設定がなされている状態になります。

- ルートユーザー: root
- ルートユーザーパスワード: pass
- 初期データベース: sampledb

そのため、voting-appがapiに接続する部分と、vote-apiがDBに接続する部分のURLは以下のようになります。
<img width="1028" alt="スクリーンショット 2020-06-05 21.10.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/512744c1-75e9-61c2-1263-2d80a7aa3a12.png">

そのため、コードは以下のようになっていました。

```voting-app.go
// vote-apiのURLを設定
apiURL := &url.URL{}
apiURL.Scheme = "http"
apiURL.Host = "vote-api:9090"  // 本番だとHostがvote-apiではなくなる
apiURL.Path = "/"
```

```vote-api.go
// ConnectDB DBと接続してポインタを返す
func ConnectDB() (*sql.DB, error) {
	dbDriver := "mysql"
    // 以下のUser, Pass, Address, Nameが本番環境では変わる
	dbUser := "root"
	dbPass := "pass"
	dbAddress := "db"
	dbName := "sampledb"

	//db, e := sql.Open("mysql", "root:pass@tcp(mysql:3306)/sampledb")
	db, e := sql.Open(dbDriver, dbUser+":"+dbPass+"@tcp("+dbAddress+":3306)/"+dbName)
	return db, e
}
```
しかし、コード内でもコメント注釈を入れた通り、APIのURLとDB設定が変わる以上、このコードが本番環境では動きません。

## (例)本番でも動くコード
コンテナ化したアプリでは、設定を環境変数に格納するのはごく一般的な手法です。
今回、ローカルと本番で参照する設定を変えるためにもこの方法を使います。

例えば、本番環境のvoting-appコンテナには環境変数`API_URL`を設定し、値に本番のAPIのURLを格納しておくとします。
すると、

- 環境変数`API_URL`が設定ずみ → 本番環境と判断。変数の値をHostとして使用する。
- 環境変数`API_URL`が未設定 → ローカル開発環境と判断。Hostの値をvote-apiとする。

というように、いちいちコードを書き換えなくても両方の環境で動くようにすることができます。

```voting-app.go
apiURL := &url.URL{}
apiURL.Scheme = "http"
apiURL.Host = "vote-api:9090"
// ここから追加
if urlENV := os.Getenv("API_URL"); urlENV != "" {
        //環境変数API_URLが設定されているなら、Hostにその値を利用
		apiURL.Host = urlENV + ":9090"
	}
//ここまで追加
apiURL.Path = "/"
```

vote-apiの方でも同様に、環境変数`DB_ENV`で環境を判断し、設定を切り替えるようにします。

```vote-api.go
// ConnectDB DBと接続してポインタを返す
func ConnectDB() (*sql.DB, error) {
	dbDriver := "mysql"
	dbUser := "root"
	dbPass := "pass"
	dbAddress := "mysql"
	dbName := "sampledb"

    //以下追加
	if os.Getenv("DB_ENV") == "production" {
		dbUser = os.Getenv("DB_USER")
		dbPass = os.Getenv("DB_PASS")
		dbAddress = os.Getenv("DB_ADDRESS")
	}
    //ここまで追加

	//db, e := sql.Open("mysql", "root:pass@tcp(mysql:3306)/sampledb")
	db, e := sql.Open(dbDriver, dbUser+":"+dbPass+"@tcp("+dbAddress+":3306)/"+dbName)
	return db, e
}
```

# デプロイ手順
本番環境用のコードに書き換えたところで、ここからは実際にAWSを使ってデプロイを進めていきます。

## 0. 目指す構成
最終的には、以下のような構成になります。
<img width="1017" alt="スクリーンショット 2020-06-04 20.53.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/31f217b0-61b3-fec3-9f22-c98c76d728d9.png">

### ECSで作成するサービス
ECSのサービスは、voting-appコンテナをサービス化したweb-service、vote-apiコンテナをサービス化したapi-serviceの2つを用意します。
この2つのサービスを、同一クラスター内に展開します。

### サービスごとにクラスターを分けるか、同一クラスタにするか
今回、クラスターへのサービス展開方法は以下の2通りが考えられます。

- web-serviceをのせるクラスターと、api-serviceをのせるクラスターに分ける
- 2つのサービスを同一クラスター内にのせる

クラスターをサービスごとに分けると、それぞれのサービスごとにオーケストレーションをすればいいので運用しやすくなります。ただその分多くインスタンスを立てることになります。
逆にお財布に負担をかけたくない・安上がりに済ませたいという場合は、クラスターを分けないことでインスタンスの節約ができます。
参考：[Amazon EC2 Container Service(ECS)の概念整理](https://qiita.com/NewGyu/items/9597ed2eda763bd504d7)

## 1. ECSのクラスター作成
クラスターの実態はEC2インスタンスです。アプリコンテナを起動させ動かすインスタンス群を、ECSではクラスターと呼びます。

ECSで利用するインスタンスには、以下のような環境が必要です。

- Docker
- ECS Container Agent

Dockerアプリを動かすためのインスタンスなので、当然のことながらDockerがインストールされている必要があります。EC2で自分でインスタンスを立てる場合とは違い、ECSクラスターとして立てられたインスタンスにはそのような環境が最初から備わっています。

また、クラスター内でリソース・スケジュール管理を行うために、ECSクラスターに所属するインスタンスの中には皆ECS Container Agentが稼働しています。
ECS Container Agentの実態はDockerコンテナで、実際にECSインスタンスにログインして`docker ps`を実行することで、その存在を確認することができます。

```bash

   __|  __|  __|
   _|  (   \__ \   Amazon Linux 2 (ECS Optimized)
 ____|\___|____/

$ docker ps
CONTAINER ID        IMAGE                            COMMAND             CREATED             STATUS                    PORTS               NAMES
e2f63b1e13ed        amazon/amazon-ecs-agent:latest   "/agent"            41 seconds ago      Up 40 seconds (healthy)                       ecs-agent
```

参考：[Amazon EC2 Container Service(ECS)の概念整理](https://qiita.com/NewGyu/items/9597ed2eda763bd504d7)
参考：[EC2 Container Serviceを使ってみる](https://qiita.com/con_mame/items/1df441d86c703a0e6fa6)

以下、ECSのコンソールからクラスターを作成します。

### クラスターテンプレを選択
<img width="1440" alt="クラスターテンプレ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/997f26ff-8074-98d5-1b5f-619ec5e45cbd.png">
クラスターとして起動するインスタンスの種類を選びます。
今回はLinuxをのせたEC2がいいので、「EC2 Linux+ネットワーキング」を選びます。

### AWS FargateとEC2はどっちがいいの？
AWS Fargateは、コンテナを起動するインスタンス群の管理をユーザー側が気にすることなく、コンテナイメージ構築することができるサービスです。そのため、Fargateを利用する場合は、利用者はコンテナイメージの開発のみに注力することができます。
その性質上、Fargateはコンテナが起動しているクラスターにssh接続することができません。

初めてECSに触れる方で、何かあったときにインスタンスの中に入ってエラーを調べたい！という方はEC2をオススメします。
参考：[AWS Fargateとは？](https://qiita.com/riywo/items/1a5b50028542d9bb06cc)

### クラスター名の設定
<img width="804" alt="クラスタ名.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/277ebfc2-744b-3ad9-4820-4e7ac74e474d.png">
これから作るクラスターの名前です。わかりやすい名前を好きにつけてください。

### インスタンスの設定
<img width="805" alt="instance setting.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/f97d8976-1e00-8b8e-ebcd-ad1a63321cd9.png">
クラスターに属するEC2インスタンスの設定です。項目の設定は以下の通り。

- プロビジョニングモデル：オンデマンドインスタンス(よくある従量課金)
- EC2インスタンスタイプ：t2.micro(お金をかけたくなかったため)
- インスタンス数：クラスターの中にいくつインスタンスを起動するか。(とりあえず1を選択)
- EC2 Ami Id：インスタンスにのせるOS。Amazon Linux2を選択。
- EBSストレージ：デフォルトは22。とりまこのままで。
- キーペア：インスタンスに接続するためのSSHキー。<br>
AWSに登録ずみのキーからご自分で勝手のいいものを選んでください。

### ネットワーク設定
<img width="818" alt="network.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/956d61af-dc47-619a-884f-19d71c08ba8f.png">
クラスターをどのVPC・サブネットで動かすかの設定です。

- VPC：デフォルトVPCを選択
- サブネット：VPCの中にあったパブリックサブネット3つを指定
- セキュリティグループ：新規作成します。インバウンドルールは後からいじれます。とりあえず今はssh用に22番ポートを開けておきましょう。

### その他詳細設定
<img width="828" alt="role.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/32dcdcb0-7db1-2780-ab90-3a0b8f460a67.png">
この部分は手をつけずにおいておきます。

IAMロールの設定も何もしなくてOKです。ecsInstanceRole(ECSクラスターへ参加を行う際に必要となるIAMロール)がない場合は自動で作成されます。
参考：[Amazon ECS 細かい箇所を整理してみた](https://qiita.com/leomaro7/items/6afd607f2df39f714a8e)

青い「作成」ボタンを押すと、クラスターと、クラスターインスタンスが属するセキュリティグループが新規作成されます。

## 2. RDS用のサブネットグループ作成
AWSでは、DBを複数AZにまたがって配置するようにしないといけません。
そのため、「このサブネットの中のどこかにDBインスタンスを作る」という複数AZサブネットのグループを作る必要があります。

### グループの設定
<img width="787" alt="スクリーンショット 2020-05-31 15.26.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/ed3f798f-5ac8-aee4-72bf-bde62de93b76.png">
名前と説明は、わかりやすいようにつけておけばOKです。
どのVPC上のサブネットに対するグループなのかもここで設定します。今回は、実際にサービスをのせるVPCを選択します。

### グループにサブネットを追加
<img width="795" alt="スクリーンショット 2020-05-31 15.26.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/667d9548-5b2e-0662-c06e-f1e393d9c0ee.png">
選んだVPC上のサブネットから、グループに追加するものを選びます。
今回はDBのパブリックアクセスを許可しないようにしたいので、プライベートサブネットを複数AZになるように追加します。

## 3. RDSインスタンス作成
RDSのコンソールから、DBインスタンスを作成します。

### DBの選択
<img width="823" alt="スクリーンショット 2020-05-31 15.18.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/1d54b318-ef46-8084-9e87-ac62a3d032bc.png">
MySQL5.7のDBを選択します。

### 認証情報の設定
<img width="816" alt="スクリーンショット 2020-05-31 15.18.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/0522b06d-12ae-ea6a-9f9d-bcef7fc864b9.png">
DBインスタンス識別子は、インスタンスにつく名前だと思ってください。これも好きな名前をつけてOKです。

認証情報は、MySQLに最初から設定されているルートユーザーとそのパスワードの設定です。
今回はパスワードを自分で設定したいので、自動生成にはチェックをつけないでおきます。
ここで設定した認証情報は本来自分にしかわからないようにしておくべきです。本記事では、今後の説明をわかりやすくするために以下のようにおいたと仮定して進めます。

- マスターユーザー名: admin
- マスターパスワード: password

### VPCの選択
<img width="779" alt="スクリーンショット 2020-05-31 15.21.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/60501f07-d3f4-148c-a115-4cfcbe7a5398.png">
DBインスタンスをおくVPCを指定します。今回サービスを展開するVPCを指定すればいいでしょう。

### 追加のネットワーク設定
<img width="800" alt="スクリーンショット 2020-05-31 15.32.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/921e7f17-7a75-2bfc-f4d4-c7f4c36efd5a.png">
VPCよりも詳細なネットワークの設定です。ここでは以下のようにしました。

- サブネットグループ: さっき作ったもの
- パブリックアクセス可能: なし
- VPCセキュリティグループ: 新規作成
- アベイラビリティーゾーン: 特定のAZを指定して動かしたいわけではないので、指定なし
- データベースポート: MySQLのデフォルト3306番を使用

### 詳細設定
<img width="921" alt="スクリーンショット 2020-05-31 19.02.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/4ec75f64-e1d6-f035-b5e9-0ef8996fe6db.png">
最初のデータベース名のところに、作りたいDBの名前を指定してください。この設定がないと、起動時に初期データベースが作られません。

パラメータグループとオプショングループはデフォルトのままにします。

以上の設定を行った上で、DBを作成します。
参考：[Amazon RDS for MySQLインスタンス作成手順](https://qiita.com/na0AaooQ/items/7c69a88c80f1efb4cad3#3-amazon-rds-for-mysqlインスタンス用のdbサブネットグループを作成します)

### エンドポイントの確認
作成したDBインスタンスのエンドポイントを確認しておきます(後で使うので)。
コンソールの以下の箇所で確認できます。
<img width="1435" alt="スクリーンショット 2020-06-01 17.38.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/078433d7-be2e-b08b-e27e-faa10bbe3abc.png">

## 4. セキュリティグループのインバウンド設定
ここまでで、2つのセキュリティグループを新規作成しました。1つはECSクラスター用、1つはRDSインスタンス用です。
今回はECSクラスター内のサービス(=vote_api)から、RDSに接続する必要があります。
また、ECSクラスター内のインスタンスに、各種サービスにアクセスするためのポートも開けなくてはいけません(ここではvoting-appが8080番、vote_apiが9090番とします)。

そのため、セキュリティグループのインバウンド設定を以下のようにします。

- ECSクラスターのセキュリティグループ
 - in22 from all(ssh接続用)
 - in8080 from all(voting-app用)
 - in9090 from all(vote_api用)
- RDSのセキュリティグループ
 - in3306 from ECSクラスターグループを設定

### (おまけ)クラスタのEC2インスタンスから接続してみる
きちんとECSクラスター内からRDSインスタンスに接続できるかどうか、実際にインスタンスの中に入って確認してみます。

ローカルマシンでターミナルを開いて、以下のコマンドを打つことでECSインスタンスに接続できます。

```bash
$ ssh -i "your-key-name.pem" ec2-user@your-instance-public-dns

   __|  __|  __|
   _|  (   \__ \   Amazon Linux 2 (ECS Optimized)
 ____|\___|____/

```
ログインしたサーバー内で以下を実行します。

```bash
$ sudo yum update
$ sudo yum install mysql
$ mysql -h your-db-endpoint -u your-root-username -p
Enter password: 
MySQL [(none)]> 
```
きちんと接続できました。
試しに、先ほど設定した初期データベースがあるかどうかも確認してみましょう。

```bash
MySQL [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| innodb             |
| mysql              |
| performance_schema |
| first_db           |
| sys                |
+--------------------+
6 rows in set (0.00 sec)
```
先ほど設定した通りの初期データベースが作成されていました。

MySQL, ECSサーバーからログアウトするためには、以下のコマンドを打ちます。

```
MySQL [(none)]> exit
Bye
$ exit
ログアウト
```
参考：[AWSのEC2からRDSに接続してmySQLを操作。EC2踏み台サーバー構築メモ](https://photo-tea.com/p/aws-ec2-to-rds-connection/#ec2の踏み台サーバーに入る)

## 5. ECRに登録
アプリのコンテナイメージをECSで使えるようにするために、レジストリにイメージをpushしなくてはいけません。
ECRはAWSで提供されているコンテナイメージのレジストリです。

### リポジトリの作成
ECRのコンソールを開くと、以下のような画面になります。
<img width="1146" alt="スクリーンショット 2020-05-09 19.13.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/60e5eb6a-f334-6876-d7a6-6a015ec08a8a.png">
リポジトリの作成をクリックします。

<img width="861" alt="スクリーンショット 2020-05-09 19.14.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/8c1a37ab-9389-0f5f-9d01-3596c391df03.png">
リポジトリの名前をつけます。これもわかりやすいように好きにつけて大丈夫です。
イメージスキャン設定はいじらなくてもOKです。

<img width="1160" alt="スクリーンショット 2020-05-30 19.19.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/c32a6990-517d-a0fd-5abe-cbc64bf7b319.png">
無事に作成成功。ここにローカルのvoteコンテナイメージを入れる。後1個作っておく。

### コンテナイメージをpush
リポジトリ名をクリックすると以下のような詳細画面が表示されます。
<img width="1157" alt="68747470733png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/7cad6e2c-bd52-d9fd-b143-c0205cb6b06c.png">
画面右上にある「プッシュコマンドを表示」ボタンで、実行するコマンドが表示されます。
以下、そのコマンドを1つ1つ追っていきます。

### ECRのdockerレジストリにログインする。
以下のコマンドで、ECRレジストリにログインするためのパスワードが取得できます。

```bash
$ aws ecr get-login-password --region ap-northeast-1
```
なので、このパスワードを使って、ログインします。(このコマンドはプッシュコマンドのところに書いてあるものです)

```bash
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [your-aws-accountid].dkr.ecr.ap-northeast-1.amazonaws.com
```

### イメージにタグを追加
ECRにpushするためには、pushするdockerイメージに指定形式の名前空間・タグをつける必要があります。
dockerイメージの名前・タグ付けを変更するためのコマンドは以下の通りです。

```bash
$ docker tag [旧repository名]:[旧tag名] [新repository名]:[新tag名]
```
注：これを実行した後も、[旧repository名]:[旧tag名] は残っています。

なので、ローカルマシン上で、pushしたいイメージに以下のようにtag付けします。
(your-ecr-repository-uriはプッシュコマンド&リポジトリの詳細情報ページにも記載してあります)
ローカルマシンで名前・タグ付けを変更するためのコマンドを実行します。

```bash
$ docker tag 【現イメージ名】:latest [your-ecr-repository-uri]:latest
```

### 実際にpush
一般的に、レジストリにイメージをpushするコマンドは`docker push [image名]:tag`です。
今回の場合は以下のように書きます。

```bash
$ docker push [your-ecr-repository-uri]:latest
```

### ECRのdockerレジストリからログアウト
一連の操作が終わったら、ECRのアカウントからログアウトしておきましょう。

```bash
$ docker logout [your-aws-account-id].dkr.ecr.ap-northeast-1.amazonaws.com
```
参考：[【AWS】初めてのECR](https://qiita.com/3utama/items/b19e2239edb6996a735f)

## 6. vote-apiのタスク定義
タスクとは、ビジネスロジックを実行するためのコンテナの組(複数個もあり)のことです。
例えば、今回の場合は以下のように当てはめることができます。

- voting-app → Webサーバーとしての役割を果たすタスク
- vote-api → APIのタスク
- (db → RDSのインスタンス)

タスク定義とは、そのタスクを実行するためには、どのコンテナイメージをどんな設定で動かすかを設定することです。
設定できる内容は多岐に渡ります。全容を知りたい方は以下の公式ドキュメントや次の記事を参考にしてください。
参考：[公式ドキュメント タスク定義パラメータ](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html)
参考：[Amazon ECS 細かい箇所を整理してみた](https://qiita.com/leomaro7/items/6afd607f2df39f714a8e)

今回はまず、vote-apiのタスクを作成していきます。

### 起動タイプの互換性の選択
<img width="1440" alt="スクリーンショット 2020-05-09 19.00.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/f99ff1cb-7628-10c6-ac31-0f53d9fd7c0d.png">
FargateとEC2、どちらのタイプのクラスターで動かすタスクなのかを設定します。
今回はEC2タイプのクラスターを作成したので、タスクもEC2互換性のあるものを選択します。

### タスクとコンテナの定義の設定
<img width="923" alt="スクリーンショット 2020-06-01 15.43.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/56845f1a-19c6-4744-e103-302fcae665c0.png">

- タスク定義名：わかりやすいように好きに設定してOKです。
- タスクロール：タスク内でIAMロールが必要ならばそれを指定(今回はなし)
- ネットワークモード：awsvpc

ネットワークモードについては、dockerコンテナ(=タスク)をつなぐネットワークの定義です。
通常のdockerネットワークにもあるnone, host, bridgeの他に、ECSではawsvpcという特別なモードがあります。
これは、1タスクごとに1ENIを取り付けるモードです。今回はこれを選択しました。

参考：[ECSでEC2インスタンスを利用する際のネットワークモードについて調べてみた](https://dev.classmethod.jp/articles/ecs-networking-mode/)

### タスクのコンテナ定義
他の設定は飛ばして、タスクで使用するコンテナを指定するところまできました。
<img width="874" alt="スクリーンショット 2020-05-09 20.23.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/9e23ef75-c74e-17ca-00e8-ec7913c4680e.png">
青い「コンテナを追加」ボタンををクリックする。するとタスクの元となるコンテナの詳細設定を行うタブが開きます。

<img width="935" alt="スクリーンショット 2020-06-01 15.45.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/dce6cc84-a4c2-7b66-e104-1f78fcf2073b.png">

- コンテナ名: 好きなようにしてOKです。
- イメージ: ECRのリポジトリURIを指定。このリポジトリのlatestタグのものが使われます。
- メモリ制限: ハード128を指定
- ポートマッピング: 今回は9090(vote-apiがlistenしているポート)

<img width="895" alt="api環境変数設定.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/2127f482-4720-2402-e0b6-846b73982fa1.png">
「環境変数」と書かれているところまで飛ばしました。
ここで、コンテナに渡す環境変数を設定できます。上記で示したコードを使用している今回は、以下のように設定しました。

- DB_ENV: production(本番環境か、開発ローカル環境かの判断に使用)
- DB_USER: さっき設定したRDSのルートユーザー名
- DB_PASSWORD: さっき設定したRDSのルートユーザーパスワード
- DB_ADDRESS: さっき作ったRDSのエンドポイント

ここまで設定ができたら、タスクを作成します。

## 7. vote-apiのサービス作成
ECSでのサービスとは、どのタスクをどのクラスタで何個起動させるかを決めたまとまりのことです。1サービスに対して1タスクが紐ずけられます。
参考：[Amazon EC2 Container Service(ECS)の概念整理](https://qiita.com/NewGyu/items/9597ed2eda763bd504d7)

これから作るサービスを起動させたいクラスターの画面から作成することができます。
<img width="1173" alt="service start.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/c84efb2e-816f-ea93-0147-dc4d7aa73da0.png">

### サービスの設定
<img width="863" alt="6877.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/ee92ca59-de43-219a-9664-9b70c7b490af.png">
サービス名、コンテナの種類や数などを指定します。

- 起動タイプ: EC2互換のタスクを使うのでEC2を選択
- タスク定義: 使用するタスク(今回は前章で作ったタスク名)と、そのバージョンを指定
- クラスター: サービス作成を実行したクラスター名がデフォルトで入力済み
- サービス名: 自分で好きにつけてOK
- サービスタイプ: クラスター内でのタスク配置の種類。今回はREPLICAを選択
- タスクの数: サービスで起動させるタスクの数。今回はとりあえず1にしておく。
- 最小ヘルス数・最大数: デフォルトのまま。

参考：[Amazon ECS 細かい箇所を整理してみた](https://qiita.com/leomaro7/items/6afd607f2df39f714a8e)

### ネットワーク設定
<img width="896" alt="スクリーンショット 2020-06-01 16.24.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/292cff5c-b177-2cb7-0ac7-db89549a4760.png">
タスクのネットワーク設定にawsvpcを選択していた場合、サービス作成時にこの設定をする必要があります。タスクごとに与えられたENIをどのネットワーク・セキュリティグループに属させるかの定義をここですることができます。

- クラスターVPC: デフォルトで入力済み
- サブネット: 今回はVPC内にあるパブリックサブネット3つを選択
- セキュリティグループ: ECSクラスターインスタンスに与えられたグループを指定
- パブリックIPの自動割り当て: DISABLEDを選択

### サービスディスカバリー
今後、voting-appの方でのサービスから、今作っているvote-apiサービスへと接続する必要があります。

ローカル開発時は`docker-compose.yml`の内容で、他コンテナに接続するときの設定を簡単に指定することができました。

ECSの場合は「現在作成しているサービスに接続するための名前を設定、そのAレコードをRoute53に自動登録されるようにする」という設定を行います。この機能がECSサービスディスカバリーです。
サービスディスカバリーを利用することによって、コンテナ間・サービス間通信を楽に行うことができるようになります。

注：サービスが動いているコンテナのIPアドレスを直で指定して接続すればいいじゃん！という力技は、オーケストレーションが行われたときなどでIPアドレスが変わった場合、動かなくなるのでおすすめできません。あくまで名前解決でサービス接続するのが望ましいです。

参考：[ECSのサービスディスカバリーが東京にやってきて、コンテナ間通信の実装が簡単になりました！](https://dev.classmethod.jp/articles/ecs-service-discovery/)
参考：[ECS サービスディスカバリでFargateのIPを自動的にDNSレコードへ紐づける](https://techblog.lclco.com/entry/2018/11/02/120000)

これから、vote-apiのサービスに、自分が決めた名前でアクセスできるように、サービスディスカバリーの設定をしていきます。
サービス作成画面の中に、以下のような「サービスの検出(オプション)」という設定箇所があるかと思います。

<img width="1012" alt="スクリーンショット 2020-05-31 19.53.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/eb9bc2cd-3fce-612b-10bc-3e18d6bb15eb.png">
ここで以下のように設定を行います。

- サービス検出の統合の有効化: onにすることでサービスディスカバリーを利用できる
- 名前空間: 新規作成を選択して、好きな名前をつけます。
- クラスターVPC: 名前空間を展開するVPCネットワーク。サービスを展開するVPCが自動入力済み
- サービスの検出サービスの設定: 新規作成を選択
- サービスの検出名: サービス名が自動入力済み

以上の設定で、`api-service.local`という名前でこのサービスにアクセスできるようになります。

ここまでの設定が済んだら、サービス作成をしましょう。

### 実際に`api-service.local`でサービスにアクセスできるか確認してみる
今作ったvote-apiサービスに、サービスディスカバリーで設定した名前`api-service.local`で接続できるか確かめてみましょう。

ECSクラスターが展開されているVPC内のインスタンスの1つにssh接続して、以下のコマンドを叩いてみましょう。

```
$ dig api-service.local

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.68.rc1.60.amzn1 <<>> api-service.local
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16007
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;api-service.local.		IN	A

;; ANSWER SECTION:
api-service.local.	60	IN	A	172.31.78.255

;; Query time: 4 msec
;; SERVER: 172.31.0.2#53(172.31.0.2)
;; WHEN: Mon Jun  1 16:26:47 2020
;; MSG SIZE  rcvd: 51

$ nslookup api-service.local
Server:		172.31.0.2
Address:	172.31.0.2#53

Non-authoritative answer:
Name:	api-service.local
Address: 172.31.78.255
```
`dig`コマンドでサービスコンテナへのAレコードが、`nslookup`コマンドでもコンテナへのIPアドレスが確認できました。きちんとRoute53の方で名前解決できるように設定されているようです。

参考：[ECS Service Discoveryを試してみる](https://qiita.com/atsumjp/items/7c4c8640629f9eff4827)

実際に、`api-service.local`という名前でAPIを叩いてみました。

```bash
$ curl api-service.local:9090
Hello World
$ curl api-service.local:9090/vote/
[]
```
期待通りの動作が行われました。確かに、vote-apiのサービスには、`api-service.local`というホストが設定されているようです。

### Cloud Map上でも確認
サービスディスカバリーの実態は、Cloud Mapの機能をECSの画面で利用している状態です。
なので、AWS Cloud Mapのコンソールからでも、名前空間`local`とサービス`api-service`が確認できます。
<img width="1388" alt="スクリーンショット 2020-06-01 17.31.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/b5ae4ad5-10e6-1313-bddb-ef4fb0c50a0d.png">
参考：[AWS Cloud Mapを試してみる](https://qiita.com/atsumjp/items/19ee0537ba23483aaa99)

## 8. voting-appのタスク定義
vote-apiのタスク定義と手順は同じです。設定内容だけvoting-appに合わせて変更します。(**変更部分を太文字**にしました)

- 起動タイプの互換性: EC2
- タスク定義名: 自分の好きな名前
- タスクロール: なし
- ネットワークモード: **default**

ネットワークモードをdefaultに変更したことによって、コンテナ定義のポートマッピング部分が「ホストポートとコンテナポートの2つを決める」というように変わっています。
<img width="822" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/66a81e3e-0c00-5698-9576-37b8692cc08e.png">

- コンテナ名: 好きな名前
- イメージ: ECRにpushしたvoting-appのリポジトリ
- メモリ制限: 基本的にハード128
- ポートマッピング: **8080:8080**(voting-appがlistenしているポート)

また、vote-apiのURLを環境変数で取得するようにしているので、以下の設定も行います。

- API_URL: api-service.local

## 9. ロードバランサの作成
voting-appが起動されているサービスに接続するためのロードバランサを作成します。

### ロードバランサの種類の選択
<img width="1415" alt="スクリーンショット 2020-06-03 16.29.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/b3f3be9a-e935-22a8-ab74-a6630d4f3bb2.png">
まず、どの種類のロードバランサを作成するかを選択します。
今回の場合はvoting-appに接続するためのhttpトラフィックを扱いたいので、Application Load Balancerを使います。

### 基本設定
まずは、ロードバランサ自体の設定を行います。

<img width="1440" alt="スクリーンショット 2020-06-03 16.29.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/af12ae06-a2a3-f02d-8dfe-c798fbb79082.png">

- 名前: 自分の好きな名前をつけます
- スキーム: パブリックにアクセスしたいので、インターネット向けを選択
- IPアドレスタイプ: IPv4を選択

<img width="1440" alt="スクリーンショット 2020-06-03 16.29.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/6e20a2a1-552b-dabb-1238-67504e4db855.png">
リスナーでは、ロードバランサが受け付けるトラフィックの種類を指定します。
今回は、インターネットからvoting-appに向けてのhttp通信をロードバランサでさばきたいので、httpプロトコル・80番ポートを指定します。

参考：[Application Load Balancerで設定する4種類のポート番号の意味を理解しよう](https://dev.classmethod.jp/articles/aws-alb-port/)

アベイラベリティーゾーンでは、ロードバランサを設置するネットワークを指定します。
VPCは、ECSクラスターを展開しているVPCを選択し、各AZごとにパブリックなサブネットを指定します。

### セキュリティグループの設定
次に、ロードバランサに付与するセキュリティグループを指定します。

<img width="1421" alt="スクリーンショット 2020-06-03 16.30.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/639acb03-3d4d-1d9b-0378-0afece106aa5.png">
今回の場合は、http(80番ポート)のインバウンドを受け付けるように設定します。
既にそのようなグループを作成済みの場合はそれを、ない場合は新規作成して付与します。

### ルーティングの設定
ルーティング設定では、ロードバランサが受け取ったトラフィックを、どこの何番ポートに転送するかを指定します。
参考：[Application Load Balancerで設定する4種類のポート番号の意味を理解しよう](https://dev.classmethod.jp/articles/aws-alb-port/)

<img width="1421" alt="スクリーンショット 2020-06-03 16.32.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/bdd76bec-299b-0ecb-412d-4fefb461a471.png">

ヘルスチェックには手をつけずに、ターゲットグループを以下のように設定します。

- ターゲットグループ: 新規作成
- 名前: わかりやすい名前をつけます
- ターゲットの種類: 今回は、voting-appがあるECSクラスターインスタンスに転送したいので、インスタンスを選択
- プロトコル: voting-appにはhttpトラフィックを流す
- ポート: voting-appがlistenしている8080番ポートを指定

### ターゲットの登録
新しく作成したターゲットグループに、どのインスタンスを属させるかの設定をここで行うことができます。

<img width="1423" alt="スクリーンショット 2020-06-03 16.32.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/866a372d-321c-33a8-860e-8d965a39129b.png">

インスタンスの欄に、現在稼働中のEC2インスタンス一覧が表示されています。追加したいインスタンスにチェックを入れて、「登録済みに追加」ボタンを押すことで追加ができます。
今回は、この後voting-appのECSサービスを作るところでこの設定を行いたいので、何もせずに飛ばします。
参考：[Dockerコンテナの作成からECSの動的ポート＋ALBでロードバランスするまで【cloudpack大阪ブログ】](https://qiita.com/taishin/items/eb759a8ec0c583fc5ebd)

ここまでの設定が行えたら、ロードバランサを作成します。

## 10. voting-appのサービス作成
基本的には、vote-apiのときと手順は同様です。以下に設定を示します。

- サービスの設定: vote-apiと同様に設定
- ネットワーク設定: タスクのネットワーク設定がawsvpcではないので、なし
- サービスディスカバリー: voting-appにアクセスするようなサービスがないので、不要

vote-apiとは違うところが、voting-appサービスとロードバランサを連携させる設定を行うところです。
<img width="908" alt="スクリーンショット 2020-06-03 16.33.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/c6df8380-9967-2dbb-70f3-70043cf1d5b7.png">

- ロードバランサの種類: 先ほど作ったのがApplication Load Balancerなのでそれにチェック
- サービス用のIAMロールの選択: 自動でecsServiceRoleが選ばれている
- ロードバランサ名: 先ほど作ったものを選択
- コンテナの選択: voting-appのタスク定義時に指定したコンテナを選択

ここまで設定できている状態で、コンテナの選択の部分の「ロードバランサに追加」ボタンをクリックすると、以下のような項目が追加で表示されます。
<img width="864" alt="スクリーンショット 2020-06-03 16.33.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/577d854f-c881-a65b-b2c7-17ec4aed44fd.png">
ターゲットグループ名のところに、先ほど新規作成したグループを指定します。
すると、他の項目が自動で入力されます。

以上の設定を行った上で、サービスを作成します。

# 実際に本番環境のアプリにアクセスしてみる
これでデプロイが完了しました。
<img width="1440" alt="スクリーンショット 2020-06-05 1.10.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/ffcc753e-6dfe-ae5c-9cd4-3731d8e096fe.png">

ブラウザにロードバランサのパブリックDNS名を入力・アクセスすることで、本番環境のアプリを見ることができます。お疲れ様でした。

# 参考文献
ECSの公式解説スライド
参考：[AWS Black Belt Online Seminar 2016 Amazon EC2 Container Service](https://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-online-seminar-2016-amazon-ec2-container-service)
参考：[[AWS Black Belt Online Seminar] Amazon Elastic Container Service (Amazon ECS) 資料及び QA 公開](https://aws.amazon.com/jp/blogs/news/webinar-bb-amazon-ecs-2020/)