Slide 1

Slide 1 text

PHPコンテナの マルチステージビルドと キャッシュ戦略 くすのき/@na_it_o

Slide 2

Slide 2 text

内容と話す人 PHP初心者がPHPアプリケーションのDockerイメージについて考えた話 ふだんはAWSをTerraformとGo使っていじりたおしている

Slide 3

Slide 3 text

やりたいこと さくさくすばやくビルドされて 軽量でとりまわしやすい PHPのコンテナをつくりたい

Slide 4

Slide 4 text

Dockerの基本

Slide 5

Slide 5 text

レイヤーキャッシュ . ├── Dockerfile ├── app.js ├── bin/ ├── package.json ├── package-lock.json ├── public/ ├── routes/ └── views/ # syntax=docker/dockerfile:1 FROM node:16 # Create app directory WORKDIR /usr/src/app # Install app dependencies # copy package.json AND package-lock.json COPY package*.json ./ RUN npm install # Bundle app source COPY . . EXPOSE 8080 CMD [ "node", "server.js" ]

Slide 6

Slide 6 text

レイヤーキャッシュ . ├── Dockerfile ├── app.js ├── bin/ ├── package.json ├── package-lock.json ├── public/ ├── routes/ └── views/ # syntax=docker/dockerfile:1 FROM node:16 # Create app directory WORKDIR /usr/src/app # Install app dependencies # copy package.json AND package-lock.json COPY package*.json ./ RUN npm install # Bundle app source COPY . . EXPOSE 8080 CMD [ "node", "server.js" ] update use cache re-build

Slide 7

Slide 7 text

マルチステージビルド 中間イメージを利用可能 最終イメージ - レイヤーを少なく - 余計なファイルを排除

Slide 8

Slide 8 text

Best Practices for writing Dockerfiles Best practices for writing Dockerfiles | Docker Documentation https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ Use multi-stage builds - if your build contains several layers, you can order them from the less frequently changed (to ensure the build cache is reusable) to the more frequently changed Don’t install unnecessary packages Minimize the number of layers

Slide 9

Slide 9 text

……ってコト!? ● レイヤーを意識して効率よくキャッシュされるように ● 最終イメージは少ないレイヤーで必要最低限のファイルを含むように

Slide 10

Slide 10 text

PHPでやってみる

Slide 11

Slide 11 text

Laravelのサンプルアプリでやってみる ローカル実行用のDockerfileと docker-composeが用意されている 1. Dockerfileの追加 2. docker-compose.ymlの変更 $ curl -s "https://laravel.build/example-app" | bash

Slide 12

Slide 12 text

こうなった # syntax=docker/dockerfile:1 FROM composer:2.2 as dep COPY composer.* /app/ RUN --mount=type=cache,target=/tmp/cache/files \ composer install \ --no-dev \ --ignore-platform-reqs \ --no-interaction \ --prefer-dist \ --no-plugins \ --no-scripts \ --no-autoloader COPY ./ /app/ RUN composer dump-autoload \ --no-dev \ --optimize FROM php:8.1-apache ENV WWWUSER www-data ENV APP_ROOT /app ENV APACHE_DOCUMENT_ROOT /app/public RUN apt-get update && apt-get install -y \ libicu-dev \ libzip-dev \ && rm -rf /var/lib/apt/lists/* RUN NPROC=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) && \ docker-php-ext-install -j${NPROC} intl opcache pdo_mysql zip RUN pecl install redis apcu xdebug && \ docker-php-ext-enable redis RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' \ /etc/apache2/sites-available/*.conf RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' \ /etc/apache2/apache2.conf /etc/apache2/conf-available/* COPY --from=dep /app/ ${APP_ROOT} RUN chown -R ${WWWUSER}:${WWWUSER} ${APACHE_DOCUMENT_ROOT} \ ${APP_ROOT}/storage WORKDIR ${APP_ROOT} ↩

Slide 13

Slide 13 text

大方針 1. composerで準備する 2. 結果だけ持ってくる # stage-0 FROM composer:2.2 AS dep COPY composer.* /app/ RUN composer install # stage-1 FROM php:8.1-apache COPY --from=dep /app/vendor/ \ /var/www/html/vendor/ COPY . /var/www/html/

Slide 14

Slide 14 text

レイヤーキャッシュを効かせる COPYでcomposer.json, composer.lockだけ 持ってくることでアプリケーションの変更 時はキャッシュが使われる # stage-0 FROM composer:2.2 AS dep COPY composer.* /app/ RUN composer install # stage-1 FROM php:8.1-apache COPY --from=dep /app/vendor/ \ /var/www/html/vendor/ COPY . /var/www/html/

Slide 15

Slide 15 text

これだけだとERRORになる $ docker build -t app:latest . => ERROR [dep 3/3] RUN composer install ∵ scriptが走っている # syntax=docker/dockerfile:1 FROM composer:2.2 AS dep COPY composer.* /app/ RUN composer install FROM php:8.1-apache COPY --from=dep /app/vendor/ \ /var/www/html/vendor/ COPY . /var/www/html/

Slide 16

Slide 16 text

