【超初心者向け（スクショ多め）】Ruby2.6 × Rails6 × CircleCi でAWS ECR・ECSに自動デプロイ 【ハンズオン形式】

## 本記事で目指す構成

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2d3b0ba2-af9e-b672-991f-01eec8548db2.png)

① GitHubへpush
② CircleCiでビルドが走る
③ テスト（Rspec）に通過した場合、Dockerイメージを作成してECRへpush
④ 最新のDockerイメージを利用し、ECSのタスク定義を更新してデプロイ完了

## 対象読者

- ECSに全く触れた事が無い人
- とりあえず手を動かして雰囲気を掴みたい人
- 就活のためにポートフォリオを作成中の人

簡単なRailsアプリ（「Hello World!」と表示するだけ）をAWS ECSにデプロイするまでの手順。がっつりその後の運用まで考慮しているわけではなく、あくまで参考程度にしかならないためその点はご注意ください。

人によって多分色々なやり方がありそうなので、一度流れを掴んだ後は各自お好みで設定していただきたいです。

デプロイ用のアプリを用意する段階から全てハンズオン形式（スクショも多数）で記載しており、書いてある通りに進めれば基本的には上手くいくはず。

要所要所で任意の値（プロフィール名やアプリ名など）を設定する部分があるので、不安な場合は全て筆者と同じように「sample-app」などで統一すると良いかもです。

`※初心者向けと銘打っているものの、「まずは実際に手を動かして雰囲気を掴む」という目的に徹しているため、各用語に関する説明はほとんど説明していません。`

`※あくまで「AWS超初心者でもとりあえず書いてある通りに従えばそれっぽくデプロイできる」というのがコンセプト。理論派の方はあらかじめ他の記事でECR・ECSの概念について学習してから入るのをおすすめします。（筆者は体で覚える派なので...）`

`※AWSの各リソース名などに関しては基本的に任意なので各自お好みで（ただし、ecs-cliでコマンドを打つ際やCircleCiのconfig内の記述をそれに合わせる必要あり）。また、特に触れていない部分に関してはとりあえずデフォルトの状態もしくは空欄で大丈夫だと思います。`

`※スクショ撮ったタイミングが違ったりしてリビジョンナンバーなどにバラつきがあるかもしれませんが、無視してください（汗）`

### 仕様
- 言語: Ruby2.6
- フレームワーク: Rails6
- データベース: MySQL5.7
- アプリケーションサーバー: Puma
- Webサーバー: Nginx

## 下準備編

まず、ECSにデプロイするための簡単なRailsアプリを用意。

### サンプル

https://github.com/kazama1209/sample-app

```
$ git clone https://github.com/kazama1209/sample-app.git
$ cd sample-app
```

### セットアップ

```
$ docker-compose build
$ docker-compose run web bundle exec rails webpacker:install

# ↑のコマンドでエラーが出た場合
$ docker-compose run web yarn install --check-files

$ docker-compose up -d
$ docker-compose run web bundle exec rails db:create
```

### master.keyを作成

↑からリポジトリをクローンした場合、config/master.keyが存在しないはず。master.keyが無いとデプロイ時にエラーが起きるため、以下の手順によりここで生成しておく。

```
$ rm config/credentials.yml.enc
$ docker-compose run -e EDITOR=vim web rails credentials:edit
```

