EC-CUBE4をKubernetesで動かす

最近Kubernetesを触る機会があったので、学習用にminikubeでEC-CUBEを動作させるサンプルを作ってみました。備忘も兼ねて手順を残しておこうと思います。

## 全体の構成

全体の構成は以下のようなイメージです。

```
| minikube ---------------------------------------------------
|                                                             |
| node ----------------------------------------------------|  |
|  |                                                       |  |
|  |  [ web(eccube) ] [ db(pgsql) ] [ mail(mailcatcher) ]  |  |
|  |        |                                              |  |
|  |     [ pvc ]                                           |  |
|  |        |                                              |  |
|  |     [ pv  ]                                           |  |
|  |                                                       |  |
|   --------------------------------------------------------  |
|                                                             |
 -------------------------------------------------------------
```

web、db(postgresql)、メール(mailcatcher)の構成で、EC-CUBEの永続化が必要なファイル群を保持する用途としてPersistentVolumeを用意しています。

## 前準備

minikubeのインストール

```sh
$ brew install minikube
```

起動しておきます

```sh
$ minikube start
```

## MailCatcherのpodを作る

メールサーバのpodから作っていきます。

```yaml:mail-deploy.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: eccube-mail
spec:
  replicas: 1
  selector:
    matchLabels:
      app: eccube-mail
  template:
    metadata:
      labels:
        app: eccube-mail
    spec:
      containers:
        - name: eccube-mail
          image: schickling/mailcatcher
          ports:
            - containerPort: 1025
            - containerPort: 1080
---
apiVersion: v1
kind: Service
metadata:
  name: eccube-mail
spec:
  type: NodePort
  selector:
    app: eccube-mail
  ports:
    - port: 1080
      targetPort: 1080
      name: mailer
    - port: 1025
      targetPort: 1025
      name: smtp
```

作成したマニフェストをapplyし、podを立ち上げます。

```sh
$ kubectl apply -f mail-deploy.yml 
deployment.apps/eccube-mail created
service/eccube-mail created
```

podの状況を確認して、StatusがRunningになっていたらOK。

```sh
$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
eccube-mail-5b8cf64bb9-85c5s   1/1     Running   0          83s
```

MailCatcherのWEBメーラを確認するには、 minikube serviceを実行します。

```
$ minikube service eccube-mail
|-----------|-------------|-------------|---------------------------|
| NAMESPACE |    NAME     | TARGET PORT |            URL            |
|-----------|-------------|-------------|---------------------------|
| default   | eccube-mail | mailer/1080 | http://192.168.49.2:31044 |
|           |             | smtp/1025   | http://192.168.49.2:30208 |
|-----------|-------------|-------------|---------------------------|
🏃  Starting tunnel for service eccube-mail.
|-----------|-------------|-------------|------------------------|
| NAMESPACE |    NAME     | TARGET PORT |          URL           |
|-----------|-------------|-------------|------------------------|
| default   | eccube-mail |             | http://127.0.0.1:58752 |
|           |             |             | http://127.0.0.1:58753 |
|-----------|-------------|-------------|------------------------|
```

上記の出力結果だと、http://127.0.0.1:58752 にアクセスし、画面が表示されることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72858/fe5c0c42-b84d-5357-282a-477f7395b0dd.png)

確認できたらminikube serviceコマンドは終了しても大丈夫です。

## PostgreSQLのpodを作る

次にPostgreSQLのpodを作ります。

```yaml:pgsql-deploy.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: eccube-pgsql
  labels:
    app: eccube-pgsql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: eccube-pgsql
  serviceName: eccube-pgsql
  template:
    metadata:
      labels:
        app: eccube-pgsql
    spec:
      containers:
        - image: postgres:10-alpine
          name: eccube-pgsql
          env:
            - name: POSTGRES_USER
              value: postgres
            - name: POSTGRES_PASSWORD
              value: password
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 500Mi
```

上記のマニフェストをapplyし、pgsqlのpodが立ち上がるのを確認します。

```
$ kubectl apply -f pgsql-deploy.yml
statefulset.apps/eccube-pgsql created
service/eccube-pgsql created
```

```
$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
eccube-mail-5b8cf64bb9-85c5s   1/1     Running   0          9m54s
eccube-pgsql-0                 1/1     Running   0          44s
```

## webサーバのpodを作る

まず、EC-CUBEを配置するための永続ボリュームを作成します。

### 永続ボリュームの作成

```yaml:eccube-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: eccube-pv
spec:
  storageClassName: standard
  capacity:
    storage: 3Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /data/eccube
```

```yaml:eccube-pv.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: eccube-pvc
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 500Mi

```

上記のマニフェストをapplyし、作成を確認します。

```sh

$ kubectl apply -f eccube-pv.yml -f eccube-pvc.yml
persistentvolume/eccube-pv created
persistentvolumeclaim/eccube-pvc created
```

