WordPress運用をKubernetes(GKE)へ移行する流れ

Kubernetesの利用頻度が増えてきたので、個人VPSで運営しているWordPressを Google Kubernetes Engine (GKE)に移行しようと思いました。新規で構築する手順は公式のチュートリアルをはじめまとまっている情報を見かけるのですが、既存の運営データを引っ越す過程から記載している情報を目にしなかったので、それらも記載しています。

これまでのVPS運営に具体的な問題があるわけではないのですが、仕事とは別に個人で運用することで新たに気づく事も多いので、意味のあるトライと位置づけています。

WordPress を GKE で動かす方法として、別の記事を作成しました。本記事執筆当時よりは Kubernetes の扱いにも慣れていることから、あわせて参考にしてください。

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

本記事の前提

  • Dockerに関する多少の理解があること(コンテナ、イメージ、タグなどの意味がわかること)
  • GCPでGKEセットアップが完了していること(GKEセットアップ手順は記載していません)
    • kubectl get node にてクラスタノードが確認できればOK
  • データベースはCloud SQL (MySQL)を利用します
  • プライベート運用のお遊びリソースなので、コストはできるだけ安くしたい。そのため、以下の通りKubernetesベストプラクティス構成と比べて若干のイレギュラーがあります
    • GKE(広義でのKubernetesという意味も含む)におけるサービスのパブリック公開・ロードバランスは通常、Ingressリソースが使われますが本構成では利用していません。そのため、作成手順についても触れていません。 (GKEの nodePort に対して、always free枠のインスタンスにNginxをセットアップしてReverse Proxyしています。この構成は別途記事にしたいと思っている)
    • 負荷の高くないWordPressサイトなので、シングルコンテナでの運用としています。トラフィックに対する可用性は非常に低いです。(但し、Kubernetesオーケストレーションによる障害復帰があるのでNode故障などでの手間はかからないはず)
  • WordPressコンテナイメージは Docker Official Imagesのwordpress を利用します
    • 最初はコンテナ構成にNginxを利用予定でしたが、不都合があり最終的にはApacheとしました。詳細は後述
  • [要注意] GKEクラスタの作成時に -enable-ip-alias オプションを有効にしています。この有無による影響の正確な確認・検証はしていないのですが、ONとする必要があるかもしれません。(Ingressによるサービス提供ならば関係ありません)
    • IP Aliasについては、ドキュメントに記載の通り、Podに対してVPCネットワーク内から疎通できるようになるオプションです。間もなくデフォルトになる旨がWebコンソール画面に記載されています(2019/01/05時点はまだ)。

記事の流れ

  • WordPressイメージの特徴を把握する
  • 既存WordPressのデータを移行する
    • サイトのデータ(.phpなど)を移行する
    • MySQLのデータを移行する
  • GKEにWordPressをデプロイする
  • Nginx(別ホスト)経由で Reverse Proxy してインターネットに公開する

WordPressイメージの特徴を把握する

