AWS EBベースのWordPress環境をローカルPCのDockerで再現してみた

# はじめに
AWS Elastic Beanstalk（PHPランタイム）を利用したポータルサイトを運営しています。APサーバはWordPress on EC2、DBサーバはRDS（MySQL）という典型的な構成です。その他、ALBやEFSを活用したスケーラブルな構成、CodePipelineを活用したCI/CDの実践など、ベストプラクティスのお世話になっています。先人たち知恵に感謝です。

そんなポータルサイトですが、ローカル開発環境の整備がおざなりになっていました、、、。この度、Docker for Desktop (on Windows 10) を利用して、ローカル開発環境を構築したので、手順やハマったことを共有します。特に、APサーバのOSにAmazon Linux 2を採用したことが、情報の混乱による遠回りを招きました、、、。この記事を読んだ方が、少しでも近道できれば幸いです。

（2019/08/12 追記）
ボリュームマウントするuploadsフォルダの所有者をhttpdプロセスの起動ユーザ（apacheなど）に変更するようにDockerfileを修正しました。これをやっておかないと、メディアのアップロードや物理削除でエラーになります。

# 開発環境を整理する
まず、開発環境について整理します。AWS上の開発環境を便宜上、リモート開発環境と呼びます。厳密には、リモート/ローカル環境間で各種バージョンを一致させるべきですが、今回はそこまで厳密性にこだわりませんでした。APサーバは、Amazon Linuxのサポート期限が2020年6月30日と迫っていることもあり、Amazon Linux 2を選択しました。これがハマった要因だったのです、、、。

### コピー元のリモート開発環境（AWS）

- APサーバ：64bit Amazon Linux 2018.03 v2.8.12 running PHP 7.2 (AWS Elastic Beanstalk)
- DBサーバ：Amazon RDS for MySQL 5.6

### コピー先のローカル開発環境（Docker for Desktop on Windows 10 Enterprise）

- APサーバ：Amazon Linux 2（Docker公式イメージ）
- DBサーバ：MySQL 5.7（Docker公式イメージ）

# ローカル開発環境でやりたいこと
ローカル開発環境でやりたいことは、とてもシンプルです。

- リモート開発環境と同じgitリポジトリを利用したい（ローカルはfeatureブランチで）
- リモート開発環境と同じデータ（データベースおよびメディアファイル）を利用したい

