WordPressをコンテナでセキュアに構築/運用

クラウド事業部の原口です。

WordPressのようなオープソースのプログラムは冗長化が難しいと考えていますが、皆様どのような解決策をお持ちでしょうか?
WordPressを冗長化したEC2で運用する上でのAWSのベストプラクティスは以下のホワイトペーパーにまとめられています。

Best Practices for WordPress on AWS
https://docs.aws.amazon.com/whitepapers/latest/best-practices-wordpress/best-practices-wordpress.pdf

こちらは非常によく考えられている構成でWordPressとは言わず、あらゆる冗長化しがたいOSSアプリ(例えばEC-CUBEなども)をステートフルにするという観点で応用できる構成なのではないかと思います。

今回はこの構成を一歩先に進め、WordPressをコンテナで運用したという実例をご紹介したいと思います。

WordPressをECS + fargateで運用する

いきなりですが今回の構成になります。

インフラのデプロイ

ここでのインフラのデプロイはfargate / ECSにおけるタスク定義で新しい設定のimageを用意し、ECSのタスク定義を更新する事とします。
ECS fargateへのデプロイはDcokerfileをGitHubで管理し、GitHubへのpushをトリガーにCode pipelineにてビルドを行い、ECRへpush、さらにCode pipelineでのデプロイレイヤーにてECSへローリングアップデートをかけるという形としています。
EC2ではなくなるという事がありOSの選択が非常に流動的になり、phpアップデート作業などもDockerfile一つ書き換えるだけで反映が可能となりました。

今構成のポイントはインフラのデプロイとアプリのデプロイは完全に切り離されているという事です。つまり、コンテナimage内にはアプリのプログラムは含まれていません。
インフラはアプリを意識せずいつでもデプロイが可能となっています。

アプリケーションのデプロイ

WordPressによる冗長化の問題点はユーザーが管理画面よりポチポチしてDBだけでなく、実ファイルなどが変更されてしまうという点にあります。例えば画像アップロード、プラグインの更新、WordPress自体のアップデートなどですね。また、手元のローカル開発環境がDBやアップロード画像を含むものにする必要があるため、基本的にはdevelop環境というサーバーを用意してしまう事が多いです。その関係でgitによる完全管理がしづらい点なども冗長化された環境へのデプロイの問題となってきます。

元々の運用として上記を解決したとしても日々の運用としてWordPressテーマファイルのデプロイをFTPファイルアップロードで行ってた場合は、それらの修正のたび、例え軽微な文言修正であってもコンテナビルドしてローリングデプロイしてと行うのはなんだか冗長な気もしますし、運用ユーザーとしてはこれまでの運用フローと大きく異なる事になっていまうのでなかなか難しいという現実があります。

そこで今構成ではデプロイ用のEC2を立ち上げて、そちらにEFSマウントし、WordPressの全データを載せる形としています。
EFSに載せる事により運用ユーザはこれまでと同じ手順でのアプリケーションデプロイが行える形となります。
コンテナからもEFSマウントしてきてWordPressを表示するという形です。

このように運用ユーザはこれまでと同じ作業でファイル更新やWordPress管理画面の操作を行なっていただければEFSを介してコンテナ(公開側)のコンテンツも変更されるという仕掛けです。

EC2を使ってしまうのは多少妥協といいますか負けた感がありますが、、、現在ではこの構成がベストと考えています。

なおこのEC2サーバーは外部には公開しないため、アクセス制限をかける事が可能です。セキュリティグループで完全に制限してしまう事でセキュリティ上のリスクを大きく減らす事ができます。

コンテナのセキュリティ

コンテナのセキュリティについては以下の2点の施策を行なっています。

codebuildでビルド時のコンテナの脆弱性スキャンを行う

こちらはtrivyおよびdockleにてスキャンを行なっています。
これらをbuildspeck.yml内で以下のように定義し、クリティカルな脆弱性がある場合はデプロイができないようにしています。