wordpressのレポジトリには多数のタグが並んでいますが、5.0.2-php7.1-apache を例に見ていきます。多少バージョンが違っても考え方は同じなようです。

  • Dockerfile: wordpress/Dockerfile at f63c0db6593494e8fee0e542af9413e22c1da15b · docker-library/wordpress
    • 序盤はApacheのセットアップなので、とりあえず気にする必要はないでしょう
    • L49-55にて指定WordPressのソースを取得し、/usr/src/wordpressに配置しています
    • 最終行で、apache2-foreground のコマンドを ENTRYPOINT であるdocker-entrypoint.shに渡しています
  • docker-entrypoint.sh: wordpress/docker-entrypoint.sh at f63c0db6593494e8fee0e542af9413e22c1da15b · docker-library/wordpress
    • L26で引数が apache2*php-fpm かを判定しています。L29-41周辺にあるuser/group設定のためのようです。
    • L48では、 index.php および wp-includes/version.php存在しない場合の処理が記載されています。これらは対象ディレクトリにWordPressが配置されているかどうか(≒初期セットアップが完了しているかどうか)をチェックしているようで、存在しない場合はDockerfileで記載のあった/usr/src/wordpress からデータを配置しているようです。
    • L87には TODO の字と共に、以下の記載があります。
      • TODO handle WordPress upgrades magically in the same way, but only if wp-includes/version.php’s $wp_version is less than /usr/src/wordpress/wp-includes/version.php’s $wp_version
      • upgradeに関する処理を実装しようとしているように見えますが、つまり現時点ではイメージとしてWordPressのupgradeを行う処理はしていないと言えます。これは逆に都合がよくて、WordPressをお使いの方ならお分かりの通りWordPressは優秀なアップグレード機構を持っています。管理画面からボタン1個でアップグレード操作を行うことができますので、現時点はWordPressアップグレードのためにDocker Imageタグを指定し直す必要はないと考えてもよいでしょう。
    • L89以降は環境変数に関する処理が続きます。wp-config.php ファイルの位置を定義するなど気になる処理もありますが、今回要件では無視してもよさそうです。
    • L148,L159周辺に記載されている通り、wp-config.php が存在しない場合は wp-config-sample.php の内容をもとに生成しています。ここで、環境変数に設定した値で置き換える処理が入っていますので、移行にあたってはwp-config.phpを一度消しておくことにします。(移行元のをそのまま利用して値だけ手動で書き換えても問題ないはずです)

だいたい特徴がつかめたので、移行作業を進めます。

Step1 既存WordPressのデータを移行する

事前に準備しておくもの

  • 既存WordPressサイトの公開コンテンツデータ(例:public_html): public_html.tar.gz とする
  • 既存WordPressサイトのMySQLデータ: wordpress.dump.gz とする

1-1 サイトのデータ(.phpなど)を移行する

1-1-1 永続ボリュームの作成

まず、Kubernetesクラスタに永続ディスクのボリュームを用意する必要があります。といってもGCPのコンソールからディスクを作成するのではなく、kubectlコマンドを用いて作成することができます。

Kubernetesにおけるディスクボリュームを作成(要求)する処理が、PersistentVolumeClaim と呼ばれるリソースです。以下のコンフィグ(yml)を kubectl apply -f pvc.yml として作成します(PVC = PersistentVolumeClaimの略)。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-disk
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

実行結果:

# WordPressデプロイ先のネームスペースを作成。`wordpress` として作る。
$ kubectl create namespace wordpress
namespace "wordpress" created


# PVCを作成。`-n` で操作対象のネームスペースを指定する。
$ kubectl -n wordpress apply -f pvc.yml
persistentvolumeclaim "wordpress-disk" created


# 作成されたPVCを確認する
$ kubectl -n wordpress get pvc
NAME             STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
wordpress-disk   Bound     pvc-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX   1Gi        RWO            standard       1X

# PVCにより作成された永続ボリュームの実態(PersistentVolume = PV)を確認する
$ kubectl -n wordpress get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                      STORAGECLASS   REASON    AGE
pvc-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX   1Gi        RWO            Delete           Bound     wordpress/wordpress-disk   standard                 1X

ポイント:

  • ReadWriteOnce で作成していますので、1つのコンテナからしかマウントできません。複数コンテナでスケールする必要がある場合は、ReadWriteMany なボリューム(NFSサービスなど)を作成する必要があります。
  • kind: StorageClass を作成しない場合は標準HDDで作成されます。SSDが必要な場合は StorageClass から作成する必要がありますが、今回は扱いません。必要な場合はこちらを参照してください:Storage Classes - Kubernetes
  • コンテンツ量が大きくないので最小のストレージサイズを要求しています。500Miで指定したところ作成された実態は1GiBボリュームでしたので、このコンフィグでも 1Gi としています。大量のコンテンツを保有している場合は、適当なサイズに変更してください。
  • PVのRECLAIM POLICYDeleteとなっています。**危ない!!**このままだとPVCを削除したときにボリュームが消えてしまいます=コンテンツが消失します。なかなか消すことはないと思いますが、念の為消えないように修正しておきます(下記参照)。

RECLAIM POLICY の修正:

