おはようこんにちはこんばんは!エンジニアの大石です。
弊社のサービスを丸ごとAmazon Elastic Container Service(ECS)に移行したので、何回かに分けてその取り組みを紹介したいと思います!
今回は第一回、コンテナイメージ作りについてお話したいと思います。
はじめに
ECSへの移行に至ったきっかけ
弊社のサービスは元々AWS Elastic Beanstalkというサービスの上にLaravelのアプリケーションを載せて動かしていました。Elastic Beanstalkとは簡単に言うとEC2インスタンス上に一通り揃ったアプリケーションの実行環境を作れてデプロイが簡単に出来るサービスのことですね。
一見簡単に運用できるように見えるElastic Beanstalkですが、初回の導入は簡単な反面、どうしてもEC2なのであとあとPHPのバージョンアップなどのミドルウェアの更新があると環境を作り直すのが大変だったりします。
そこで、環境をコードで構築してイメージにアプリケーションと実行環境をひとくくりにしてデプロイ出来るコンテナベースの環境(ECS、Lambda、EKSなどなど)は環境の変化に柔軟に対応出来るため、レガシーからの脱却と将来的な投資を込めて今回ECSに移行するに至りました。
コンテナって?コンテナイメージって?
簡単に説明しますと、コンテナイメージはコンテナを立ち上げるためにテンプレートのようなもので、コンテナはイメージから起動した実体... といった形です。
AWSの公式の記事にとても分かりやすい解説があるので、今回は細かい話は置いておいてイメージ作りにフォーカスしてお話したいと思います。
コンテナイメージに入れるものを整理する
Laravelを動かすぞ!!といっても大元にphp-fpmが必要だったり、アプリケーションから依存するpeclモジュールが必要だったり... などなどアプリケーションの構成によって必要なものが変わってきます。
そこで、まずアプリケーションを動かすのに何が必要かを洗い出していきましょう。
弊社のアプリケーションを例に挙げると、以下のものが必要であることが分かりました。
- アプリケーションのLaravelのコードそのもの
- php-fpm
- nginx
- composer
- aptで導入できる各種ライブラリ(libpng, libjpegなど)
- PHPの各種extension(pdo_mysql, opcacheなど)
- AWS CloudWatch Agent(アプリケーションのログの転送に使用)
必要なものが分かってきたので、実際に必要なコンテナを配置していきます。
そして、弊社のアプリケーションの場合、最終的なコンテナやECSのサービスの構成としてはこのような形になりました。
コンテナイメージに関してはアプリケーションが入ったイメージとリバースプロキシ(nginx)が入ったイメージの2つが必要なことが分かってきましたね!
ちなみに、運用のコストを削減するためにLaravelのアプリケーションが入ったイメージは1つの共通のイメージを使用して、コンテナ起動時に起動コマンドを書き換える形で動かしています。
余談ですが、ECSでコンテナの実行コマンドを上書きする方法、Laravelのschedule workerやqueue workerをコンテナ上でいい感じに(しっかり安全にシャットダウンするなどなど)動かす方法については別途記事にする予定なのでお楽しみに!
コンテナイメージを作る
ECSといっても通常のDocker同様で、Dockerfileを書いてDockerイメージを組み立ててそれを何らかのコンテナレポジトリ(今回の場合はECR)にpushして使う形になります。
nginxのイメージを作る
Docker Hubを見に行くとnginxのイメージ(https://hub.docker.com/_/nginx)は既に用意されているので、それを元に必要な設定ファイルをひとまとめにしたイメージを作りました。
FROM nginx:1.21.6 COPY ./docker-prod/nginx/nginx.conf /etc/nginx/ COPY ./docker-prod/nginx/default.conf /etc/nginx/conf.d/
とてもかんたんですね!
あらかじめ用意しておいた設定ファイルをコピーして起動すればすぐにnginxが使える状態にしておきました。
アプリケーションのイメージを作る
こちらも同様にPHPの実行環境が入ったイメージ(https://hub.docker.com/_/php)が公式から提供されているので、ウェブサーバを動かす前提になっているphp-fpmのイメージを使って組み上げていきます。
といってもこちらはアプリケーションコンテナのビルドになり、ブログに載せるにはあまりにも長くなってしまうので、要所のみ記載します。
ちなみに、おおまかなイメージの作り方はこちらの記事が分かりやすくておすすめです。
Composerをインストールする
COPY --from=composer:2.2.7 /usr/bin/composer /usr/bin/composer
実はこれもCOPYコマンドで超簡単に1行で出来ちゃったりします。公開されているイメージから特定のファイルだけを持ってくることが出来るので、curlでダウンロードしてきて...のようなことは実は要らなかったりします。
AWS CloudWatch Agentをコンテナに入れる
弊社のアプリケーションではファイル上に書き出したログを収集してCloudWatchに転送するといったことをElasticBeanstalkで運用していた時代は行ってましたが、これを継続して行いたいのでAgentを入れてあげて転送できるようにしました。
(CloudWatch Logsにアプリケーションから直接AWSのAPIを通してログを転送する方法もありますが、ログを書き込む度に毎回通信が生じてしまいAPIのレートリミットに引っかかるといった問題が起こりがちなので、一旦ファイルに書き込んでおいて裏側でAgentから転送する方法が個人的にはおすすめです)
入れるといっても今回もCOPY技で簡単に入れてあげます。
COPY --from=amazon/cloudwatch-agent:1.247350.0b251780 /opt/aws/amazon-cloudwatch-agent /opt/aws/amazon-cloudwatch-agent COPY docker-prod/app/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json COPY docker-prod/app/cloudwatch-agent-common-config.toml /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml
設定ファイルの記載方法については公式ドキュメントを参照して書けば完了です!
ちなみに、CloudWatch AgentはENTRYPOINTに指定されているスクリプトを書き換えて裏側でnohupで立ち上げてあげることで、Agentのことを意識せずに動かせるようにしてあります。
#!/bin/sh set -e # Agentを裏側で実行する nohup /opt/aws/amazon-cloudwatch-agent/bin/start-amazon-cloudwatch-agent > /dev/null 2> /dev/null & # first arg is `-f` or `--some-option` if [ "${1#-}" != "$1" ]; then set -- php-fpm "$@" fi # ユーザ切り替え後に/dev/stderrと/dev/stdoutをappユーザから触れないのでパーミッションを変えておく chmod 777 /dev/stderr chmod 777 /dev/stdout # appユーザで指定されたコマンドを実行する(php-fpmなど) sudo -u app "$@"
USER app
などでDockerfileの最後にユーザを切り替えてしまうとAgentが正常に作動しないので、rootとして最初は実行して実アプリケーションはユーザを降格して動かすという技を使って動かしています。
ENTRYPOINTのスクリプトはDockerfile内でCOPYして書き換えてあげると簡単に動かせる状態になります。
COPY --chmod=775 docker-prod/app/docker-php-entrypoint /usr/local/bin/docker-php-entrypoint
ビルドが遅い.... 遅すぎるぞ!!!!!
さて、弊社のアプリケーションのデプロイはCircleCIから行っている都合でビルドもCI上で行っていますが、どうしてもCIサーバ上だとライブラリのダウンロードが遅かったりPHPのエクステンションのビルドが重たかったりします。
レイヤーキャッシュ*1をしっかり効かせるのも大事ですが、CircleCIのビルドサーバからキャッシュが時間が経つと揮発してしまう*2ことがありデプロイの度にとても待たされることが多くありました。
そこで、レイヤーキャッシュのみならずBuildKit*3も有効化してマルチステージビルドにしている箇所についてはしっかり並列ビルドが走るようにしました。
composer installをマルチステージビルドに持っていってみる
今回は並列で処理できそうなcomposerのパッケージのダウンロードと展開処理を並列で実行できるようにしてみました。
FROM composer:2.2.7 AS composer-cache WORKDIR /var/www/html # キャッシュ作成用にcomposer.jsonとcomposer.lockだけ持ってくる COPY composer.json composer.lock /var/www/html/ # ダウンロードと展開のみ行う RUN composer install --no-scripts --no-autoloader --ignore-platform-reqs FROM php:7.4-fpm WORKDIR /var/www/html # ここで重たいモジュールのビルド処理などを行う # ~~各インストール、ユーザ作成処理は省略~~ # キャッシュ展開!!! COPY --from=composer-cache --chown=app:app /var/www/html /var/www/html # アプリケーションコードのコピーとcomposer install実行 COPY --chown=app:app . /var/www/html RUN composer install
BuildKitが有効化された状態でこれを実行するとダウンロードは並行して裏側で行ってくれて、 composer install
は実質オートローダの生成のみで爆速で終わるので依存ライブラリが多い方は試してみると良いかと思います。
高速化の結果、レイヤーキャッシュが効いた状態で2分程度(キャッシュなしで5分程度)でビルドが完了するようになりました。
何もしていないときは10分かかっていたビルドが5分、キャッシュありで2分まで縮められたので大きな進歩です!!!
僕はいつもなが〜〜いDockerイメージのビルドが走ってる間は推しのアイドル*4のことを考えながら過ごしてましたが、流石にその時間が長すぎるのも辛いので高速化して浮いた時間で仕事を早く終わらせてアイドル現場に行ったほうが良いですね!
(完全に余談ですが、最近弊社では #アイドル部 が出来ました!!好きな人が勝手に集まってやっている小さな部活動ですが、地下アイドルなど様々なアイドルが好きなひとが集まってます!)
さいごに
Dockerやコンテナの運用は非常に奥が深く語ると記事に書ききれない程ハマりポイント、気をつけなければならないポイントがあったりするので、記事では移行にあたり特筆すべきものだけ紹介させていただきました。
最後に... M&Aクラウドでは現在エンジニアやPdMなど幅広く募集中です!!もし興味を持ってくださった方はお気軽にお声がけください!
*1:イメージビルドの過程で変更が無いレイヤを再利用する仕組みのこと https://circleci.com/docs/ja/2.0/docker-layer-caching/
*2:CircleCIのドキュメントによると3日で揮発するとのことです https://circleci.com/docs/ja/2.0/docker-layer-caching/#how-dlc-works
*3:並列的な依存関係の解決などを備えたビルドツールキットのこと。オプトインすることで使えるようになる。 https://docs.docker.jp/v19.03/develop/develop-images/build_enhancements.html
*4:筆者の推しはAppare!の藍井すずちゃんです