[ Rails & Nuxt ] AWS ECSを使ってデプロイをする

| No. | タイトル |
|:-:|:-:|
| 1 | [Dockerで開発環境を構築する](https://qiita.com/15recruit15/items/85ab5bbf6b900c004186) |
| 2 | [ログイン認証を機能を実装する](https://qiita.com/15recruit15/items/9d6803ad4aa412848a4f) |
| 3 | 記事投稿機能を実装する |
| 4 | [AWS ECSを使ってデプロイする](https://qiita.com/15recruit15/items/73eea044288bfdbb4fa0) |
| 5 | Circle CIを使って自動テスト•デプロイをする |
---
##はじめに
Rails & Nuxtでポートフォリオを作成するシリーズの第4弾になります。
全5部構成でDocker,CircleCI,AWS等のモダンな技術を組み込んだ作品を完成させる予定です。

本章ではAWSのECSを利用してアプリのデプロイを行います。

以下、完成イメージ
![AWS_インフラ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/b75dde2c-261e-95c7-89c0-50512fb87b3c.png)


##事前学習
↓AWSのネットワークの仕組みについて

https://www.youtube.com/watch?v=aHEVvsk6pkI

↓ECSについて

https://www.youtube.com/watch?v=tmMLLjQrrRA&t=3231s

##VPC作成
最初にリージョンを東京に切り替えておいて下さい。

|name|CIDRブロック|
|:--|:--|
|sample-vpc|10.20.0.0/16|

##サブネット作成
後に作成するロードバランサーではサブネットが2つ必要な為、Nuxt,Rails,RDSでそれぞれ2つずつサブネットを用意します。

|name|CIDRブロック|Region/AZ|
|:--|:--|:--|
|sample-front-subnet-1a|10.20.1.0/24|ap-northeast-1a|
|sample-front-subnet-1c|10.20.2.0/24|ap-northeast-1c|
|sample-back-subnet-1a|10.20.3.0/24|ap-northeast-1a|
|sample-back-subnet-1c|10.20.4.0/24|ap-northeast-1c|
|sample-rds-subnet-1a|10.20.5.0/24|ap-northeast-1a|
|sample-rds-subnet-1c|10.20.6.0/24|ap-northeast-1c|

##IGWの作成
`VPC > インターネットゲートウェイ`より、**sample-igw**という名前でIGW作成。
その後、`[アクション] > [VPCにアタッチ]`で**sample-vpc**に関連づける。

##ルートテーブルの作成
`VPC > ルートテーブル > [ルートテーブルの作成]`より作成画面に移ります。

|name|
|:--|
|sample-front-route|
|sample-back-route|
|sample-rds-route|

##IGW,サブネット,ルートテーブルの関連付け

- `ルートテーブル > アクション > ルートを編集 > ルートを追加`より、frontとbackのルートテーブルに対して、0.0.0.0に先ほど作ったIGWを関連付ける。
- `ルートテーブル > アクション > サブネットの関連付けを編集` より以下の表のように関連付け

|サブネット|ルートテーブル|
|:--|:--|
|sample-front-subnet-1a|sample-front-route|
|sample-front-subnet-1c|sample-front-route|
|sample-back-subnet-1a|sample-back-route|
|sample-back-subnet-1c|sample-back-route|
|sample-rds-subnet-1a|sample-rds-route|
|sample-rds-subnet-1c|sample-rds-route|

##セキュリティグループの作成
|セキュリティグループ名|タイプ|プロトコル|ポート範囲|ソース|
|:--|:--|:--|:--|:--|
|sample-front-sg|HTTP|TCP|80|0.0.0.0/0|
||SSH|TCP|22|My IPアドレス|
||HTTPS|TCP|443|0.0.0.0/0|
||カスタム|TCP|3000|0.0.0.0/0|
|sample-back-sg|カスタム|TCP|8000|0.0.0.0/0|
||HTTPS|TCP|443|0.0.0.0/0|
|sample-rds-sg|MYSQL/Aurora|TCP|3306|0.0.0.0/0|


##Route53

###ドメインの取得
[お名前.com](https://www.onamae.com/)より、ドメインの取得をします。Route53からでも取得可能ですが、値段が高いです。
あくまで練習用なので、適当に安いドメイン(.xyzとか)を取得すればOKです。

お名前.comでのドメイン取得方法については、以下を参照してください。

https://qiita.com/nakanishi03/items/25278fb4dfad60ebfac4

今回は下記を取得したという前提で進めていきます。

|ドメイン名|役割|
|:--|:--|
|sample.com|front|
|sample-api.com|back|

###ホストゾーンの作成
`Route53 > ホストゾーン > [ホストゾーンの作成]`
ホストゾーン詳細より、NSレコードの**値/トラフィックのルーティング先**をコピーして下さい。
![スクリーンショット 2021-06-22 16.11.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/fc14947b-bb55-0fbc-9af3-855e72b61830.png)


###お名前.comのネームサーバにNSレコードの値を登録
先ほどコピーしたNSレコードをお名前.comのDNSサーバーに登録します。
![スクリーンショット 2021-06-22 16.03.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/2d51e46f-43da-60ff-ec29-dfa48a4f98be.png)

![スクリーンショット 2021-06-22 16.05.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/e8d32573-aa0e-9767-08e1-d7b7c0021199.png)

![スクリーンショット 2021-06-22 16.08.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/805db4c6-d06d-20a7-dec1-9ba01506cab4.png)


```
$: dig sample.com(取得したドメイン) +short NS
```
上記コマンドをターミナルから打ち、無事登録したNSレコードが表示されていれば反映されています。

##ACMを使ってドメインに証明書を発行する
 **sample.comとsample-api.com**について、それぞれSSL証明書を取得します。
`ACM > 証明書のリクエスト > パブリック証明書のリクエスト`を選択。

|||
|:--|:--|
|ドメイン名|sample.com or sample-api.com|
|検証方法|DNS検証|
|タグ|なし|

リクエスト後、検証画面に移るので、**Route53でのレコード作成**を押します。
そして、しばらくして(30分くらい)検証保留中から、発行済みステータスとなれば成功です。

##RDSの作成
###サブネットグループの作成
RDS > サブネットグループ > [DBサブネットグループを作成]

RDSの作成にはサブネットグループが必要なので、先に作成しておきます。
サブネットは**sample-rds-subnet1aとsample-rds-subnet1c**を選択
グループ名は**sample-subnet-group**とします。

###RDSの作成・設定
|設定||備考|
|:--|:--|:--|
|作成方法|標準作成||
|エンジンのタイプ|Maria||
|バージョン|10.4.3||
|テンプレート|無料利用枠||
|DB インスタンス識別子|sample-rds||
|マスターユーザー名|sample(任意)|
|マスターパスワード|samplepassword(任意)||
|DB インスタンスクラス情報|db.t2.micro||
|ストレージタイプ|汎用SSD|
|ストレージ割り当て|20||
|VPC|先ほど作ったVPC(sample-vpc)|
|サブネットグループ|先ほど作ったグループ(sample-subnet-group)|
|既存のVPCセキュリティグループ|先ほど作ったグループ(sample-rds-sg)|
|アベイラビリティゾーン|指定なし||
|最初のデータベース名|app_production|

あとはデフォでOKです。
しばらくすると利用可能ステータスとなるので、そうしたらRDS作成完了です。

##database.yml、credentials.yml.encの修正

```yml:./back/config/database.yml
production:
  <<:         *default
　　　　database: app_production
  host:       <%= Rails.application.credentials.rds[:host] %>  
  username:   <%= Rails.application.credentials.rds[:username] %>
  password:   <%= Rails.application.credentials.rds[:password] %>
```

これらの値は知られたくないので、credentials.yml内に環境変数として定義します。

```termnal:
$: docker-compose run -e EDITOR="vi" back rails credentials:edit 
-----------------------------------
rds:
  host: エンドポイント #RDS詳細画面のエンドポイントの欄の値
  username: sample #設定したユーザーネーム
  password: samplepassword #設定したパスワード
```
エンドポイントは↓の値をコピーしてください。

![スクリーンショット 2021-06-22 16.25.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/1032cfe3-d276-76e6-2c50-bda13d796928.png)


#ロードバランサーの作成

EC2 > ロードバランサー > [ロードバランサーの作成]より作成画面へ移ります。

##ロードバランサーの設定
**front**

|||
|:--|:--|
|種類の選択|Application Load Balancer|
|ロードバランサーの設定||
|名前|sample-front|
|スキーム|インターネット向け|
|IPアドレスタイプ|ipv4|
|リスナー|HTTP & HTTPS|
|VPC|sample-vpc|
|アベイラビリティゾーン|sample-subnet-front1a & 1c|
|セキュリティ設定の構成||
|証明書タイプ|ACM から証明書を選択する||
|証明書の名前|sample.com|
|セキュリティポリシー|デフォルト|
|セキュリティグループ|sample-front-sg|
|ターゲットグループ||
|名前|sample-front-tg|
|ターゲットの種類|インスタンス|
|プロトコル|HTTP|
|ポート|8000|
|プロトコルバージョン|HTTP 1.1|
|ヘルスチェック||
|プロトコル|HTTP|
|パス|/|
|ターゲットの登録|スキップ|

**back**

|||
|:--|:--|
|種類の選択|Application Load Balancer|
|ロードバランサーの設定||
|名前|sample-back|
|スキーム|インターネット向け|
|IPアドレスタイプ|ipv4|
|リスナー|HTTP & HTTPS|
|VPC|sample-vpc|
|アベイラビリティゾーン|sample-subnet-back1a & 1c|
|セキュリティ設定の構成||
|証明書タイプ|ACM から証明書を選択する||
|証明書の名前|sample-api.com|
|セキュリティポリシー|デフォルト|
|セキュリティグループ|sample-back-sg|
|ターゲットグループ||
|名前|sample-back-tg|
|ターゲットの種類|インスタンス|
|プロトコル|HTTP|
|ポート|3000|
|プロトコルバージョン|HTTP 1.1|
|ヘルスチェック||
|プロトコル|HTTP|
|パス|/|
|ターゲットの登録|スキップ|

※ヘルスチェックのパスを`"/"`としていますが、ルートパスに何かしら200レスポンスを返すページを用意しておいて下さい。しないとエラーになります。
ターゲットはECSに登録した際に自動で追加されます。

##Route53にAレコードを追加
Route53 > ホストゾーン > [レコードを作成]
- レコードタイプ: A
- 値: エイリアス
    - エンドポイント: Application Load Balancer
    - リージョン: 東京
    - ロードバランサー: sample-front または sample-back

**sample.comとsample-api.com両方のドメインで行って下さい**

##アプリを本番環境用に変更
AWSにデプロイするにあたり、本番環境用にいくつかファイルを変更します。
書き換えるのが面倒ならば、`Dockerfile.dev,Dockerfile.pro`など環境毎に分けて作成すると良いかもです。

```dockerfile:back/Dockerfile
FROM ruby:2.6.3-alpine3.10

ENV RUNTIME_PACKAGES="linux-headers libxml2-dev make gcc libc-dev nodejs tzdata mysql-dev mysql-client yarn" \
    DEV_PACKAGES="build-base curl-dev" \
    HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo

WORKDIR ${HOME}
COPY Gemfile ${HOME}/Gemfile  
COPY Gemfile.lock ${HOME}/Gemfile.lock
RUN apk update && \
    apk upgrade && \
    apk add --update --no-cache ${RUNTIME_PACKAGES} && \
    apk add --update --virtual build-dependencies --no-cache ${DEV_PACKAGES} && \
    bundle install -j4 && \
    apk del build-dependencies && \
    rm -rf /usr/local/bundle/cache/* \
    /usr/local/share/.cache/* \
    /var/cache/* \
    /tmp/* \
    /usr/lib/mysqld* \
    /usr/bin/mysql*

ADD . ${HOME}
EXPOSE 8000
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0", "-p", "8000", "-e", "production"]
```

```dockerfile:front/Dockerfile
FROM node:16.3.0-alpine

ENV HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo

ENV HOST 0.0.0.0

WORKDIR ${HOME}
RUN apk update && \
    apk upgrade && \
    npm install -g n && \
    yarn install &&\
    rm -rf /var/cache/apk/*

ADD . ${HOME}
EXPOSE 3000
RUN yarn run build
CMD ["yarn", "start"]
```

```js:./front/nuxt.config.js
  axios: {
    //baseURL: "http://localhost:3000",
    //ベストプラクティスとしてはURLを環境変数で設定した方が良いですが、今回は手直しします。
      baseURL: "https://sample-api.com"
  },
```

##ECRにリポジトリを作成・イメージのプッシュ

###ECRの作成・設定

`ECR > [レポジトリの作成]`よりレポジトリを作成します。
可視設定はプライベート、レポジトリ名は**sample-front,sample-back**とします。

作成後`[プッシュコマンドの表示]`を押すとコマンドが表示されるので、それに従いターミナルで実行します。尚、このコマンドを実行するには**AWS-CLIの事前インストール**が必要なので、やっておきましょう。

https://qiita.com/NorihitoYamamoto/items/badd32785078bc944089

実行後、ECRレポジトリ内にイメージが追加されたのを確認して下さい。

##ECSの作成・設定
以下、個人的な用語整理

- クラスター: ECSの適用範囲を決める
- タスク定義: docker-compose.yml
- タスク: docker-compose.ymlによって作成されたコンテナ群
- サービス:クラスターとタスク定義を紐付ける

###ECSクラスターの作成
`ECS > クラスター > [クラスターの作成]`より作成画面に移ります。

|||
|:--|:--|
|クラスターテンプレート|EC2 Linux + ネットワーキング|
|クラスター名|sample-cluster|
|プロビジョニングモデル|オンデマンドインスタンス|
|EC2 インスタンスタイプ|t3.small|
|インスタンス数|1|
|EC2 AMI ID*|Amazon Linux2|
|ボリュームサイズ|デフォ値|
|キーペア|任意 (無しにするとSSH接続でデバックできません)|
|VPC|sample-vpc|
|サブネット|sample-front-subnet1a & 1c|
|パブリック IP の自動割り当て|適用|
|セキュリティグループ|sample-front-sg|
|コンテナインスタンスの IAM ロール|デフォ値|

###タスク定義の作成
`ECS > タスク定義 > [新しいタスク定義の作成]`より作成画面に移ります。
**front**

|設定|Value|
|:--|:--|
|起動タイプの互換性|EC2|
|タスク定義名|sample-front|
|タスクロール|ecsTaskExecutionRole|
|ネットワークモード|ホスト|
|タスク実行ロール|ecsTaskExecutionRole|
|タスクメモリ (MiB)|700|
|タスク CPU (単位)|256|
|コンテナ名|sample-front|
|イメージURL|ECR sample-frontレポジトリのURI|
|ポート|8000|

**back**

|設定|Value|
|:--|:--|
|タスク定義名|sample-back|
|タスクロール|ecsTaskExecutionRole|
|ネットワークモード|ホスト|
|タスク実行ロール|ecsTaskExecutionRole|
|タスクメモリ (MiB)|700|
|タスク CPU (単位)|256|
|コンテナの追加||
|コンテナ名|sample-back|
|イメージURL|ECR sample-backレポジトリのURI|
|ポート|3000|

###サービスの作成
`ECS > クラスター > sample-service >作成`より作成画面に移ります。
**front**

|設定|Value|
|:--|:--|
|起動タイプ|EC2|
|タスク定義|sample-front|
|サービス名|sample-front-service|
|サービスタイプ|REPLICA|
|タスクの数|1|
|次のステップ||
|ロードバランシングの種類|Application Load Balancer|
|ヘルスチェックの猶予期間|30|
|サービス用の IAM ロールの選択|ecsServiceRole|
|ロードバランサー名|sample-front|
|ロードバランサーに追加||
|ターゲットグループ名|sample-front-tg|
|Service Auto Scaling|サービスの必要数を直接調整しない|
**back**

|設定|備考|
|:--|:--|
|起動タイプ|EC2|
|タスク定義|sample|
|サービス名|sample-back-service|
|サービスタイプ|REPLICA|
|タスクの数|1|
|次のステップ||
|ロードバランシングの種類|Application Load Balancer|
|ヘルスチェックの猶予期間|30|
|サービス用の IAM ロールの選択|ecsServiceRole|
|ロードバランサー名|sample-back|
|ロードバランサーに追加||
|ターゲットグループ名|sample-back-tg|
|Service Auto Scaling|サービスの必要数を直接調整しない|

以上で完了です。最後に少しだけやることがあります。

##コンテナ内に入ってdb:migrateする
EC2インスタンスの中にssh接続し、その上でdockerコンテナ内に入り込み`rails db:migrate`します。
RDSを作成時に初期DBとして[app_production]を設定したため、db:createの必要ありません。

```terminal
ssh -i {pemファイル} ec2-user@{インスタンスのpublicIP}
```

```terminal:EC2インスタンス内
docker ps
```
で、現在起動しているコンテナが表示されます。
railsを起動しているコンテナのCONTAINER IDを指定して、以下コマンドを打つと、Dockerコンテナ内部に入れます。

```terminal:EC2インスタンス内
docker exec -it {CONTAINER ID} sh
```

ここで、環境を指定してdb:migrateします。

```terminal:Dockerコンテナ内
rails db:migrate RAILS_ENV=production
```

##起動確認
`https://sample.com`にアクセスすると、ホーム画面が表示されると思います！

##うまくいかない場合...

- 記入ミス等、ケアレスミスしてないか見直す
- 英語の記事がないか調べてみる
- `docker logs {コンテナid}`でエラー原因を探る
- タスク詳細画面でエラー原因を見てみる

などなど...