参照:
[Rails on Dockerでcredentialsをeditしたい](https://qiita.com/at-946/items/8630ddd411d1e6a651c6)
[ActiveSupport::MessageEncryptor::InvalidMessage](https://qiita.com/suzy1031/items/64598d44b428ccd7abc5)

### localhostにアクセス

![2202d539-1270-4db1-a75c-397be9b8a0c5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/7f879657-3eb9-3002-22be-d16bf067102e.png)

`http://localhost`にアクセスしていつもの画面が表示されればOK。

### トップページ「Hello World!」を作成

今回の最終目的である「Hello World!」と返すトップページを作成する。

```ruby:./app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
end
```

```erb:./app/views/home/index.html.erb
<h1>Hello World!</h1>
```

```ruby:./config/routes.rb
Rails.application.routes.draw do
  root 'home#index'
end
```

![スクリーンショット 2020-09-23 3.38.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/857e9f99-84d6-251b-d7ef-01541c791ba2.png)

「Hello World!」が返ってくればサンプルアプリの準備は完了。

## デプロイ編

アプリの準備ができたので、ECSにデプロイしていく。

### 各種ツールをインストール

今回、ECSにデプロイするにあたり以下2つのツールを使用する。

- [AWS CLI](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-welcome.html)
- [Amazon ECS CLI](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI.html)

```ruby
$ brew install awscli
$ brew install amazon-ecs-cli
```

### aws configureを設定

上記のツールを使用するためにaws configureの設定を行う。

#### IAMユーザーを作成

![7c16be50-b5d0-412b-be53-bd7b692fec3a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/219d8ac9-86fb-f151-8911-fb02a1c1761c.png)

AWSのコンソールからサービス→IAMを選択し、「ユーザーの追加」をクリック。

![スクリーンショット 2020-09-24 1.13.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/d0af9a8d-5b94-0c2b-4fcf-7e03971fd11c.png)


任意のユーザー名（今回は「sample-app」）を入力し、「プログラムによるアクセス」にチェックをつけて次のステップへ。

![5fd6a2b4-d332-4b99-bdf3-a75b5a185d2c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c99b911e-1dff-68bd-f99b-9bd8babeab56.png)

「既存のポリシーを直接アタッチ」から以下の2つのポリシーをアタッチして次のステップへ。

- AmazonECS_FullAccess
- AmazonEC2ContainerRegistryFullAccess

![8f248761-358c-4a85-b5c4-f69d74e32b20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c7f759e1-4bb2-db04-56ae-9caba8aa29ae.png)

タグに関しては今回はは無視で次のステップへ。

![スクリーンショット 2020-09-24 1.14.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/df0ca29a-c153-fa74-6501-316c32503fc8.png)

最後に入力情報の確認画面が表示されるので、特に問題無ければ「ユーザーの作成」をクリック。

![スクリーンショット 2020-09-24 1.14.29_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/9e3ef2fd-773e-efa7-d2c9-3d9d280bf01a.jpeg)

ユーザーの作成に成功すると「アクセスキー」「シークレットアクセスキー」の2つが発行されるので、メモを取るなりcsvファイルをダウンロードするなり大事に保管。

#### ターミナルで「aws configure」を実行

```
$ aws configure --profile <先ほど作成したIAMユーザー名（今回は「sample-app」）>

AWS Access Key ID # 先ほど作成したアクセスキー
AWS Secret Access Key # 先ほど作成したシークレットアクセスキー
Default region name # ap-northeast-1 
Default output format # json 
```

それぞれ上記のように入力。

### 追加でポリシーを作成

先ほどIAMユーザーを作成した際、

- AmazonECS_FullAccess
- AmazonEC2ContainerRegistryFullAccess

2つのポリシーをアタッチしたが、これだけだとこの後に使用する「ecs-cli」というツールの中で権限エラーが発生するため、ここで別途追加しなければならない。

![a00a8b0d-964e-4fb5-9a28-bfcc03193c22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/1f8733f1-499b-148a-0a97-1784d5988456.png)

AWSのコンソールからサービス→IAM→ポリシーを選択し、「ポリシーの作成」をクリック。

![6b1fb686-37e2-4882-a1d1-83506504bd46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/740888a7-1e4c-6dda-437d-198a5cf1bb9c.png)


JSONタブを開いて以下の記述を行う。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:AttachRolePolicy",
                "iam:AddRoleToInstanceProfile",
                "iam:CreateInstanceProfile",
                "iam:CreateRole",
                "iam:DeleteInstanceProfile",
                "iam:DeleteRole",
                "iam:DetachRolePolicy",
                "iam:PassRole",
                "iam:RemoveRoleFromInstanceProfile",
                "ec2:DeleteInternetGateway",
                "ec2:DeleteSecurityGroup",
                "ec2:DeleteRouteTable"
            ],
            "Resource": "*"
        }
    ]
}
```

![98bcb131-f696-42dd-a11b-0853f81dfc05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/52465768-b3cf-6588-4e02-403cb82c4ed5.png)



ポリシーの名前や説明を適当に入力し、「ポリシーの作成」をクリック。

### ポリシーをユーザーにアタッチ

![f204b7a2-5b8c-4d3b-8029-a4e131037496.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/513516ed-2bee-ee60-61e1-9132d5f2804b.png)

AWSのコンソールからサービス→IAM→ユーザーを選択し、「アクセス権限の追加」をクリック。

![2e033472-a7e7-4022-94be-fc40f57057c2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/21e0928e-75d6-6ed1-3df5-ae312b8bb94f.png)


「既存のポリシーをアタッチ」から先ほど作成したポリシーを選択し、アクセス権限を追加。

### キーペアを作成

後々EC2内へ入る際などに必要になるのでキーペアを作成しておく。

![25045ba0-973e-4270-922b-4ae9d8fc19aa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/9c51a603-bf34-ba1e-a134-010faeeedc9a.png)

AWSのコンソールからサービス→EC2→キーペアを選択し、「キーペアの作成」をクリック。

![c36ab4a3-27ca-4439-9c53-f2e62e3030e7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/a7af766b-b1b6-2817-1938-c4755fd628bd.png)

名前とファイル形式を入力し、「キーペアを作成」をクリック。

```
$ mv Downloads/sample-app.pem .ssh/
$ chmod 600 ~/.ssh/sample-app.pem
```

完了すると「.pem」形式のファイルがダウンロードされるので、「.ssh」ディレクトリに移動させて権限を変更する。

### クラスターを作成

コンソールから手動でぽちぽち作成する事も可能だが、vpcやサブネットなども一緒に作る必要があるため、今回はecs-cliでまとめて作成してしまう。

次のコマンドを実行。

```
$ ecs-cli configure profile --profile-name <任意のプロフィール名> --access-key <先ほど作成したアクセスキー> --secret-key <先ほど作成したシークレットアクセスキー>
$ ecs-cli configure --cluster <任意のクラスター名> --default-launch-type EC2 --config-name <任意の設定名> --region ap-northeast-1
$ ecs-cli up --keypair <先ほど作成したキーペア> --capability-iam --size 2 --instance-type t2.small --cluster-config <任意の設定名> --ecs-profile <任意のプロフィール名>
```

キーなどの各値は各自異なる。
筆者の場合は下記のような感じ。

```
$ ecs-cli configure profile --profile-name sample-app --access-key ******************** --secret-key ****************************************
$ ecs-cli configure --cluster sample-app-cluster --default-launch-type EC2 --config-name sample-app-cluster --region ap-northeast-1
$ ecs-cli up --keypair sample-app --capability-iam --size 2 --instance-type t2.small --cluster-config sample-app-cluster --ecs-profile sample-app

