Dockerに慣れてきた人がECSで複数コンテナのデプロイをしてみる

## はじめに

弊社LIFULLの２年目研修で、AWSを自由に使ってECSを学ぶ機会があったため、今回ECSについて執筆したいと思います。

本記事ではECSの複数コンテナでのアプリケーションのデプロイについて解説します。

本記事の対象読者

 - Dockerfileが書ける。
 - docker-composeが使える
 - dockerを使ったデプロイ方法がわからない
 - ECSって何だろう？

## 試しに複数コンテナのアプリケーションを考えてみる

まずはdocker-composeを使用した、ローカルでの開発環境を考えてみましょう。
今回は下記の様な構成でローカルの環境を整えてみます。

- フロントエンド
 - Nuxt.js(SSR)
- APIサーバ
 - express
 
##  ディレクトリ構成
```
project
├ backend
├ frontend
├ docker
│  ├ express
│  │  └ Dockerfile
│  └ nuxt
│     └ Dockerfile
├ .env
└ docker-compose.yml
```

## 環境構築

必要最小限の環境を作ります。

### backend

```js:backend/index.js
import express from 'express';
const app = express();
app.get('/', (req, res) => res.send('hello-world'));
```

### frontend

```js:frontend/pages/index.vue
<template>
  <div class="container">
    {{name}}
  </div>
</template>

<script>
export default {
  data() {
      return {
        name: null
      }
    },
  async asyncData({ $axios }) {
    const res = await $axios.$get('hello');
    return {name:res}
  }
}
</script>
```

```js:frontend/nuxt.config.js
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',
  ],
  axios: {
    baseURL: "http://backend:8888/",
    browserBaseURL: "http://localhost:8888/"
  },
```


### docker

```dockerfile:docker/express/Dockerfile
FROM node:14.4.0-alpine3.12
WORKDIR /app
COPY ./backend/package*.json ./
RUN npm ci
EXPOSE 8888
CMD ["npm", "run", "dev"]
```
```dockerfile:docker/nuxt/Dockerfile
FROM node:14.4.0-alpine3.12
WORKDIR /app
ENV NUXT_HOST 0.0.0.0
COPY ./frontend/package*.json ./
RUN npm ci
EXPOSE 3000
CMD ["npm", "run", "dev"]
```

```yml:docker-compose.yml
version: "3"
services:
  front:
    tty: true
    build:
      context: .
      dockerfile: ./docker/nuxt/Dockerfile
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - 3000:3000
  backend:
    tty: true
    build:
      context: .
      dockerfile: ./docker/express/Dockerfile
    volumes:
      - ./backend:/app
      - /app/node_modules
    ports:
      - 8888:8888
```

これで、nuxtを立ち上げるとhello-worldと表示されるだけの簡単なアプリケーションができました。


# ECSでの構成を考える

今回はサービスやタスク、タスク定義の概念をおさらいした上で構成を考えていきたいと思います。

## タスクとは

タスクとはコンテナがまとまって動いている1つの単位です。
フロントタスクに対して、nuxtとnginxのコンテナを
バックエンドタスクに対して、expressのコンテナをといった分け方もできます。

後述するサービスに1対1で紐づいて、サービス内で1つのタスクを並列で稼働させることができます。

## タスク定義とは

前述したタスクの定義を示す物で、どのDockerイメージを使用するのか、どれだけCPUやメモリを使用するのかなどを定義しておく物で

プログラムで説明すると「タスク定義」が「クラス」で「タスク」が「インスタンス」の様な関係性です。

環境変数などもこちらに定義することができます。

## サービスとは

サービスとは複数コンテナの集合体です。1つのタスク内でも複数のコンテナが動作していたりしますが、
サービスではそのタスクをさらに並列に集合として扱うことができます。
1つのサービス内で

実際にサービスを作るときには以下のような設定を行います

 - どのタスク定義を使うのか
 - Fargateで動かすかEC2で動かすか
 - 何個のタスクを同時に起動するか

他にもELBとの紐付けの設定やCodeDeployの設定等もサービスで行うことができます。


