【手順】RailsTutorialで作成したWebアプリをMySQLにしてECS on Fargateでデプロイしてみた


Railsチュートリアルで作成したWebアプリケーションをECS on Fagateでデプロイすることに成功したので数ヶ月後の自分のためにその手順についてまとめていこうと思います。データベースはSQLiteではなくMySQLに変更しました。
基本的には参考教材を修了している前提でのECS on Fargateへのデプロイ手順となります。この中でもUdemyのしまさんの教材とRailsチュートリアルは本当にわかりやすいので初学者には特におすすめです。
※文章中に出てくるtsunarhythmは作成しているポートフォリオのアプリ名です。
※もしできない部分があれば一度飛ばしてから戻ってやってみると成功するかもしれません。

# 【参考教材】
### Webアプリの基礎
・Railsチュートリアル (第7版)
https://railstutorial.jp/
デプロイするWebアプリケーションの基礎組としてここで作成したアプリケーションを使用しました。

### Dockerの基礎
・YouTube「Dockerの基礎」by データサイエンス研究所
https://www.youtube.com/watch?v=2s90o8ma8aQ&list=PL7BUpEjz_maRKfBY4N9De2YDwVu9pbNSa&index=1
主にDockerとdocker-composeの基礎理解に大変役立ちました。

・YouTube Docker超入門講座 合併版 | ゼロから実践する4時間のフルコース by だれでもエンジニア / 山浦清透
https://www.youtube.com/watch?v=lZD1MIHwMBY

・YouTube　【rails環境構築】docker + rails + mysql で環境構築（初心者でも30分で完了！） by FarStep【プログラミング講座】
https://www.youtube.com/watch?v=Fq1PH0Gwi8I

### AWSの基礎
・Udemy 「AWSコンテナサービス入門―AWSの基本からECS・Copilot CLI・CI/CD・App Runnerまで」by しま (大嶋勇樹)
https://www.udemy.com/course/aws-container/?couponCode=KEEPLEARNING
AWSの基礎理解として大変役立ちました。

## 【開発環境の情報】
・MacOS
・Apple M1
・メモリ16GB
・ruby "3.2.2"
・rails "7.0.4.3"
・mySQL8.0.36(gem "mysql2","~> 0.5.6")
・VScode
・使用したアプリのGitHubリンク
https://github.com/siosawa/TsunaRhythm 
※Railsチュートリアル修了後に何度か変更を加えています。


### 【手順の概要】
※手順1はスキップします。
1. CodespacesからVScodeにエディタの変更
2. データベースをSQLiteからMySQLに変更
2. Dockerを導入(Dokcerfile)
2. docker-composeの導入(docker-compose.yml)
2. VPCの作成
2. RDSのサブネットグループの作成とDB作成
2. AWSアクセスキーとシークレットアクセスキーを作成してAWSCLIをインストール
2. ECRリポジトリの作成とイメージのプッシュ
2. クラスタの作成
2. タスク定義とタスク定義SGの作成
2. ELB(ALB)の作成
2. サービスを作成
2. ALBとRDSのSGを編集
2. アセットプリコンパイルエラーの対処
2. レコードの重複エラーの対処
2. まとめ


## 【手順】
#### 1. CodespacesからVScodeにエディタの変更
VScodeのインストールをしGithubのリンクからクローンを行ってSSH設定など行なって進めていきました。
ここは具体的な手順をメモしていなかったのでスキップします。

#### 2. データベースをSQLiteからMySQLに変更
Gemfileにある以下の記述をコメントアウトしMySQLに変更する。
```Gemfile
~
# gem "sqlite3",       "1.6.1"
gem "mysql2",          "~> 0.5.6"
~
```
config/database.ymlのsqliteの記述をコメントアウトしアダプターをmysql2に変更する。timeoutも不要なのでコメントアウトする。

```config/database.yml
~
default: &default
# adapter: sqlite3
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
# timeout: 5000

development:
  <<: *default
# database: db/development.sqlite3
  database: app_development

test:
  <<: *default
# database: db/test.sqlite3
  database: app_test

# production:
# <<: *default
# adapter: postgresql
# encoding: unicode
# url: <%= ENV['DATABASE_URL'] %>

production:
  <<: *default
  database: <%= ENV['MYSQL_NAME'] %>
  username: myuser
  password: <%= ENV['MYSQL_PASSWORD'] %>
  host: <%= ENV['MYSQL_HOST'] %>
  
```
以上で完了。
developmentとtestのdatabase名については変更したが名前はおそらくなんでも良い。
productionについては後述する。