INFO[0006] Using recommended Amazon Linux 2 AMI with ECS Agent 1.44.3 and Docker version 19.03.6-ce 
INFO[0007] Created cluster                               cluster=sample-app-cluster region=ap-northeast-1
INFO[0009] Waiting for your cluster resources to be created... 
INFO[0009] Cloudformation stack status                   stackStatus=CREATE_IN_PROGRESS
INFO[0070] Cloudformation stack status                   stackStatus=CREATE_IN_PROGRESS
INFO[0131] Cloudformation stack status                   stackStatus=CREATE_IN_PROGRESS

VPC created: vpc-*****************
Security Group created: sg-*****************
Subnet created: subnet-*****************
Subnet created: subnet-*****************
Cluster creation succeeded.
```

上手くいくと↑のようにクラスター用のVPC、セキュリティグループ、サブネットなどが自動で作成される。

![55a730be-fce1-40b5-9f8e-70f5ede20419.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e731e072-1ba7-9241-69e0-4fa0ea865b28.png)

AWSのコンソールからサービス→Elastic Container Service→クラスターを選択し、無事作成されていれば成功。

### RDSを作成

データベースとして使うRDSを作成。

![4b3d00a9-6680-469d-bf81-fba2d93d5f0a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/7fe1caaf-6ac0-0257-0abd-02c2e2738c62.png)

AWSのコンソールからサービス→RDSを選択し、「データベースの作成」をクリック。

![5d59f52f-b3a6-4d2e-be49-0c76722e028c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/94ca7cf7-b143-ff17-3ed5-dfc4de01a3c6.png)

- 作成方法: 標準作成
- エンジンのタイプ: MySQL
- DBインスタンスサイズ: 無料利用枠

![6abaabd6-b44c-44f6-98e9-33b36251d297.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/8b6a6f5e-0aa7-fac1-5190-4ce1f4a69ea8.png)

- DBインスタンス識別子: sample-app-db
- マスターユーザー名: root
- パスワード: password

`※この辺は全て任意。`

![37879b71-73ff-4404-b6be-0bdce587d21f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/90d7d288-ca73-f42a-b475-c783e50d483e.png)

- vpc: 先ほど作成したvpc
- サブネットグループ: 新しいDBサブネットグループの作成
- パブリックアクセス: あり

`※今回はあくまで練習なので「あり」を選択しているが、本番環境では「なし」にした方が良いかも。`

![6f890dbf-3582-4a4b-9007-6622826b494c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/cfeee36b-c399-4b9e-5a63-21702d8c1895.png)

最初のデータベース名: sample_app_production

`※記事冒頭で用意したサンプル（sample-app）を使用する場合、※データベース名はsample-appの「config/database.yml」内で定義しているため「sample_app_production」で固定`

`※特に触れていない部分は空欄もしくはデフォルトのままでOK。`

問題なければ「データベースの作成」をクリック。

![スクリーンショット 2020-09-15 3.30.28_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/a0333020-3937-d627-2507-ca402b5dfedd.jpeg)

↑こんな感じで作成されていれば成功。

![スクリーンショット 2020-09-15 3.32.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/579fe672-f694-9b4e-2332-8a68f23c5031.png)

また、セキュリティグループの設定も必要なので「VPCセキュリティグループ」の下に記載されているリンクをクリック。

![スクリーンショット 2020-09-15 3.33.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2c58cb7e-7560-6518-351b-c5a1197c9de2.png)

「インバウンドルールの編集」から次のように設定。

- タイプ: MYSQL/Aurora
- プロトコル: TCP
- ポート範囲
- ソース: 0.0.0.0/0

![400d93f6-48e9-4918-a06e-08360d608b18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/47bc895a-22f4-ab83-6dc0-085f2df5c5de.png)

```
$ mysql -h <RDSのエンドポイント> -u <RDSのユーザー名> -p
```

試しにターミナルで↑のコマンドを叩き、接続できれば成功。


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

![f5713aaf-b79d-4a40-8397-d8a141841e07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/b7c27b0c-0d75-338d-39a8-921230e8d0e0.png)

AWSのコンソールからサービス→EC2→ロードバランサーを選択し、「ロードバランサーの作成」をクリック。

![19330f58-f800-4327-b5a8-c6ded7fb5582.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e2c2b703-2702-e87e-2812-cfb683e557ed.png)

3種類あるが、「Application Load Balancer」を選択。

![b493adaf-4c11-41ec-acb0-99be877b09c8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/35ac3edd-4650-fc3b-6b14-24a0cfaee4ce.png)
![44195828-7054-45b2-a6ea-3d9e9732fd99.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/054aae32-13d2-b17d-7b95-6b11efbaedd7.png)


- 名前: sample-app-alb ※任意
- リスナー: そのままでOK
- VPC: 先ほど自動作成されたものを選択
- subnet: 同上

![08e422f3-1b33-4d93-abf4-ef3aecdb29d3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/02158e00-7922-4ce7-29f3-1bda10de325e.png)

先に進むとセキリュティグループの設定画面になるので、「新しいセキリュティグループを作成する」から適当にセキュリティグループを作成。

![b532da52-92e9-4994-8638-d151bcaa5b42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/53b18e4c-5cfb-2801-6e4e-cef3eadc0908.png)
![71847cd0-551a-4341-a433-0c7b8caefc90.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/718622bc-bf9d-4c93-dba6-864e392218a2.png)
![8c6d09bb-18ca-442c-b039-8a63cb94d88c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f1df3f59-e394-f2e2-8fc2-7ce90d2dfe88.png)


ターゲットグループの設定。

- ターゲットグループ: 新しいターゲットグループ
- 名前: sample-app-alb-tg ※任意

クラスター作成時に自動で作られたEC2を登録し、確認画面から問題なければ「作成」をクリックして完了。

### ECRにdockerイメージをpush

![d6678645-ca93-4a28-8195-1ed7f9141a7e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c895f6e0-b4a0-1797-5371-a7b33a44bef1.png)

AWSのコンソールからサービス→Amazon Elastic Container Registryを選択し、「リポジトリの作成」をクリック。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f61666361646565352d623963372d316261662d386532352d3330373238663433353338312e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e43cb41d-e704-959f-828f-21dd8fcf87e7.jpeg)
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f32633938643366302d353166312d383934382d653430332d6535353230346565383562342e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2e20004f-374a-367a-2cac-884d246dbd46.jpeg)


それぞれ適当なリポジトリ名を入力し、「リポジトリを作成」をクリック。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f63653130356164622d396230342d383661342d666363612d6364666464623137363036342e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/022c69a6-bede-8574-e384-8a97ccdca63d.jpeg)

プッシュコマンドを表示し、書いてある通り上から順に4つ実行していく。

※2番目のコマンドでbuildを行う際は-fでDockerfileのコンテキストを変える。

```
# Rails（本番用のDockerfileを使用する）
$ docker build -f ./prod.Dockerfile . -t sample-app-rails 