#pre_buildステージにて
# trivyをインストール
- TRIVY_VERSION=$(curl -sS https://api.github.com/repos/aquasecurity/trivy/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
- rpm -ivh https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-ARM64.rpm
# dockleをインストール
- DOCKLE_VERSION=$(curl -sS https://api.github.com/repos/goodwithtech/dockle/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
- rpm -ivh https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-ARM64.rpm

シーズでは基本的に使える限りはgravitonを使っているのでcodebuildもgravitonになります。そのためアーキテクチャはARM64となってますのでx86の場合は適宜修正してください。
上記のコマンドでインストールしたtrivyやdockleは以下のようなコマンドでイメージスキャンができます。

#buildステージにて
## trivyによるイメージスキャン
- trivy image --no-progress --ignore-unfixed --exit-code 1 --severity CRITICAL $AWS_ECR_REPO:php-latest
- exit `echo $?`

## dockleによるイメージスキャン
- dockle --format json -o dockle_results.json --exit-code 1 --exit-level "FATAL" $AWS_ECR_REPO:php-latest
- exit `echo $?`

コンテナのReadOnlyRootFileSystem化

前回の私が書いた記事

コンテナのセキュリティ対策!AWS ECS (fargate)でタスクをreadonlyRootFilesystemで運用する
https://www.seeds-std.co.jp/blog/creators/2022-02-08-114520/

でお伝えした通り、コンテナをReadOnlyでマウントしてしまえば非常にセキュリティレベルが高くなります。仮に脆弱性があっても攻撃の可能性をかなり減らす事ができますね。

さらに今回はEFSをマウントする構成ですがRootFileSystemに加えてEFSのマウントも「読み取り専用」としてマウントする事にします。
これにより、コンテナ側からはEFS(wordpress)のファイルを編集できないが、EC2(writer origin)側からは編集する事ができるという形にできます。

これらの対策を行なっていきます。

EFSの追加ですが、タスク定義で行います。タスク定義の一番下の方にあるボリュームの追加でEFSを選択し、WordPressデータを格納しているボリュームを選択します。

各コンテナの設定では「読み取り専用ルートファイルシステム」にチェックを入れ、マウントポイントとして先ほど設定したボリュームを選択してマウントポイントを指定します。
こちらも読み取り専用としてマウントする事を忘れないでください。

こちらでECS側の設定は完了です。後はDockerfileがread-onlyでも動くように調整していきます。
今回はWordPressですのでnginxコンテナとphp-fpmコンテナの例を記載します

FROM nginx:alpine

RUN apk upgrade --no-cache

RUN touch /var/run/nginx.pid && \
  chown -R nginx:nginx /var/run/nginx.pid && \
  chown -R nginx:nginx /var/cache/nginx && \
  mkdir -p /app/public

VOLUME /var/cache/nginx
VOLUME /var/run
VOLUME /etc/nginx/conf.d

COPY ./path/to/prod.conf /etc/nginx/conf.d/default.conf
FROM php:8.1.3-fpm-alpine
# composer
COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer

# package
RUN apk upgrade --no-cache \
    && docker-php-source extract \
    && pecl bundle -d /usr/src/php/ext redis-5.3.4 \
    && pecl bundle -d /usr/src/php/ext apcu \
    && docker-php-ext-install -j$(nproc) opcache bcmath pdo_mysql mysqli redis apcu \
    && docker-php-ext-enable mysqli \
    && docker-php-source delete \
    # wp-cli
    && curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
    && chmod +x wp-cli.phar \
    && mv wp-cli.phar /usr/bin/wp \
    && wp --info

RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
WORKDIR /app/public

COPY ./path/to/php-prod.ini /usr/local/etc/php/conf.d/php.ini
COPY ./path/to/www.conf /usr/local/etc/php-fpm.d/www.conf
VOLUME /tmp
VOLUME /var/run

ポイントはVOLUMEのところになりまして、ミドルウェアとして必ず書き込まないといけない領域をそれぞれのコンテナの中でVOLUMEとしています。

AWS WAFの設定

こちらはAWSのマネージドルールを積極的に利用していきます。
AWS WAF のAWSマネージドルールにはWordPressルールが存在しますのでこちらを適用する事でかなりのセキュリティ向上となります。

以下のルールセットを有効化するのがよいと思います。
・WordPress application
・PHP application
・SQL database
・Core rule set

WordPress アプリケーションルールグループには、WordPress サイト固有の脆弱性の悪用に関連するリクエストパターンをブロックするルールが含まれています。WordPress を実行している場合は、このルールグループを評価する必要があります。このルールグループは、SQL database および PHP application ルールグループと組み合わせて使用する必要があります。

https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/aws-managed-rule-groups-list.html

上記のページの説明にもありますが悪名高いxmlpc.php などはWordPressの基本的なセキュリティ対策としては無効化する事が推奨されていますが、万が一設定が漏れていたとしてもこのWAFルールでxmlpc.phpへのアクセスなどをブロックしてくれるようです。

EFSが遅い問題への対応

EFSは一つの大きなファイルの転送などの速度はかなりのもので心配はないのですが、WordPressなどのindex.phpなどから多数のファイルが読まれるようなプログラムをEFSに入れた場合、その遅さは無視できるものではありません。

このような問題に対してAWSでは以下のページにて対策を提示されています。

Amazon EFS を使用して WordPress のパフォーマンスを最適化する
https://aws.amazon.com/jp/blogs/news/optimizing-wordpress-performance-with-amazon-efs/

要約してしまうと「OPCacheを使え」という話でして、PHPファイルのプリコンパイルされたスクリプトバイトコードを共有メモリに格納することで、そもそもの元ファイルであるEFSへアクセスさせないという施策が紹介されています。

OPCacheを入れてしまえば初回のアクセスだけは遅いけど以降は中間キャッシュをメモリから呼ぶので早いですよ、という話なのですが、、、
この初回アクセスが10sもかかる時もあり、それはそれで無視できないものでした。

そこで、php-fpmコンテナが立ち上がった時にキャッシュをウォームアップするスクリプトを叩くという形で以下を参考に実装を行なってみました。

Apache再起動後の初回リクエスト前からOPCacheキャッシュウォーミングを試みた話
https://qiita.com/boscoworks/items/90a4b5e3e78082f73dd0

<?php
/**
 * opcacheのキャッシュウォーマー
 * コンテナ内から以下を実行
 *
 * usage:
 * curl http://localhost/opcache_warmer.php
 *
 * 参考:
 * https://qiita.com/boscoworks/items/90a4b5e3e78082f73dd0
 */

ini_set('memory_limit', -1);

// TODO: なんらかのアクセス制限を実施したほうがよい

$successNum = 0;
$failureNum = 0;
$noChangeNum = 0;
$totalNum = 0;

$command = 'find [キャッシュさせたいディレクトリ] -type f -name "*.php"';
exec($command,$files);
foreach ($files as $file) {
    if (!opcache_is_script_cached($file)) {
        opcache_compile_file($file) ? $successNum++ : $failureNum++;
    } else {
        $noChangeNum++;
    }
}

$totalNum = $successNum + $failureNum + $noChangeNum;
$msg = "success: ${successNum} \tfailure: ${failureNum} \tno_change: ${noChangeNum} \ttotal: ${totalNum}\n";
echo $msg;

//コンテナなので標準エラーへ結果を出力
$stderr = fopen('php://stderr', 'w');
fwrite($stderr, $msg);
fclose($stderr);

次にコンテナタスクが開始された時に上記プログラムを叩けるようにします。
ECS Exec などの多数の方法がありますが、「起動した後に」というトリガーが難しかったり起動後1回だけ確実にというところがあったので、curlコンテナを作成する事で解決しました。

FROM alpine:latest
RUN apk add --no-cache curl

# fargateタスク内なのでlocalhostでnginxコンテナへ接続可能
CMD GET "http://localhost/opcache_warmer.php"

こちらをECRでイメージにできたらタスク定義からcurlコンテナを追加します。

curlコンテナは実行すると終了するコンテナですので以下のように「基本」の項目を外します。ここを外していない場合はcurlコンテナの終了とともにタスク全てが終了してしまいます。




また、curlコンテナは nginxコンテナ、php-fpmコンテナが立ち上がった後としたいのでcurlコンテナのスタートアップ依存順序を設定しておきましょう。

このように設定する事でnginxコンテナとphp-fpmコンテナが立ち上がった後、キャッシュウォームスクリプトへnginx経由で実行させる事ができるようになりました。

このような形で curl コンテナがSTOPPEDとなってもタスクが継続して動いていれば成功です。curlコンテナのログなどからキャッシュウォーマーが正常に稼働したか確認しましょう。

最後に

いかがでしたでしょうか?コンテナを使ったWordPressの運用例でした。WordPressのようなオープソースのプログラムは冗長化が難しいのですが、EFSという解決策がありましたが、そちらをコンテナ化する事によりよりセキュアにしようという試みでした。

コンテナという技術からすると思想から外れた構成である事は間違いないのですが、冗長構成のEC2 WordPressだったものをコンテナにしていくという観点では間違いの構成であるとも言い切れないかな、と個人的には思っております。

今回の構成の課題としては、writer origin としたEC2が残っているところです。
こちらのEC2は外部には出さない、とはいえOS脆弱性などには対応する必要があったり、コンテナのphpバージョンと合わせるために定期的なアップデートが必要、というところが完全に運用コストを削減しきれていないなと思う点です。

EC2はあったとしてもせめてEC2 on Dockerくらいにはしたいですね。
それではまた!