[Kubernetes] WordPress WP-Cron を WP-CLI で実行する CronJob
この記事では以下の内容について記載します。
- コマンドラインツール WP-CLI を利用して WP-Cron を実行するための設計
- WP-CLIを利用して、Kubernetes 上の WordPress 本体のサービスとは異なる Pod で WP-Cron を実行するための設計
Kubernetes を軸にした説明となりますが、 WP-CLI に関してはどのような条件にでも活用できる情報だと思います。
前提
- この記事では、ビルド方法の詳細は省略します。
- リソースの宣言ファイル(YAML)は説明に必要な部分抜粋であり、完全体ではありません。ある程度は自身で補完できることを想定しています。
Kubernetes 上に WordPress をインストールする方法の一例は以下の記事を参照してください。
背景
Kubernetes 上にシングルインスタンスで WordPress (Multisite モード) で動かしていましたが、重たい WP-Cron ジョブが動いているせいで WordPress の Webサイトが遅延していました。WordPress サイト側での工夫余地はまだあるのですが、WP-Cron ジョブの実行環境(Pod)を分離できれば影響範囲の整理としてはとてもシンプルになるので、WP-CLI の練習を兼ねて設計を見直すことにしました。
最終構成は以下の通りです。
WP-CLI入りの Dockerイメージを作成する
- WordPress 本体のイメージをベースにして、WP-CLIコマンドをインストールします
- WordPress のバージョンは、ここでは 5.9 としていますが任意です
- WP-CLIのバージョンも必要に応じて見直します
install-wordpress.sh
は後述します
Dockerfile
FROM wordpress:5.9
# M1/M2 などの armアーキテクチャ MacBook で動かす場合は以下(要Rosetta)
# FROM --platform=linux/amd64 wordpress:5.9
ENV WP_CLI_VERSION 2.7.1
COPY install-wordpress.sh /usr/local/bin/
RUN \
cd /tmp ;\
# Install WP-CLI
curl -L -sS -o wp-cli.phar https://github.com/wp-cli/wp-cli/releases/download/v${WP_CLI_VERSION}/wp-cli-${WP_CLI_VERSION}.phar &&\
chmod +x wp-cli.phar &&\
mv wp-cli.phar /usr/local/bin/wp ;\
# isntall WordPress script to execute Cronjob using wp-cli
chmod +x /usr/local/bin/install-wordpress.sh ;\
install-wordpress.sh
前のセクションで作成した Dockerfile は WordPress オフィシャルイメージそのものです。永続ディスクを設定しない限りは wp-content
には何のデータもありません。
この場合、オフィシャルイメージでは起動コマンドが apache
や php-fpm
だった場合には WordPress ファイル群のインストールをしてくれるのですが( entrypoint
)、今回は WP-CLI で起動するためこの判定をスキップしてしまいます。
そのため少々強引ですが、オフィシャルの docker-entrypoint.sh
からインストールスクリプトを拝借してきます。オリジナルのファイル配下を参照してください。
if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then
# if the directory exists and WordPress doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory)
if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then
chown "$user:$group" .
fi
echo >&2 "WordPress not found in $PWD - copying now..."
if [ -n "$(find -mindepth 1 -maxdepth 1 -not -name wp-content)" ]; then
echo >&2 "WARNING: $PWD is not empty! (copying anyhow)"
fi
sourceTarArgs=(
--create
--file -
--directory /usr/src/wordpress
--owner "$user" --group "$group"
)
targetTarArgs=(
--extract
--file -
)
if [ "$uid" != '0' ]; then
# avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted"
targetTarArgs+=( --no-overwrite-dir )
fi
# loop over "pluggable" content in the source, and if it already exists in the destination, skip it
# https://github.com/docker-library/wordpress/issues/506 ("wp-content" persisted, "akismet" updated, WordPress container restarted/recreated, "akismet" downgraded)
for contentPath in \
/usr/src/wordpress/.htaccess \
/usr/src/wordpress/wp-content/*/*/ \
; do
contentPath="${contentPath%/}"
[ -e "$contentPath" ] || continue
contentPath="${contentPath#/usr/src/wordpress/}" # "wp-content/plugins/akismet", etc.
if [ -e "$PWD/$contentPath" ]; then
echo >&2 "WARNING: '$PWD/$contentPath' exists! (not copying the WordPress version)"
sourceTarArgs+=( --exclude "./$contentPath" )
fi
done
tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}"
echo >&2 "Complete! WordPress has been successfully copied to $PWD"
fi
wpEnvs=( "${!WORDPRESS_@}" )
if [ ! -s wp-config.php ] && [ "${#wpEnvs[@]}" -gt 0 ]; then
for wpConfigDocker in \
wp-config-docker.php \
/usr/src/wordpress/wp-config-docker.php \
; do
if [ -s "$wpConfigDocker" ]; then
echo >&2 "No 'wp-config.php' found in $PWD, but 'WORDPRESS_...' variables supplied; copying '$wpConfigDocker' (${wpEnvs[*]})"
# using "awk" to replace all instances of "put your unique phrase here" with a properly unique string (for AUTH_KEY and friends to have safe defaults if they aren't specified with environment variables)
awk '
/put your unique phrase here/ {
cmd = "head -c1m /dev/urandom | sha1sum | cut -d\\ -f1"
cmd | getline str
close(cmd)
gsub("put your unique phrase here", str)
}
{ print }
' "$wpConfigDocker" > wp-config.php
if [ "$uid" = '0' ]; then
# attempt to ensure that wp-config.php is owned by the run user
# could be on a filesystem that doesn't allow chown (like some NFS setups)
chown "$user:$group" wp-config.php || true
fi
break
fi
done
fi
WP-Cron を実行する CronJob
前の手順で WP-CLI がインストールされた WordPress イメージが完成しているはずです。このセクションでは、そのイメージを利用して WP-CLIを実行する CronJob
リソースを作成します。
CronJob リソース
先に完成品は以下の通り。
- 30分周期の起動としていますが好きな値に調整してください。
- MySQLへの接続方法には言及しません。CloudSQLをサイドカー構成で利用する場合は少し面倒なことがあるので後述します。
args
の部分に bashスクリプトを記載しています。スクリプトそのものですので、任意にカスタマイズができます。
apiVersion: batch/v1
kind: CronJob
metadata:
name: job-wordpress-cron
namespace: wordpress
spec:
schedule: "30/* * * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
# CONTAINER: 1
- name: wp-cron
# ビルドした Docker Image を指定
image: your-docker-registry/wordpress-5-9:latest
ports:
- name: http-port
containerPort: 80
command: ["/bin/bash", "-c"]
args:
- |
install-wordpress.sh
echo "[Info] Copy the plugin under wp-contnet ..."
echo "Plugin が必要な場合はここで配置するスクリプトを記載してもよい"
echo "[Info] Show event list ..."
wp cron event list --url=example.com --allow-root
echo "[Info] Execute WP-Cron using WP-CLI ..."
wp cron event run --due-now --url=example.com --allow-root
echo "[Info] WP-Cron finished. Terminate the job process ..."
env:
- name: WORDPRESS_DB_HOST
value: "127.0.0.1"
WP-CLI の説明
wp cron event run --due-now
とすることで実行可能状態のイベントを実行します- WordPress を Multisite (マルチサイト)構成で利用している場合は、
--url=example.com
のようにドメインを指定することで対応するサイトのイベントを操作することができます - WordPress オフィシャルイメージは(少なくとも当該バージョンでは)root ユーザーで実行されますが、その状態で WP-CLI を利用するとクライアントから警告が発生して強制停止してしまいます。この検知を無視するために
--allow-root
オプションを渡しています
wp cron event run | WP-CLI Command | WordPress Developer Resources
実行ログ
以下のような実行結果が得られます。イベントごとに処理時間がログで表示されています。
Executed the cron event 'wp_program_a_hook' in 16.126s.
Executed the cron event 'wp_program_a_hook' in 31.962s.
Executed the cron event 'wp_program_a_hook' in 26.384s.
Executed the cron event 'wp_program_a_hook' in 19.223s.
Executed the cron event 'wp_program_a_hook' in 55.699s.
Executed the cron event 'wp_program_a_hook' in 19.225s.
Executed the cron event 'wp_program_a_hook' in 56.49s.
...
Executed the cron event 'wp_update_plugins' in 0.18s.
Executed the cron event 'wp_update_themes' in 0.017s.
Success: Executed a total of 32 cron events.
[Info] WP-Cron finished. Terminate the job
SQLプロキシを Sidecar 構成にしている場合の補足
GKE かつ CloudSQL 構成としている場合は CloudSQL Proxy を利用している可能性が高いでしょう。CloudSQL Proxy は Sidecar コンテナとして、WordPress と同一Podに共存している場合には少々厄介な問題が生じます。
WordPress (WP-CLI) コンテナの実行が完了して Terminate しても、CloudSQLコンテナは実行し続けてしまいます。これの何が問題かというと、 Job (Pod) としては READY 1/2
のようになってしまうので完全に終了してくれません。この問題には以下のIssue などで言及されています。
Terminating Sidecar after Kubernetes Job · Issue #262 · GoogleCloudPlatform/cloud-sql-proxy
ここでは shareProcessNamespace: true
を利用して Pod 内でプロセス名を共有し、そして WordPress (WP-CLI) の実行が完了したら pkill cloud_sql_proxy
で CloudSQL コンテナを終了させる方法をとります。必要な設定は2箇所です。
jobTemplate.spec.template.spec.shareProcessNamespace: true
- シェルスクリプトに
pkill cloud_sql_proxy
をベタで記述する
# CronJob リソース
spec:
schedule: "15 * * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
shareProcessNamespace: true
restartPolicy: Never
containers:
# ...
# WordPress (WP-CLI) 実行コンテナのスクリプト
args:
- |
install-wordpress.sh
echo "[Info] Copy the plugin under wp-contnet ..."
echo "[Info] Show event list ..."
wp cron event list --url=ragmity.com --allow-root
echo "[Info] Execute WP-Cron using WP-CLI ..."
wp cron event run --due-now --url=ragmity.com --allow-root
echo "[Info] WP-Cron finished. Terminate the job process ..."
pkill cloud_sql_proxy
CloudSQL を利用している場合のその他アプローチ
Sidecar 構成で頑張るパターン
- ステータスを表すファイルを監視して Pod 自ら削除するパターン
- 同様のアプローチをコマンド化したもの
kubexit
その他アプローチ
- Pod (CronJob) が完全に終了しなくたって構わないから Replace Concurrency Policy で再作成する
- Sidecar プロキシとせずに、直接DBに IP接続する
- 類似方法として、 CloudSQL Proxy は利用するが CloudSQL Proxy だけを独立して Pod 起動させておき必要なときにアクセスするパターン
参考: CronJob で構成する他のパターン
この記事では CronJob リソースでジョブを定期実行し、かつジョブは専用の Pod 自身がバッチ処理をすることで、メインサービスを提供している WordPress Pod には負荷を与えない(負荷を分離する)ことを目的とした構成の一例を記載しました。
ただ、WP-CLIのセットアップなど面倒なのも事実です。もう少し手軽な手順で CronJob を利用する方法は関連記事を参照してください。
まとめ
この記事では WP-CLI を利用した WP-Cron の実行方法を説明しました。
構成的には Kubernetes Native な設計に近づいていて気持ちよさはあるものの、面倒さはそれなりにありますね。重たいジョブがあるせいで WordPress 本体に影響を与える場合の一例として参照していただければ幸いです。