# Nginx
$ cd containers/nginx
$ docker build -f ./Dockerfile . -t sample-app-nginx 
```

#### 本番用のDockerfile

```
$ touch prod.Dockerfile
```

```dockerfile:./prod.Dockerfile
FROM ruby:2.6.6
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
apt-get install nodejs

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

RUN mkdir /sample-app
WORKDIR /sample-app

ADD Gemfile /sample-app/Gemfile
ADD Gemfile.lock /sample-app/Gemfile.lock

RUN gem install bundler:2.1.4
RUN bundle install

ADD . /sample-app

# Nginxと通信を行うための準備
RUN mkdir -p tmp/sockets
VOLUME /sample-app/public
VOLUME /sample-app/tmp

RUN yarn install --check-files
RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile
```

基本的に開発用のものと同じだが、最後の数行でNginxと通信を行うための準備などを行っている。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f37656637363735612d323131382d633435662d373032302d6136653735303162353033612e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/d96f3f52-4619-7c3f-e988-2ec8b27e757a.jpeg)

全て打ち終わったらリポジトリを確認し、イメージが追加されていれば成功。

### タスクの作成

先ほどpushしたイメージをもとに、タスクの作成を行う。

```
$ mkdir ecs
$ touch ecs/docker-compose.yml
```

```yml:./ecs/docker-compose.yml
version: 2
services:
  app:
    image: # ECRのリポジトリURI（Rails）
    command: bash -c "bundle exec rails db:migrate && bundle exec rails assets:precompile && bundle exec puma -C config/puma.rb"
    environment: # 練習なので直書きスタイルだが、実際はdotenvなどを使った方が良いと思う。
      RAILS_ENV: production
      RAILS_MASTER_KEY: # config/master.keyの値
      DATABASE_NAME: sample_app_production
      DATABASE_USERNAME: root
      DATABASE_PASSWORD: password
      DATABASE_HOST: # RDSのエンドポイント
      TZ: Japan
    working_dir: /sample-app
    logging:
      driver: awslogs
      options:
        awslogs-region: ap-northeast-1
        awslogs-group: sample-app-production/app
        awslogs-stream-prefix: sample-app-production
  nginx:
    image: # ECRのリポジトリURI（Nginx）
    ports:
      - 80:80
    links:
      - app
    volumes_from:
      - app
    working_dir: /sample-app
    logging:
      driver: awslogs
      options:
        awslogs-region: ap-northeast-1
        awslogs-group: sample-app-production/nginx
        awslogs-stream-prefix: sample-app-production
