RailsアプリをEC2からECS/Fargate構成に移行してホストする【ECRへイメージ登録編】


# はじめに

インフラ構成をEC2からECS/Fargateに移行した作業をまとめています。

アプリケーションのコードはRailsで記述しています。
(Nginxはまだ実装できていません…Vue実装時に考える予定です。)

RDSにはMySQLを使っていて、Railsがアクセスできる仕様です。

個人的にECS/Fargate構成で迷走してましたので、
自分が整理できるように記事を書きました。

同じように悩んでいる初学者の方の参考になれば幸いです。



# 使用技術

* ECS/Fargate(blue/greenデプロイメント)
* Rails6(Ruby2.7)
* MySQL8



# インフラ構成図 (EC2 → ECS/Fargate)

■移行前のインフラ構成
![スクリーンショット 2021-12-12 6.15.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/6fb6601e-2405-9747-2520-ad1102372ce0.png)


■移行後のインフラ構成
![スクリーンショット 2021-12-13 7.28.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/fb15c2c1-4d8f-c1c8-7541-06d74e47b619.png)




# 【移行前】EC2インフラの構成について

**`CicleCIを除く、`**移行前のインフラ構築は
下記に参照している@take18k_tech(やんばるエキスパート)さんのQiita記事を参考にさせて頂きました。

https://qiita.com/take18k_tech/items/5710ad9d00ea4c13ce36

https://qiita.com/take18k_tech/items/2efd778c8dd9aae9496e

初学者にも分かりやすくまとめられているので、EC2インフラの振り返りにも使用しています。

**`※当記事のRDS、EC2も@take18k_tech(やんばるエキスパート)さんの記事をベースに作成しています。`**



# 記事：　4部構成

作業内容が多いので、以下の4つに記事を分けました。

1. ECRへイメージ登録編
2. ECS起動編
3. HTTPS/固定ドメイン編
4. CircleCI連携編(自動デプロイ)


# 作業内容

今回はECRへイメージ登録編で、作業内容は以下の通りです。

■第一部(ECRへイメージ登録編)
`1. ネットワーク構築`
`2. RDSの作成`
`3. 管理用EC2の作成`
`4. ECR構築とイメージ保存`

■第二部(ECS起動編)

https://qiita.com/Shuhei_Nakada/items/247d6f1781d6a3765482

■第三部(HTTPS/固定ドメイン編)

https://qiita.com/Shuhei_Nakada/items/a7f2fd64f0af9663ed29

■第四部(CircleCI連携編)

https://qiita.com/Shuhei_Nakada/items/2e3a5afde0bd215b334d

※ エラー時のデバッグ

https://qiita.com/Shuhei_Nakada/items/dd749771b5edfd12fea3


# 1. ネットワーク構築

まず、ネットワークのリソースをそれぞれCIDRブロックで構想します。


**`VPC：ネットワーク全体`**

| VPC名称 | CIDR | アドレス範囲 |
|:-:|:-:|:-:|
| app-production-vpc | 10.0.0.0/16 | 10.0.0.0 - 10.0.255.255 |


**`サブネット：リソースごとの領域`**

| サブネット名称 | CIDR | アドレス範囲 | region | 用途 | ルートテーブル |
|:-:|:-:|:-:|:-:|:-:|:-:|
| app-production-ecs-1a-subnet | 10.0.1.0/24 | 10.0.1.0 - 10.0.1.255 | ap-northeast-1a | ECS(ALB/fargate) | Public |
| app-production-ecs-1c-subnet | 10.0.2.0/24 | 10.0.2.0 - 10.0.2.255 | ap-northeast-1c | ECS(ALB/fargate) | Public |
| app-production-db-1a-subnet | 10.0.11.0/24 | 10.0.11.0 - 10.0.11.255 | ap-northeast-1a | DB | Private |
| app-production-db-1c-subnet | 10.0.12.0/24 | 10.0.12.0 - 10.0.12.255 | ap-northeast-1c | DB | Private |
| app-production-management-1a-subnet | 10.0.21.0/24 | 10.0.21.0 - 10.0.21.255 | ap-northeast-1a | 管理用EC2 | Public |
| app-production-management-1c-subnet | 10.0.22.0/24 | 10.0.22.0 - 10.0.22.255 | ap-northeast-1c | 管理用EC2 | Public |


**`インターネットゲートウェイ：インターネットに繋がる唯一の出入り口`**

| インターネットゲートウェイ名称 |
|:-:|
| app-production-gw |


**`ルートテーブル：サブネットの通信経路を決める`**

| ルートテーブル名称 | 送信先 | ターゲット |
|:-:|:-:|:-:|
| Public | 10.0.0.0/24, 0.0.0.0/0 | local, app-production-gw |
| Private | 10.0.0.0/24 | local |


**`サブネットグループ：サブネットをまとめた集合（RDSで必要）`**

| サブネットグループ名称 | グループ |
|:-:|:-:|
| app_rds_subnet_group | 10.0.11.0/24, 10.0.12.0/24 |


