M&Aクラウドを丸ごとAmazon Elastic Container Service(ECS)に移行しました!〜コンテナイメージ作り編〜

f:id:kazuki09:20220324123449p:plain

おはようこんにちはこんばんは!エンジニアの大石です。

弊社のサービスを丸ごとAmazon Elastic Container Service(ECS)に移行したので、何回かに分けてその取り組みを紹介したいと思います!

今回は第一回、コンテナイメージ作りについてお話したいと思います。

はじめに

ECSへの移行に至ったきっかけ

弊社のサービスは元々AWS Elastic Beanstalkというサービスの上にLaravelのアプリケーションを載せて動かしていました。Elastic Beanstalkとは簡単に言うとEC2インスタンス上に一通り揃ったアプリケーションの実行環境を作れてデプロイが簡単に出来るサービスのことですね。

一見簡単に運用できるように見えるElastic Beanstalkですが、初回の導入は簡単な反面、どうしてもEC2なのであとあとPHPのバージョンアップなどのミドルウェアの更新があると環境を作り直すのが大変だったりします。

そこで、環境をコードで構築してイメージにアプリケーションと実行環境をひとくくりにしてデプロイ出来るコンテナベースの環境(ECS、Lambda、EKSなどなど)は環境の変化に柔軟に対応出来るため、レガシーからの脱却と将来的な投資を込めて今回ECSに移行するに至りました。

コンテナって?コンテナイメージって?

簡単に説明しますと、コンテナイメージはコンテナを立ち上げるためにテンプレートのようなもので、コンテナはイメージから起動した実体... といった形です。

aws.amazon.com

AWSの公式の記事にとても分かりやすい解説があるので、今回は細かい話は置いておいてイメージ作りにフォーカスしてお話したいと思います。

コンテナイメージに入れるものを整理する

Laravelを動かすぞ!!といっても大元にphp-fpmが必要だったり、アプリケーションから依存するpeclモジュールが必要だったり... などなどアプリケーションの構成によって必要なものが変わってきます。

そこで、まずアプリケーションを動かすのに何が必要かを洗い出していきましょう。

弊社のアプリケーションを例に挙げると、以下のものが必要であることが分かりました。

  • アプリケーションのLaravelのコードそのもの
  • php-fpm
  • nginx
  • composer
  • aptで導入できる各種ライブラリ(libpng, libjpegなど)
  • PHPの各種extension(pdo_mysql, opcacheなど)
  • AWS CloudWatch Agent(アプリケーションのログの転送に使用)

必要なものが分かってきたので、実際に必要なコンテナを配置していきます。

そして、弊社のアプリケーションの場合、最終的なコンテナやECSのサービスの構成としてはこのような形になりました。

f:id:kazuki09:20220324144444p:plain
構成図

コンテナイメージに関してはアプリケーションが入ったイメージとリバースプロキシ(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のイメージを使って組み上げていきます。

といってもこちらはアプリケーションコンテナのビルドになり、ブログに載せるにはあまりにも長くなってしまうので、要所のみ記載します。

ちなみに、おおまかなイメージの作り方はこちらの記事が分かりやすくておすすめです。

www.digitalocean.com

Composerをインストールする

COPY --from=composer:2.2.7 /usr/bin/composer /usr/bin/composer

実はこれもCOPYコマンドで超簡単に1行で出来ちゃったりします。公開されているイメージから特定のファイルだけを持ってくることが出来るので、curlでダウンロードしてきて...のようなことは実は要らなかったりします。

qiita.com

AWS CloudWatch Agentをコンテナに入れる

弊社のアプリケーションではファイル上に書き出したログを収集してCloudWatchに転送するといったことをElasticBeanstalkで運用していた時代は行ってましたが、これを継続して行いたいのでAgentを入れてあげて転送できるようにしました。

(CloudWatch Logsにアプリケーションから直接AWSAPIを通してログを転送する方法もありますが、ログを書き込む度に毎回通信が生じてしまい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

設定ファイルの記載方法については公式ドキュメントを参照して書けば完了です!

docs.aws.amazon.com

ちなみに、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も有効化してマルチステージビルドにしている箇所についてはしっかり並列ビルドが走るようにしました。

qiita.com

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 は実質オートローダの生成のみで爆速で終わるので依存ライブラリが多い方は試してみると良いかと思います。

f:id:kazuki09:20220323192529p:plain

高速化の結果、レイヤーキャッシュが効いた状態で2分程度(キャッシュなしで5分程度)でビルドが完了するようになりました。

何もしていないときは10分かかっていたビルドが5分、キャッシュありで2分まで縮められたので大きな進歩です!!!

僕はいつもなが〜〜いDockerイメージのビルドが走ってる間は推しのアイドル*4のことを考えながら過ごしてましたが、流石にその時間が長すぎるのも辛いので高速化して浮いた時間で仕事を早く終わらせてアイドル現場に行ったほうが良いですね!

(完全に余談ですが、最近弊社では #アイドル部 が出来ました!!好きな人が勝手に集まってやっている小さな部活動ですが、地下アイドルなど様々なアイドルが好きなひとが集まってます!)

さいごに

Dockerやコンテナの運用は非常に奥が深く語ると記事に書ききれない程ハマりポイント、気をつけなければならないポイントがあったりするので、記事では移行にあたり特筆すべきものだけ紹介させていただきました。

最後に... M&Aクラウドでは現在エンジニアやPdMなど幅広く募集中です!!もし興味を持ってくださった方はお気軽にお声がけください!

www.wantedly.com

www.wantedly.com

www.wantedly.com

*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!の藍井すずちゃんです