```

次のコマンドを実行。

```
$ ecs-cli compose --project-name sample-app-task -f ./ecs/docker-compose.yml up --create-log-groups --cluster-config sample-app-cluster --ecs-profile sample-app
```
![スクリーンショット 2020-09-15 19.40.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/173525c8-ff8a-b3e4-1efc-1eb3627fea7e.png)

上手く行った場合、実行中のタスクに「1」と表示される。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f35333835383462302d613664332d336237642d663636372d3639656465346235393266392e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/69ec68c3-702d-1b1a-c4ab-148b1ca3e944.jpeg)

最後に、ロードバランサーのDNS名をURLに貼り付けてアクセス。

![スクリーンショット 2020-09-23 3.32.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e0e3c6fc-672a-0d2f-1580-791af8b157b9.png)

ローカル環境で作成したものと同じように「Hello World!」と表示されれば成功。

![04a9c9de-a557-4252-9554-b7bcda1a8be5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/cbdbe273-3e65-cc30-c9a2-e68b31a9cbcd.png)

※デプロイに際して何か不具合があった場合はCloudWatchのログを確認して修正。

### サービスの作成

クラスターとタスクだけでもアプリは動くが、その中間に「サービス」と呼ばれるものを作成すると、コンテナが止まった際に再起動をかけてくれたりロードバランサーを通じてオートスケーリングしてくれたり何かと便利ぽいので作成しておく。

![0adea5cb-a222-43dc-b9a5-df79edc3c337.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e4126b96-4a81-094b-ddc1-d15d28507a8d.png)


AWSのコンソールからサービス→Amazon Elastic Container Service→ クラスター名をクリックし、「サービス」タブを開いて作成ページに進む。

- 起動タイプ: EC2
- タスク定義: sample-app-task ※先ほど作成したもの
- クラスター: sample-app-cluster ※同上
- サービス名: sample-app-service ※任意
- その他: 画像の通り

![60c91d38-5370-438c-a271-1cdf6212de17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e97aa27f-4354-b885-08f4-e04204f68737.png)

- ロードバランサーの種類: Application Load Balancer
- ロードバランサー名: 先ほど作成したもの
- その他: 画像の通り

![2b08d195-a6db-47b3-9261-28ded05830d6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/a761576d-bb01-3581-f823-19be2dc2b50a.png)

- ターゲットグループ名: 先ほど作成したもの
- その他: 画像の通り

![https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_688854_7a541af7-b735-8124-8ef8-887378dd4f3f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/dc242df9-3f29-0f47-d2ee-a63cdb8145c1.png)

最後に確認画面が表示されるので、問題無ければ作成をクリック。

![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/bfdf3bf5-0e35-d869-a66c-eb95d208d8fb.png)
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f32633064353363342d663463662d616162342d333262372d3034316136363634363231622e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f6d273cd-c6f7-1ed3-75c5-026841d9dcde.jpeg)


無事作成されれば完了。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f37336530653262342d303062642d366238342d643164362d6164616465633234626636312e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2dc4631e-8e9f-ca57-3ce9-788d9bd0addf.jpeg)

このままだと2つのタスク（片方はecs-cliでターミナルから開始したもの、もう片方はサービスの作成により開始されたもの）が実行中になってしまっているため、前者は停止してしまってOK。


## おまけ（CircleCiと連携して自動デプロイ）

このままだと変更点があるたびに手動で「ビルド→プッシュ→タスク再定義」といった面倒な作業が必要になるため、「CirlcleCiにプッシュ→ビルド&テスト→ECR・ECSへ自動デプロイ」といった良くある仕組みを構築していく。

### Rspecを導入

まず、デプロイ前にテストを行うためにRspecを導入する。

#### gemをインストール

```ruby:./Gemfile
group :development, :test do
  gem 'rspec-rails'