つまり、(1) APサーバ的なAWS Elastic Beanstalkがやっていることの再現、(2) DBサーバ的なデータ移行です。ただ単に、WordPress環境をローカルPC上に構築するのであれば、WordPressおよびMySQL両方ともDocker公式イメージから構築すれば良いので瞬殺です（[クイックスタート・ガイド：Docker Compose と Wordpress](http://docs.docker.jp/compose/wordpress.html#docker-compose-wordpress)）。しかし、AWS Elastic Beankstalk環境と同じようにgitリポジトリを利用するために、Amazon Linux 2から環境構築することにしました。

# Dockerコマンドで手順を確認してみる

Dockerfileやdocker-compose.ymlをゴリゴリ書き始める前に、Dockerコマンドで地道に手順を確認することにしました。

まず、最新のAmazon LinuxイメージすなわちAmazon Linux 2イメージを取得して、`/sbin/init`でOS起動してみましたが、いきなり以下のエラーが発生してしまいました。`/sbin/init`がない、、、これは先が思いやられます。

```
$ docker pull amazonlinux:latest
$ docker run -it -d --name hoge-web amazonlinux:latest /sbin/init
4cb36ea9...(省略)
docker: Error response from daemon: OCI runtime create failed: container_linux.g
o:344: starting container process caused "exec: \"/sbin/init\": stat /sbin/init:
 no such file or directory": unknown.
```

Amazon Linux 2の公式イメージは、かなりスリムアップされており、systemdすら初期インストールされていないようです。Qiitaの記事を検索してみると、「まず`/bin/bash`で起動して`systemd`をインストールしてから`/sbin/init`で起動せよ」とのことでしたので、やってみました。まず、`/bin/bash`で起動します。

```
$ docker rm hoge-web
$ docker run -it -d --name hoge-web amazonlinux:latest /bin/bash
b567ea12...(省略)
```

初期状態では、`httpd`が未インストールどころか`ps`コマンドや`ping`コマンドすら打てない、ということが分かりましたので、色々追加していきます。細やかなピックアップが面倒だったので、LAMPをインストールしてしまいます。

```
$ docker exec -it hoge-web /bin/bash
bash-4.2# amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2
bash-4.2# yum install -y systemd xinetd httpd mod_ssl php php-mbstring wget curl tar
bash-4.2# exit
```

続いて、必要な設定ファイルをコピーしておきます。`aws_env.conf`はAWS Elastic Beanstalkが生成するファイルのひとつで、Apacheプロセスへ環境変数を渡すためのものです。その他、`systemctl`で`httpd`を自動起動するための設定ファイルやSSL関連ファイルなどがあります。

```
$ docker cp web/etc/httpd/conf/httpd.conf hoge-web:/etc/httpd/conf
$ docker cp web/etc/httpd/conf.d/aws_env.conf hoge-web:/etc/httpd/conf.d
$ docker cp web/etc/sysconfig/httpd hoge-web:/etc/sysconfig
$ docker cp web/lib/systemd/system/httpd.service hoge-web:/lib/systemd/system
$ docker cp web/ssl/hoge.crt hoge-web:/etc/pki/tls/certs/localhost.crt
$ docker cp web/ssl/hoge.key hoge-web:/etc/pki/tls/private/localhost.key
```

続いて、`systemd`インストール済みのコンテナを`/sbin/init`で起動します。`httpd`の自動起動を設定後、再びコンテナイメージをコミットします。これがマスタイメージになります。

```
$ docker commit hoge-web hoge-web:latest
sha256:2f1d9b04...(省略)

$ docker stop hoge-web && docker rm hoge-web
$ docker run -it -d --name hoge-web hoge-web:latest /sbin/init
$ docker exec hoge-web systemctl enable httpd
$ docker commit hoge-web hoge-web:latest
```

# Dockerコマンドから設定ファイルへ

Dockerコマンドで手順が確認できましたので、その内容を設定ファイルで再現していきます。webサービス（WordPress）とdbサービス（MySQL）を利用するので、`docker-compose.yml`を使います。

### Dockerコマンドで確認した手順の流れ

簡単にまとめると、以下の4ステップです。

1. Amazon Linux公式イメージを`/bin/bash`で起動
1. `systemd`や`httpd`などをインストールし、マスタイメージをコミット
1. マスタイメージを`/sbin/init`で起動
1. `httpd`の自動起動を設定し、マスタイメージをコミット

### 手順をDockerfileに落とし込む

手順1と手順2は、`Dockerfile`で対応します。先ほどの確認手順に加えて、PHPデバッグ向けに`xdebug`をインストールしています。また、AWS Elastic BeanstalkのPHPランタイム環境を再現するために、ディレクトリ作成やシンボリックリンク作成も行っています。

```dockerfile:docker/web-init/Dockerfile
FROM amazonlinux:latest
CMD /bin/bash

# install software packages
RUN amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2 \
&& yum update -y && yum clean all \
&& yum install -y gcc xinetd httpd mod_ssl php php-mbstring php-devel php-pear wget tar \
&& pecl install xdebug \
&& mkdir -p /var/app/current \
&& cd /var/www \
&& rmdir html \
&& ln -s /var/app/current html \
&& mkdir -p /var/www/hoge/local/wp-content/uploads \
&& chown -R apache:apache /var/www/hoge

# copy configration files
COPY web/etc/httpd/conf/httpd.conf /etc/httpd/conf
COPY web/etc/httpd/conf.d/hoge-alias.conf /etc/httpd/conf.d
COPY web/etc/httpd/conf.d/aws_env.conf /etc/httpd/conf.d
COPY web/etc/php.d/xdebug.ini /etc/php.d
COPY web/etc/sysconfig/httpd /etc/sysconfig
COPY web/lib/systemd/system/httpd.service /lib/systemd/system

# copy certificate & private key
COPY web/ssl/hoge.crt /etc/pki/tls/certs/localhost.crt
COPY web/ssl/hoge.key /etc/pki/tls/private/localhost.key
```

続いて、手順3と手順4を`Dockerfile`に落とし込みます。

```dockerfile:docker/web/Dockerfile
FROM hoge-web:latest
CMD /sbin/init

# enable httpd auto-start
RUN systemctl enable httpd
```

### 手順をdocker-compose.ymlに落とし込む

さあ、いよいよdbサービスとwebサービスをまとめるときがやってきました。最終的な`docker-compose.yml`は以下の通りです。何点か工夫を加えました。

- `db/initdb.d`に初期化用のSQLスクリプトを配置
  - CREATE DATABASEステートメントでデータベース作成
  - EXPORTしたダンプファイルでDROP/CREATE/INSERT（＝データ移行）
- ローカルPC側のクライアントツールからのアクセスを考慮し、3306ポートを公開
- buildを二段階にするため、web-initとwebに分割
- OSに渡す環境変数を`env_file`として独立
- 自動起動する`systemctl start httpd`がエラーにならないよう特権（`privileged: true`）を指定

```yaml:docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    container_name: hoge-db
    volumes:
      - "./db/data:/var/lib/mysql"
      - "./db/initdb.d:/docker-entrypoint-initdb.d"
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: hoge_wordpress
      MYSQL_USER: hoge_wordpress
      MYSQL_PASSWORD: hoge_wordpress
    ports:
      - "3306:3306"
  web-init:
    image: hoge-web:latest
    build:
      context: ./
      dockerfile: ./docker/web-init/Dockerfile
    command: /bin/bash
    container_name: hoge-web-init
  web:
    depends_on:
      - db
    image: hoge-web:latest
    build:
      context: ./
      dockerfile: ./docker/Dockerfile
#   command: /usr/sbin/httpd -DFOREGROUND
    command: /sbin/init
    container_name: hoge-web
    volumes:
      - "./web/HOGE_WP:/var/app/current"
      - "./web/uploads:/var/www/hoge/local/wp-content/uploads"
    links:
      - db
    ports:
      - "80:80"
      - "443:443"
    privileged: true
    restart: always
    env_file: ./web/etc/sysconfig/httpd
```

# Docker Composeで環境構築する

ついに、AWS Elastic Beanstalk（PHPランタイム）を利用したポータルサイトのローカル開発環境を構築するときがやってきました。面倒な手順はすべて設定ファイルにまとめましたので、コマンドはとてもシンプルです。

```
$ docker-compose build web-init web
$ docker-compose up -d db web
```

なお、`docker-compose up`するときにサービス名を明示しないと、`web-init`サービスも`create`されてしまい、ごみコンテナが残ってしまうので、注意が必要です。

# さいごに

`docker-compose.yml`のコメントにあるように、試行錯誤を重ねる中で`/sbin/init`ではなく、`/usr/sbin/httpd`で起動してしまう方法も考えました。この場合、`systemctl`を利用せずに`httpd`を起動/停止するため、`/sbin/init`でのコンテナ起動が不要となります。そうすると、かなりシンプルになるのですが、Amazon Linux 2上での`systemd`を利用したサービス制御ができなくなってしまうため、`/sbin/init`での起動にこだわりました。

また、手順を確認したときのように、Dockerコマンドで対応すれば何とでもなるのですが、Docker Composeの利用にこだわりました。少しトリッキーな気もしますが、環境構築のための手順はかなりシンプル化できたと思います。

# 参考

https://qiita.com/harukisan/items/6910684bbf2043a29812
https://qiita.com/Targityen/items/6e80b855b79d521412f0
https://qiita.com/issei_0403/items/0dd1ce7407e720338ea8
https://qiita.com/idani/items/79de4a525eaeaa16e497