# RECLAIM POLICY を Delete から Retain に変更する
$ kubectl -n wordpress patch pv pvc-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX -p '{"spec": {"persistentVolumeReclaimPolicy":"Retain"}}'
persistentvolume "pvc-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" patched

  • patch に続くサブコマンドは pv です(pvcではありません!)ので注意
    • 改めて get pv することで変更されていることを確認できるはずです

1-1-2 移行用の一時的なコンテナの作成

さて、先のステップで作成したボリュームにデータをコピーします。このあと出てくるwordpresイメージのデプロイ前にはデータを用意しておきたいので、データ移行用の一時的なPodを作成して kubectl cp コマンドにて配置することとします。(他に良いやり方があれば教えていただけると嬉しいです!)

データ移行用のPodはbusyboxコンテナにて作成します。busyboxはサイズが1MB程度の非常にコンパクトなイメージですので、このような用途にはもってこいです。コンフィグは以下の通り。

datacopy-pod.yml:

apiVersion: v1
kind: Pod
metadata:
  name: data-copy
spec:
  containers:
    - name: sleep-pod
      image: busybox
      command:
      - sleep
      - "3600"
      volumeMounts:
      - name: disk
        mountPath: "/tmp/wp-disk"
  volumes:
    - name: disk
      persistentVolumeClaim:
        claimName: wordpress-disk

  • 見ての通り busybox を sleep 3600 として起動しているのみです。1時間もあればデータ移行作業終わるでしょう、ということでの数字ですが、もっと時間がほしい場合は調整してください。
  • 先程作成したPVC(ディスク)を /tmp/wp-disk のパスにマウントしています。つまり、持っていきたいデータは最終的にここに配置すればOKです。この後の wordpress イメージでは改めて別の path にマウントしますので、ここは適当で構いません。
# Podの作成
$ kubectl -n wordpress apply -f datacopy-pod.yml
pod "data-copy" created

$ kubectl -n wordpress get all
NAME            READY     STATUS    RESTARTS   AGE
pod/data-copy   1/1       Running   0          18s

# 持っていくコンテンツを public_html.tar.gz として、コンテナにコピー `cp` する
$ kubectl -n wordpress cp ./public_html.tar.gz data-copy:/tmp
# 転送完了後、レスポンスメッセージは特になし。

# データ移行用コンテナにINする
$ kubectl -n wordpress exec -it data-copy sh
/ #

# データがあることを確認しましょう。
$ cd /tmp/
$ ls -al

# ...その後、よしなにデータを配置します
# Ownerにも注意しましょう。Apacheの場合は www-data (id 33) です

# 前述の通り、`wp-config.php` は自動生成するので一旦削除(あるいはリネームで退避)しておきます!!

$ eixt

自動生成される wp-config.php の情報源は wp-config-sample.php ですので、ボリュームに無い場合は配置しておきましょう。 WordPress/wp-config-sample.php at master · WordPress/WordPress

コピーが済んだらデータ移行用Podは削除します。

$ kubectl -n wordpress delete -f datacopy-pod.yml
pod "data-copy" deleted

$ kubectl -n wordpress get all
NAME            READY     STATUS        RESTARTS   AGE
pod/data-copy   1/1       Terminating   0          5m
                          # Terminatingの後しばらく放っておくと消えます

ここまでで wordpress コンテンツのデータ移行は完了しました。

1-2 MySQLのデータを移行する

続いて MySQL データを移行します。まず、CloudSQLに対してどのように接続するのか?を考える必要があります。手段はいくつか考えられますが、今回あまりシビアなデータ移行は求めていない & mysqldump サイズがさほど大きくない(100MB以内)ということで、ローカルの macOS から Cloud SQL Proxy 経由でインポートすることとします。(要は、ローカル環境からインターネット経由での mysql 接続となります)

Cloud SQL Proxy とは何ぞや?という方はこちらのドキュメントを参照ください。 Cloud SQL Proxy について  |  Cloud SQL for MySQL  |  Google Cloud