**`セキュリティグループ：リソース同士の接続を制限するルール`**

| セキュリティグループ名称 | タイプ | ソース | 用途 |
|:-:|:-:|:-:|:-:|
| app-ecs-sg | HTTP, HTTP, HTTPS, HTTPS, カスタムTCP | 0.0.0.0, 0::, 0.0.0.0, 0::, app-management-sg | ALB/fargate |
| app-db-sg | MySQL | app-ecs-sg | DB |
| app-management-sg  | SSH | マイIP | 管理用EC2 |

■セキュリティグループ補足：
　・app-ecs-sgのみ、5つインバウンドルールを設定しています。
　・アウトバウンドルールは全てのトラフィックとしています。



構想ができたら、AWSのマネジメントコンソール上のVPC画面で
ポチポチとネットワークを構築していきます。
（コンソールのGUIで構築していく手順は省略します。お許しください。）


# 2. RDSの作成

続いて、RDSを作成していきます。

構築手順は、@take18k_tech(やんばるエキスパート)さんの下記Qiita記事をベースに作成します。
（ここでもコンソール画面での手順は省略します…すいません。）

https://qiita.com/take18k_tech/items/5710ad9d00ea4c13ce36#3-rds


前工程のネットワーク構築で作成したVPC/サブネット(DB用)/セキュリティグループ(DB用)を
RDSに紐付けて作成を完了してください。

なお、AWS無料枠の対象となるMySQLを選択しています。


# 3. 管理用EC2の作成

次はEC2の作成です。

この作業は必須ではないはずですが

`EC2がAWS無料枠で作成できる　かつ　blue/greenデプロイメントのテストリスナーを置きたい`

といった観点から作成しています。

ここも@take18k_tech(やんばるエキスパート)さんの下記Qiita記事をベースに作成します。
（コンソール画面での手順は省略…ご了承を…）

https://qiita.com/take18k_tech/items/5710ad9d00ea4c13ce36#4-ec2

前工程のネットワーク構築で作成したVPC/サブネット(管理用EC2)/セキュリティグループ(管理用EC2)を
EC2に紐付けて作成を完了してください。


構築後はローカルからEC2にSSH接続できるかを検証してみましょう。

```shell:ターミナル

# SSH接続に必要な情報(Host, Hostname, User, IdentityFile)が設定されているか確認
% vi ~/.ssh/config

# ManagementEC2
Host 任意のホスト名
  Hostname EC2に紐付けたElasticIPアドレス
  User ec2-user（EC2デフォルトのユーザー名）
  IdentityFile ~/.ssh/〇〇.pem（EC2作成に生成したkey）

# 確認後.ssh/configファイルをもとにSSH接続する
% ssh ホスト名

# 質問に回答する
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

### ログイン成功！
       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

```


# 4. ECR構築とイメージ保存

ここでは、ECR作成してリポジトリにイメージを保管します。

`ECR: AWS版のレジストリサービス`

DockerHubのようにDockerイメージを保管したり、取得したりできる場所です。

では、マネジメントコンソールを参照しながら説明します。

まず、AWSサービス一覧からECRを選択します。
![スクリーンショット 2021-12-12 9.41.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/04dd192f-5422-d8e9-0fb3-3e6614d3409e.png)


続いて、ECR画面から`リポジトリを作成`をクリックします。
![スクリーンショット 2021-12-12 9.42.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/73e64068-379d-52c3-0cb4-2113ba190c46.png)


各種リポジトリの設定を行なって、`リポジトリを作成`を押します。
![スクリーンショット 2021-12-12 9.46.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/a95f9482-5dfb-44a9-1366-885b677ef87e.png)


自動的にページ遷移し、リポジトリ一覧が表示されます。
ここで、`作成したリポジトリをチェックしてから
プッシュコマンドの表示`を押します。
![スクリーンショット 2021-12-12 11.19.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/d91f658a-d9ff-7f55-a27f-60d027b411d1.png)


すると、DockerイメージをECRに保存するまでに必要なコマンドが
４つ自動表示されます。

![スクリーンショット 2021-12-12 11.24.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/513292a4-9d72-59e1-886a-b0bd504eb05d.png)

それぞれ簡単に説明すると以下の通りです。
**`一部編集するため、コマンド実行はまだしないでください。`**

```shell:ターミナル

# 1.Dockerコマンドが使用できるようにAWSに認証させる
% aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com

# 2.Dockerfileをもとにイメージを生成する
% docker build -t app-backend .

# 3.ECRに保存するためにAWS指定のタグ付けを行う
% docker tag app-backend:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/app-backend:latest

# 4.指定タグをつけたイメージをECRに保存する
% docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/app-backend:latest

```

上記コマンドを実行する前に、２つのファイルを準備します。

`1.本番用Dockerfile：　Dockerfile.production`

Dockerイメージの生成に必要なファイルです。

ここでは、開発用Dockerfileと本番用Dockerfileを区別して
ファイルを`Dockerfile.production`としています。

