[Kubernetes] WordPress WP-Cron を WP-CLI で実行する CronJob

この記事では以下の内容について記載します。

  • コマンドラインツール WP-CLI を利用して WP-Cron を実行するための設計
  • WP-CLIを利用して、Kubernetes 上の WordPress 本体のサービスとは異なる Pod で WP-Cron を実行するための設計

Kubernetes を軸にした説明となりますが、 WP-CLI に関してはどのような条件にでも活用できる情報だと思います。

前提

  • この記事では、ビルド方法の詳細は省略します。
  • リソースの宣言ファイル(YAML)は説明に必要な部分抜粋であり、完全体ではありません。ある程度は自身で補完できることを想定しています。

Kubernetes 上に WordPress をインストールする方法の一例は以下の記事を参照してください。

WordPressをKubernetes に構築する手順(GCP GKE)
WordPressをKubernetes に構築する手順(GCP GKE)
https://fand.jp/wordpress/how-to-build-word-press-on-kubernetes/
WordPress のオフィシャル Docker イメージを利用して Kubernetes 環境に配置するための流れを説明します。

背景

Kubernetes 上にシングルインスタンスで WordPress (Multisite モード) で動かしていましたが、重たい WP-Cron ジョブが動いているせいで WordPress の Webサイトが遅延していました。WordPress サイト側での工夫余地はまだあるのですが、WP-Cron ジョブの実行環境(Pod)を分離できれば影響範囲の整理としてはとてもシンプルになるので、WP-CLI の練習を兼ねて設計を見直すことにしました。

最終構成は以下の通りです。

WP-CLIによるWP-Cronの実行構成

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 には何のデータもありません。

この場合、オフィシャルイメージでは起動コマンドが apachephp-fpm だった場合には WordPress ファイル群のインストールをしてくれるのですが( entrypoint )、今回は WP-CLI で起動するためこの判定をスキップしてしまいます。

そのため少々強引ですが、オフィシャルの docker-entrypoint.sh からインストールスクリプトを拝借してきます。オリジナルのファイル配下を参照してください。

https://github.com/docker-library/wordpress/blob/2da47025b1ef891fe08f08d79d042f9615f06f41/latest/php7.4/apache/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 構成で頑張るパターン

その他アプローチ

参考: CronJob で構成する他のパターン

この記事では CronJob リソースでジョブを定期実行し、かつジョブは専用の Pod 自身がバッチ処理をすることで、メインサービスを提供している WordPress Pod には負荷を与えない(負荷を分離する)ことを目的とした構成の一例を記載しました。

ただ、WP-CLIのセットアップなど面倒なのも事実です。もう少し手軽な手順で CronJob を利用する方法は関連記事を参照してください。

WordPress WP-Cron を止めて OS層の Cron で実行する方法
WordPress WP-Cron を止めて OS層の Cron で実行する方法
https://fand.jp/wordpress/how-to-stop-word-press-wp-cron-and-run-it-with-system-cron/
WordPressの定期ジョブを実行する WP-Cron を停止してOSレイヤーの Cron で定期ジョブを実行する方法と処理概要の解説

まとめ

この記事では WP-CLI を利用した WP-Cron の実行方法を説明しました。

構成的には Kubernetes Native な設計に近づいていて気持ちよさはあるものの、面倒さはそれなりにありますね。重たいジョブがあるせいで WordPress 本体に影響を与える場合の一例として参照していただければ幸いです。