Cloud SQL Proxy を使用して Cloud SQL インスタンスにアクセスする利点は以下のとおりです。

  • 安全な接続: プロキシは、TLS 1.2 と 128 ビット AES 暗号を使用して、データベースとの間で送受信されるトラフィックを自動的に暗号化します。クライアントとサーバーの ID の確認には、SSL 証明書が使用されます。
  • 簡単な接続管理: プロキシが Cloud SQL との認証を処理するので、静的な IP アドレスを提供する必要がなくなります。

MySQLサービスまでの安全な接続経路を担保してくれるもの、ということですね。

Cloud SQL を利用しない場合はVM上での作成あるいはコンテナでのMySQL作成になると思いますが、普通にmysqlコマンドで実施してもらえればよいので、Cloud SQL Proxyの手順は不要です。

1-2-1 Cloud SQL Proxy コンテナの起動

用意するもの:

接続構成概要は以下の通り。

引用: Cloud SQL Proxy について  |  Cloud SQL for MySQL  |  Google Cloud

準備ができたら mysqldump ファイルを有する環境(今回はローカルマシン)にて、以下のコマンドで Cloud SQL Proxy を立ち上げます。

$ docker run -it \\
  -v /path/to/keyfile.json:/config \\
  -p 127.0.0.1:3306:3306 \\
  gcr.io/cloudsql-docker/gce-proxy:1.12 /cloud_sql_proxy \\
  -instances=<INSTANCE CONNECTION NAME>=tcp:0.0.0.0:3306 -credential_file=/config

 ...
 2019/01/03 06:35:59 Listening on 0.0.0.0:3306 for <INSTANCE CONNECTION NAME>
 2019/01/03 06:35:59 Ready for new connections

/path/to/keyfile.json および <INSTANCE CONNECTION NAME> の部分は正しい文字に置き換えてください。

Readyとなったあとは、127.0.0.1:3306 にバインドしていますので、mysql -u<USER NAME> -p -h127.0.0.1 -P3306 で接続ができるようになっているはずです。

1-2-2 MySQLユーザーの作成

先程立ち上げた Cloud SQL Proxy コンテナはそのままで、別のコンソールで作業を進めます。

続いてアカウントの作成ですが、root権限でよいか・権限を絞りたいかで作業が変わってきます。

  • root権限で良い場合: Cloud SQLのWebコンソールからアカウントを作成すればよいので手っ取り早いです。
  • 権限を狭めたい場合: 通常通りMySQLコマンドを用いて CREATE USER / GRANT 操作をする必要があります。今回、wordpressデータベースへのアクセス権限のみ設定したいのでこちらの手段をとりますが、内容そのものは一般的なものですので詳細説明は省きます。

MySQL ユーザー | Cloud SQL ドキュメント | Google Cloud

$ mysql -uroot -p$ROOT_PASS -h127.0.0.1 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.

Welcome to the MySQL monitor.  Commands end with ; or \\g.
Your MySQL connection id is XXXXXX
Server version: 5.7.14-google-log (Google)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.

mysql>

mysql> create database wordpress;
Query OK, 1 row affected (0.12 sec)

mysql> select User,Host from user;
+-----------+-----------+
| User      | Host      |
+-----------+-----------+
| root      | %         |
| mysql.sys | localhost |
+-----------+-----------+
2 rows in set (0.10 sec)


mysql> CREATE USER 'wordpress'@'%' IDENTIFIED BY 'XXXXXXXX';
Query OK, 0 rows affected (0.10 sec)

mysql> GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'%' WITH GRANT OPTION;
Query OK, 0 rows affected (0.10 sec)

mysql> SHOW GRANTS FOR 'wordpress'@'%';
+----------------------------------------------------------------------------+
| Grants for wordpress@%                                                     |
+----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'wordpress'@'%'                                      |
| GRANT ALL PRIVILEGES ON `wordpress`.* TO 'wordpress'@'%' WITH GRANT OPTION |
+----------------------------------------------------------------------------+
2 rows in set (0.10 sec)


mysql> select User,Host from user;
+-----------+-----------+
| User      | Host      |
+-----------+-----------+
| root      | %         |
| wordpress | %         |
| mysql.sys | localhost |
+-----------+-----------+
3 rows in set (0.10 sec)

