Rails, Dockerで動くアプリをAWS Elastic Beanstalkにデプロイするまでの全て

# この記事の目的
- Beanstalkを用いて、Docker × railsでアプリをデプロイする
- その際、railsのコンテナとnginxのコンテナの2つを立ち上げるので、マルチコンテナ用Dockerプラットフォームを用いる。

です。
ひとまず、Beanstalkで用意してくれている簡易的な（汚い）URLで、httpでアクセスできるようにするところまでいきましょう！！
また、自分のアプリをデプロイする前に、<a href="https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create_deploy_docker_ecstutorial.html" target="_blank">公式チュートリアル</a>で練習しておくのが得策かと思われます（僕はそのようにしてまず感覚を掴みました。）

# 環境
- docker
- rails 5.2
- MySQL 5.6 (本番環境ではRDSを利用）
- nginx 1.18.0（本番環境のみ）

# Elastic Beanstalkについて
>Elastic Beanstalk では、アプリケーションを実行しているインフラストラクチャについて学習することなく、AWS クラウドでアプリケーションをすばやくデプロイし、管理できます。
Elastic Beanstalk は、選択肢を狭めたり制御を制限したりすることなく、管理の複雑さを軽減します。アプリケーションをアップロードするだけで、Elastic Beanstalk が自動的に容量のプロビジョニング、負荷分散、拡張、およびアプリケーションの状態のモニタリングといった詳細を処理します。
引用：https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/Welcome.html

簡単に言えば、インフラにリソースをあまり割けない or インフラについて学習歴が浅いようなスタートアップなどに適している、簡単にアプリケーションをデプロイ出来るサービスです。（逆にいうと、beanstalkの裏で何が起きているのかに関して深く把握していません。）
具体的には以下のような利点が挙げられます。

- 手動で設定する項目が少ない
- GUIでの作業が多い
- `eb`コマンドを用いて様々な作業が可能

# Elastic Beanstalkの全体像

1. ローカルで本番用のimageをbuild
2. 2つのimageをECRにpush
3. BeanstalkがECRからimageをpullし、EC2上でコンテナを起動

※ 1と2のimageのbuild, pushは毎回行う必要はない（というか、gemを追加した時などのみかと）
※ 1でローカルでimageを作成する際は本番環境用のdocker-compoase.prod.ymlを使用しています。開発環境用のdocker-compose.ymlと本番環境用のdocker-compose.prod.ymlを分けています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348384/77628f0f-541c-539a-6e53-a83487a412e2.png)



# デプロイまでの作業の全体像
1. ソースコードの変更
2. ECRへのpush
3. Beanstalkで環境の作成
4. RDSの作成
5. デプロイ

では、次から一つずつ進めていきましょう！！！

# 1. ソースコードの変更

## ファイル構造（抜粋）

```
├── Dockerfile
├── Dockerrun.aws.json
├── Gemfile
├── Gemfile.lock
├── README.md
├── app
├── config
│   ├── application.rb
│   ├── database.yml
│   ├── environments
│   │   └── production.rb
│   └── puma.rb
├── containers
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
├── docker-compose.prod.yml
├── docker-compose.yml
├── log
├── public
└── tmp
```


- nginxのDockerfileの作成

```dockerfile:containers/nginx/Dockerfile
FROM nginx:1.18.0

# インクルード用のディレクトリ内を削除
RUN rm -f /etc/nginx/conf.d/*

# Nginxの設定ファイルをコンテナにコピー
ADD nginx.conf /etc/nginx/conf.d/appname.conf

# ビルド完了後にNginxを起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

- nginxの設定ファイルの作成

```config:containers/nginx/nginx.conf
upstream appname {
  server unix:/var/www/appname/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name .*;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  root /var/www/appname/public;

  # アップロードできるファイルの最大サイズ
  client_max_body_size 100m;

  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;

  location / {
    try_files $uri @appname;
  }

  location @appname {
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://appname;
  }
}
```

- config/puma.rbの変更

unixソケット通信を行うための設定を追記します。

```ruby:config/puma.rb
（追記）
app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"
```

- app(rails）コンテナ作成用のDockerfileに、tmp/sockersを作成するように追記します。これは、config/puma.rbで設定したunixソケット通信を行うためのファイルを作成するためです。

```dockerfile:Dockerfile
（追記）
RUN mkdir -p tmp/sockets
```

- docker-compose.prod.ymlの作成

本番環境用と開発環境用ではimageが異なります。あとでECRにpushする本番環境用のimageを立ち上げるために、開発環境用とは分けてdocker-composeファイルを作ります。具体的には、以下の点が異なります。

  - 本番環境ではmysqlのコンテナが不必要になる（RDSに接続するため）
  - 本番環境ではnginxのコンテナが必要になる
  - 本番環境用のgemをインストールするために、`RAILS_ENV=production`を渡してimageをたてる


```yml:docker-compose.prod.yml
version: '3.7'
services:
  app:
    build: .
    command: bundle exec puma -C config/puma.rb
    environment:
      - RAILS_ENV=production
    volumes:
      - .:/var/www/appname
      - tmp-prod-data:/var/www/appname/tmp

  web:
    build:
      context: containers/nginx
    volumes:
      - tmp-prod-data:/var/www/appname/tmp
    ports:
      - 80:80
    depends_on:
      - app