#### 3. Dockerを導入(Dokcerfile)
参考教材の「Dockerの基礎」で示した教材を参考に以下のDockerfileを作成。
```Dockerfile
FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y nodejs default-libmysqlclient-dev
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]
```
基本的には山浦清透さんの動画を参考にして、2行目のRUNの部分はFarStep【プログラミング講座】さんを参考にしている。

entrypoint.sh /usr/binについては、Railsチュートリアルでいうところのrender-build.shの役割をになう。
entrypoint.shも記述する。
```entrypoint.sh
#!/bin/bash
# 上記はマジックコメントと呼ばれコンピュータが読み込む
# set -e はエラーが起きた時、一旦処理を中断させるコード
set -e

# server.pidが残っていると新たにサーバが起動できないエラーになるため削除
rm -f /tmp/pids/server.pid

# アセットプリコンパイルエラー対策。CSSやJSなどのアセットファイルをデプロイする時は事前にコンパイルする必要がある。この説明はRailsチュートリアルでもある。
bundle exec rails assets:precompile # いろんなファイルを一緒に実行
bundle exec rails assets:clean # assetsの使わないものを無効化

#　データベースの作成、マイグレーション、シードデータの投入
bundle exec rails db:create --trace
bundle exec rails db:migrate --trace
bundle exec rails db:seed --trace

# コマンドライン引数で指定されたコマンドを実行
exec "$@"
```
dockerコマンドによる起動手順は以下のようになる。
```
docker system prune -a
docker build -t tsunarhythm:v1 .
docker network create my_app_network
docker run -d --name db --network my_app_network -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 mysql:8.0.36
docker run -d --name web --network my_app_network -p 3000:3000 -v $(pwd):/app tsunarhythm:v1 bundle exec rails s -p 3000 -b '0.0.0.0'
http://0.0.0.0:3000/ へアクセス
```
system prune -aは起動していないコンテナとイメージを全削除するコマンドなので使用に注意が必要。すでに同名のコンテナやイメージを起動・作成しまわないように行っている。
Dockerイメージをビルドしてネットワークを作り、そのネットワークを指定してwebコンテナとdbコンテナをrunで作成・起動しています。
tsunarhythmはアプリ名です。RailsチュートリアルならSampleAppの部分です。-tでタグオプションとしてv1を付けています。latestだと後々問題が起こるからです。詳しくは後述しますができればUdemyの教材を見て下さい。
この時dbのポートはデフォルトの3306に設定し、mysqlのバージョンを現在最新のものを指定します。
webコンテナではRailsでのポート3000をどこからでもアクセスできるように-b '0.0.0.0'にして起動しています。
この辺りUdemyの教材とだいぶ違うと思いますがChatGPT4の助けもあってできました。

dockerコマンドによる停止手順は以下のようになる。
```
# コンテナIDを確認
docker ps
# 確認したIDをそれぞれwebIDとMySQLIDに打ち込む
docker stop webID 
docker stop MySQLID
docker system prune -a
```
#### 4. docker-composeの導入(docker-compose.yml)
docker-composeを導入するにあたってdatabase.ymlとdocker-compose.ymlは以下のようになる。
```docker-compose.yml
version: '3'

services:
  db:
    image: mysql:8.0.36
    platform: linux/x86_64 
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./db/mysql/volumes:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"

  web:
    image: tsunarhythm:v1
    platform: linux/x86_64
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - db
```
```config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: "password"
  host: db

development:
  <<: *default
  database: app_development

test:
  <<: *default
  database: app_test

production:
  <<: *default
  database: <%= ENV['MYSQL_NAME'] %>
  username: myuser
  password: <%= ENV['MYSQL_PASSWORD'] %>
  host: <%= ENV['MYSQL_HOST'] %>
```
基本的には山浦清透さんの動画を参考に作っている。
docker-compose.ymlのplatformでLinuxを指定しているのはdocker-compose upがLinux上で起動されるようになるのでOSが変わっても問題なく作動できるようにしている。
MYSQL_ROOT_PASSWORDをpasswordとして、database.ymlでもデフォルトをpasswordにして対応づける、hostもdbとしてdb　serviceを指定し対応づけることが重要。これがないと接続エラーだったかな、になる。
一点、Docker-composeの環境設定でMYSQL_USER: rootという記述をしてしまい、データベースの接続エラーが出たので注意が必要。
database.ymlのproductionについては後述する。