1-2-3 データのインポート

ここまでくれば、あとはMySQL知識範囲の操作です。

gzcat wordpress.dump.gz | mysql -uwordpress -p$WORDPRESS_PASS -h127.0.0.1 -P3306 wordpress

  • 大量データで時間を要する場合は、コピー元の環境をよく考えましょう。特に、mysqldumpしてからインポートまでの間隔(データ欠損の許容時間)により変わってきます。
    • 今回、移行時間で失うデータは若干のアクセスログ程度なので普通に移行元サイトをAactiveのまま並行作業しています。
    • CloudSQLと同じリージョンのGCEインスタンスからやればレイテンシは小さくなります。
  • macOSからの操作なので、zcact ではなく gzcat を利用しています

インポート後の結果を確認:

mysql> show tables;
+------------------------+
| Tables_in_wordpress    |
+------------------------+
| wp_ak_404_log          |
| wp_commentmeta         |
| wp_comments            |
 ...

Step2 GKEにWordPressをデプロイする

ようやくデータが整いましたので、ここからサービスをデプロイしていきます。

先程は wordpress 5.0.2 のイメージを対象に内容を確認していましたが、アップグレードに関する一連の流れの動作確認をしておきたかったので、移行元サイトで利用していた wordpress 4.9.8 のイメージで作業を進めます。

2-1 wordpress イメージタグの確認

4.9.8 タグのバリエーションを確認します。registry.hub.docker.com のAPIに対するGETリクエストで確認する方法が手軽そうです。

# `jq` はJSONフォーマットを操作する定番コマンドです。入ってない場合は `brew install jq` にて入れておきましょう。
$ curl -s <https://registry.hub.docker.com/v1/repositories/wordpress/tags> | jq -r '.[].name' | grep 4.9.8

4.9.8
4.9.8-apache
4.9.8-fpm
4.9.8-fpm-alpine
4.9.8-php5.6
4.9.8-php5.6-apache
4.9.8-php5.6-fpm
4.9.8-php5.6-fpm-alpine
4.9.8-php7.0
4.9.8-php7.0-apache
4.9.8-php7.0-fpm
4.9.8-php7.0-fpm-alpine
4.9.8-php7.1
4.9.8-php7.1-apache
4.9.8-php7.1-fpm
4.9.8-php7.1-fpm-alpine
4.9.8-php7.2
4.9.8-php7.2-apache
4.9.8-php7.2-fpm
4.9.8-php7.2-fpm-alpine

元々 Nginx + php-fpm 構成で運用していたので今回 4.9.8-php*.*-fpm-alpine を利用するつもりだったのですが、VolueMountまわりで不都合あり一旦見送り、 *-apache を採用しました。詳細はAppendixにて。

2-2 wordpress リソースコンフィグの作成

先に結論ですが、最終的には以下のコンフィグをKubernetesにデプロイします。

Serviceリソース

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    name: wordpress
spec:
  type: NodePort
  selector:
    app: wordpress
  ports:
  - name: http
    port: 80
    targetPort: 80
    # GKEクラスタ外からWordPressへ接続するためのポート番号 (範囲: 30000-32767 から指定)
    nodePort: 30000

