LaravelアプリをDockerfile, ECR, ECS, RDSを使用してデプロイする。


こんにちは。
[KEN](https://twitter.com/ken_0205_jp)と申します。

## 簡単な自己紹介
2020年4月から渋谷のとある企業でWebエンジニアをしてます、エンジニア一年生です。
エンジニアの研修でDockerを使用して開発環境を構築し、それをAWSの各種サービスを使用してコンテナデプロイをしました。
初めての経験で、コンテナデプロイ関連の記事も少なかったため、今回は自分の頭の整理を兼ねて記事を書くことにしました。

## 概要
今回の記事では、Laravelが動く環境をdockerで作成し、簡単なアプリを制作し、それをAWSのECR, ECS, RDS等のサービスを使用してデプロイするまでの流れについて書いていきます。

## 対象読者
Docker, AWSで何かアプリを制作、デプロイしてみたい人。
Dockerコンテナをデプロイしてみたい人。

## 環境
Docker version 19.03.8
docker-compose version 1.25.4

# DockerでLaravelが動く環境を作る

基本的にこのフェーズはこの記事を参考にさせて頂きましたので、この記事を参考に進めてください。

[【初心者向け】20分でLaravel開発環境を爆速構築するDockerハンズオン](https://qiita.com/ucan-lab/items/56c9dc3cf2e6762672f4)

一部ファイル構成、docker-compose.ymlの記述が上記サイトと異なる部分がありますので、その点について説明します。


今回の最終的なディレクトリ構成は以下です。
    
    .
    ├── README.md
    ├── docker
    │   ├── mysql
    │   │   └── my.cnf
    │   ├── nginx
    |   |   |__ Dockerfile
    │   │   └── default.conf
    │   └── php
    │       ├── Dockerfile
    │       └── php.ini
    ├── docker-compose.yml
    ├── logs
    │   ├── access.log
    │   ├── error.log
    │   ├── mysql-error.log
    │   ├── mysql-query.log
    │   ├── mysql-slow.log
    │   └── php-error.log
    └── src(アプリケーションのルートディレクトリ)
        └── readme.md


### docker-compose.yml
    
    version: "3"
    services:
      app:
        build:
          context: .
          dockerfile: ./docker/php/Dockerfile
          args:
            - TZ=${TZ}
        ports:
          - ${APP_PORT}:8000
        volumes:
          - ./src:/work
          - ./logs:/var/log/php
          - ./docker/php/php.ini:/usr/local/etc/php/php.ini
        working_dir: /work
        environment:
          - DB_CONNECTION=mysql
          - DB_HOST=db
          - DB_DATABASE=${DB_NAME:-homestead}
          - DB_USERNAME=${DB_USER:-homestead}
          - DB_PASSWORD=${DB_PASS:-secret}
          - TZ=${TZ:-Asia/Tokyo}

      web:
        build:
          context: .
          dockerfile: ./docker/nginx/Dockerfile
        depends_on:
          - app
        ports:
          - ${WEB_PORT:-80}:80
        volumes:
          - ./src:/work
          - ./logs:/var/log/nginx
          - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
        environment:
          - TZ=${TZ:-Asia/Tokyo}

      db:
        image: mysql:8.0
        volumes:
          - db-store:/var/lib/mysql
          - ./logs:/var/log/mysql
          - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
        environment:
          - MYSQL_DATABASE=${DB_NAME:-homestead}
          - MYSQL_USER=${DB_USER:-homestead}
          - MYSQL_PASSWORD=${DB_PASS:-secret}
          - MYSQL_ROOT_PASSWORD=${DB_PASS:-secret}
          - TZ=${TZ:-Asia/Tokyo}
        ports:
          - ${DB_PORT:-13306}:3306

    volumes:
      db-store:

### docker/php/Dockerfile
    
    FROM php:7.4-fpm-alpine
    COPY ./src /work
    COPY ./docker/php/php.ini /usr/local/etc/php/php.ini
    LABEL maintainer="ucan-lab <info@u-can.tech>"

    SHELL ["/bin/ash", "-oeux", "pipefail", "-c"]

    # tinker(psysh)
    ARG PSYSH_DIR=/usr/local/share/psysh
    ARG PSYSH_PHP_MANUAL=$PSYSH_DIR/php_manual.sqlite
    ARG PHP_MANUAL_URL=http://psysh.org/manual/ja/php_manual.sqlite

    # timezone
    ARG TZ=Asia/Tokyo

    # composer
    ENV COMPOSER_ALLOW_SUPERUSER 1
    ENV COMPOSER_HOME /composer

    RUN apk update && \
      apk add --update --no-cache --virtual=.build-dependencies \
        autoconf=~2.69 \
        gcc=~9.2 \
        g++=~9.2 \
        make=~4.2 \
        tzdata=2020a-r0 \
        git=~2.24 && \
      apk add --update --no-cache \
        icu-dev=~64.2 \
        libzip-dev=~1.5 \
        oniguruma-dev=~6.9 && \
      cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
      echo ${TZ} > /etc/timezone && \
      pecl install xdebug && \
      git clone https://github.com/phpredis/phpredis.git /usr/src/php/ext/redis && \
      apk del .build-dependencies && \
      docker-php-ext-install intl pdo_mysql mbstring zip bcmath redis && \
      docker-php-ext-enable xdebug && \
      mkdir $PSYSH_DIR && curl $PHP_MANUAL_URL -o $PSYSH_PHP_MANUAL && \
      curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer && \
      composer config -g process-timeout 3600 && \
      composer config -g repos.packagist composer https://packagist.jp && \
      composer global require hirak/prestissimo


### docker/nginx/Dockerfile
    
    FROM nginx:1.17-alpine
    COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf
    COPY ./src /work
    EXPOSE 80

### docker/nginx/default.conf

    server {
        listen 80;
        root /work/public;
        index index.php;
        charset utf-8;
        error_log /var/log/nginx/error_test.log;

        location / {
            root /work/public;
            try_files $uri $uri/ /index.php$is_args$args;
        }

        location ~ \.php$ {
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            fastcgi_pass app:9000;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
        }
    }

# ECRのリポジトリを作成
AWSのコンソール画面にログインして、次の画像の画面にアクセスしてください。
ここで２つのリポジトリを作ります。

nginxのイメージを保存するリポジトリとphp-fpmのイメージを保存するリポジトリの二つです。

<img width="1058" alt="スクリーンショット 2020-05-04 17.39.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/f47b3443-f937-9436-f41d-2d4508845ba1.png">


# ECRにDockerイメージをPushする

次にそれぞれのリポジトリにDockerイメージをPushしていきます。
下の画像のように、プッシュコマンドの表示というボタンがあるので、それを押して出てきたコマンドに従って進めてください。
`AWS CLI`がインストールされていることが前提です。

<img width="1767" alt="スクリーンショット 2020-05-04 17.40.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/472bf869-4974-3352-6980-6c82af72e9e7.png">

<img width="1302" alt="スクリーンショット 2020-05-04 17.40.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/e14e103e-418b-0d12-2076-0203264195ce.png">

# ECSのタスク定義を作成

次にタスク定義を作成します。
タスク定義とは、アプリケーションを構成する 1 つ以上(最大 10 個)のコンテナを記述するJSON形式のテキストファイルです。
アプリケーションの設計図のようなものです。

下記の画面で新しいタスク定義の作成をクリックしてください。
<img width="1792" alt="スクリーンショット 2020-05-04 17.54.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/62dc1a34-c809-62a1-2bb3-a04948e88aaf.png">

今回はタスクの起動タイプを`EC２`に指定します。
<img width="1271" alt="スクリーンショット 2020-05-04 17.54.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/b0adaaab-dfc7-558f-ce8c-d03b289e4ff2.png">


続いてタスク定義名を決め、`コンテナの追加`をクリックします。
<img width="1792" alt="スクリーンショット 2020-05-04 18.22.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/673fd81e-0db3-1f68-c9a0-ca180b0bf85f.png">
<img width="1770" alt="スクリーンショット 2020-05-04 18.22.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/1274f5bd-e5b8-449d-c4b4-bfa86752d4b8.png">

下の画像のように`appコンテナとwebコンテナ`を作成してください。
まずはappコンテナの定義です。
<img width="1123" alt="スクリーンショット 2020-05-04 18.28.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/db95266f-d495-f377-b9c9-6f6e2fae775d.png">

コマンドの蘭に、`コンテナ起動時に実行したいコマンド`を入力しておきます。
あとは環境変数も指定します。私は本番環境では.env.productionを使用しているので、`APP_Env=production`と指定しています。
<img width="1161" alt="スクリーンショット 2020-05-04 18.28.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/086fbfb2-1296-2bf2-e9d1-964f2f56d9e5.png">

続いてWebコンテナを定義しましょう。
<img width="1132" alt="スクリーンショット 2020-05-04 18.37.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/3a471f01-28bd-37ea-6d14-109f558bf239.png">

こちらも作業ディレクトリと環境変数を指定します。
<img width="1145" alt="スクリーンショット 2020-05-04 18.37.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/505dc39d-9ae3-9eef-d922-52efc07749b2.png">

Webコンテナのネットワーク設定のリンクにはapp(appコンテナのこと)と記述しましょう。
docker/nginx/default.confで`fastcgi_pass app:9000;`という記述があるので、ここと合わせるためappと記述します。
<img width="1128" alt="スクリーンショット 2020-05-04 18.40.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/cca84615-54c2-6a90-3940-807ef6057ac3.png">

これでタスク定義を作成してください！


# ECSのクラスターを作成
次にクラスターを作成します。
今回は`EC２Linux + ネットワーキング`を選択して作成します。

<img width="1325" alt="スクリーンショット 2020-05-04 18.45.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/f9b17b25-964e-2a95-0329-5994ff1e5b0f.png">

EC2インスタンスタイプは`t2.micro`、後はご自身のキーペアを選択してください。
<img width="1125" alt="スクリーンショット 2020-05-04 18.46.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/224effbf-7ff6-334c-1892-cd3a7aa46a0b.png">

<img width="828" alt="スクリーンショット 2020-05-04 18.47.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/05fc91d9-f262-c1a1-b5f0-bb4b0a04bb13.png">

これでクラスターを作成します。

# タスクの起動

先ほど作成したタスク定義の画面に行ってください。
そこからアクションをクリックして、`タスクの実行`をクリックします。
<img width="1097" alt="スクリーンショット 2020-05-04 18.55.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/bb585223-79d6-9a0e-95a1-e6fe3131c92a.png">

次の画面でタスク定義とクラスターをそれぞれご自身が先ほど作成したものを選択し、実行をクリックします。
<img width="1405" alt="スクリーンショット 2020-05-04 18.55.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/99d7d3b8-c470-a97e-f2f6-80da9d9c2921.png">

（すみません、この画像ではタスク定義とクラスター名がsample-appになっていません。この記事の流れではこの二つは本来sample-appという名前になります。）

続いてクラスターの画面に行き、先ほど実行したタスクのステータスが`RUNNING`になっていることを確認してください。
<img width="857" alt="スクリーンショット 2020-05-04 19.01.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/0e5c25d4-ca98-076f-8851-d19e893f1d8a.png">

これでEC2インスタンスの`パブリックIPアドレス`にアクセスすれば/srcディレクトリ以下に作成したアプリのトップページが表示されるはずです。


# RDSの作成　

データベースを使用するアプリの場合は`RDS`を作成しましょう。
RDSの画面を開いたらデータベースの作成をクリックします。
<img width="1787" alt="スクリーンショット 2020-05-04 21.10.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/66dd46d5-13f1-0937-7be5-63cfe67cc73a.png">

`標準作成`で今回は`MySQL`を使用します。（Dockerの開発環境もMySQlを使用しているので）
<img width="902" alt="スクリーンショット 2020-05-04 21.11.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/c1d655e9-a8a2-1f2e-8fa4-a2497a9aa19c.png">

`無料利用枠`で、`マスターユーザー名`と`マスターパスワード`を決めます。
<img width="838" alt="スクリーンショット 2020-05-04 21.11.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/a6bfd8f1-422b-d2ff-fa7e-9e6323e900d4.png">


追加設定でアプリに利用するデータベースも作成しておきましょう。
今回は`sample_app`という名前でデータベースを作成しておきます。
<img width="812" alt="スクリーンショット 2020-05-04 21.13.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/bb647850-42c8-bd16-f008-d7bf7a93fb15.png">

これで作成をクリックしてください！

作成されたデータベースは、RDSダッシュボードの「データベース」で確認できます。


# RDSの設定
EC2インスタンスからDBインスタンスにアクセスするために、
「セキュリティグループのルール」から使用中のセキュリティーグループをクリックし、インバウンドの編集でアドレス入力の箇所に`EC２のプライベートIPアドレス/32`を入力してください。

RDSで構築したMySQLは、日本語の保存ができないそうです。
日本語で保存できるように設定を変更します。`パラメータグループの設定を、日本語データを保存できるように変更する必要があるみたいです！`
詳しくはググってみてください。ここでは割愛します。

# PHPのコンテナ内でマイグレーション

では最後です。
/srcディレクトリの下に、`env.production`を作成します。
ECSのタスク定義で、appコンテナ、webコンテナ両方にAPP_ENV=productionという記述をしたので、本番環境ではこの.env.productionが参照されます。
記述は.envの内容とほとんど同じで大丈夫ですが、一部変更します。
下記を参考に記述してください。

    
    DB_CONNECTION=mysql
    DB_HOST=RDSのエンドポイントを記述
    DB_PORT=3306
    DB_DATABASE=sample_app(RDSの追加設定で作成したデータベース名)
    DB_USERNAME=root
    DB_PASSWORD=RDS作成の際に設定したマスターパスワード

ではsshでEC2インスタンスに接続してください。
ssh接続するには、EC2インスタンスのセキュリティーグループのインバウンドで`22番ポート`のアクセスを受け入れる設定ができている必要があります。
下記手順で進めていってください。
    
sshが機能するには、キーが公開されていないことが必要なので、キーペアのアクセス権を変更する。
    
    chmod 400 xxx.pem(ご自身のキーペア)

sshでEC2インスタンスのパブリックDNSに接続する
    
    ssh -i "xxx.pem" ec2-user@ec2-xx-xxx-xxx-xxx.ap-northeast-1.compute.amazonaws.com

EC2インスタンスに接続できたら、MySQLのソフトウェアをインストールする。
    
    sudo yum -y install mysql
    
MySQLに接続する(rootは今回のマスターユーザー名、xxxの所にはエンドポイントを記述する)
    
    mysql -u root -p -h XXX.xxx.ap-northeast-1.rds.amazonaws.com


MySQLへ接続できたことが確認できたら、:qで接続を終了します。

次に`docker ps`で今起動しているコンテナのIDを確認し、下記のコマンドでapp(phpが動いているコンテナ)の内部に入りましょう。
    
    docker exec -i -t コンテナID

そして下記コマンドでマイグレーションを実行します。
    
    php artisan migrate


無事マイグレーションが完了し、テーブルが作成されていることが確認できたら、EC2インスタンスのパブリックIPアドレスにアクセスしてみましょう。
トップページが表示されるだけでなく、新規登録機能など、DBが絡む機能も動くようになっているはずです。


以上が、LaravelアプリをECR, ECS, RDSを使用してデプロイする手順です。

最後までお付き合いくださりありがとうございました！！


### 補足
説明が至らない所、私の認識が間違っていて、おかしな説明がされている所があるかもしれません。
何かありましたらコメント等で教えて頂きますと幸いです。