docker-composeコマンドによる起動手順は以下のようになる。
```
docker-compose build
docker-compose up -d
docker-compose down
http://localhost:3000/ へアクセス
```
#### 5. VPCの作成
1. マネジメントコンソールの展開
2. 東京リージョンの選択
2. 新しくVPCを作成(以下設定だと無料)
    2.「VPCなど」を選択
	2. 名前の設定(sawata-aws-container)
	3. AZの数は3、パプリックサブネットの数3、プライベートサブネットの数3
	4. NATゲートウェイなし、VPCエンドポイントなし
	5. DNSホスト名とDNS解決の有効化はオン
#### 6. RDSのサブネットグループの作成とDB作成
サブネットグループを作成する(RDS)
 	1. サブネットをグループを選択
	2. 名前の設定
	3. 作成したVPCを選択
	4. アベイラビリティゾーンはap-northeastの1c、1a、1dの全てを選択
	5. VPCサブネット一覧で名前にprivateがつくサブネットのIDをサブネット選択する
	6. 説明文を入力する。空欄だと進めないためサブネットの名前でよい。

RDSのデータベースを作成する。
 	1. 標準作成
	2. mySQL8.0.36を選択
	3. テンプレートは無料利用枠
	4. DBインスタンス識別子(DBの名前作成)
	5. マスターユーザー名を入力(今回はmyuser)
	6. マスターパスワードの入力
	7. インスタンス設定でdb.t2.microを選択(安いから)
	8. ストレージはデフォルト
	9. 接続で作成したVPCとサブネットグループを選択
	10. パブリックグルーブはなし
	11. 新しいVPCセキュリティグループ(ファイアウォール)を作成。名前を入力。
	12. アベイラビリティゾーン(AZ)は指定なし
	13.データベース認証はパスワード認証
	14. DB名を設定
	15. 自動バックアップを無料にする(料金を下げるため)
	16. データベースを作成をクリック
	※t2.microとストレージは有料だが12ヶ月無料枠がある場合は無料
	※RDSはインスタンスを停止してもストレージ料金が発生するうえ、7日間自動で起動する

 DB作成のマスターユーザー名はconfig/database.ymlのproductionのusernameと連動しているのでここを同一の名前に設定する必要がある。
同じくproductionのMYSQL_NAMEはDB名と連動し、MYSQL_PASSWORDはマスターパスワードと連動している。
MYSQL_HOSTはまた別で後述する。
```config/database.yml
~
production:
  <<: *default
  database: <%= ENV['MYSQL_NAME'] %>
  username: myuser
  password: <%= ENV['MYSQL_PASSWORD'] %>
  host: <%= ENV['MYSQL_HOST'] %>
```
#### 7. AWSアクセスキーとシークレットアクセスキーを作成してAWSCLIをインストールする。
IAM Userを作成して権利をアドミンとします。具体的には「Railsチュートリアル13.4.4 本番環境での画像アップロード」にあるので割愛します。
その上で開発環境でAWS CLIをインストールします。
AWS CLIの導入手順はすみません、以下を参照してください。
・公式: AWS CLI の最新バージョンのインストールまたは更新
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions

以下の記事も参考になりました。
・AWS ECRとDokcer連携する際のエラー 対処法
https://zenn.dev/iroristudio/articles/e165426f79d70a

・ECRにコンテナイメージをアップロードして起動してみる
https://www.ipride.co.jp/blog/8598

#### 8. ECRリポジトリの作成とイメージのプッシュ
ECRからリポジトリを作成します。名前を入力しタグのイミュータビリティをオンにしてください。リポジトリはプライベートリポジトリです。

> タグのイミュータビリティをオンにすることで、イメージタグが上書きされるのを防ぐようにリポジトリを設定できます。リポジトリをイミュータブルタグ用に設定した後、リポジトリに既に存在しているタグ付きイメージをプッシュしようとすると ImageTagAlreadyExistsException エラーが返されます。リポジトリでタグのイミュータビリティがオンになっている場合、これはすべてのタグに影響し、一部のタグをイミュータブルにすることはできません。引用:https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-tag-mutability.html

リポジトリにpushしたイメージのタグは自動だとlatestになるのですが、何度もイメージをpushするとどれもタグがlatestになってしまい、本当の最新バージョンがわからなくなってしまうので、dockerビルド時にタグでバージョンをしてするようにするのがスタンダートのようです。
先ほどbuildした時にv1タグをつけていた理由がこれですね。

リポジトリを開くとpushコマンドの表示があるので、そのコマンドをコピーしてAWS　CLIにログインします。