## 構成


### サービスの構成

今回だととかなり簡易なアプリケーションとなるので

まずサービスについては下記の二つで問題ないでしょう
・ フロントエンドサービス
・ バックエンドサービス

別々のサービスで分けることでそれぞれのコンテナ群の責務をわけ、例えばバックエンドサービスのみタスク数を増やして冗長化構成をとる様なこともできます。

### タスクの構成

今回はnuxtとexpressの2つしか存在しないので、下記の2つのタスクを定義してタスクを動作させていきたいと思います。

・nuxtタスク
・expressタスク


### 図解

図解してみた図が下記になります。

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273496/3afd9085-1c65-f973-c3b1-3d853d7d0b68.png)

## デプロイ準備

では次にデプロイについて考えてみましょう。

ECSにデプロイする上で準備しないといけないことがいくつかあります。

1.サービス間の通信方法
2.デプロイ用のDockerfileを作成する
3.ECRにデプロイ用のイメージをpushする

まずECSでコンテナを起動させるためには、どこからか起動するコンテナのイメージを持ってこなければいけないので、ECRにDockerのimageを入れて使える様にしてあげる必要があります。
ただ、現状だと開発環境用のイメージになっているため、デプロイ用のDockerfileを作成する必要があります。

また、ローカル環境だと、localhostやdocker-composeのサービス名で通信できていましたが、ECS上ではそのままでは通信ができないので
そこの通信方法も考えてあげる必要があります。


### サービス間の通信方法を決める

現状ECSがサービス間同士で通信する方法は2種類あります。
まず1つ目が、サービス毎にALBを設置してコンテナ同士を通信させる方法です。
2つ目がサービスディスカバリを使用したコンテナ間通信です。

ALBを使うと料金がかさんでしまうので、今回はサービスディスカバリを利用したいと思います。
今回は、`backend.ecs-deploy.com`というエンドポイントでAPIに対して通信を行いたいと思います。

注意：サービスディスカバリを使用する場合、VPCのDNS ホスト名の設定を有効にしてください

※サービスディスカバリを使用するデメリットとしては、ALBが存在しないため、CodeDeployによるBlue/Greenデプロイなどが行えなかったり、細かいALBが提供している機能が使えなかったりもします。

### デプロイ用のDockerfileを作成する
デプロイ用のイメージではソースをマウントする必要はなく、全てのソースがイメージ内に最初からある必要があるため、下記の様にDockerfileをデプロイ用に作成してあげます。

```dockerfile:docker/express/prod/Dockerfile
FROM node:14.4.0-alpine3.12
WORKDIR /app
COPY ./backend/ ./
RUN npm ci
EXPOSE 8888
# 今回はnpm run devも startも同じだが開発環境のみnodemonを使っていることもあるため別のdockerfileを用意
CMD ["npm", "run", "start"]
```

```dockerfile:docker/nuxt/prod/Dockerfile
FROM node:14.4.0-alpine3.12
WORKDIR /app
ENV NUXT_HOST 0.0.0.0
ENV NUXT_PORT=80
COPY ./frontend/ ./
COPY ./frontend/.env.prod ./.env
RUN npm ci
RUN npm run build
CMD ["npm", "run", "start"]
```

LocalでもECSでも問題なく動くよう、axiosのエンドポイントを環境によって変える様に設定します。

```bash:bash
npm i @nuxtjs/proxy
npm i @nuxtjs/dotenv
```

```frontend/.env.local
SSR_API_URL=http://backend:8888/
BROWSER_API_URL=http://localhost:3000/api/
```

```:frontend/.env.prod
SSR_API_URL=http://backend.ecs-deploy.com:8888/
BROWSER_API_URL=http://{elbのURL}/api/
```

```js:frontend/nuxt.config.js
require('dotenv').config()
const {SSR_API_URL, BROWSER_API_URL} = process.env;

export default {
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',
    '@nuxtjs/proxy'
  ],
  proxy: {
    '/api': {
      target: SSR_API_URL,
      pathRewrite: {
        '^/api': '',
      },
    },
  },

  // Axios module configuration (https://go.nuxtjs.dev/config-axios)
  axios: {
    baseURL: SSR_API_URL,
    browserBaseURL: BROWSER_API_URL
  },
}
```