Deploymentリソース

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wordpress
  strategy:
    # kubectl apply 時の挙動。詳細後述 *1
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0%
      maxUnavailable: 100%
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      # CONTAINER 1: WordPressコンテナ
      - name: wordpress
        image: wordpress:4.9.8-php7.1-apache
        # このコンテナではどの程度の最低リソースを要求するかを記載 *2
        resources:
          requests:
            cpu: 50m
            memory: 200Mi
        env:
        # Cloud SQL に接続するための情報。同一Podなので 127.0.0.1 でアクセスできる
        - name: WORDPRESS_DB_HOST
          value: "127.0.0.1"
        - name: WORDPRESS_DB_NAME
          value: "wordpress"
        # パスワードのような秘匿情報は Secret リソースから取得する(コンフィグに値を直接書かない)
        - name: WORDPRESS_DB_USER
          valueFrom:
            secretKeyRef:
              name: cloudsql-db-credentials
              key: username
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: cloudsql-db-credentials
              key: password
        - name: WORDPRESS_DB_CHARSET
          value: "utf8"
        - name: WORDPRESS_DB_COLLATE
          value: ""
        - name: WORDPRESS_TABLE_PREFIX
          value: "wp_"
        # データ移行用コンテナで準備したボリュームを配置する場所
        volumeMounts:
        - name: disk
          mountPath: "/var/www/html"

      # CONTAINER 2: CLOUD SQLコンテナ
      - name: cloudsql-proxy
        image: gcr.io/cloudsql-docker/gce-proxy:1.11
        command: ["/cloud_sql_proxy",
                  "-instances=<INSTANCE CONNECTION NAME>=tcp:3306",
                  "-credential_file=/secrets/cloudsql/credentials.json"]
        securityContext:
          runAsUser: 2  # non-root user
          allowPrivilegeEscalation: false
        # IAMサービスアカウントのキーファイルをマウントしている。ファイル作成は後述
        volumeMounts:
        - name: cloudsql-instance-credentials
          mountPath: /secrets/cloudsql
          readOnly: true

      volumes:
      - name: disk
        persistentVolumeClaim:
          claimName: wordpress-disk
      - name: cloudsql-instance-credentials
        secret:
          secretName: cloudsql-instance-credentials

2-3 Secret(秘密情報)の準備

コンフィグ内に出てくる3つのキーを作成します。

  • cloudsql-instance-credentials: CloudSQLに接続するためのキーファイル(JSON)
  • cloudsql-db-credentials: MySQL接続に必要な以下の値を格納するSecret
    • username
    • password

KubernetesのSecret系情報を保管する領域に情報を預けるような操作をしていきます。

$ kubectl -n wordpress create secret generic cloudsql-instance-credentials \\
    --from-file=credentials.json=/path/to/keyfile.json

    # 先の手順で mysqldumpファイルをインポートした時にローカルで利用したものと同じキーファイルを指定する


$ kubectl -n wordpress create secret generic cloudsql-db-credentials \\
    --from-literal=username=<string username> --from-literal=password=<string password>

    # username / password それぞれの値を記載する。カッコ <> は不要です

作成したら、値が入っていることを確認します。

$  kubectl -n wordpress get secret
NAME                            TYPE                                  DATA      AGE
cloudsql-db-credentials         Opaque                                2         1d
cloudsql-instance-credentials   Opaque                                1         1d

2-4 Service / Deployment の作成

あとは、前述の service.yml および deployment.ymlkubectl apply することで起動してくるはずです。順番はどちらが先でも構いません。

Podが Running になっていることを確認しましょう。 (一瞬 Running になっていてもしばらくして Crash していることもあるので数分間確認しておきましょう。kubectl describe pod XXXXEvents欄に Warning が出ていないことを確認するのも有用です。Crashしてしまった場合は、内容に応じて対処が変わってきます。)

$ kubectl -n wordpress get all
NAME                             READY     STATUS    RESTARTS   AGE
pod/wordpress-XXXXXXXXXX-YYYYY   2/2       Running   0          13h

NAME                TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
service/wordpress   NodePort   10.X.Y.Z      <none>        80:30000/TCP   1d

NAME                        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/wordpress   1         1         1            1           1d

2-5 Nginx(別ホスト)経由で Reverse Proxy してインターネットに公開する

いよいよInternetへ公開となります。普通ならここでIngressリソースによるロードバランサーを作成するところですが、今回は冒頭に記載の通りコスト成約のため、 always free枠のGCEインスタンスに構築したNginxを経由します。

まず、KubernetesノードのIPを確認します。

$ kubectl describe node | grep IP
  InternalIP:  10.0.0.4
  ExternalIP:  XX.XXX.XXX.C
  InternalIP:  10.0.0.3
  ExternalIP:  XX.XXX.XXX.B
  InternalIP:  10.0.0.2
  ExternalIP:  XX.XXX.XXX.A
  # 動かしているKubernetesノードの数により返却数は異なる