volumes:
  tmp-prod-data:
```

- config/database.yml

RDSに対応する設定を記述します。これらの環境変数はbeanstalkの管理画面からRDSインスタンスを作成すると自動で渡されます。

```yml:comfig/database.yml
（追記）
production:
  <<: *default
  database: <%= ENV['RDS_DB_NAME'] %>
  username: <%= ENV['RDS_USERNAME'] %>
  password: <%= ENV['RDS_PASSWORD'] %>
  host: <%= ENV['RDS_HOSTNAME'] %>
  port: <%= ENV['RDS_PORT'] %>
```

- Dockerrun.aws.jsonの作成

こちらはelastic beanstalk固有の設定ファイルです。イメージとしては、docker-compose.ymlの本番用という感じです。
具体的には、以下のような設定を記述します。詳細は<a href="https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create_deploy_docker_v2config.html", target="_blank">AWS公式のドキュメント</a>を参照

  - 作成するコンテナの名前
  - pullするimageの保存先
  - 作成する・マウントするvolume
  - コマンドやポート番号の指定
  - マシンの設定（メモリは小さいとrailsコマンドが動かないことがあります。最初は128MBを指定していましたが、メモリを増やしたら解決しました。各自のプランに合わせてご変更下さい）
  - などなど

```json:Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": 2,
  "volumes": [
    {
      "name": "source-bundle",
      "host": {
        "sourcePath": "/var/app/current"
      }
    },
    {
      "name": "public-data",
      "host": {
        "sourcePath": "/var/app/current/public"
      }
    },
    {
      "name": "tmp-data"
    }
  ],
  "containerDefinitions": [
    {
      "name": "appname-app",
      "image": "◯◯◯◯.dkr.ecr.region.amazonaws.com/appname_app:latest",
      "essential": true,
      "memory": 2000,
      "command": ["bundle","exec","puma","-C","config/puma.rb"],
      "mountPoints": [
        {
          "sourceVolume": "source-bundle",
          "containerPath": "/var/www/appname"
        },
        {
          "sourceVolume": "tmp-data",
          "containerPath": "/var/www/appname/tmp"
        },
        {
          "sourceVolume": "awseb-logs-appname-app",
          "containerPath": "/var/www/appname/log"
        }
      ]
    },
    {
      "name": "nginx",
      "image": "◯◯◯◯.dkr.ecr.region.amazonaws.com/appname_web:latest",
      "essential": true,
      "memory": 2000,
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ],
      "links": [
        "appname-app"
      ],
      "dependsOn": [
        {
          "containerName": "appname-app",
          "condition": "START"
        }
      ],
      "mountPoints": [
        {
          "sourceVolume": "tmp-data",
          "containerPath": "/var/www/appname/tmp"
        },
        {
          "sourceVolume": "public-data",
          "containerPath": "/var/www/appname/public"
        },
        {
          "sourceVolume": "awseb-logs-nginx",
          "containerPath": "/var/log/nginx"
        }
      ]
    }
  ]
}
```

- .dockerignoreの作成

ECRにpushする際に、imageに不必要なファイル（特に容量の大きなログなど）が含まれているとサイズが肥大化します。dockerignoreを作成しておくことをおすすめします。

```text:.dockeringnore
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
/public/uploads
/spec/reports/
/spec/examples.txt
/test/tmp/
/test/version_tmp/
/.env
.idea
```

- .gitignoreへの追加

beanstalkの本番ログをローカルに持ってくる（`eb logs --all`コマンド）ときに作成される.elastikbeanstalkディレクトリや、assets:precompile時に作成される/public/assetsディレクトリを追記します。（precompileをする必要性については後に説明します）

```text:.gitignore
（追記）
/public/assets