end
```
```ruby
# Gemfileを更新したので再度ビルド
$ docker-compose build
```

#### 各種ファイルを作成&編集

```
$ docker-compose run web bundle exec rails generate rspec:install

create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb
```

```ruby:./.rspec
--format documentation
```

↑の1行を追記しておくと、Rspecを実行した際の出力表示が見やすくなる。

```ruby:./spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
```

必須ではないが、後ほどテスト用のヘルパーメソッドを作成する事になった場合、ファイルの置き場として「spec/support」を使用するので一応設定しておく。

デフォルトではコメントアウトされているので、それを外せばOK。

```ruby:./config/application.rb
config.generators do |g|
  g.test_framework :rspec, 
    view_specs: false, 
    helper_specs: false, 
    controller_specs: false, 
    routing_specs: false
end
```
このままだと`rails g`コマンドを打ち込んだ際に自動で諸々のテストファイルが作成されてしまうので、余計なものを作成したくない場合は「config/application.rb」で設定を行う。

※この辺はお好みで。

#### rspecを実行

```
$ docker-compose run web bundle exec rspec

No examples found.

Finished in 0.00276 seconds (files took 0.12693 seconds to load)
0 examples, 0 failures
```

まだ何もテストを書いていないので、当然こうなる。

#### とりあえずRequest Specを書いてみる

手始めに、リクエストに対して正常なレスポンスが返ってくるかどうかを確認するためのRequest Specを書いてみる。

##### テストファイルを作成

```
$ docker-compose run web bundle exec rails g rspec:request home

create  spec/requests/homes_spec.rb
```

```ruby:./spec/requests/home_spec.rb
require 'rails_helper'

RSpec.describe "Home", type: :request do
  describe "GET /" do
    it "works successfully" do
      get root_path
      expect(response).to have_http_status(200)
    end
  end
end
```

「/」にアクセスした際、 200番のステータスコードが返ってくるかどうかのテスト。

```
$ docker-compose run web bundle exec rspec

Home
  GET /
    works successfully

Finished in 0.53664 seconds (files took 8.4 seconds to load)
1 example, 0 failures
```

再度rspecを実行し、問題無くパスしていれば成功。

### CircleCIと連携

次に、実際にCircleCiと連携するための設定を行う。

#### gemをインストール

```ruby:./Gemfile
group :development, :test do
  gem 'database_cleaner'
  gem 'rspec_junit_formatter'
  gem 'webdrivers', '~> 3.0'
end
```

```ruby
# Gemfileを更新したので再度ビルド
$ docker-compose build
```

#### 各種ファイルを作成&編集

```
$ mkdir .circleci
$ touch .circleci/config.yml

$ touch config/database.yml.ci

$ docker-compose run web bundle exec rails db:schema:dump 
```

```yml:./.circleci/config.yml
version: 2
jobs:
  build:
    docker:
    - image: circleci/ruby:2.6.6-node-browsers
      environment:
        - BUNDLER_VERSION: 2.1.4
        - RAILS_ENV: 'test'

    - image: circleci/mysql:5.7
      environment:
        - MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
        - MYSQL_ROOT_HOST: '127.0.0.1'

    working_directory: ~/sample_app

    steps:
    - checkout

    - restore_cache:
        keys:
        - v1-dependencies-{{ checksum "Gemfile.lock" }}
        - v1-dependencies-

    - run:
        name: install dependencies
        command: |
          gem install bundler -v 2.1.4
          bundle install --jobs=4 --retry=3 --path vendor/bundle
    - save_cache:
        paths:
        - ./vendor/bundle
        key: v1-dependencies-{{ checksum "Gemfile.lock" }}

    # database setup
    - run: mv ./config/database.yml.ci ./config/database.yml

    # database setup
    - run:
        name: setup database
        command: |
           bundle exec rake db:create
           bundle exec rake db:schema:load
    
    # install yarn
    - run:
        name: install yarn
        command: yarn install

    # install webpack
    - run:
        name: install webpack
        command: bundle exec bin/webpack

    # run tests
    - run:
        name: run rspec
        command: |
          mkdir /tmp/test-results
          TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
            circleci tests split --split-by=timings)"
          bundle exec rspec \
            --format progress \
            --format RspecJunitFormatter \
            --out /tmp/test-results/rspec.xml \
            --format progress \
            $TEST_FILES
    # collect reports
    - store_test_results:
        path: /tmp/test-results

    - store_artifacts:
        path: /tmp/test-results
        destination: test-results