その後pushコマンドは無視して以下のコマンドを実行します。
```
docker system prune -a
docker build --platform linux/amd64 -t AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/tsunarhythm:v1 .
docker push 504252798833.dkr.ecr.ap-northeast-1.amazonaws.com/tsunarhythm:v1
```
docker system prune -aは起動していないコンテナと今あるイメージを全て削除するので気をつけてください。同じタグのイメージを作成しないための処置なのでdocker imagesでv1タグがなければ実行しなくても大丈夫です。v1タグのイメージだけ削除しても良いです。
その後Linuxをプラットフォームに指定してイメージをビルドします。これを行わないとイメージのフォーマットエラーがデプロイ時に発生するためです。これで何時間も取られました。
その後-t 以下は先ほど無視したpushコマンドを参考にすると良いでしょう。とくにAWSアカウントIDとイメージ名を適宜差し替えることに注意してください。

その後AWS ECRリポジトリにイメージがpushされていれば成功です。
#### 9. クラスタの作成
ECSからクラスターの作成
1. クラスターテンプレートでネットワークのみを選択
2. クラスタ名を入力
3. 作成

#### 10. タスク定義とタスク定義SGの作成
ECSのタスク定義で新しいタスク定義を作成
1. ECS on Fargateを選択し、名前を入力、タスクロールなし
2. オペレーティングシステムはLinuxタスクメモリとタスクCPUは一番小さいものを選択(安いので)
3. コンテナの追加をクリック
    1. 名前をアプリケーションの名前に設定
    1. イメージをECSの作成したリポジトリのイメージのURIを入力して追加
    1. コンテナポートを80から3000に変更
4. 環境変数を追加

| 環境変数のキー | 値 | 
|:-----------|------------:|
| AWS_ACCESS_KEY_ID|AWS CLIをインストールした時に作成したAWSアクセスキーを入力| 
| AWS_BUCKET|Railsチュートリアルの13.4.4を参照| 
| AWS_REGION|ap-cortheast-1|
| AWS_SECRET_ACCESS_KEY|AWS CLIをインストールした時に作成したAWSシークレットアクセスキーを入力| 
| MYSQL_HOST|RDSで作成したDBの詳細情報に記されているエンドポイント| 
| MYSQL_NAME|DB作成時の名前| 
|MYSQL_PASSWORD|DB作成時のマスターパスワード|
|RAILS_ENV|production|
|RAILS_LOG_TO_STDOUT|1|
|RAILS_MASTER_KEY|config/master.keyを参照|
|RAILS_SERVE_STATIC_FILES|1|

4. 作成をクリックしてタスク定義が作成される。修正したい時は新しいリビジョンを作成するで更新していく。
	※タスク定義はバージョン管理され各タスク定義をリビジョンといいナンバーが付与されて確認できる

タスク定義SGの作成と紐付け
1. VPCのセキュリティグループからセキュリティグループの作成
1. SGの名前を入力し作成したVPCを選択
1. サブネットはパブリック1を選択
1. タイプはカスタムTCPのポート3000、ソースをあらゆる場所(0.0.0.0)にする
1. 作成
1. タスク定義で新しいリビジョンを作成をクリックしてタスク実行ロールを作成したタスクSGにしてもう一度リビジョンを作成する
 
#### 11. ELB(ALB)の作成

1. ALBを選択
1. 名前を作成しスキームはインターネット向け、IPアドレスタイプはIPv4
1. VPCは作成したものを選択し、AZは3つ全てを選択。全てパブリックのものを選ぶ
1. ALBのSGを新規作成する。名前を入力しVPCも作成したものを選び残りはそのままで作成
1. ロードバランサー作成画面のsgを先ほど作成したものに選択しデフォルトは削除
1. リスナーとルーティングのプロトコルをHTTPの80番ポートにする※ALBが待ち受けるポート番号を指定している
1. ターゲットグループを新規作成をクリックし、IPアドレスを選択し名前を入力して次へ
    1. VPCは作成したものを使う
    2. IPアドレスは入力がないとAWS側が自動で設定してくれるので削除
	3. TGを作成する。これで空のTGが作成できたのでロードバランサー作成画面で選択
8. TGを作成
8. ALBの作成画面に戻り作成したTGを選択してALBを作成する
8. ロードバランサーがActiveになるのを確認

### 12. サービスを作成
1. ECSでクラスターを選択し、作成したクラスターからサービスタブの作成をクリックする
2. 起動タイプはFARGATE、オペレーティングシステムはLinux、サービス名の作成、タスク数は1
3. VPCは作成したVPCを選択、サブネットはパブリックのものを3つ選択
4. セキュリティグループは既存の先ほど作成したタスクsgを選択する
5. ALBを選択しロードバランサー名を先ほど入力したロードバランサー名(alb)にする
6. ロードバランス用のコンテナを既存のものにして80:HTTPと作成したコンテナターゲットにする
7. オートスケーリングはこの時点ではなしにしてサービスを作成する
作成したALBのセキュリティのインバウンドルールをHTTP80番、ソース0.0.0.0/0にしてルールを追加する