# Elastic Beanstalk Files（これらは自動で追加されました）
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
```

- .ebignoreの作成

`eb deploy`コマンドによるデプロイ時に、デプロイするソースに含めないものを記述します。基本的にgitignoreと変わらないので、新規ファイルだけ作成し、中身はgitignoreと同じものを記述します。
ただし、1点だけ違いがありがあり、ebignoreには/public/assetsを含めません。理由としては、`eb deploy`をする直前に`assets:precompile`を実行してpublic/assets配下にコンパイルされたファイルを作成するからです。


# 2. ECRへのpush

ローカルでimageを立て、ECRにpushします（必ずしもECRである必要はなく、dockerhubなどでも可能です）。

- 手順1. `docker-compose -f docker-compose/prod.yml build`
app image(rails)とweb image(nginx)を作成します。
- 手順2. AWS CLIを利用してECRにログイン
- 手順3. 作成したimageにタグをつける
- 手順4. ECRにpush

手順1~4ともに、AWSのGUIとコマンドラインを同時に使います。
手順2~4は、ECRの画面の中で以下のように指示がありますので、それにしたがって実行してもらえれば良いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348384/1ddad0fd-e57f-13f2-7d78-52c6028d4c23.png)


# 3. Beanstalkでの環境の作成

AWSのGUIでの作業になります。
<a href="https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create_deploy_docker_ecstutorial.html" target="_blank">チュートリアル</a>を参考にしてください。

設定一覧

項目 | 選択
--- | ---
アプリケーション名 | 任意
環境名 | アプリケーション名による自動生成
プラットフォーム | Docker
プラットフォームのブランチ | Multi-container Docker running on 64bit Amazon Linux 
アプリケーションコード | コードのアップロード（ローカルファイルをアップロードします）

最後のコードのアップロードのところは、Dockrun.aws.jsonがあれば問題ないです。
「環境の作成」をクリックすると環境が作成され、以下のような画面が表示されます。10分以内くらいに完了します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348384/8aca176c-8d16-1e1d-f005-039067a63232.png)


# 4. RDSの作成

<a href="https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/using-features.managing.db.html" target="_blank">公式ドキュメント</a>を参考に進めます。
Beanstalkの左側のバーから[設定] > [データベース] > [編集]と進みます。

データベースの作成がGUIで完了したら、ソースコードのdatabase.ymlの変更だけを行えば接続ができます。
database.ymlで、production用に以下のように環境変数を読み込んでいますが、こちらに関しては自分で環境変数を用意する必要はありません。
RDSを作成した時点でこちらの環境変数が渡されます。

```yml:comfig/database.yml
（追記）
production:
  <<: *default
  database: <%= ENV['RDS_DB_NAME'] %>
  username: <%= ENV['RDS_USERNAME'] %>
  password: <%= ENV['RDS_PASSWORD'] %>
  host: <%= ENV['RDS_HOSTNAME'] %>
  port: <%= ENV['RDS_PORT'] %>
```

# 5. デプロイ

## Beanstalkでのデプロイの概要
beanstalkでのデプロイ作業は、「ソースをアップロードすること」「そのための設定（Dockerrun.aws.json）を書くこと」で構成されます。
beanstalkではデプロイの指示がなされると、Dockerrun.aws.jsonによって指定された（ECR上の）imageと、アップロードしたソースコードによってデプロイが実行されます。
imageを変更する必要はほとんどないので、基本的にはソースコードのアップロードによって行われます。
ちなみにアップロードされたソースは、EC2インスタンス上の`/var/app/current`に保存されています。
（ただしこれだけでは、containerのソースとアップロードしたソースが同期されず、デプロイした内容が変更されません。そこで、Dockerun.aws.jsonの設定で`var/app/current`のvolumeを作成し、指定したcontainerにマウントします。）

## デプロイの方法
ソースバンドルをどのようにEC2上にアップロードするか、の方法が大きく2つあります。

1. `eb deploy`コマンドを叩く
2. ソースバンドル（複数のファイルを束ねたzipファイル）をGUIからアップロードする

僕の場合はbeanstalkでデプロイできる（httpでアクセスできるようになる）までは、まだEB CLIをいれていなかったので2の方法で行い、途中から1に切り替えて今もずっと1で運用しています。
herokuに近い感覚でデプロイできるので1はおすすめです（これがbeanstalkの良さであるとAmazon公式サポートの人が言ってた）

## 1. eb deployコマンドでデプロイ

beanstalkでは、EB CLIを利用して様々なコマンドを扱うことができます。
※ EBコマンドを利用するためには、<a href="https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/eb-cli3-install.html" target="_blank">公式のチュートリアル</a>を参考に準備をします。

デプロイの手順は、
`eb deploy （環境名)`
で終わり！！

ただし、assets配下を変更した場合は、`assets:precompile`をする必要があります。
これは僕自身が詰まったところなのですが、ソースバンドルをアップロードしたあとに、EC2上にSSHでログインして、docker container上で`assets:precompile`を実施→containerを再起動、とした場合にcontainerが落ちてしまいました。ゆえに、今は先にローカルでassets:precompileをzisshishite`/public/assets`配下にコンパイルされたファイルを作ってからデプロイしています。

## 2. ソースバンドルをGUIからアップロードする
EB CLIを利用せずに、自分でソースバンドル（zipファイル）を作成し、beanstalkの管理画面からアップロードすることも可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348384/00b95386-5cc3-523f-3dd5-67fff866d2d4.png)

ソースバンドルの作成は<a href="https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/applications-sourcebundle.html#using-features.deployment.source.commandline" target="_blank">こちらの公式ドキュメント</a>に書かれています。
僕は、`git archive -v -o myapp.zip --format=zip HEAD`によって作成していました。
これはgitignoreのファイルを無視してソースバンドルが作成されます。

# まとめ
以上で、httpでデプロイするところまではいけましたでしょうか？
僕自身が、このQiitaをまとめるのが作業をしてから1ヶ月以上経っていたため、若干うろ覚えのところがあったのでミスがあったら申し訳ないです。
ぜひコメントよろしくお願いします。

今回の作業は、<a href="https://voil-intern.com" target="_blank">長期インターンの口コミサイトVoil</a>のインフラをherokuからAWSに移行する際に実施しました。学生で、長期インターンを探している人がいたらぜひ見てみてください！