Dockerfileのコードは、下記Qiita記事やUdemy動画をベースに
Try＆Errorで作り直して現時点で私のRailsアプリが動作できているものですので
ご自身のアプリに合わせて作成してください。

https://qiita.com/notozeki/items/ac1bf72cf44b7ed6a6ad

https://qiita.com/hatsu/items/22e11e94a0a981d78efa#dockerfile%E4%BD%9C%E6%88%90

```dockerfile:Dockerfile.production
# ベースイメージは軽量なalpineを採用する(ローカルのrubyバージョンに合わせる)
FROM ruby:2.7.3-alpine

# Dockerfile内で使う変数としてRUNTIME_PACKAGESとDEV_PACKAGESの２つを定義する
ARG RUNTIME_PACKAGES="bash imagemagick nodejs yarn tzdata mysql-dev mysql-client git"
ARG DEV_PACKAGES="build-base curl-dev"

# コンテナの作業用ディレクトリを設定する
WORKDIR /app

# Railsの実行環境に関わる環境変数をproductionに変更する(コンテナに渡すのでENVとする)
ENV RAILS_ENV="production"

# ローカルにあるGemfileとGemfile.lockをコンテナに複製する
COPY Gemfile Gemfile.lock /app/

# alpine特有のapkコマンドを使って、パッケージのインストールと削除
RUN apk update && \
    apk upgrade && \
    apk add --no-cache ${RUNTIME_PACKAGES} && \
    apk add --virtual build-dependencies --no-cache ${DEV_PACKAGES} && \
    bundle install -j4 && \
    apk del build-dependencies

# ローカルのアプリコード一式をコンテナのappディレクトリに複製する
COPY . /app

# アセットのプリコンパイルを実行する
RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile \
    && yarn cache clean \
    && rm -rf node_modules tmp/cache

# 静的アセットの配信を行う設定
ENV RAILS_SERVE_STATIC_FILES="true"

# entrypoint.production.shを複製してシェルスクリプトを実行する
COPY entrypoint.production.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.production.sh
ENTRYPOINT ["entrypoint.production.sh"]

# コンテナはポート番号を80で開放する
EXPOSE 80

# Railsサーバーが本番環境で起動する(上記ENVで"production"としている)
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "80"]
```

`2.シェルスクリプト：　entrypoint.production.sh`

Dockerfile.production側のコードから一時的に処理が渡されるファイルです。
こちらでRailsのマイグレーションを実行しておきます。

それ以外はDockerの公式ドキュメントに載っている
Rails用の設定コードから変更はありません。

https://docs.docker.com/samples/rails/


```sh:entrypoint.production.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /app/tmp/pids/server.pid

# AWSコンソール画面で作成したRDSに対して、マイグレーションを実行する
# (データベース自体は作成されているので、マイグレーションファイルでテーブルを作成する)
rails db:migrate RAILS_ENV=production

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
```

以上で２つのファイルが作成できたら、先ほどの４つのコマンドを順番に実行していきます。

2つ目のコマンドのみ、Dockerfile.productionを指定するように修正します。
 -> デフォルトだとDockerfile（開発用）が読み取られてしまうので注意します。

```shell:ターミナル

# 1.Dockerコマンドが使用できるようにAWSに認証させる
% aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com

###(ここだけ修正する)###
# 2.本番用のDockerfile.productionをもとにイメージを生成する
% docker build -f Dockerfile.production -t app-backend .

# 3.ECRに保存するためにAWS指定のタグ付けを行う
% docker tag app-backend:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/app-backend:latest

# 4.指定タグをつけたイメージをECRに保存する
% docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/app-backend:latest

```

最後にイメージがECRに保存できているか確認します。

ECR画面から対象リポジトリを選択します。
![スクリーンショット 2021-12-12 12.38.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/3c3088f3-a34a-2ddd-eda9-de3b7896a5d1.png)

最新のlatestタグがついたイメージが存在していることを確認します。
![スクリーンショット 2021-12-12 12.39.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/37b8f8db-ccad-5840-5a7b-adbe35185cc8.png)

以上で、第一部のECRへイメージ登録編が終了です。



# 続編

https://qiita.com/Shuhei_Nakada/items/247d6f1781d6a3765482



# 参考書籍

・AWSコンテナ設計・構築[本格]入門

https://www.amazon.co.jp/AWSコンテナ設計・構築［本格］入門-新井-雅也-ebook/dp/B09DKZC1ZH

https://www.sbcr.jp/product/4815607654/




# 参考記事

https://qiita.com/notozeki/items/ac1bf72cf44b7ed6a6ad

https://qiita.com/hatsu/items/22e11e94a0a981d78efa#dockerfile%E4%BD%9C%E6%88%90



# 終わりに
ECS/Fargateは学習コストが低いとされているけど、コンテナを本番環境にホストが
初経験の自分では非常に難しかった…

これより更にムズイKubernetesは震えが止まりませんね。

無理はせずに、先にterraformを学習しようと思います！