### ECRにイメージをpush

ここまでできたらECRにイメージをpushします。
ECRの説明は今回は割愛します。

```
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ********.dkr.ecr.ap-northeast-1.amazonaws.com

docker build -t ecs-deploy/nuxt -f ./docker/nuxt/prod/Dockerfile .
docker tag ecs-deploy/nuxt:latest ********.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-deploy/nuxt:latest
docker push ********.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-deploy/nuxt:latest

docker build -t ecs-deploy/express -f ./docker/express/prod/Dockerfile .
docker tag ecs-deploy/express:latest ********.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-deploy/express:latest
docker push ********.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-deploy/express:latest
```

## ECSの設定(デプロイ)

ECRの準備ができたので、ECSの設定をします。

### クラスターの作成

今回はFargateを利用したデプロイを行うので「AWS Fargateを使用」と書いてある箇所を選択してクラスターを作成します。![スクリーンショット 2020-12-13 3.03.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273496/30b7a781-9482-f78c-f9c8-692fcebe0c1b.png)


### タスク定義の作成

#### nuxt

1.まずは起動タイプで「Fargate」を選択

![スクリーンショット 2020-12-13 3.04.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273496/2f385da0-c377-5ada-983b-4012b33b2079.png)

2.続いてタスクとコンテナ定義の設定

```
タスク定義名: nuxt(任意の名前)
タスクロール:なし
タスクメモリ:0.5GB
タスクCPU 0.25vCPU

コンテナの追加
コンテナ名:nuxt(任意の名前)
イメージ:(ECRのURIを入力)
メモリ制限: ソフト制限 500
ポートマッピング:80 tcp
```

上記以外の項目は基本的にdefault値で設定していきます。

#### express

1.起動タイプで「Fargate」を選択

2.続いてタスクとコンテナ定義の設定

```
タスク定義名: express(任意の名前)
タスクロール:なし
タスクメモリ:0.5GB
タスクCPU 0.25vCPU

コンテナの追加
コンテナ名:express(任意の名前)
イメージ:(ECRのURIを入力)
メモリ制限: ソフト制限 500
ポートマッピング:8888 tcp
```

これでタスク定義の作成は以上です。

### サービスの作成

#### nuxt

サービスの設定では先ほど作成したnuxtのタスク定義を選択し、任意のサービス名を設定します。
タスクの数を変更することで、複数のタスクを並列で動作させることが可能です。

![スクリーンショット 2020-12-13 3.17.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273496/1cf501a6-66c4-b68a-749b-ec787a7718c3.png)

ロードバランス用のコンテナで「ロードバランサーに追加」を押下し
プロダクションリスナーポートとターゲットグループの作成をします。

![スクリーンショット 2020-12-13 3.25.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273496/f2dd62e5-a1e6-b0e0-db92-c54c41e395f6.png)


#### express

同様にexpressのほうも設定していきます

![スクリーンショット 2020-12-13 3.26.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273496/b58c864b-61ce-57c0-94ed-3a0663f1ca09.png)

expressの方ではALBを選択せず、サービスディスカバリの設定をしていきます。

「サービスの検出の統合の有効化」にチェックを入れ
名前空間に任意のドメイン名を、サービスの検出名にも任意の検出名を入力します。

![スクリーンショット 2020-12-13 3.28.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273496/07835db2-2ed9-b702-d2bb-d181cda48384.png)

上記だと、VPC内からは「backend.ecs-deploy.com」で名前解決ができるようになります。


## 動作確認

以上でサービスを作り終わると、タスクが自動で起動し、デプロイの完了です。
無事hello-worldと表示されればOKです！


## 最後に

こちらに今回のソースを置いておくので参考にしてみてください！！
https://github.com/Lucas-Poppy/ecs-deploy

※axiosのURLの設定等はもしかするともっとスマートな方法があるかもしれません。