### 13. ALBとRDSのSGを編集
1. 作成したALBの詳細のセキュリティからインバウンドルールの編集をしてHTTPタイプのソース0.0.0.0に設定する
1. RDSから作成したDBの詳細のVPCセキュリティグループからインバウンドルールの編集をしてタイプMYSQL/Auroraで前に作成したタスクSGを設定する
### 14. レコードの重複エラーの対処
おそらくこのまま進めるとデプロイ時に以下のようなエラーが出るかもしれません。
```
・　Database '作成したDB名' already exists
・　ActiveRecord::RecordInvalid: Validation failed: Email has already been taken
```
これはECSへのデプロイを何度も実行したことでentrypoint.shの中の bundle exec rails db:seed が何度も実行されて、seeds.rb のコードが何度も実行されたことで発生したエラー(レコードの重複エラー)です。
なので原因は「seedデータを作成する際に重複チェックを実施していないこと」なので、下記の記事等を参考にしてそのあたりを修正すればエラーは出なくなります。
https://toshpit.com/rails-seeds-not-duplicate/

具体的にはdb/seeds.rbのユーザー情報のユーザー追記ファイルにてcreateコマンドをfind_or_create_byに変更させます。

変更前
```db/seeds.rb
# メインのサンプルユーザーを1人作成する
User.create!(name:  "Example User",
  email: "example@railstutorial.org",
  password:              "foobar",
  password_confirmation: "foobar",
  admin: true,
  activated: true,
  activated_at: Time.zone.now)

# 追加のユーザーをまとめて生成する
99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
    email: email,
    password:              password,
    password_confirmation: password,
    activated: true,
    activated_at: Time.zone.now)
end
# ユーザーの一部を対象にマイクロポストを生成する
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(word_count: 5)
  users.each { |user| user.microposts.create!(content: content) }
end

# ユーザーフォローのリレーションシップを作成する
users = User.all
user  = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }
```
変更後

```db/seeds.rb
# メインのサンプルユーザーを1人作成する
User.find_or_create_by(email: "example@railstutorial.org") do |user|
  user.name = "Example User"
  user.password = "foobar"
  user.password_confirmation = "foobar"
  user.admin = true
  user.activated = true
  user.activated_at = Time.zone.now
end

# 追加のユーザーをまとめて生成する
99.times do |n|
  name = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.find_or_create_by(email: email) do |user|
    user.name = name
    user.password = password
    user.password_confirmation = password
    user.activated = true
    user.activated_at = Time.zone.now
  end
end

# ユーザーの一部を対象にマイクロポストを生成する
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(word_count: 5)
  users.each { |user| user.microposts.create!(content: content) }
end

# ユーザーフォローのリレーションシップを作成する
users = User.all
user = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) unless user.following?(followed) }
followers.each { |follower| follower.follow(user) unless follower.following?(user) }
```
これで再度イメージ削除、イメージビルドとデプロイ手順を踏んでいけばエラーが消えました。

### 15.アセットプリコンパイルエラーの対処
おそらくこのまま進めるとデプロイ時に以下のようなエラーも出ます。
```
ActionView::Template::Error (The asset "rails.svg" is not present in the asset pipeline.
```
このエラーは、"rails.svg" というアセットがアセットパイプラインに存在しないために発生しています。
CSSやJavaScriptなどのアセットは開発環境では不要ですがデプロイ時には特有の設定をしなければいけません。
そこで以下のファイルのconfig.assets.compile = false を true に変更します。
```config/environments/production.rb
~
# config.assets.compile = false
 config.assets.compile = true
~
```
次に以下のコマンドをローカルで実行して下さい。
```
docker-compose build
docker-compose up -d
docker-compose exec web /bin/bash
RAILS_ENV=production bundle exec rake assets:precompile
```
開発環境を立ち上げてwebコンテナの中に入りアセットをプリコンパイルしています。
再度ECRのイメージ削除からサービスの更新までやってみてください。

## まとめ
上記の手順を踏んだのちにALBにあるDNSネームをコピーしてhttp://DNSネーム に問題なくアクセスできていれば成功です！
エラーが出た場合は新しいリビジョンをクラスターのサービス更新の際に選択できているかを確認し、サービスのログからエラー文章を探します。このエラー文章が時間が経つと消えてしまうので注意が必要です。
駆け足でしたがぜひ参考にしてみて下さい。