ダメだった対処 scriptsの依存関係を解消する ./app/の中まで見ている ……沼 # syntax=docker/dockerfile:1 FROM composer:2.2 AS dep COPY artisan /app/ COPY bootstrap/app.php \ /app/bootstrap/ COPY app/Console/Kernel.php \ /app/app/Console/ COPY app/Exceptions/Handler.php \ /app/app/Exceptions/ COPY composer.* /app/ RUN composer install FROM php:8.1-apache COPY --from=dep /app/vendor/ \ /var/www/html/vendor/ COPY . /var/www/html/

Slide 17

Slide 17 text

良さそうな対処 要はscriptを後回しにできればいい 1. --no-scripts --no-autoloaderオプション でダウンロードだけやってしまう 2. その後にまるっとファイルをCOPYして composer dump-autoload 3. appイメージの方もvendor以外もCOPY するように変更 # syntax=docker/dockerfile:1 FROM composer:2.2 AS dep COPY composer.* /app/ RUN composer install \ --no-scripts \ --no-autoloader COPY ./ /app/ RUN composer dump-autoload FROM php:8.1-apache COPY --from=dep /app/ /var/www/html/

Slide 18

Slide 18 text

ファイルキャッシュでもっと速く BuildKit(`--mount`) `RUN --mount=type=cache,target=/tmp/cache/files composer install`

Slide 19

Slide 19 text

結論 # syntax=docker/dockerfile:1 FROM composer:2.2 as dep COPY composer.* /app/ RUN --mount=type=cache,target=/tmp/cache/files \ composer install \ --no-dev \ --ignore-platform-reqs \ --no-interaction \ --prefer-dist \ --no-plugins \ --no-scripts \ --no-autoloader COPY ./ /app/ RUN composer dump-autoload \ --no-dev \ --optimize FROM php:8.1-apache ENV WWWUSER www-data ENV APP_ROOT /app ENV APACHE_DOCUMENT_ROOT /app/public RUN apt-get update && apt-get install -y \ libicu-dev \ libzip-dev \ && rm -rf /var/lib/apt/lists/* RUN NPROC=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) && \ docker-php-ext-install -j${NPROC} intl opcache pdo_mysql zip RUN pecl install redis apcu xdebug && \ docker-php-ext-enable redis RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' \ /etc/apache2/sites-available/*.conf RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' \ /etc/apache2/apache2.conf /etc/apache2/conf-available/* COPY --from=dep /app/ ${APP_ROOT} RUN chown -R ${WWWUSER}:${WWWUSER} ${APACHE_DOCUMENT_ROOT} \ ${APP_ROOT}/storage WORKDIR ${APP_ROOT} ↩ 本番環境向けに--no-devとか--optimizeとかい れたり、拡張とかApacheの設定とか諸々加えて いくとこんな感じになりそう (.dockerignoreも使おうね!)

Slide 20

Slide 20 text

CI/CD上の扱いを考える

Slide 21

Slide 21 text

暗黙的なキャッシュはローカルだけ レイヤーキャッシュもBuildKitのmountもDockerが実行されるホストに保存される リモートのCI/CDサービス上ではどうする?

Slide 22

Slide 22 text

CI/CDのキャッシュ ● レイヤーキャッシュ ○ 有料だったりする、、、 ● ファイルキャッシュ ○ BuildKitのmountはこれでいける ○ Dockerのレイヤー ● アーティファクト ○ イメージやパッケージを固めて 残しておけるか? actions/cache - https://github.com/actions/cache/blob/main/examples.md#php---composer docker/build-push-action - https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#local-cache

Slide 23

Slide 23 text

リモートのイメージをキャッシュ利用 BuildKit(`--cache-from`) CI/CDサービスに依存しない(好き) (でもパッケージ変更時に毎回ダウンロード走るならvendorのダウンロードと変わ らなくね?)

Slide 24

Slide 24 text

継続的アップデートする

Slide 25

Slide 25 text

変更タイミング 1. ソースコードを変更した時 2. パッケージを更新した時 3. ベースイメージが更新された時 2, 3 はツール(depedabotとか)にお任せ Dockerfileは 1 > 2 > 3 の頻度で更新がある想定 保守フェーズに入いると頻度が 1 < 2 になってくるかも? Dockerfileも見直せる?

Slide 26

Slide 26 text

まとめ ● レイヤーキャッシュを上手に使ってさくさくビルド ○ composerはダウンロードとスクリプト処理を分ける ● マルチステージビルドで最終イメージは小さく ● ファイルキャッシュも有効 ● CI/CDではファイルキャッシュ ● BuildKitでリモートのイメージをキャッシュとして使える ● ツールにまかせて継続的にイメージを更新

Slide 27

Slide 27 text

余談 AWS LambdaでPHP扱う方法としてもMulti-stage buildが紹介されている https://aws.amazon.com/jp/builders-flash/202106/new-lambda-container-dev elopment-3/?awsf.filter-name=*all https://aws.amazon.com/jp/blogs/compute/building-php-lambda-functions-wit h-docker-container-images/ 最初のステージでよりたくさんのことをしている 1. PHPのインストール 2. PHPをLambdaで動かすためのruntimeの用意 3. composer requireでのパッケージ追加 composer.jsonでの依存を解決するにはレイヤー重ねるか3 stage buildにする?

Slide 28

Slide 28 text

おしまい ご意見ご指摘お待ちしています