ここで確認するのは InternalIP の部分です。GCEインスタンス(Nginx)からアクセスするのはVPC内部通信になるため。

  • [要注意] 冒頭で記載した -enable-ip-alias を有効化せずに GKE クラスタ作成をした場合、InternalIP がどうなるのか未確認です。もしかすると、IP宛通信ができないかもしれません。
  • ExternalIP を利用することもできるはずですが、ファイアウォールの設定調整が必要になってくると思われます(未検証)。

続いて、InternalIP:nodePort に対して疎通確認してみましょう。どのIP宛でも問題ありません。

$ curl -I 10.0.0.4:30180
HTTP/1.1 301 Moved Permanently
Server: Apache/2.X.XX (Debian)
X-Powered-By: PHP/7.X.XX
Location: <http://10.0.0.4/>
Content-Type: text/html; charset=UTF-8

こんな感じでHTTPレスポンスがあればOKです。あとは、Nginxの設定です。

upstream k8snodes {
    least_conn;
    server 10.0.0.2:30000;
    server 10.0.0.3:30000;
    server 10.0.0.4:30000;
}

server {
    server_name your.domai.n;

    listen 443 ssl http2;

    ...中略...

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_pass <http://k8snodes>;
    }
}

Nginxの設定はお使いの環境に応じて調整してください。

まとめ

Kubernetes上へのWordPressの移行作業が完了しました。正確には、移行元サイトで利用していた Let’s Encrypt 証明書の移行などの手順も入ってくるのですが本質ではない話が膨らんでいくので端折っています。

Version 4.9.8 のイメージで移行後、Web管理画面からバージョンアップを試みたところ、最新の 5.0.2 (2019/01/05現在) へのバージョンアップが正常にできることを確認しました。なので、しばらく wordpress イメージを apply し直す必要はないでしょう。バージョンが不一致で気持ち悪い場合、latest タグを利用するのも選択肢ですが、docker-entrypoint.sh の処理がいつ変わるかも分かりませんので、把握できているものに固定化しておく方がよいかとは思います。

今回は求める可用性レベル・少ないKubernetesリソースの有効活用・および作業時間の都合でシングルコンテナ構成となってしまいましたが、いつか ReadWriteMany ボリュームによる複数コンテナ構成も試したいところです。

Appendix

Nginx + php-fpm 構成を諦めた理由

  • php-fpm* のコンテナはその名の通り php-fpm しか入っていないので、Webサーバー(Nginx)コンテナが別途必要になります。すると、WordPressのコンテンツファイルは Nginxコンテナ および php-fpmコンテナ 両方に対して必要になってくるのですが、同一ボリュームを複数コンテナからマウントしたらPodが起動できませんでした。Nginxコンテナのボリュームを readOnly: true としましたが結果変わらずです。
Events:
...
Warning  FailedAttachVolume      1m    attachdetach-controller                       Multi-Attach error for volume "pvc-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" Volume is already used by pod(s) wordpress-XXXXXXXXX-XXXXX
Normal   SuccessfulAttachVolume  47s   attachdetach-controller                       AttachVolume.Attach succeeded for volume "pvc-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"

となると、ReadWriteManyなボリュームで再設計するか、それか他のうまいやり方を考えるかなのですが・・・。ApacheコンテナならばWebサーバー件PHP実行環境を単一コンテナで実現できるので、一旦 *-apache で回避することにしました。

コンフィグを修正して apply し直してもコンテナが入れ替わらない *1

検証中は何回も kubectl apply -f deployment.yml することになりますが、なかなか新しいPodに切り替わってくれないことがありました。describeEvents:欄を見ると以下の通り Warning が出ています。

Warning  FailedAttachVolume      11m   attachdetach-controller                       Multi-Attach error for volume "pvc-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" Volume is already used by pod(s) wordpress-XXXXXXXXX-XXXXX

入れ替え前のPodがVolumeを掴んだまま新しいPodでVolumeマウントしようとしているため Multi-Attach error が出ているように見えます。デフォルトが RollingUpdate であることがドキュメントに記載されています。