```


```yml:./config/database.yml.ci
test:
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: 'root'
  port: 3306
  host: '127.0.0.1'
  database: sample_app_test
```

```ruby:./spec/rails_helper.rb
RSpec.configure do |config|

  # config DataBaseCleaner
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
    Rails.application.load_seed
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end
```

```ruby:./db/schema.rb
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `rails
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 0) do

end
```

#### CircleCiとGitHubを接続

![58a2db41-e9e6-4521-897b-7a779346f00c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c9267d58-635e-e88e-d9cb-edfe8ef89ec1.png)

https://app.circleci.com/projects/project-dashboard/github/GitHubのアカウント名/

↑CircleCiのダッシュボードから連携したいリポジトリを探し、「Set Up Project」をクリック。画面に表示される指示に従い設定。

![2cd144b5-f09f-4d1a-8e6c-7b2c2cfd9bdb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/763651f0-a292-c74d-5bdf-73e5a05b2a9b.png)

これで今後GitHubへ新しいプッシュを行った際、「.circleci/config.yml」に書いた内容に基づき自動でビルド&テストが走るようになる。

![1ee0ab1a-a288-4e39-b8ed-5652aa923f97.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/ee983813-cb66-cdab-6c6a-22329b16a1bc.png)

特に問題が無ければ「SUCCESS」と表示されるはず。
これで初期設定は完了。

### 自動デプロイ（ECR・ECS）

CircleCiのバージョン2.1から追加されたOrbを使い、masterブランチに変更が加えられた際、CircleCiでのビルド&テストを行い自動でイメージを作成しECRへプッシュし、ECSのサービスを更新してタスクの再定義を行うようにする。

#### 環境変数の登録

![4a34abde-8e22-485a-bd8a-bae327fa0362.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/1ec9c2dd-c57e-3a3f-42ab-3ec1c48c6823.png)

あらかじめCircleCiの設定画面からデプロイに必要な環境変数を登録しておく。

- AWS_ACCESS_KEY_ID
  - 先ほど作成したIAMユーザーのアクセスキー
- AWS_SECRET_ACCESS_KEY
  - 先ほど作成したIAMユーザーのシークレットキー
- AWS_ACCOUNT_ID
  - AWSのアカウントID（コンソールの「マイアカウント」から確認可能）
- AWS_REGION
  - ap-northeast-1
- AWS_ECR_ACCOUNT_URL
 - ECRのリポジトリURI（例: <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com）
- DATABASE_HOST
  - RDSのエンドポイント（例: ********.ap-northeast-1.rds.amazonaws.com）
- DATABASE_USERNAME
  - RDSのユーザー名（例: root）
- DATABASE_PASSWORD
  - RDSのパスワード（例: password）
- DATABASE_NAME
  - 使用するデータベース名（例: sample_app_production)
- RAILS_MASTER_KEY
  - config/master.keyの値
- TZ
  - JAPAN
- MY_APP_PREFIX
  - 任意（例: sample-app）
  - 変数を使い回してなるべくコンパクトに書けるようにクラスター名やタスク名は共通のワードを含めて作成しておいた方が良い（sample-app-cluster、sample-app-taskなど）。

#### .circleci/config.ymlを編集

```yml:./.circleci/config.yml
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@6.7.0
  aws-ecs: circleci/aws-ecs@1.1.0

jobs:
  test:
    docker:
      - image: circleci/ruby:2.6.6-node-browsers
        environment:
          - BUNDLER_VERSION: 2.1.4
          - RAILS_ENV: 'test'

      - image: circleci/mysql:5.7
        environment:
          - MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          - MYSQL_ROOT_HOST: '127.0.0.1'

    working_directory: ~/project

    steps:
      - checkout

      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "Gemfile.lock" }}
          - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            gem install bundler -v 2.1.4
            bundle install --jobs=4 --retry=3 --path vendor/bundle

      - save_cache:
          paths:
          - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      - run: mv ./config/database.yml.ci ./config/database.yml

      - run:
          name: setup database
          command: |
            bundle exec rake db:create
            bundle exec rake db:schema:load

      - run:
          name: install yarn
          command: yarn install

      - run:
          name: install webpack
          command: bundle exec bin/webpack

      - run:
          name: run rspec
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"
            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES

      - store_test_results:
          path: /tmp/test-results

      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

workflows:
  version: 2
  test_and_deploy:
    jobs:
      - test
      # ビルドした後にイメージをECRへプッシュ
      - aws-ecr/build-and-push-image:
          requires:
            - test
          account-url: AWS_ECR_ACCOUNT_URL
          region: AWS_REGION
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          create-repo: true
          dockerfile: ./prod.Dockerfile
          repo: "${MY_APP_PREFIX}-rails"
          tag: "${CIRCLE_SHA1}"
          filters:
            branches:
              only:
                - master
      # ECSのサービスを更新してタスクを再定義
      - aws-ecs/deploy-service-update:
          requires:
            - aws-ecr/build-and-push-image
          family: "${MY_APP_PREFIX}-task"
          cluster-name: "${MY_APP_PREFIX}-cluster"
          service-name: "${MY_APP_PREFIX}-service"
          container-image-name-updates: "container=app,tag=${CIRCLE_SHA1}"
```

