はじめに
2016年前半くらいまではローカル環境をVagrant
で仮想化していました。
それ以降は徐々にDocker
を使って構築するようになりました。
しかし、本番環境は、インフラチームが構築してくれたサーバーにdeployerなどのデプロイツールを使ってデプロイしていました。
ただ、せっかくローカルとはいえDocker
化できているのに、それをそのまま本番環境にもっていけないのは、もったいないし非効率なのは否めません。
「のぼりーさんのクラウドインフラPodcast」でも、その話題に触れていますので、興味ある方は聴いてみてください。
今回は、Laravel
アプリケーションをローカルでも本番でも全く同じDocker
コンテナでホストすることを目指して、AWS ECSにDocker
環境をデプロイしてみます。
プロジェクト概要
実際にデプロイしたアプリのソースコード、Dockerfile
などはgithub
にて公開しています。
ディレクトリ構成
標準的なLaravel
アプリケーションに、docker
ディレクトリを作成して、Dockerfile
などを格納しています。
├── app
├── bootstrap
├── config
├── database
├── docker
│ ├── nginx
│ │ ├── Dockerfile
│ │ └── site.conf
│ ├── nodejs
│ │ └── Dockerfile
│ └── php-fpm
│ └── Dockerfile
├── node_modules
├── public
├── resources
├── routes
├── storage
├── tests
├── artisan
├── buildspec.yml
├── composer.json
├── composer.lock
├── docker-compose.yml
├── package-lock.json
├── package.json
├── phpunit.xml
├── README.md
├── server.php
└── webpack.mix.js
docker-compose.yml
version: '3.2'
services:
nginx:
build:
context: .
dockerfile: ./docker/nginx/Dockerfile
ports:
- "80:80"
depends_on:
- php-fpm
php-fpm:
build:
context: .
dockerfile: ./docker/php-fpm/Dockerfile
nodejs:
build:
context: .
dockerfile: ./docker/nodejs/Dockerfile
volumes:
- .:/build
working_dir: /build
nginx
コンテナ
public
ディレクトリ配下のファイルはnginx
コンテナに格納します。
# docker/nginx/Dockerfile
FROM nginx
ADD ./docker/nginx/site.conf /etc/nginx/conf.d/default.conf
RUN mkdir -p /app/public
ADD ./public/ /app/public/
静的ファイルは、nginx
コンテナにて配信するように設定しています。
それ以外のリクエストは、php-fpm
コンテナに送信します。
# docker/nginx/site.conf
server {
index index.php index.html;
server_name localhost;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /app/public;
location ~ ^.+/\.(png|jpg|jpeg|gif|ico|css|js|html?)$ {
try_files $uri $uri/ =404;
}
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
php-fpm
コンテナ
アプリのソースコードは、volumes
で共有するのではなく、コンテナ内にコピーして、コンテナ内でセットアップします。
ビルド時間を短縮するために、hirak/prestissimoをインストールしています。
FROM php:7-fpm
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y git zlib1g-dev zip unzip
RUN docker-php-ext-install zip
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
RUN composer global require hirak/prestissimo
RUN mkdir -p /app
ADD . /app/
WORKDIR /app
RUN composer install
RUN cp .env.example .env
RUN php artisan key:generate
RUN chmod -R a+w storage/ bootstrap/cache
ECS環境の構築
ECS環境を構築する手順をざっくりと説明します。
ECR(Elastic Container Registry)の構築
Docker
イメージ用のリポジトリを作成します。
nginx
とphp-fpm
用のリポジトリを作成します。
下記のようにnamespace
をつけることも可能です。
laravel-ecs-demo/nginx
laravel-ecs-demo/php-fpm
ECRへのログイン
ECRへawscli
を使ってログインします。
get-login コマンドは AWS CLI のバージョン 1.9.15 以降で利用できます。
ただし、Docker の最新バージョン (17.06 以降) を使用している場合は、バージョン 1.11.91 以降をお勧めします。
AWS CLI のバージョンは、aws --version コマンドを使用して確認できます。
私の環境でもバージョンが1.11.91
以前だったので、アップグレードしました。
$ pip install awscli --upgrade
$ $(aws ecr get-login --no-include-email --region ${AWS_DEFAULT_REGION})
nodejs
コンテナのビルド、アセット(css/js)のコンパイル
nodejs
コンテナは、ECRにはプッシュしませんが、アセット(css/js)のコンパイルのためにビルドします。
$ docker build -t nodejs -f docker/nodejs/Dockerfile .
$ docker run --rm -v $(pwd):/build -w /build nodejs npm install
$ docker run --rm -v $(pwd):/build -w /build nodejs npm run dev
ビルドしたアセットは、nginx
コンテナ内に配置されます。
public/css/app.css
public/js/app.js
nginx
/php-fpm
コンテナのビルド
$ docker build -t ${REPOSITORY_URI_NGINX}:latest -f docker/nginx/Dockerfile .
$ docker build -t ${REPOSITORY_URI_PHP_FPM}:latest -f docker/php-fpm/Dockerfile .
Dockerイメージへのタグ付け
Dockerイメージに対して、タグ付けします。
$ docker tag ${REPOSITORY_URI_PHP_FPM}:latest
$ docker tag ${REPOSITORY_URI_NGINX}:latest
Dockerイメージのプッシュ
タグ付けされたイメージをリポジトリにプッシュします。
$ docker push ${REPOSITORY_URI_PHP_FPM}:latest
$ docker push ${REPOSITORY_URI_NGINX}:latest
タスク定義
項目 | 値 |
---|---|
起動タイプの互換性 | EC2 |
タスク定義名 | laravel-ecs-demo |
php-fpm
コンテナ
項目 | 値 |
---|---|
コンテナ名 | php-fpm |
イメージ | ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/php-fpm:latest |
作業ディレクトリ | /app |
メモリ制限(MB) | ハード制限 300(MB) |
nginx
コンテナ
項目 | 値 |
---|---|
コンテナ名 | nginx |
イメージ | ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/nginx:latest |
メモリ制限(MB) | ハード制限 300(MB) |
リンク | php-fpm |
ポートマッピング | tcp 80:80 |
ECSクラスタの作成
項目 | 値 |
---|---|
クラスターテンプレートの選択 | EC2 Linux + Networking |
クラスター名 | laravel-ecs-demo-cluster |
EC2 インスタンスタイプ | t2.small |
プロビジョニングモデル | Spot |
スポットインスタンスの配分戦略 | 最低価格 |
インスタンス数 | 2 |
Maximum bid price (per instance/hour) | 10($) |
ALB(Application load balance)の作成
ECSクラスタの作成が完了したら、ALB(Application load balance)を作成します。
作成したECSクラスタに紐づくVPCを選択しないと、ブラウザからアクセスしても、502/503が返ってくるだけなので、注意してください。
項目 | 値 |
---|---|
アベイラビリティゾーン | (ECSクラスタ内のVPCから選択) |
リスナー | HTTP 80 |
ターゲット | (アベイラビリティゾーンから選択) |
サービスの作成
項目 | 値 |
---|---|
起動タイプ | EC2 |
タスク定義 | laravel-ecs-demo |
クラスタ | laravel-ecs-demo-cluster |
サービス名 | laravel-ecs-demo |
サービスタイプ | REPLICA |
ELB タイプ | Application Load Balancer |
負荷分散用のコンテナ | nginx:80:80 |
リスナーポート | 80:HTTP |
Target Group | (作成したALBに紐づくターゲットグループを選択) |
動作確認
ここまで問題がなければ、ECSにコンテナが配置され、ブラウザからアクセスできるようになるはずです。
http://xxxxx.${AWS_REGION}.elb.amazonaws.com
Laravel on ECS Demo
の文字が表示されれば成功です。
苦戦したところ
サービスが起動しない
サービスがいつまでたっても起動しないので、イベントを確認すると、下記のメッセージが出力されていました。
service laravel-ecs-demo was unable to place a task because no container instance met all of its requirements. The closest matching container-instance xxxxxxxxxxxxx has insufficient memory available. For more information, see the Troubleshooting section.
下記の記事にもありますが、原因はメモリ不足のようです。
ECSクラスタを作り直してもいいのですが、なぜかクラスタの削除に失敗してしまうので、Cloud Formation
にて変更セットを作成・実行するのがオススメです。
インスタンスタイプが非力すぎる
今回の例だとt2.micro
では起動できませんでした。
t2.small
に変更したら、メモリ不足にはならなくなりました。
スポットインスタンスの最高入札額が低すぎる?
インスタンスタイプをt2.small
にしているのに起動しないことがあったので、試しにMaximum price (per instance/hour)
を5
から10
にCloud Formation
から設定変更したところ起動するようになりました。
入札額が低すぎると、需要と供給の関係で落札できない?ってことがあるんでしょうか。
継続的に使うインスタンスであれば、オンデマンドインスタンスにしたほうが良さそうです。
ECSクラスタの削除に高確率で失敗する
逆に成功したことがないくらい必ず失敗します。
いずれAWSが解決してくれるとは思いますが、メモリ不足などの理由で構成を変更したい場合は、Cloud Formation
から構成を変更しましょう。
ブラウザからアクセスしても502
/503
になる
原因はおそらく2つあって、いずれも設定ミスです。
ターゲットグループにターゲットインスタンスを登録していない
ALBを(再)作成したタイミングで、ターゲットグループにターゲットインスタンスを登録し忘れてしまうと、単純にリクエストを送信する先がいないのでエラーになります。
ターゲットインスタンスが何も表示されない
ECSクラスタで作成したVPCとは異なるVPCを選択してしまっている可能性があります。
ECSクラスタが削除できないので、何個も作ってしまうと間違えやすくなるので注意しましょう。
おわりに
ローカルで動かしている環境をそのままデプロイできるのは安心感あるので、色々なプロジェクトでECS使っていきたいと思いました。
CodePipeline
/CodeBuild
を使ったデプロイ自動化の話は別記事にしていますので、興味ある方はこちらもご覧になってください。
ではでは。