.spec.strategy specifies the strategy used to replace old Pods by new ones. .spec.strategy.type can be “Recreate” or “RollingUpdate”. “RollingUpdate” is the default value.

Deployments - Kubernetes

というわけで Recreate に設定して apply し直したところ、別のエラーが発生。

The Deployment "wordpress" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy `type` is 'Recreate'

既に RollingUpdate で作成しているのに apply で変更しようとしたために発生したErrorと思われます。一度 delete のうえ改めて create or apply すればうまくいくのだと思いますが、今後スケールアウト設計にする時が来たら改めて RollingUpdate にするわけなので、RollingUpdate のままで Recreate と同様の挙動になる設定としました。それが以下の値です。

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0%
      maxUnavailable: 100%

strategy については以下のページが分かりやすいです: Kubernetes道場 8日目 - ReplicaSet / Deploymentについて - Toku’s Blog

リソース要求について *2

Deploymentリソースで以下の記載をしています。

  resources:
    requests:
      cpu: 50m
      memory: 200Mi

これらを指定しないでデプロイすると、Kubernetesノードは以下のような内容になります(とある1ノード)。

$  kubectl describe node

Non-terminated Pods:         (6 in total)
  Namespace                  Name                                              CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ---------                  ----                                              ------------  ----------  ---------------  -------------
  kube-system                fluentd-gcp-v3.1.0-7b4jr                          100m (10%)    1 (106%)    200Mi (17%)      500Mi (43%)
  kube-system                heapster-v1.6.0-beta.1-155fb8fd97-pafmt           138m (14%)    138m (14%)  301856Ki (25%)   301856Ki (25%)
  kube-system                kube-dns-148976df6c-ra98z                         260m (27%)    0 (0%)      110Mi (9%)       170Mi (14%)
  kube-system                kube-proxy-gke-default-pool-b76614f2-8agq         100m (10%)    0 (0%)      0 (0%)           0 (0%)
  wordpress                  wordpress-XXXXXXXXX-XXXXX                         0 (0%)        0 (0%)      0 (0%)           0 (0%)

最終行の wordpress を見てみると、一律で 0 になっている状態です。指定しなくても動くときは動くのですが、Kubernetesリソースがギリギリの状況では不都合な挙動になると思いますので、不都合を理解のうえ設定しましょう。詳細は以下のページが詳しいです。

Kubernetesで実際のメモリを超えるコンテナアプリを動かすと、どうなるか? - あさのひとりごと

それにしても、fluentdの要求するリソースがちょっと大きいですね・・・。ギリギリコストのインスタンススペックで運用しようと思うと存在感が大きいです。

_phpX.Y_ に関する選定基準

移行元サイトでPHP5.Xを利用していましたが、今回PHP7.Xのイメージに使うに当たり、簡単に(超雑に)abでテストしました。

実行環境は以下の通り。

  • v1.11.5-gke.5
  • Container-Optimized OS from Google
  • instance-type=g1-small
  • 負荷元: 日本
  • 負荷先: 米国リージョン

今回 php7.1 の方が良かったのでこれを選びましたが、試行回数も同一条件で2回程度、同じ傾向なのを確認したうえですがサンプルが少なすぎるのであくまで雰囲気レベルで見てください。

wordpress:4.9.8-php7.1-apache:

Concurrency Level:      4
Complete requests:      100
Failed requests:        0
Write errors:           0
Requests per second:    5.59 [#/sec] (mean)
Transfer rate:          328.74 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      287  291   4.6    290     321
Processing:   355  397  58.2    373     646
Waiting:      261  299  54.6    277     552
Total:        644  688  60.3    663     936

wordpress:4.9.8-php7.2-apache:

Concurrency Level:      4
Complete requests:      100
Failed requests:        0
Write errors:           0
Requests per second:    3.76 [#/sec] (mean)
Transfer rate:          220.89 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      287  292   5.5    290     320
Processing:   364  714 925.1    551    7613
Waiting:      269  478 613.2    399    5724
Total:        653 1006 927.0    843    7920