```sh
$ kubectl get pv,pvc
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                         STORAGECLASS   REASON   AGE
persistentvolume/eccube-pv                                  3Gi        RWO            Retain           Bound    default/eccube-pvc            standard                2m14s
persistentvolume/pvc-fd0555f5-0bd1-4d78-b969-ba05167ac274   500Mi      RWO            Delete           Bound    default/data-eccube-pgsql-0   standard                16h

NAME                                        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data-eccube-pgsql-0   Bound    pvc-fd0555f5-0bd1-4d78-b969-ba05167ac274   500Mi      RWO            standard       16h
persistentvolumeclaim/eccube-pvc            Bound    eccube-pv                                  3Gi        RWO            standard       113s
```

### apacheのpodの作成

次に、apacheのpodを作成していきます。
コンテナは、[eccube/php-ext-apache](https://hub.docker.com/r/eccube/php-ext-apache)を使っています。

```yaml:eccube-deploy.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: eccube
spec:
  selector:
    matchLabels:
      app: eccube
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: eccube
    spec:
      containers:
        - name: eccube
          image: eccube/php-ext-apache:7.3
          volumeMounts:
            - name: htdocs-volume
              mountPath: /var/www/html
            - name: persistent-volume
              mountPath: /opt/ec-cube
      volumes:
        - name: htdocs-volume
          emptyDir: {}
        - name: persistent-volume
          persistentVolumeClaim:
            claimName: eccube-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: eccube
spec:
  type: NodePort
  selector:
    app: eccube
  ports:
    - port: 80
      targetPort: 80
```

上記のマニフェストをapplyし、作成を確認します。

```sh
$ kubectl apply -f eccube-deploy.yml 
deployment.apps/eccube created
service/eccube created
```

```sh
$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
eccube-bc7745978-rg64c         1/1     Running   0          98m
eccube-mail-5b8cf64bb9-85c5s   1/1     Running   0          161m
eccube-pgsql-0                 1/1     Running   0          152m
```

これで各pod群の準備ができました。

## EC-CUBEの配置

ここからEC-CUBEの配置をしていきます。

アプリケーションによって生成・変更されるファイルがあり、そういったファイルはpodの再起動で初期化されるため、永続化することが必要になります。

今回永続化の対象にするのは以下のファイル群です。

|||永続化|
|--- | --- | --- ||
|app | Customize ||
|app | DoctrineMigrations |  
|app | Plugin | ○|
|app | PluginData | ○
|app | config |  
|app | proxy | ○
|app | template | ○
|bin |   |  
|html |   | ○
|src |   |  
|var |   | ○
|vendor |   |  
|composer.json |   | ○
|composer.lock |   | ○
|symfony.lock |   | ○
|index.php |   |  
|maintenance.php |   |  
|robots.txt |   |  
|.env |   | ○

これを元に、初期化スクリプトを作ります。

```sh:eccube_install.sh
#!/usr/bin/env bash

set -x

TMP_DIR=/tmp
ECCUBE_VER=4.0.5
ECCUBE_FILE=${TMP_DIR}/eccube-${ECCUBE_VER}.tar.gz
DOCROOT_DIR=/var/www/html
PV_DIR=/opt/ec-cube

cd ${TMP_DIR}
curl -O https://downloads.ec-cube.net/src/eccube-${ECCUBE_VER}.tar.gz

if [ -d ${DOCROOT_DIR} ]; then
  rm -rf ${DOCROOT_DIR}
  mkdir -p ${DOCROOT_DIR}
fi

# document root配下に展開
cd ${DOCROOT_DIR}
tar xvzf ${ECCUBE_FILE} --strip-components 1

rm -rf ${DOCROOT_DIR}/composer.json
rm -rf ${DOCROOT_DIR}/composer.lock
rm -rf ${DOCROOT_DIR}/symfony.lock
rm -rf ${DOCROOT_DIR}/app/Plugin
rm -rf ${DOCROOT_DIR}/app/PluginData
rm -rf ${DOCROOT_DIR}/app/template
rm -rf ${DOCROOT_DIR}/html
rm -rf ${DOCROOT_DIR}/var

# link作成
ln -s ${PV_DIR}/.env ${DOCROOT_DIR}/.env
ln -s ${PV_DIR}/composer.json ${DOCROOT_DIR}/composer.json
ln -s ${PV_DIR}/composer.lock ${DOCROOT_DIR}/composer.lock
ln -s ${PV_DIR}/symfony.lock ${DOCROOT_DIR}/symfony.lock
ln -s ${PV_DIR}/app/Plugin ${DOCROOT_DIR}/app/Plugin
ln -s ${PV_DIR}/app/PluginData ${DOCROOT_DIR}/app/PluginData
ln -s ${PV_DIR}/app/template ${DOCROOT_DIR}/app/template
ln -s ${PV_DIR}/html ${DOCROOT_DIR}/html
ln -s ${PV_DIR}/var ${DOCROOT_DIR}/var

# 永続ボリュームへ展開
cd ${PV_DIR}
ARCHIVE_ROOT=$(tar tf ${ECCUBE_FILE} | sort | head -n 1)

if [ ! -f ${PV_DIR}/.env ]; then

  cat << EOF > .env
APP_ENV=${APP_ENV}
APP_DEBUG=${APP_DEBUG}
DATABASE_URL=${DATABASE_URL}
DATABASE_SERVER_VERSION=${DATABASE_SERVER_VERSION}
MAILER_URL=${MAILER_URL}
ECCUBE_AUTH_MAGIC=${ECCUBE_AUTH_MAGIC}
ECCUBE_ADMIN_ROUTE=${ECCUBE_ADMIN_ROUTE}
EOF

  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}composer.json --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}composer.lock --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}symfony.lock --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}app/Plugin --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}app/PluginData --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}app/template --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}html --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}var --strip-components 1

  cd ${DOCROOT_DIR}
  bin/console e:i --no-interaction

else

  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}composer.json --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}composer.lock --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}symfony.lock --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}vendor --strip-components 1
  tar xfz ${ECCUBE_FILE} ${ARCHIVE_ROOT}html --strip-components 1

  cd ${DOCROOT_DIR}
  bin/console cache:clear --no-warmup --env=prod
  bin/console eccube:composer:require-already-installed
  bin/console doctrine:schema:update --force --dump-sql
  bin/console doctrine:migrations:migrate --no-interaction

fi

bin/console cache:clear --no-warmup --env=prod
bin/console cache:warmup --no-optional-warmers --env=prod
composer dump-autoload -o

chown -R www-data:www-data ${PV_DIR}
chown -R www-data:www-data ${DOCROOT_DIR}
```

DocumentRoot直下は毎回クリア・ソースコードが展開され、永続化されている/opt/ec-cubeへシンボリックリンクを貼る構成にしています。

また、初回起動時はEC-CUBEのインストールを実行し、2回目以降はスキーマ更新やマイグレーションを行います。

このスクリプトをpod初期化時に実行できるよう、コンテナ化します。

```dockerfile:Dockerfile
FROM eccube/php-ext-apache:7.3

ADD eccube_install.sh /
```

ローカルのDockerイメージにminikubeからアクセスできるよう、ビルド前に以下を実行します。

```sh
$ eval $(minikube -p minikube docker-env)
```

ビルドします。

```sh
$ docker build . -t local/eccube-maintenance
```

初期化スクリプトの用意はできたので、apacheのpod初期化時に実行されるよう、`eccube-deploy.yml`を以下のように修正します。

```diff:eccube-deploy.yml
apiVersion: apps/v1
...
    spec:
+      initContainers:
+        - name: eccube-init
+          image: local/eccube-maintenance:latest
+          imagePullPolicy: IfNotPresent
+          envFrom:
+            - configMapRef:
+                name: eccube-config
+          volumeMounts:
+            - name: htdocs-volume
+              mountPath: /var/www/html
+            - name: persistent-volume
+              mountPath: /opt/ec-cube
+          command: ["/eccube_install.sh"]
      containers:
        - name: eccube
          image: eccube/php-ext-apache:7.3
          volumeMounts:
            - name: htdocs-volume
              mountPath: /var/www/html
            - name: persistent-volume
              mountPath: /opt/ec-cube
...
```

また、EC-CUBEの初期化時に渡す環境変数を定義しておきます

```yaml:eccube-config.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: eccube-config
data:
  APP_ENV: 'prod'
  APP_DEBUG: '0'
  DATABASE_URL: 'postgresql://postgres:password@eccube-pgsql:5432/eccube.db'
  DATABASE_SERVER_VERSION: '10'
  MAILER_URL: 'smtp://eccube-mail:1025'
  ECCUBE_ADMIN_USER: 'admin'
  ECCUBE_ADMIN_PASS: 'password'
  ECCUBE_AUTH_MAGIC: 'Myvpx7CecbURw9cF'
  ECCUBE_ADMIN_ROUTE: 'admin'
```

上記のマニフェストをapplyします。

```sh
$ kubectl apply -f eccube-config.yml -f eccube-deploy.yml
configmap/eccube-config created
deployment.apps/eccube configured
service/eccube unchanged
```

## 動作確認

`minikube service eccube` でEC-CUBEの動作確認をします。
ブラウザでEC-CUBEの画面が立ち上がれば成功です。

```sh
$ minikube service eccube
|-----------|--------|-------------|---------------------------|
| NAMESPACE |  NAME  | TARGET PORT |            URL            |
|-----------|--------|-------------|---------------------------|
| default   | eccube |          80 | http://192.168.49.2:30541 |
|-----------|--------|-------------|---------------------------|
🏃  Starting tunnel for service eccube.
|-----------|--------|-------------|------------------------|
| NAMESPACE |  NAME  | TARGET PORT |          URL           |
|-----------|--------|-------------|------------------------|
| default   | eccube |             | http://127.0.0.1:62911 |
|-----------|--------|-------------|------------------------|
🎉  Opening service default/eccube in default browser...
❗  Dockerドライバーをdarwin上で動かしているため、実行するにはターミナルを開く必要があります。
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72858/6f780563-cae3-5743-72e9-84c4cc63ba3c.png)

以上になります。

マニフェストやDockerfileは[こちら](https://github.com/chihiro-adachi/eccube-minikube)にも上げていますので、参考にしていただければ幸いです。