北川です。
Symfony アプリを AWS Elastic Beanstalk Multi-Container Docker を用いて構築・運用する事例の紹介です。
弊社が提供するフォームブリッジというサービスは実際にこの記事の内容に基づき作られています。
なおベストプラクティスを模索中だったり、生々しい妥協案も出てくるので暖かい目で読んでいただきますと幸いです。
構成
- Nginx
- Symfony, Composer
- Vue, Yarn
フロントエンドとバックエンドがある程度分離されたSPA
Symfonyアプリでの事前準備
EB での運用を快適にするためにはアプリ側にもいくつか要件があります。
環境変数の設定
アプリの振る舞いを変える方法を環境変数のみにしておきます。
Symfony では parameters.yaml
を配置して振る舞いを変えますが、parameters.yaml から環境変数を読み込ませる機能がありますのでこれを利用します。Symfony 3系でも使えます。
parameters:
database_host: '%env(DATABASE_HOST)%'
database_port: '%env(int:DATABASE_PORT)%'
database_name: '%env(DATABASE_NAME)%'
database_user: '%env(DATABASE_USER)%'
database_password: '%env(DATABASE_PASSWORD)%'
注意点として、環境変数にセットした値がデフォルトでは string でパースされるのでポート番号など型を指定したい場合は '%env(int:DATABASE_PORT)%
のように記述する必要があります。
環境変数は EB の環境プロパティでセットします。
ログを標準出力に出す
必要なログを標準出力に出し CloudWatch にストリーミングするようしておくと ssh で接続してファイルを取得といったことをする必要が無くなります(後述)。
また Docker でコンテナ内のログファイルを永続化するのは面倒です。
このためアプリケーションログはファイルでは無く標準出力に出しておくと便利です。
monolog を使用した例です。
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
nested:
type: stream
path: "php://stdout"
level: debug
console:
type: console
CI やテストがやかましくなるので test env ではファイルに出力するようにしておくと良いでしょう。
ジョブのAPI化
バッチサーバーなどを EB で運用する場合、Worker環境を使うことになります。
詳細はドキュメントを読めば分かりますが、Woker環境の仕組み上、HTTPリクエストを受けジョブを実行する流れになります。
逆に言えば、Web環境とまったく同じ構成でWorker環境を作ることもできます。FormBridge でも Web と Worker は同じ構成です。
例えば、バッチサーバーで以下の cron を実行している場合、Symfony の Controller にアクションを追加し処理を移行すれば良いです。スケジューリングは cron.yaml という仕組みが用意されています(後述)。
0 9 * * * php /var/www/html/app/bin/console app:hoge
非同期ジョブなどはWorker環境では以下のように処理します。
- SQS にメッセージを送信
- aws-sqsd がメッセージを取得し指定のpathにHTTPリクエスト
- ジョブを実行
この場合、SQS にメッセージを送信する部分とジョブのAPI化の2箇所を対応する必要があります。
Docker のビルド
ECR
イメージのリポジトリには ECR を利用しています。タグ運用をする場合はイメージタグの変更可能性を変更不可にしておくと事故が減ります。
PHP Dockerfile
FROM composer:version as composer
WORKDIR /app
COPY . /app/
RUN composer install --no-dev
FROM php:7.2-fpm
WORKDIR /app
RUN apt-get update && apt-get install -y ...
RUN docker-php-ext-install ...
RUN pecl install ...
ADD ./path/to/ini/*.ini $PHP_INI_DIR/conf.d/
COPY . /app/
COPY --from=composer /app/vendor /app/vendor
RUN cp app/config/parameters_env.yaml app/config/parameters.yaml
RUN chown -R www-data:www-data /app
ところどころぼかしていますが、これが Symfony アプリのイメージビルドに使用している Dockerfile です。ソースコードを丸ごとコピーしているように見えますが .dockerignore
で不要なディレクトリをコピーしないようにしています。parameters_env.yaml
は、先ほどの環境変数を読み込むようにした parameters.yaml
のことです。最後にデフォルトの PHP-FPM 実行ユーザーの www-data にパーミッションを変更しています。
Nginx Dockerfile
FROM node:version as node
WORKDIR /app
ADD ./package.json /app/
ADD ./yarn.lock /app/
RUN yarn install
RUN yarn build
FROM nginx:version
ADD ./path/to/nginx.conf /etc/nginx/nginx.conf
ADD ./path/to/default.conf /etc/nginx/conf.d/default.conf
COPY ./document-root /app/document-root
COPY --from=node /app/document-root/dist /app/document-root/dist
ところどころぼかしていますが、これが Nginx での最も単純な Dockerfile です。Nginx には静的コンテンツを管理している document-root
と webpack でのビルドの成果物のみを配置します。
ところが、使用しているライブラリによってはこう単純にはいかず、実際には以下のような Dockerfile を使っています。
FROM composer:version as composer
WORKDIR /app
ADD . /app
RUN composer install
FROM node:version as node
WORKDIR /app
ADD . /app/
RUN yarn install
COPY --from=composer /app/document-root/js /app/document-root/js
RUN yarn build
FROM nginx:version
ADD ./path/to/nginx.conf /etc/nginx/nginx.conf
ADD ./path/to/default.conf /etc/nginx/conf.d/default.conf
ADD ./document-root /app/document-root
COPY --from=composer /app/vendor /app/vendor
COPY --from=composer /app/document-root/bundles /app/document-root/bundles
COPY --from=node /app/document-root/dist /app/document-root/dist
node のマルチステージビルドでは、 composer でインストールしたライブラリに同梱された js/css が document-root/js
に落ち、その中から必要な js/css を yarn build
の成果物に含めるようにしています。どんな状況だよという感じですね。
下の方では composer でインストールしたライブラリに同梱された js/css が document-root/bundles
に落ちてくるパターンに対応しています。document-root/bundles
からさらにシンボリックリンクが vendor
以下に貼られる場合もあるので vendor
もイメージに含めています。vendor
は js/css のみを目当てに配置しているということです。つらい。
Elastic Beanstalk 設定ファイルの管理
ディレクトリ構成
EB 管理用リポジトリは下記のようなディレクトリ構成を取ります。
.gitignore
eb
├── production
│ ├── worker
│ │ ├── .ebextensions
│ │ │ ├── ...
│ │ │ └── cache_clear.config
│ │ ├── .elasticbeanstalk
│ │ │ └── config.yaml
│ │ ├── Dockerrun.aws.json
│ │ └── cron.yaml
│ └── web
│ ├── .ebextensions
│ │ ├── cache_clear.config
│ │ ├── ...
│ │ └── migrate_db.config
│ ├── .elasticbeanstalk
│ │ └── config.yaml
│ └── Dockerrun.aws.json
└── staging
├── worker
│ ├── .ebextensions
│ │ ├── ...
│ │ └── cache_clear.config
│ ├── .elasticbeanstalk
│ │ └── config.yaml
│ ├── Dockerrun.aws.json
│ └── cron.yaml
└── web
├── .ebextensions
│ ├── cache_clear.config
│ ├── ...
│ └── migrate_db.config
├── .elasticbeanstalk
│ └── config.yaml
└── Dockerrun.aws.json
production と staging の2種類のEBアプリケーションがあり、それぞれのEBアプリケーションはworker環境とweb環境を持ちます。
各環境には .ebextensions/, .elasticbeanstalk/, Dockerrun.aws.json が配置されています。
config.yaml
デプロイの利便性のため .elasticbeanstalk/config.yaml をソース管理に含めています。EB CLI コマンド の eb init
を実行した場合、デフォルトではソース管理から外されますがあえてソース管理しています。
# .gitignore
# Elastic Beanstalk Files
eb/**/.elasticbeanstalk/*
!eb/**/.elasticbeanstalk/config.yaml #追加
config.yaml にはEBアプリケーション名および環境名を記述します。
branch-defaults:
default:
environment: web
...
global:
application_name: formbridge-production
...
各環境ディレクトリに対して eb deploy
を実行することでデプロイを行います。
弊社では完全な自動化はしていませんが、さらに .ebextensions 以下に config を追加したりJSONドキュメントを活用することで eb deploy
だけでなく eb init
や eb create
による自動化も達成できるはずです。
参考
Dockerrun.aws.json
各環境毎の Dockerrun.aws.json では ECR リポジトリとイメージタグの指定やログの設定を行なっています。
# Dockerrun.aws.json
{
"AWSEBDockerrunVersion": 2,
"containerDefinitions": [
{
"name": "php",
"essential": true,
"memory": 1024,
"image": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-repository-name:tag",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-group": "/aws/elasticbeanstalk/containers/php"
}
}
},
{
"name": "nginx",
"essential": true,
"memory": 256,
"image": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-repository-name:tag",
"links": [
"php"
],
"portMappings": [
{
"containerPort": 80,
"hostPort": 80
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-group": "/aws/elasticbeanstalk/containers/nginx"
}
}
}
]
}
Dockerrun.aws.json のコンテナ定義セクションとボリュームセクションは、Amazon ECS タスク定義ファイルの対応するセクションと同じ形式を使用できますので、logConfiguration を設定し CloudWatch へログをストリーミングしています。awslogs-group
に対応するロググループを CloudWatch 上で手で作成する必要があります。
cron ジョブ
Worker環境に cron.yaml を配置すると cron のように定期的なタスクを実行することができます。
version: 1
cron:
- name: "hoge"
url: "/hoge"
schedule: "0 0 * * *"
注意点としては、スケジューリングをUTCで指定する必要があることと、FIFOキューとの併用ができないことです(2019.8 時点)。実際に動かしてみると cron.yaml
を元にキューにメッセージを詰める aws-sqsd がエラーを出すので単に対応していないようです。このためFIFOキューを使うWorker環境が欲しい場合、cronジョブを捌く環境とは別の環境を用意すると良さそうです。
.ebextensions
Symfony アプリの運用では cach_clear.config
と migrate_db.config
があると便利なので紹介します。それぞれDockerコンテナ起動後に実行されるコマンドです。
上記のディレクトリ構成ではこれらのファイルが重複し冗長な構成となっていますが、別にいいんじゃないかなという気持ちです。
# cache_clear.config
files:
"/opt/elasticbeanstalk/hooks/appdeploy/post/99_cache_clear.sh":
mode: "000755"
owner: root
group: root
content: |
#!/usr/bin/env bash
docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` php bin/console cache:clear -e prod
docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` chown -R www-data:www-data /app/var/cache
Symfony は Cache まわりがよく問題となるので、どこかのタイミングで php bin/console cache:clear
を実行し Cache を削除しておくと安心です。この例では root ユーザーで実行しているので PHP-FPM の実行ユーザー www-data にパーミッションを戻しています。
# migrate_db.config
files:
"/opt/elasticbeanstalk/hooks/appdeploy/post/10_post_migrate.sh":
mode: "000755"
owner: root
group: root
content: |
#!/usr/bin/env bash
if [ -f /tmp/leader_only ]
then
rm /tmp/leader_only
docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` php bin/console doctrine:database:create --if-not-exists
docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` php bin/console doctrine:migrations:migrate --allow-no-migration
fi
container_commands:
01_migrate:
command: "touch /tmp/leader_only"
leader_only: true
migrate_db.config
のポイントは冗長化されていても1回だけ実行するためにリーダーインスタンスのみで実行していることです。
デプロイ
ローリングデプロイ
EB コンソールで次のような設定をしておくとデプロイ時にコンテナ起動中の状態で公開されてしまい 502 Bad GateWay といった事態を無くすことができます。
手順
最後にこれまでのまとめとしてデプロイの手順を紹介します。例として eb/production/web
の環境にデプロイします。
vi eb/production/web/Dockerrun.aws.json
docker build -t nginx-image /path/to/nginx/Dockerfile
docker build -t php-image /path/to/php/Dockerfile
aws ecr get-login --no-include-email --region ap-northeast-1
docker tag nginx-image:latest xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-repository:tag
docker tag php-image:latest xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-repository:tag
docker push xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-repository:tag
docker push xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-repository:tag
cd eb/production/web
eb deploy
あとはこれと似たようなものをそれぞれの環境についても記述し、お好みのツールとビルド環境で自動化すればおしまいです。
トヨクモでは一緒に働いてくれるエンジニアを募集しております。
採用に関する情報を公開しております。
気になった方はこちらからご応募ください。