各環境変数に間違いが無いか良く確認しておく事。

```erb:./app/views/home/index.html.erb
<h1>Hello World!</h1>
<p>Completed auto deploy with CircleCi</p>
```

自動デプロイが上手くいったかわかりやすいようにトップページを少し変えておく。

#### masterブランチに変更を加える

![7cd89fad-8143-45d7-bb81-40db1e805abe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f8b363a7-af20-d6c6-5ac8-7f08706f273b.png)

実際にmasterブランチにプッシュして変更を加えてみると、CircleCi上でデプロイ込みのjobsが動き始める。

![7454ceea-ac3f-4ec2-97eb-4c89d8cb0f26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/6dc4be45-440d-a52f-1753-8b99578fc510.png)

`※全てのフローが完了するまでに大体10〜15分くらいかかるので注意。`

![7142f5cc-00b7-444d-ab4f-feaa7f5f36a9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e8b4cb48-368c-4bc0-04eb-3f833388759f.png)

再度ロードバランサーのDNS名にアクセスし、先ほどの変更がちゃんと更新されていれば成功。

`※反映されるまで多少時間がかかるので気長に待つ。`

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638383835342f39316133323831642d613733362d326132612d616534372d3930303731363832373266352e706e67_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/d23b0f2f-ae23-750a-03da-4b0df40ef0f9.jpeg)

古いタスクと新しいタスクの2つが実行されているが、時間の経過で古い方は勝手に消されるため（サービスのおかげ？）そのまま放置でOK。

お疲れ様でした。

## あとがき

自分もまだまだ勉強中の身なので、何かあれば随時更新予定です。

現状、コンソール上で手を動かしながら行う作業とターミナルでコマンドを叩いて行う作業がごちゃ混ぜになってしまっているため、できれば全て後者に統一したいと考えていいます。

Terraformとかも使って一発バシっとできるようにしたい...。

どこか詰まった部分やもっとこうした方が良いなどあればコメントいただけると嬉しいです。

（2021年4月3日追記: Terraformで一発デプロイできるようになりました。）

https://qiita.com/kazama1209/items/8e8f4d83a90e782abe5e

### 個人的に詰まった部分

#### EC2のインスタンスタイプ
- 「t2.small」以上を推奨。無料枠での使用だと「t2.micro」が定番だと思うが、筆者の場合、t2.microだとメモリ不足になる不具合が生じた。

```
service sample-app-service was unable to place a task because no container instance met all of its requirements. The closest matching container-instance c4b1a3e7-3209-408c-9501-7b3ea30f97f7 has insufficient memory available. For more information, see the Troubleshooting section.
```

参照: https://aws.amazon.com/jp/premiumsupport/knowledge-center/ecs-container-instance-cpu-error/

本記事内だと、クラスター作成時のコマンド

```
$ ecs-cli up --keypair sample-app --capability-iam --size 2 --instance-type t2.small --cluster-config sample-app-cluster --ecs-profile sample-app
```

↑この部分でインスタンスタイプを指定している。


#### Nginxの設定
- nginx.confファイルの中身は各自変更しないと正常に動かない部分があるので、ググりながら適宜修正する必要がある。

appコンテナとnginxコンテナの接続が上手くいかないと

```
2020/09/13 20:02:57 [crit] 7#7: *456 connect() to unix:///sample-app/tmp/sockets/puma.sock failed (2: No such file or directory) while connecting to upstream, client: *********, server: localhost, request: "GET / HTTP/1.1", upstream: "http://unix:///sample-app/tmp/sockets/puma.sock:/500.html", host: "***********"
```

↑こんな感じのエラーで延々と悩まされる。

#### 各環境変数の設定
- CircleCiに登録する環境変数の値がしっかり合っているか何度も確認した方が良い。ビルドするのにやたら時間がかかるので、1回失敗するとそれだけでかなりの時間の無駄になる。