MySQL の where 狙いと order by 狙い

わたしは参加していないのですが YAPC::Asia Tokyo 2014 で次のような発表があったようです。

このセッションを聞いている人を WHERE gender = 'male' なら ORDER BY を狙った方がいいだろうし、WHERE gender = 'female' なら WHERE を狙った方がいい。

ちょっと試してみます。

次のようなテーブルがあったとします。age は年齢で gender は性別です。

drop table if exists yapcasia;
create table yapcasia (
  id int not null primary key,
  age int not null,
  gender char(6) not null
);

create index idx_yapcasia__age on yapcasia (age);
create index idx_yapcasia__gender on yapcasia (gender);

次のようにデータを入れます。

time seq 10000 |
  mysql test -e "
    truncate table yapcasia;
    load data local infile '/dev/stdin' into table yapcasia (@id)
      set id = @id,
          age = floor(rand() * 40 + 20),
          gender = if(@id % 500, 'male', 'female');
    analyze table yapcasia;
  "

男性が 9980 人に対して、女性が 20 人しかいません。

select count(*) from yapcasia where gender = 'male';
/* 9980 */

select count(*) from yapcasia where gender = 'female';
/* 20 */

性別ごとに年齢の若い順に 5 人列挙します。

select * from yapcasia where gender = 'male' order by age asc limit 5;
+-----+-----+--------+
| id  | age | gender |
+-----+-----+--------+
|  18 |  20 | male   |
|  51 |  20 | male   |
|  75 |  20 | male   |
| 122 |  20 | male   |
| 181 |  20 | male   |
+-----+-----+--------+

select * from yapcasia where gender = 'female' order by age asc limit 5;
+------+-----+--------+
| id   | age | gender |
+------+-----+--------+
| 4000 |  20 | female |
| 4500 |  20 | female |
| 1500 |  22 | female |
| 7500 |  22 | female |
| 3500 |  22 | female |
+------+-----+--------+

実行計画を見てみると、

explain select * from yapcasia where gender = 'male' order by age asc limit 5 \G
/*
           id: 1
  select_type: SIMPLE
        table: yapcasia
         type: index
possible_keys: idx_yapcasia__gender
          key: idx_yapcasia__age
      key_len: 4
          ref: NULL
         rows: 10
        Extra: Using where
*/

explain select * from yapcasia where gender = 'female' order by age asc limit 5 \G
/*
           id: 1
  select_type: SIMPLE
        table: yapcasia
         type: ref
possible_keys: idx_yapcasia__gender
          key: idx_yapcasia__gender
      key_len: 18
          ref: const
         rows: 20
        Extra: Using index condition; Using where; Using filesort
*/

男性を検索するときは ORDER BY 狙いでインデックスが、女性を検索するときは WHERE 狙いでインデックスが使われています。

InnoDB の統計情報はカーディナリティが極端に低い列だと値ごとの統計情報も持っているのでしょうかね?


この例なら yapcasia (gender, age) なインデックスがあれば両狙いになるのは言うまでもありません。

drop index idx_yapcasia__age on yapcasia;
drop index idx_yapcasia__gender on yapcasia;
create index idx_yapcasia__gender_age on yapcasia (gender, age);
analyze table yapcasia;

explain select * from yapcasia where gender = 'male' order by age asc limit 5 \G
/*
           id: 1
  select_type: SIMPLE
        table: yapcasia
         type: ref
possible_keys: idx_yapcasia__gender_age
          key: idx_yapcasia__gender_age
      key_len: 18
          ref: const
         rows: 5078
        Extra: Using where; Using index
*/

explain select * from yapcasia where gender = 'female' order by age asc limit 5 \G
/*
           id: 1
  select_type: SIMPLE
        table: yapcasia
         type: ref
possible_keys: idx_yapcasia__gender_age
          key: idx_yapcasia__gender_age
      key_len: 18
          ref: const
         rows: 20
        Extra: Using where; Using index
*/

AWS で WordPress を RDS とか s3fs とか使って動かしてみる

前回は WordPress にアップロードされた画像などのファイルを AWS SDK for PHP の S3 ストリームラッパーを使って無理やり S3 に保存しましたが、

s3fs-fuse を使えばもっと簡単そうなのでやってみました。

下記の手順は前回と変わらないので省略します。

前回の手順では S3 Budket の作成で Permissions の bucket policy でアップロードされたファイルが自動的に公開されるようにしていましたが、s3fs ならマウントオプション (default_acl) で指定できるので Permissions の設定は必要ありません(もしかしたら AWS SDK for PHP のストリームラッパーでもできたのかも?)。

s3fs-fuse のインストール

s3fs-fuse をインストールします。

まずはビルドに必要なパッケージをインストールします。

yum install kernel-devel libxml2-devel libcurl-devel gcc-c++ fuse fuse-devel openssl-devel git automake

ソースを取得してビルドします。

git clone https://github.com/s3fs-fuse/s3fs-fuse.git
cd s3fs-fuse/
./autogen.sh
./configure
make
sudo make install

マウントポイントを作成します。

mkdir -p /s3fs/wordpress/

fstab に追記します。

cat <<EOS>> /etc/fstab
/usr/local/bin/s3fs#example-wordpress /s3fs/wordpress fuse\
 _netdev,rw,allow_other,uid=$(id -u apache),gid=$(id -g apache),default_acl=public_read,iam_role=wp-role 0 0
EOS
cat /etc/fstab

マウントします。

mount -a

WordPress のインストール

WordPress をダウンロードして /var/www/html に展開します。

cd /usr/local/src/
wget http://ja.wordpress.org/wordpress-3.9.2-ja.zip
unzip wordpress-3.9.2-ja.zip
rm -fr /var/www/html/
mv wordpress /var/www/html/

wp-config.php を作ります。DB のホスト名には RDS の Endpoint のホスト名を指定します。

cd /var/www/html
mv wp-config-sample.php wp-config.php

sed wp-config.php -e "
  /^define('DB_NAME'/c     define('DB_NAME', 'wordpress');
  /^define('DB_USER'/c     define('DB_USER', 'wordpress');
  /^define('DB_PASSWORD'/c define('DB_PASSWORD', 'wordpress');
  /^define('DB_HOST'/c     define('DB_HOST', 'wordpress.xxxxxxxx.ap-northeast-1.rds.amazonaws.com');
" -i

curl https://api.wordpress.org/secret-key/1.1/salt/ > /tmp/secret

sed wp-config.php -e "
  /^define('AUTH_KEY'/r /tmp/secret
  /^define('AUTH_KEY'/d
  /^define('SECURE_AUTH_KEY'/d
  /^define('LOGGED_IN_KEY'/d
  /^define('NONCE_KEY'/d
  /^define('AUTH_SALT'/d
  /^define('SECURE_AUTH_SALT'/d
  /^define('LOGGED_IN_SALT'/d
  /^define('NONCE_SALT'/d
" -i

cat wp-config.php

ブザウザで http://aws.example.net/ にアクセスして WordPress のインストールウィザードに必要項目を入力してインストールを完了します。

この後、アップロードファイルの保存先の設定をするので、まだ記事の投稿は行いません。

WordPress のアップロード先設定

WordPress の次の URL を開きます。

  • http://aws.example.net/wp-admin/options.php

upload_url_path を探して、次の通りに入力して保存します。

upload_url_path  http://example-wordpress.s3-ap-northeast-1.amazonaws.com

WordPress のアップロード先ディレクトリにシンボリックリンクを作ります。

ln -sf /s3fs/wordpress /var/www/html/wp-content/uploads

動作確認

WordPress に画像をアップロードしてみます。上手くアップロードできれば成功です。

参考にした記事

参考にした記事だと IAM ロールを使うときは rc.local でマウントしていたけれど、fstab で _netdev を指定することでマウントのタイミングを遅らせるようにしてみました。

AWS で WordPress を RDS とか S3 とか使って動かしてみる

少し前に AWS で DRBD やら Pacemaker やら Heartbeat やら GlusterFS を使って WordPress を HA 構成にしてみましたが、

なんで RDS とか S3 とか使わないの? 馬鹿なの? 死ぬの? とセルフツッコミが入ったので RDS や S3 を使ってみました。

VPC の作成

VPC は以前作ったものをそのまま使います。

おさらいすると、次のような感じです。

- VPC
   - Name tag               wp-vpc
   - CIDR block             10.1.0.0/16
   - Tenancy                Default
- Subnets
   - Name tag               wp-net-a
   - VPC                    wp-vpc
   - Availability Zone      ap-northeast-1a
   - CIDR block             10.1.1.0/24
   - Auto-assign Public IP  yes
- Subnets
   - Name tag               wp-net-c
   - VPC                    wp-vpc
   - Availability Zone      ap-northeast-1c
   - CIDR block             10.1.2.0/24
   - Auto-assign Public IP  yes
- Route Tables
   - Routes
      - Destination         0.0.0.0/0
      - Target              wp-gw
- Internet Gateways
   - Name tag               wp-gw
   - VPC                    wp-vpc
- Security Group
   - Security group name    default
   - VPC                    wp-vpc
   - inbound Rule
      - Type                ALL Traffic
      - Source              sg-xxxxxxxx (default)
   - inbound Rule
      - Type                SSH(22)
      - Source              My IP
   - inbound Rule
      - Type                All ICMP
      - Source              My IP
- Security Group
   - Security group name    public
   - VPC                    wp-vpc
   - inbound Rule
      - Type                HTTP
      - Source              Anywhere

IAM ロールを作成

WordPress から S3 にアップロードできるようにするために IAM ロールを作成します。

IAM ユーザーを作成して、そのユーザーの Access Key ID と Secret Access Key を使って S3 にアクセスすることも出来ますが、その場合は Access Key ID と Secret Access Key を EC2 インスタンス内に保存する必要があります。

一方、IAM ロールならインスタンスの作成時にロールを紐つけるだけで良いので、Access Key ID や Secret Access Key をインスタンスに保存する必要がなく、簡単かつ安全です。

特別な事情で IAM ロールが使えない場合以外は、IAM ロールを使うことが奨励されているようです。

IAM ロールを作成

IAM の Management console を開いて、左ペインの Roles から Create New Role をクリックします。

Step 1 : Set Role Name

Role Name に wp-role などと適当な名前を入力します。

Step 2 : Select Role Type

AWS Service Roles の Amazon EC2 を選択します。

Step 4 : Set Permissions

Custom Policy を選択して、テキストエリアに次を貼り付けます。Policy Name はなんでも良いです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "*"
    }
  ]
}

この IAM ロールが紐付けられた EC2 インスタンスは S3 に対するフルアクセスが可能になります。

本当はもっと絞ったほうがいいのだろうけど Resource の指定方法がよくわかりませんでした。

RDS サブネットグループの作成

RDS のインスタンスを作る前に RDS サブネットグループを作成します。ここで指定したサブネットで RDS インスタンスは起動します。

RDS の Management Console の左ペインで Subnet Groups を選択して、Create DB Subnet Group をクリックします。

次のように入力して作成します。

  • Name wp-db-nets
  • Description WordPress Subnet Group
  • VPC ID wp-vpc (前回作ったやつ)
  • Availability Zone と Subnet ID を選択して以下のサブネットを追加
    • wp-net-a
    • wp-net-c

RDS インスタンス作成

RDS のインスタンスを作成します。

Step 1: Select Engine

mysql を選択します。

Step 2: Production?

Yes を選択します。

Step 3: Specify DB Details

Instance Specifications を次のように指定します。

  • DB Instance Class db.t2.micro
  • Allocated Storage 5 GB
  • Use Provisioned IOPS No

Settings には次のように入力します。

  • DB Instance Identifier wordpress
  • Master Username wordpress
  • Master Password wordpress
  • Confirm Password wordpress

Step 4: Configure Advanced Settings

次のように設定します。

  • Network & Security
    • VPN wp-vpc (前回作ったやつ)
    • DB Subnet Group wp-db-nets (↑で作ったやつ)
    • Publicly Accessible No
    • VPC Security Group default (同じセキュリティグループ同士で通信可能)
  • Database Options
    • Database Name wordpress
  • Backup
    • Backup Retention Period 0 days
  • Maintenance
    • Auto Minor Version Upgrade No

とくに記載していない部分はデフォルトのままにします。

インスタンスが立ち上がったら、Management console でインスタンスの画面を開いて Endpoint に表示されているホスト名を覚えておきます。

S3 Budket を作成

画像などをアップロードする先として S3 のバケットを作成します。

まず、S3 の Management console を開いて Create Bucket をクリックして適当な名前のバケットを作成します(仮に example-wordpress とします)。

作成したバケットを選択して、右上の方にある Properties をクリックします。

Permissions をクリック、Add bucket policy をクリックして、テキストエリアに下記を貼り付けて Save をクリックします

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::example-wordpress/*"
        }
    ]
}

これで、このバケットにアップロードされたファイルは自動的に公開されるようになります。

なお、バケットの名前はグローバルに一意(すべてのアカウントで一意)でなければならないので、実際に example-wordpress などという名前のバケットは作れません、他の人が既にその名前でバケットを作っているからです。

EC2 インスタンスの作成

EC2 の Management Console の Instances から Lunch Instance をクリックして、次のようなインスタンスを1台だけ作成します。

Step 1: Choose an Amazon Machine Image (AMI)

Quick Start で Amazon Linux AMI 2014.03.2 (HVM) を選択します。

Step 2: Choose an Instance Type

t2.micro を選択します。

Step 3: Configure Instance Details

下記のみ変更して、その他はデフォルトのままにします。

Network                 wp-vpc
Auto-assign Public IP   Enable
IAM role                wp-role

Step 4: Add Storage

Volume Type を General Purpose (SSD) に変更します。

Step 5: Tag Instance

Name に wp-ap などと判りやすい名前を入力します。

Step 6: Configure Security Group

Select an existing security group を選択して default を選択します。

Step 7: Review Instance Launch

ざっくり内容を確認して、Launch をクリックします。

しばらく待つとインスタンスが起動するので SSH でログインします。

いろいろインストール

作成したインスタンスにいろいろインストールします。

yum

yum update します。

yum update

mysqlapachephp をインストールします。

yum install httpd24 php55 php55-opcache php55-mysqlnd mysql55

リブートします。

reboot

各種設定

各種設定を行います。

php

php の設定ファイルを作ります。

cat <<EOS> /etc/php-5.5.d/_default_.ini
expose_php = Off
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/php.log
date.timezone = Asia/Tokyo
post_max_size = 500M
upload_max_filesize = 500M
memory_limit = 1G
EOS
cat /etc/php.d/_default_.ini

ログディレクトリを準備します。

mkdir /var/log/php/
chmod 777 /var/log/php/
touch /var/log/php/php.log
chmod 666 /var/log/php/php.log

ログローテートの設定を作成します。

cat <<EOS> /etc/logrotate.d/php
/var/log/php/*.log {
    missingok
    daily
    rotate 10
    ifempty
    create 0666 root root
}
EOS
cat /etc/logrotate.d/php

apache

いらなさそうな設定ファイルを空にします。

echo -n "" > /etc/httpd/conf.d/autoindex.conf
echo -n "" > /etc/httpd/conf.d/userdir.conf
echo -n "" > /etc/httpd/conf.d/welcome.conf

仮の index.html と ELB からのヘルスチェック先のページを作成します。

echo index > /var/www/html/index.html
echo readme > /var/www/html/readme.html

自動起動を有効にして起動します。

chkconfig httpd on
service httpd start

ELB の作成

ELB を作成します。

1. Define Load Balancer

Load Balancer name  wp-lb
Create LB Inside    wp-vpc

その他はデフォルトのままにします。

2. Configure Health Check

Ping Path           /readme.html

その他はデフォルトのままにします。

3. Select Subnets

ap-northeast-1aap-northeast-1c を追加します。

4. Assign Security Groups

defaultpublic を選択します。

5. Add EC2 Instances

↑で作成したインスタンス wp-ap を選択します。

Stickiness

ELB の作成後、Description の Port Configuration の近くにある Edit をクリックして Enable Load Balancer Generated Cookie Stickiness を選択します。

これをやっておかないと AP が複数になったときにセッションが維持されないことがあります(たぶん)。

ELB の DNS 名を登録

Route53 で ALIAS レコードで ELB を登録します。仮に aws.example.net だとします。

なお、ELB を作成してからしばらく時間をおかないと登録できませんでした。

登録したドメイン名をブラウザで閲覧して index.html が表示されれば正常に登録できています。

WordPress のインストール

WordPress をダウンロードして /var/www/html に展開します。

cd /usr/local/src/
wget http://ja.wordpress.org/wordpress-3.9.2-ja.zip
unzip wordpress-3.9.2-ja.zip
rm -fr /var/www/html/
mv wordpress /var/www/html/

wp-config.php を作ります。DB のホスト名には RDS の Endpoint のホスト名を指定します。

cd /var/www/html
mv wp-config-sample.php wp-config.php

sed wp-config.php -e "
  /^define('DB_NAME'/c     define('DB_NAME', 'wordpress');
  /^define('DB_USER'/c     define('DB_USER', 'wordpress');
  /^define('DB_PASSWORD'/c define('DB_PASSWORD', 'wordpress');
  /^define('DB_HOST'/c     define('DB_HOST', 'wordpress.xxxxxxxx.ap-northeast-1.rds.amazonaws.com');
" -i

curl https://api.wordpress.org/secret-key/1.1/salt/ > /tmp/secret

sed wp-config.php -e "
  /^define('AUTH_KEY'/r /tmp/secret
  /^define('AUTH_KEY'/d
  /^define('SECURE_AUTH_KEY'/d
  /^define('LOGGED_IN_KEY'/d
  /^define('NONCE_KEY'/d
  /^define('AUTH_SALT'/d
  /^define('SECURE_AUTH_SALT'/d
  /^define('LOGGED_IN_SALT'/d
  /^define('NONCE_SALT'/d
" -i

cat wp-config.php

ブザウザで http://aws.example.net/ にアクセスして WordPress のインストールウィザードに必要項目を入力してインストールを完了します。

この後、アップロードファイルの保存先の設定をするので、まだ記事の投稿は行いません。

WordPress のアップロード先設定

AWS SDK for PHP には S3 のストリームラッパーがあります。次のようなパスで S3 へのアップロードやダウンロードができるようになります。

  • s3://<BucketName>/<PathToFile>

WordPress がアップロードするディレクトリのパスをストリームラッパーのパスに変更して、S3 にアップロードされるようにしてみます。

まず、WordPress の次の URL を開きます。普通には弄れない内部的なオプションを変更するための画面なのだと思います。

  • http://aws.example.net/wp-admin/options.php

upload_pathupload_url_path を探して、次の通りに入力して保存します。

upload_path         s3://example-wordpress
upload_url_path     http://example-wordpress.s3-ap-northeast-1.amazonaws.com

upload_path に指定した値は /var/www/htmlWordPress のインストール先)からの相対パスになるようです。

なので、このままだと /var/www/html/s3://example-wordpress などという意味不明なパスになるので、WordPress のソースを弄って修正します。

wp-includes/functions.php の 1644 行目辺りをコメントアウトします。

    //} elseif ( 0 !== strpos( $upload_path, ABSPATH ) ) {
    //  // $dir is absolute, $upload_path is (maybe) relative to ABSPATH
    //  $dir = path_join( ABSPATH, $upload_path );

後は、S3 のストリームラッパーを登録するだけです。

AWS SDK for PHP は composer でインストールできるので、まずは composer をインストールします。

curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
chmod +x /usr/local/bin/composer

AWS SDK for PHP をインストールします。

cd /var/www/html
composer require aws/aws-sdk-php:\*

こんな場所で composer require すると composer.json とか vendor/ とかが丸見えになるのでよろしくありません。が、とりあえずそのままにします。

さらに wp-config.php に下記を追記します。

require 'vendor/autoload.php';
use Aws\S3\S3Client;
use Aws\Common\Enum\Region;
S3Client::factory(array('region' => Region::AP_NORTHEAST_1))->registerStreamWrapper();

これで S3 のストリームラッパーが登録され、アップロードファイルが S3 に保存されるようになりました。

適当に画像などをアップロードした後に S3 のバケットを閲覧してみると、アップロードされたファイルが保存されているのがわかります。

また、ブラウザで表示される画像の URL も S3 の URL になっています。

WordPress の補足

たぶん普通はこんなことしません。プラグインかなにかを使うものなのだと思いますが、実は WordPress ほとんど使ったことがなく、よく判らなかったので適当にソースを弄りました。

ググると下記のプラグインが出てきたのですが、プラグインのインストール方法とかをよく知らないので使いませんでした。

あるいは s3fs-fuse を使えば、単に /var/www/html/wp-content/uploads にマウントするだけで終わりそうです。

AMI の作成

最後に Auto Scaling を設定してみます、と言っても単にインスタンスをたくさん立ち上げただけです。

Auto Scaling には AMI が必要なので、作成済の EC2 インスタンスを AMI 化します。

インスタンスにログインして、AMI に含めたくないファイルを削除してシャットダウンします。

rm -f /root/.ssh/authorized_keys
rm -f /home/ec2-user/.ssh/authorized_keys

rm -f /etc/ssh/ssh_host_dsa_key
rm -f /etc/ssh/ssh_host_dsa_key.pub
rm -f /etc/ssh/ssh_host_key
rm -f /etc/ssh/ssh_host_key.pub
rm -f /etc/ssh/ssh_host_rsa_key
rm -f /etc/ssh/ssh_host_rsa_key.pub

find /var/log -type f | xargs rm -fv

rm -f /root/.bash_history

shutdown -h now

EC2 の Management console で インスタンスを選択して Create Image で AMI を作ります。

Launch configuration の作成

EC2 の Management console の Launch configuration で Create launch configuration をクリックします。

1. Choose AMI

My AMIs を選択して、↑で作成した AMI を選択します。

2. Choose Instance Type

t2.micro を選択します。

3. Configure details

Name に wp-auto などと判りやすい名前を設定します。

IAM role には wp-role を指定します。

4. Add Storage

Delete on Termination が On になっていることを確認します。なっていなければ On にします。

5. Configure Security Group

Select an existing security group を選択して default を選択します。

6. Review

ざっくり内容を確認して右下の Create launch configuration をクリックします。

Auto Scaling Group の作成

EC2 の Management console の Auto Scaling Groups で Create Auto Scaling Groups をクリックします。

Create an Auto Scaling group from an existing launch configuration を選択して、先ほど作成した Launch configuration を選択します。

1. Configure Auto Scaling group details

Group name          wp-group
Group size          起動したいインスタンスの数
Network             wp-vpc
Subnet              wp-net-a wp-net-c

Advanced Details を開いてさらに設定します。

Load Balancing      On wp-lb (チェックボックスを On にしてテキストボックスに ELB を入力)

2. Configure scaling policies

特になにも変更せずに次に進みます。

3. Configure Notifications

特になにも変更せずに次に進みます。

4. Configure Tags

特になにも変更せずに次に進みます。

5. Review

ざっくりと内容を確認して右下の Create Auto Scaling Groups をクリックします。

しばらく待つと、インスタンスがバコバコ起動して ELB に追加されます。

さいごに

試しにインスタンスの数を 100 台に設定してみたのですが、20 台目以降はインスタンスの作成に失敗しました。

EC2 の Management console の Limits を見ると t2.micro は 20 台がリミットになっているので、申請しなければ 20 台より多くは起動できないようです。

下図は 20 台起動したときの EC2 の Management console のキャプチャです。

f:id:ngyuki:20140824140252p:plain

祭りの後

f:id:ngyuki:20140824140326p:plain

Zend Framework 2 で Smarty を使うためのモジュール

そういえばだいぶ前に Zend Framework 2 で Smarty を使うためのモジュールを作りました。

なお、ZfcTwig を大いに参考にしました。

最初に作ったのは1年ぐらい前だったと思います。その当時に GitHub とかで同じようなものを探してみたところ、いくつか見つかったのですが次のような点で手に馴染まない気がしたので作ることにしました。

  • phtml を完全に置き換えてしまう
    • 既存のモジュールで phtml を使っていると動かなくなる
  • コントローラーのアクションで SmartyModel などを返さなければならない
    • コントローラーでテンプレートエンジンを意識したくない
  • ビューヘルパーが呼べない
    • 呼びたい

なお、今改めて確認すると下記とか良い感じかも知れません。

インストール

Packagist に登録しているので composer でインストールできます。

$ composer require ngyuki/zf2-smarty:dev-master

インストールが終わったら vendor/ngyuki/zf2-smarty/config/smarty.global.phpconfig/autoload/ にコピーします。

$ cp vendor/ngyuki/zf2-smarty/config/smarty.global.php config/autoload/

そして config/autoload/smarty.global.php を編集します。

基本的には smarty の部分に Smarty のオプションを書けば OK です。ひな形のファイルは development な感じにしているので production ならそれっぽく変更が必要です。

<?php
return array(
    'smarty' => array(
        // here!
    ),
);

さらに config/application.config.phpZendSmarty を追記します。

<?php
return array(
    'modules' => array(
        'Application',
        'ZendSmarty', // this is it!
    ),

    // ...
);

使い方

既存の .phtml と同じ命名規則で、拡張子だけ .tpl に変更したファイル名で Smarty のテンプレートを作成します。

module/Application/view/application/index/index.tpl

<h1>I am smarty</h1>
<code>PHP {$smarty.const.PHP_VERSION}</code>

拡張子 .tpl でファイルが見つかれば Smartyレンダリングされます。見つからなければ .phtml がレンダリングされます。

ビューヘルパー

ビューヘルパーは次のように書けます。

{url home}
{url application [controller => index, action => index]}

これは .phtml での次のコードと同じです。

<?= $this->url("home") ?>
<?= $this->url("application", ["controller" => "index", "action" => "index"]) ?>

echo が不要な場合は次のように書けます。

{do headTitle "Index Page"}

これは .phtml での次のコードと同じです。

<?php $this->headTitle("Index Page") ?>

これらは Smartyコンパイラ関数プラグインで実現しているのですが、メソッドチェインには対応していません。

メソッドチェインしたい場合は $this を使います。

{$this->headTitle("ZF2 Smarty")->setSeparator(' - ') nofilter}

これは .phtml での次のコードと同じです。

<?= $this->headTitle("ZF2 Smarty")->setSeparator(' - ') ?>

テンプレート継承

ZfcTwig だとレイアウトとコンテンツの両方のテンプレートが Twig であれば ZF2 のレイアウト機能を使わずに Twig のテンプレート継承でレイアウトを実現することができます(zfctwig.disable_zf_model デフォルトで true)。

Smarty3 でもテンプレート継承はできますが、残念ながらわたしのモジュールは ZfcTwig のような機能は実装していません。

というのも、ZfcTwig を使っていたときに、すべてのビューで共通する値をアサインするために、

<?php
class Module
{
    public function onBootstrap(MvcEvent $ev)
    {
        /* @var $view \Zend\View\View */
        $view = $ev->getApplication()->getServiceManager()->get('View');
        $view->getEventManager()->attach(ViewEvent::EVENT_RENDERER_POST, function (ViewEvent $ev) {
            $ev->getModel()->val = 123;
        });
    }
}

と、したところ、ZfcTwig だと意図通りに動かなかったからです。

ZfcTwig でレイアウトとコンテンツのテンプレートが両方 Twig の場合、レイアウトのレンダリング時に独自にコンテンツのレンダリングを行うようになっており、コンテンツのレンダリングに関して EVENT_RENDERER_POST イベントが発生しないためです。

気になったのはこれだけですが、なんとなく他にもドハマりしそうなものが隠れている気がしたので封印することにしました。

なので、このモジュールでもそのような機能は設けていません(ZfcTwig のようにオプションで切替えられるならそれでもいいかな―と思いましたが、どうせ使わないし)。

さいごに

Zend Framework 2 のモジュール名やコンフィグのエントリ名ってベンダ名っぽいプレフィックスを付けるものみたいですね。

ZendSmarty という名前はよろしくなさそうなので、変えたほうが良いですね・・・・

AWS で HA クラスタで WordPress を動かす

最近お仕事でも AWS を使わざるを得ない状況になってきたので、ためしに AWSWordPress を HA 構成にしてみました。

なお、RDS や S3 は使っていないのであまり AWS っぽくはありません(オンプレをできるだけそのまま AWS に移行する想定だったので)。

構成

次のような構成にします。

  • APx2 DBx2 NASx2 で Multi-AZ
    • すべて CentOS 6.5
    • 東京リージョン
  • AP は Apache/PHP
    • 負荷分散に ELB を使用する
  • DB は DRBD/Pacemaker/Heartbeat/MySQL
    • MHA よりもこの構成の方がなれているから
  • NAS は GlusterFS
    • DRBD/Pacemaker/Heartbeat/NFS でもできるけど面倒なのと GlusterFS 使ってみたかった
  • ドメインは既存のもののサブドメインを Route53 に委任
    • 詳細は省略

IAM ユーザーを作成

aws cli を使うために IAM ユーザーを作成します。

本当は必要最小限のパーミッションにするべきなのでしょうが、お試しなので Power User Access で作成しました。

Access Key ID と Secret Access Key はあとで必要になるのでメモっておきます。

VPC を作成

VPC

次のように VPC を作成します。

Name tag            wp-vpc
CIDR block          10.1.0.0/16
Tenancy             Default

Subnets

VPC に2つの AZ のサブネットを作成します。なぜか ap-northeast-1b だと作成できなかったので ap-northeast-1a と ap-northeast-1c で作りました。

Name tag            wp-net-a
VPC                 wp-vpc
Availability Zone   ap-northeast-1a
CIDR block          10.1.1.0/24
Name tag            wp-net-c
VPC                 wp-vpc
Availability Zone   ap-northeast-1c
CIDR block          10.1.2.0/24

Internet Gateways

Internet Gateway を追加します。

Name tag            wp-gw

追加後に wp-vpc にアタッチしておきます。

Route Tables

Route Tables で wp-vpc のルーティングを追加します。

Destination     0.0.0.0/0
Target          igw-xxxxx | wp-gw

Target には↑で作成した Internet Gateway を指定します。

Security Group

EC2 の Management Console を開いて wp-vpc の Security Group を設定します。

VPC の Management Console でもできますが、そちらだと後述の MyIP が選択できなかったので。

VPC を新規作成したときにデフォルトの Security Group が作成されています。この Security Group は、同じ Security Group のインスタンス同士の通信をすべて許可するようになっています。

インスタンス間の通信はこの Security Group で通すことにして、これから作業するために SSH で繋げられる必要があるので次のように inbound Rules を追加します。

Type    SSH(22)
Source  My IP
Type    All ICMP
Source  My IP

Source に My IP を選択すると、今現在 Management Console に接続しているグローバルIPアドレスが設定されます。

ついでに Name tag も wp-default などとわかりやすく変更しておきます。

さらに、最終的に ELB を HTTP で公開する必要があるので、次のように Security Group を新規作成します。

Security group name     public
Description             public
VPC                     wp-vpc
inbound Rules
    Type                HTTP
    Source              Anywhere

Name tag も wp-public などとわかりやすく変更しておきます。

ベースとなるインスタンスの作成

この後、インスタンスを合計6台起動して HA 構成にするのですが、いきなり6台起動して最初から構築するのは面倒なので、ベースとなる AMI を作成します。

EC2 の Management Console の Instances から Lunch Instance をクリックして、次のような Instance を1台だけ作成します。

Step 1: Choose an Amazon Machine Image (AMI)

AWS Marketplace を CentOS で検索して CentOS 6.5 の AMI を選択します。

Step 2: Choose an Instance Type

t1.micro を選択します。

本当は t2.micro が良いのですが、仮想化の種類が paravirtual なので t2.micro は選択できません。

Step 3: Configure Instance Details

下記のみ変更して、その他はデフォルトのままにします。

Network                 wp-vpc
Auto-assign Public IP   Enable

Step 4: Add Storage

Volume Type を General Purpose (SSD) に変更します。

Step 5: Tag Instance

Name に wp-base などと判りやすい名前を付けます。

Step 6: Configure Security Group

Select an existing security group を選択して default と public を選択します。

Step 7: Review Instance Launch

ざっくり内容を確認して、Launch をクリックします。

しばらく待つと Instance が起動するので SSH でログインします。

ベースに必要なソフトウェアをインストール

作成したベースとなるインスタンスにいろいろインストールします。

SELinuxiptables

/etc/selinux/config を編集して SELinux を無効にします。

sed -i '/^SELINUX=/c SELINUX=disabled' /etc/selinux/config
cat /etc/selinux/config

iptables を無効にします。

service iptables stop
service ip6tables stop
chkconfig iptables off
chkconfig ip6tables off

再起動します。

reboot

yum

再起動が終わったら yum で update と base とかの基本的なものをインストールします。

yum update
yum groupinstall base
yum install bash-completion nc git

mysqlapachephp をインストールします。

yum install mysql-server httpd php php-pecl-zendopcache php-mysql

chkconfig mysqld off
chkconfig httpd off

サードパーティリポジトリを追加して drbd と heartbeat と pacemaker をインストールします。

yum localinstall http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
yum localinstall http://www.elrepo.org/elrepo-release-6-6.el6.elrepo.noarch.rpm

cd /usr/local/src
wget "http://sourceforge.jp/frs/redir.php?m=iij&f=%2Flinux-ha%2F60151%2Fpacemaker-1.0.13-1.2.el6.x86_64.repo.tar.gz" \
  -O pacemaker-1.0.13-1.2.el6.x86_64.repo.tar.gz
tar xvzf pacemaker-1.0.13-1.2.el6.x86_64.repo.tar.gz
mv pacemaker-1.0.13-1.2.el6.x86_64.repo /tmp

cd /tmp/pacemaker-1.0.13-1.2.el6.x86_64.repo
sed -i '/name=/a exclude=pacemaker pacemaker* clusterlib' \
  /etc/yum.repos.d/CentOS-Base.repo

yum install -c pacemaker.repo heartbeat pacemaker
yum install kmod-drbd83 drbd83-utils

chkconfig heartbeat off
chkconfig drbd off

glusterfs をインストールします。

wget http://download.gluster.org/pub/gluster/glusterfs/LATEST/EPEL.repo/glusterfs-epel.repo \
  -O /etc/yum.repos.d/glusterfs-epel.repo

yum install glusterfs-server glusterfs-fuse

chkconfig glusterd off
chkconfig glusterfsd off

aws cli をインストールします。

AWS Access Key ID と AWS Secret Access Key には↑の方で作成した IAM ユーザーのものを指定してください。

Default region name は東京リージョンである ap-northeast-1 を指定します。

Default output format はなんでもいいです。

yum install python-pip
pip install awscli
aws configure

ベースの各種設定

後の作業が楽になるように、ベースイメージに各種設定ファイルをあらかじめ作成しておきます。

hosts

/etc/hosts を編集します。

cat <<EOS> /etc/hosts
127.0.0.1    localhost.localdomain localhost
::1          localhost6.localdomain6 localhost6
10.1.1.11    db01.aws.example.net db01
10.1.2.11    db02.aws.example.net db02
10.1.1.22    ns01.aws.example.net ns01
10.1.2.22    ns02.aws.example.net ns02
EOS
cat /etc/hosts

AP は固定の IP アドレスが必要ないので記述していません。

drbd

/etc/drbd.d/global_common.conf を編集します。

cp -a /etc/drbd.d/global_common.conf{,.orig}
cat <<EOS> /etc/drbd.d/global_common.conf
global {
    usage-count no;
}

common {
    protocol C;
    startup {
        wfc-timeout 60;
    }
    net {
        cram-hmac-alg sha1;
        shared-secret "drbd_no_himitu";
    }
    syncer {
        rate 5M;
    }
}
EOS
cat /etc/drbd.d/global_common.conf

/etc/drbd.d/r0.res を編集します。drbd のためのディスクはあとでアタッチするので、仮で /dev/xvdj とします。

cat <<EOS> /etc/drbd.d/r0.res
resource r0
{
    protocol C;
    device /dev/drbd0;
    meta-disk internal;

    on db01.aws.example.net {
        address 10.1.1.11:7791;
        disk /dev/xvdj;
    }

    on db02.aws.example.net {
        address 10.1.2.11:7791;
        disk /dev/xvdj;
    }
}
EOS
cat /etc/drbd.d/r0.res

heartbeat

/etc/ha.d/ha.cf を編集します。AWS はブロードキャストやマルチキャストができないので ucast で設定します(そもそも Multi-AZ だと別セグメントなのでブロードキャストは届かないし)。

cat <<EOS> /etc/ha.d/ha.cf
keepalive 5
deadtime 60
warntime 20
initdead 120

udpport 694
ucast eth0 10.1.1.11
ucast eth0 10.1.2.11

node db01.aws.example.net
node db02.aws.example.net

watchdog /dev/watchdog
pacemaker on
uuidfrom nodename
debug 0
use_logd yes
EOS
cat /etc/ha.d/ha.cf

/etc/logd.cf を編集します。

cat <<EOS> /etc/logd.cf
debugfile /var/log/ha-debug
logfile /var/log/ha-log
logfacility none
EOS
cat /etc/logd.cf

/etc/ha.d/authkeys を編集してパーミッションを変更します。

cat <<EOS> /etc/ha.d/authkeys
auth 2
2 sha1 heartbeat_no_himitu
EOS
cat /etc/ha.d/authkeys
chown root:root /etc/ha.d/authkeys
chmod 600 /etc/ha.d/authkeys

/etc/logrotate.d/heartbeat を作成します。これはログローテートのためです。

cat <<EOS> /etc/logrotate.d/heartbeat
/var/log/ha-debug {
        missingok
        daily
        rotate 10
        ifempty
}

/var/log/ha-log {
        missingok
        daily
        rotate 10
        ifempty
}
EOS
cat /etc/logrotate.d/heartbeat

mysql

/etc/my.cnf を編集します。

ログや pid や socket のパスを変えているのは単なる好みですが、Pacemaker のリソース設定で必要なので、変更した値は覚えておきます。

その他のパラメータは適当です。

cat <<EOS> /etc/my.cnf
[client]
port = 3306
socket = /var/run/mysql/mysql.sock

[mysqld_safe]
log-error = /var/log/mysql/error.log
pid-file = /var/run/mysql/mysqld.pid

[mysqld]
user = mysql
port = 3306

log-error = /var/log/mysql/error.log
pid-file = /var/run/mysql/mysqld.pid
socket = /var/run/mysql/mysql.sock
datadir = /var/lib/mysql

general_log_file = /var/log/mysql/query.log
slow_query_log_file = /var/log/mysql/slow.log

log-warnings
general_log
slow_query_log

query_cache_type = 0
symbolic-links = 0
skip-external-locking
skip-name-resolve

character-set-server = utf8
default-storage-engine = innodb

key_buffer_size = 16M
max_allowed_packet = 16M
table_open_cache = 64
sort_buffer_size = 512K
join_buffer_size = 512K
thread_cache_size = 2
thread_concurrency = 2
net_buffer_length = 8K
read_buffer_size = 256K
read_rnd_buffer_size = 512K
myisam_sort_buffer_size = 8M

# innodb
innodb_data_file_path = ibdata1:10M:autoextend
innodb_buffer_pool_size = 128M
innodb_additional_mem_pool_size = 8M
innodb_log_buffer_size = 8M
innodb_log_file_size = 16M
innodb_file_per_table
#innodb_file_format = Barracuda

[mysqldump]
quick
max_allowed_packet = 16M

[mysql]
connect-timeout = 5
no-auto-rehash
show-warnings

[myisamchk]
key_buffer_size = 20M
sort_buffer_size = 20M
read_buffer = 2M
write_buffer = 2M

[mysqlhotcopy]
interactive-timeout
EOS
cat /etc/my.cnf

ログや pid や socket のためのディレクトリを準備します。

mv /var/run/mysqld /var/run/mysql
mkdir /var/log/mysql
chown -R mysql:mysql /var/log/mysql/
rm -f /var/log/mysqld.log

/etc/logrotate.d/mysql を作成します。これはログローテートのためです。

cat <<'EOS'> /etc/logrotate.d/mysql
/var/log/mysql/*.log {
    missingok
    daily
    rotate 10
    ifempty
    nocreate
    sharedscripts
    postrotate
        if test -x /usr/bin/mysqladmin && \
            /usr/bin/mysqladmin ping &>/dev/null
        then
            /usr/bin/mysqladmin flush-logs
        fi
    endscript
}
EOS
cat /etc/logrotate.d/mysql

/etc/cron.d/mysql を作成します。これは日次で Analyze するためですが、無くてもいいです。

cat <<EOS> /etc/cron.d/mysql
09 04 * * * root /usr/bin/mysqladmin ping 1>/dev/null 2>&1 && /usr/bin/mysqlcheck -a --all-databases 1> /dev/null
EOS
cat /etc/cron.d/mysql

apache

apachewelcome.conf を無効にします。削除すると yum update で復活することがあるので空にするのがオススメです。

echo -n "" > /etc/httpd/conf.d/welcome.conf

php

php の設定ファイルを作ります。

/etc/php.ini を直接編集せずに /etc/php.d/ に変更分を作成します。

cat <<EOS> /etc/php.d/_default_.ini
expose_php = Off
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/php.log
date.timezone = Asia/Tokyo
post_max_size = 500M
upload_max_filesize = 500M
memory_limit = 1G
EOS
cat /etc/php.d/_default_.ini

ログディレクトリを準備します。

mkdir /var/log/php/
chmod 777 /var/log/php/
touch /var/log/php/php.log
chmod 666 /var/log/php/php.log

ログローテートの設定を作成します。

cat <<EOS> /etc/logrotate.d/php
/var/log/php/*.log {
    missingok
    daily
    rotate 10
    ifempty
    create 0666 root root
}
EOS
cat /etc/logrotate.d/php

wordpress

WordPress をダウンロードして /var/www/html に展開します。

cd /usr/local/src/
wget http://ja.wordpress.org/wordpress-3.9.2-ja.zip
unzip wordpress-3.9.2-ja.zip
rm -fr /var/www/html/
mv wordpress /var/www/html/

wp-config.php を作ります。

DB サーバとして指定している 192.168.0.10 は Routing-Based HA のためのものです。

cd /var/www/html
mv wp-config-sample.php wp-config.php

sed wp-config.php -e "
  /^define('DB_NAME'/c     define('DB_NAME', 'wordpress');
  /^define('DB_USER'/c     define('DB_USER', 'wordpress');
  /^define('DB_PASSWORD'/c define('DB_PASSWORD', 'wordpress');
  /^define('DB_HOST'/c     define('DB_HOST', '192.168.0.10');
" -i

curl https://api.wordpress.org/secret-key/1.1/salt/ > /tmp/secret

sed wp-config.php -e "
  /^define('AUTH_KEY'/r /tmp/secret
  /^define('AUTH_KEY'/d
  /^define('SECURE_AUTH_KEY'/d
  /^define('LOGGED_IN_KEY'/d
  /^define('NONCE_KEY'/d
  /^define('AUTH_SALT'/d
  /^define('SECURE_AUTH_SALT'/d
  /^define('LOGGED_IN_SALT'/d
  /^define('NONCE_SALT'/d
" -i

cat wp-config.php

ec2-route lsb

後で Pacemakedr で Routing-Based HA をするので、そのための lsb のスクリプトを作成します。

まずは Management Console の VPC の画面で Route Table の ID を調べます。

もしくはインスタンス内で次のように調べることもできます。

mac=$(curl -s http://169.254.169.254/latest/meta-data/network/interfaces/macs/ | head -1)
vpc=$(curl -s http://169.254.169.254/latest/meta-data/network/interfaces/macs/$mac/vpc-id)
route_table_id=$(aws ec2 describe-route-tables --output text --query "RouteTables[?VpcId==\`$vpc\`].[RouteTableId][]")
echo $route_table_id

Route Table を変更するための lsb を作成します。rtb-xxxxxxxx の部分は↑で調べた ID に読み替えてください。

cat <<'EOS'> /etc/rc.d/init.d/ec2-route
#!/bin/sh
#
# chkconfig: 12345 01 99
# description: EC2 route change

. /etc/rc.d/init.d/functions

route_table_id=rtb-xxxxxxxx
destination_cidr_block=192.168.0.10/32
instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

function start () {
  if status > /dev/null; then
    echo -n "already create"
  else
    echo -n "create route ..."
    aws ec2 delete-route --output text \
      --route-table-id $route_table_id \
      --destination-cidr-block $destination_cidr_block \
        2>&1 | logger -t ec2-route-delete
    aws ec2 create-route --output text \
      --route-table-id $route_table_id \
      --instance-id $instance_id \
      --destination-cidr-block $destination_cidr_block \
        2>&1 | logger -t ec2-route-create
  fi
  success; echo
  return 0
}

function stop()
{
  if ! status > /dev/null; then
    echo -n "already delete"
  else
    echo -n "delete route ..."
    aws ec2 delete-route --output text \
      --route-table-id $route_table_id \
      --destination-cidr-block $destination_cidr_block \
        2>&1 | logger -t ec2-route-delete
  fi
  success; echo
  return 0
}

function status()
{
  cnt=$(aws ec2 describe-route-tables --output text \
    --filters Name=route.instance-id,Values=$instance_id | wc -l)

  if [ "$cnt" -gt 0 ]; then
    echo "ok"
    return 0
  else
    echo "ng"
    return 3
  fi
}

case "$1" in
  start)
    start
    exit $?
    ;;
  status)
    status
    exit $?
    ;;
  stop)
    stop
    exit $?
    ;;
  *)
    echo "Usage: ec2-route {start|stop|status}"
    exit 2
esac
EOS

バーミッションを変更して chkconfig --add します。

chmod +x /etc/rc.d/init.d/ec2-route
chkconfig --add ec2-route
chkconfig ec2-route off
chkconfig --list | grep ec2-route

なお、このスクリプトだと結構な頻度で aws cli を実行するし、失敗時の例外処理も無いので、よろしくありません。実運用で使うためには修正が必要です。

ベースの AMI 化

ここまでできたらベースとなるインスタンスを AMI にします。

Marketprice の CentOS 6.5 は起動時に /root/firstrun があれば root のパスワードが自動設定されるので作成します。

touch /root/firstrun

SSH の公開鍵認証の設定も、未作成なら自動的に作成設定されるので削除します。

rm -f /root/.ssh/authorized_keys

MAC アドレスが変わってもインタフェース名が変わらないように 70-persistent-net.rules を削除します。

rm -f /etc/udev/rules.d/70-persistent-net.rules

SSH のホスト鍵が全部同じになるのは好ましくないので削除します。

rm -f /etc/ssh/ssh_host_dsa_key
rm -f /etc/ssh/ssh_host_dsa_key.pub
rm -f /etc/ssh/ssh_host_key
rm -f /etc/ssh/ssh_host_key.pub
rm -f /etc/ssh/ssh_host_rsa_key
rm -f /etc/ssh/ssh_host_rsa_key.pub

ログはいらないので削除します。

find /var/log -type f | xargs rm -fv

最後に root の .bash_history も削除してシャットダウンします。

rm -f /root/.bash_history
shutdown -h now

シャットダウンが終わったら(Instance State が stopped になったら)、インスタンスから AMI を作成します。

名前はわかりやすく wp-base-20140809 などとしておきます。

AMI の作成が終わったら、元になったインスタンスは削除して構いません。

AMI からインスタンスを作成

↑で作成した AMI から AZ ap-northeast-1a と ap-northeast-1c でそれぞれ3台ずつ、合計6台起動します。

  • VPC は作成済の wp-vpc を指定
  • サブネットは各 AZ に作成済の wp-net-awp-net-c を指定
  • Auto-assign Public IP を Enable にする
  • セキュリティグループは Select an existing security group を選択して default を指定
  • ストレージや NIC はデフォルトのまま

作成後にインスタンスの名前をわかりやすくするための次のように変更します。

  • ap-northeast-1a
    • wp-ap01
    • wp-db01
    • wp-ns01
  • ap-northeast-1c
    • wp-ap02
    • wp-db02
    • wp-ns02

セカンダリ IP アドレスをインスタンスに指定

Management Console で DB と NAS にそれぞれセカンダリ IP アドレスを設定します。

  • db01 10.1.1.11
  • ns01 10.1.1.22
  • db02 10.1.2.11
  • ns02 10.1.2.22

DB サーバの Source/Dest チェックを無効 にする

Management Console で DB サーバのインスタンスの Source/Dest チェックを無効にします。

これは Routing-Based HA のために必要な設定です。

DB と NAS のボリュームを作成してアタッチ

Management Console で DB と NAS 用にボリュームを作成してアタッチします。AZ の異なるボリュームはアタッチできないので ap-northeast-1a と ap-northeast-1c でそれぞれ2つずつ作成します。

作成できたら DB と NASインスタンスに1つずつアタッチします。

NAS に関しては ルートボリュームをそのまま使っても良いのですが・・・なんとなくです。

インスタンスの IP アドレスとホスト名を設定

DB と NAS にログインしてセカンダリ IP アドレスを設定します。

次のようにメタデータから設定すると楽です。

cat <<EOS> /etc/sysconfig/network-scripts/ifcfg-eth0:0
DEVICE=eth0:0
BOOTPROTO=none
ONBOOT=yes
IPADDR=$(curl -s http://169.254.169.254/latest/meta-data/network/interfaces/macs/$(
  curl -s http://169.254.169.254/latest/meta-data/mac/
)/local-ipv4s | tail -1)
PREFIX=24
EOS
cat /etc/sysconfig/network-scripts/ifcfg-eth0:0
ifup eth0:0

ホスト名を設定します。

hostname $(hostname -A)
hostname
sed -i "/^HOSTNAME=/c HOSTNAME=$(hostname)" /etc/sysconfig/network
cat /etc/sysconfig/network

ホスト名を変更したあとは再起動するのがマイルールです。

reboot

再起動後に IP アドレスとホスト名が意図通りになっているか確認しておきます。

hostname
ip addr

drbd

DB サーバで drbd のセットアップをします。

まずはアタッチされた EBS のデバイス名を調べます。

fdisk -l

/etc/drbd.d/r0.res のデバイス名を修正します。デバイス名が /dev/xvdj なら修正の必要はありません。

vi /etc/drbd.d/r0.res

メタデータを作成して drbd を開始します。

drbdadm create-md r0
service drbd start
service drbd status

db01 を Primary として同期します(db01 のみ)。

drbdadm -- --overwrite-data-of-peer primary r0
watch -n 1 service drbd status

同期が終わったらフォーマットしてファイルシステムを作ります(db01 のみ)。

mkfs.ext4 /dev/drbd/by-res/r0
tune2fs -c 0 /dev/drbd/by-res/r0

マウントして MySQL のデータディレクトリを初期化します(db01 のみ)。

mkdir -p /drbd/r0
mount -t ext4 /dev/drbd/by-res/r0 /drbd/r0 

mkdir /drbd/r0/mysql
chown mysql:mysql /drbd/r0/mysql
rm -fr /var/lib/mysql
ln -s /drbd/r0/mysql /var/lib/mysql

service mysqld start
mysql -e 'select 1'
service mysqld stop

db02 にもマウントポイントと mysql のデータディレクトリのシンボリックリンクを作成します(db02 のみ)。

mkdir -p /drbd/r0
rm -fr /var/lib/mysql
ln -s /drbd/r0/mysql /var/lib/mysql

動作確認のために db01 を Secondary に戻します(db01 のみ)。

umount /drbd/r0
drbdadm secondary r0
service drbd status

動作確認のために db02 を Primary にして MySQL を起動します(db02 のみ)。

drbdadm primary r0
service drbd status

mount -t ext4 /dev/drbd/by-res/r0 /drbd/r0 

service mysqld start
mysql -e 'select 1'
service mysqld stop

動作確認が終わったら db02 も Secondary に戻します。

umount /drbd/r0
drbdadm secondary r0
service drbd status

drbd を停止しておきます(db01 と db02 の両方)。

service drbd stop
service drbd status
lsmod | grep drbd

pacemaker

次に DB サーバで pacemaker/heartbeat の設定をします。

logd は常時起動していても問題ないので常時起動にします。

chkconfig logd on
service logd start
service logd status
tail /var/log/messages

heartbeat を起動します。

rm -fr /var/lib/heartbeat/crm/*
service heartbeat start
service heartbeat status

ps auxw | grep heartbeat
watch crm_mon -1

crm_mon でお互いを認識していることを確認できたら、どちらが DC になっているかを確認します。

これから crm を用いた pacemaker の設定を行いますが、この作業は DC で行います。

まずは基本設定。

crm configure
  property stonith-enabled="false"
  property start-failure-is-fatal="false"
  rsc_defaults migration-threshold="5"
  rsc_defaults resource-stickiness="INFINITY"
  rsc_defaults failure-timeout="3600s"
  verify
  commit
  quit

続いてリソース設定。

crm configure

  primitive drbd \
    ocf:linbit:drbd \
    params drbd_resource="r0" drbdconf="/etc/drbd.conf" \
    op start timeout="240s" on-fail="restart" \
    op stop  timeout="100s" on-fail="block" \
    op monitor interval="20s" role="Slave"  timeout="20s" start-delay="1m" on-fail="restart" \
    op monitor interval="10s" role="Master" timeout="20s" start-delay="1m" on-fail="restart"

  ms ms_drbd \
    drbd \
    meta master-max="1" master-node-max="1" clone-max="2" clone-node-max="1" notify="true"

  primitive vip ocf:heartbeat:IPaddr2 \
    params ip="192.168.0.10" cidr_netmask="32" nic="lo"

  primitive fs \
    ocf:heartbeat:Filesystem \
    params device="/dev/drbd/by-res/r0" directory="/drbd/r0" fstype="ext4" \
    op start   interval="0s"  timeout="60s" on-fail="restart" \
    op monitor interval="10s" timeout="60s" on-fail="restart" \
    op stop    interval="0s"  timeout="60s" on-fail="block"

  primitive mysql \
    ocf:heartbeat:mysql \
    params binary="/usr/bin/mysqld_safe" \
      log="/var/log/mysql/error.log" \
      pid=/var/run/mysql/mysqld.pid \
      socket=/var/run/mysql/mysql.sock \
    op monitor interval="10s" timeout="30s"  on-fail="restart" start-delay="30" OCF_CHECK_LEVEL="20" \
    op start   interval="0s"  timeout="120s" on-fail="restart" \
    op stop    interval="0s"  timeout="120s" on-fail="block"

  primitive route lsb:ec2-route \
    op start   interval="0s"  timeout="60s" on-fail="restart" \
    op monitor interval="60s" timeout="60s" on-fail="restart" \
    op stop    interval="0s"  timeout="60s" on-fail="block"

  group group_db \
    fs mysql vip route

  colocation colocation_db_drbd \
    inf: group_db ms_drbd:Master

  order order_drbd_db \
    inf: ms_drbd:promote group_db:start

  verify
  commit
  quit

設定内容が正しいことを確認し、mysql が起動するのを待ちます。

crm configure show
watch crm_mon -1n

mysql

crm_mon で db01 と db02 のどちらで mysql が起動しているか調べて、起動している方で mysql に接続します。

crm_mon -1n
mysql mysql

いらない権限を削除して wordpress 用のデータベースを作成します。

delete from user;
delete from host;
delete from db;

grant all on *.* TO root@localhost with grant option;
grant all on *.* TO root@127.0.0.1 with grant option;

flush privileges;

create database wordpress;
create user wordpress@'%';
set password for wordpress@'%' = password('wordpress');
grant all on wordpress.* to wordpress@'%';

mysql が起動していない方のインスタンスから接続できることを確認します。

mysql -h 192.168.0.10 -u wordpress -p wordpress

glusterfs

NAS サーバで glusterfs の設定を行います。

まずは ns01 と ns02 にアタッチされたディスクのデバイス名を調べます。

fdisk -l

パーティションを切ります。

fdisk /dev/xvdj
n
p
1


w

フォーマットします。

mkfs.ext4 /dev/xvdj1
e2label /dev/xvdj1 glfs

/etc/fstab に追記します。

cat <<EOS>> /etc/fstab
LABEL=glfs /glfs/vols     ext4    defaults         0 0
EOS
cat /etc/fstab

マウントします。

mkdir -p /glfs/vols
mount -a
df

ns01 と ns02 間の通信はセカンダリ IP アドレスで指定した固定アドレスがソースアドレスになる必要があるようなので、ルーティングを変更します。

ip route add 10.1.0.0/16 \
  via $(ip -4 -o route get 8.8.8.8 | grep -Eo 'via [.0-9]+' | cut -d' ' -f2) \
  src $(ip -4 -o addr show secondary | grep -Eo 'inet [.0-9]+' | cut -d' ' -f2) \
  dev eth0

ip route list

glusterfs を開始します。

service glusterd start

ns01 を作業ノードとして ns02 を peer に登録します(ns01 のみ)

gluster peer probe ns02

ns01 と ns02 で互いに認識していることを確認します。

gluster peer status

次のように表示されます。

Number of Peers: 1

Hostname: ns02
Uuid: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
State: Peer in Cluster (Connected)

ns01 と ns02 でブリックを作成します。

mkdir /glfs/vols/data

ns01 でボリュームを作成・開始します(db01 のみ)。

gluster volume create data replica 2 transport tcp  ns01:/glfs/vols/data ns02:/glfs/vols/data
gluster volume start data

ns01 と ns02 でボリュームが作成されていることを確認します。

gluster volume info data

次のように表示されます。

Volume Name: data
Type: Replicate
Volume ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Status: Started
Number of Bricks: 1 x 2 = 2
Transport-type: tcp
Bricks:
Brick1: ns01:/glfs/vols/data
Brick2: ns02:/glfs/vols/data

ns01 と ns02 でマウントします。

mkdir -p /glfs/data
mount -t glusterfs localhost:/data /glfs/data

AP で glusterfs をマウント

ap01 と ap02 で glusterfs をマウントします。

mkdir -p /glfs/data
mount -t glusterfs ns01:/data /glfs/data

ap01 で wordpress の wp-content の中身をごっそり移動します(ap01 のみ)。

rsync -av /var/www/html/wp-content/ /glfs/data/wp-content/
chown -R apache:apache /glfs/data/wp-content/

ap01 と ap02 で wordpress の wp-content をシンボリックリンクにします。

rm -fr /var/www/html/wp-content/
ln -sf /glfs/data/wp-content /var/www/html/wp-content
ll /var/www/html/wp-content/

ap01 と ap02 から mysql に接続できることを確認します。

mysql -h 192.168.0.10 -u wordpress -p wordpress -e "select 1"

Apache を起動します。

service httpd start
chkconfig httpd on

ELB の作成

ELB を作成します。

1. Define Load Balancer

  • Load Balancer name は wp-lb
  • Create LB Inside は wp-vpc を選択
  • その他はデフォルト

2. Configure Health Check

  • Ping Path は /readme.html に変更
  • その他はデフォルト

3. Select Subnets

  • ap-northeast-1a と ap-northeast-1c を追加

4. Assign Security Groups

  • default と wp-public を選択

5. Add EC2 Instances

  • なにも選択せずに次へ(あとで cli で追加する)

ELB に AP を追加

ap01 と ap02 にログインして awc cli で ELB に自分自身を追加します。

aws elb register-instances-with-load-balancer \
  --load-balancer-name wp-lb \
  --instances "$(curl -s http://169.254.169.254/latest/meta-data/instance-id)"

ELB の DNS 名を登録

Route53 で ALIAS レコードで ELB を登録します。

ブラウザで閲覧

↑で登録したDNS名でブラウザで閲覧して、wordpress のインストール画面が表示されたら成功です。

・・・この時点で全世界にインストール画面が公開されているのでよくありませんね・・・

インストール画面で必要な項目を入力し、適当な投稿を POST できることを確認します。

フェイルオーバー

wordpress で適当に何件か投稿したあと、EC2 の Management Console で mysql が起動している側の AZ のインスタンスを皆殺しにします。

しばらくは閲覧できなくなりますが、フェイルオーバーが完了すれば wordpress が再び閲覧できるようになります。

さいごに

そのまま放置しておくと凄い勢いでチャリンチャリンと課金されていくので、いらないものは削除しておきましょう。

特にインスタンスと ELB がお高いです、EBS はそこそこです、AMI とスナップショットはお安いので記念に残しておいても良いでしょう。

ISC dhcpd で IP アドレスに基いてホスト名を自動設定

AWS EC2 のインスタンスを立ち上げたとき、(AMI によるかもしれませんが)DHCP で付与された IP アドレスに基いて ip-10-11-12-13 のようにホスト名が設定されるのを見て、さすが AWS は不思議なチカラでよくわからんことをしよる、と思いました。

と、思ったら ISC DHCP でも普通にできました。

使用した dhcpd のバージョンは次の通りです。

CentOS 7.0.1406 / dhcp-4.2.5-27.el7.centos.x86_64

man dhcp-eval すると色々でてきます。

dhcpd.conf

log-facility daemon;
not authoritative;

ddns-updates on;
ddns-update-style interim;
ignore client-updates;

subnet 10.1.1.0 netmask 255.255.255.0 {
    default-lease-time          21600;
    max-lease-time              43200;
    range dynamic-bootp         10.1.1.100 10.1.1.200;
    option routers              10.1.1.1;
    option subnet-mask          255.255.255.0;
    option domain-name-servers  10.1.1.10;
    option domain-name          "test";
    option domain-search        "test";
    ddns-domainname             "test";
    ddns-ttl                    3600;

    zone test. {
        primary 10.1.1.10;
    }

    zone 1.1.10.in-addr.arpa. {
        primary 10.1.1.10;
    }

    if not exists host-name {
        option host-name = concat("ip-" , binary-to-ascii(10, 8, "-", leased-address), ".", config-option domain-name);
        ddns-hostname = concat("ip-" , binary-to-ascii(10, 8, "-", leased-address));
    } else {
        option host-name = concat(lcase(option host-name), ".", config-option domain-name);
    }
}

上の設定では・・・

  • クライアントがホスト名を送ってきている場合(仮に oreore とすると)
    • oreore.test というホスト名をクライアントに返します
  • クライアントがホスト名を送ってこない場合
    • ip-10.11.12.13.test のようにリースした IP アドレスに基づいたホスト名を返します

となります。ついでに同じ名前で DDNS でネームサーバに更新もかけます。

CentOS 7 の dhcpd で DDNS が失敗する

次のような感じで、リースした IP アドレスを Dynamic DNS でネームサーバに登録する DHCP サーバを CentOS 6 で作っていたのですが、なぜか CentOS 7 にすると同じような設定でも DDNS での更新が失敗するようになりました。

dhcpd.conf

log-facility daemon;
not authoritative;

ddns-updates on;
ddns-update-style interim;
ignore client-updates;

subnet 10.1.1.0 netmask 255.255.255.0 {
    default-lease-time          21600;
    max-lease-time              43200;
    range dynamic-bootp         10.1.1.100 10.1.1.200;
    option routers              10.1.1.1;
    option subnet-mask          255.255.255.0;
    option domain-name-servers  10.1.1.10;
    option domain-name          "test";
    option domain-search        "test";
    ddns-domainname             "test";
    ddns-ttl                    3600;
}

CentOS 6 で成功していたときは次のようなログが記録されていました。

dhcpd: Added new forward map from aaa.test to 10.1.1.100
dhcpd: added reverse map from 100.1.1.10.in-addr.arpa. to aaa.test

CentOS 7 で失敗したときには次のようなログが記録されていました。

dhcpd: Unable to add forward map from aaa.test to 10.1.1.100: not found

各バージョンは次の通りです。

CentOS 6.5 / dhcp-4.1.1-38.P1.el6.centos.x86_64
CentOS 7.0.1406 / dhcp-4.2.5-27.el7.centos.x86_64

試行錯誤の結果、CentOS 7 でも dhcpd.conf で zone でネームサーバを明示すれば大丈夫でした。

log-facility daemon;
not authoritative;

ddns-updates on;
ddns-update-style interim;
ignore client-updates;

subnet 10.1.1.0 netmask 255.255.255.0 {
    default-lease-time          21600;
    max-lease-time              43200;
    range dynamic-bootp         10.1.1.100 10.1.1.200;
    option routers              10.1.1.1;
    option subnet-mask          255.255.255.0;
    option domain-name-servers  10.1.1.10;
    option domain-name          "test";
    option domain-search        "test";
    ddns-domainname             "test";
    ddns-ttl                    3600;

    zone test. {
        primary 10.1.1.10;
    }

    zone 1.1.10.in-addr.arpa. {
        primary 10.1.1.10;
    }
}

CentOS 7 で zone を書いて成功したときは次のようなログが記録されました(CentOS 6 で成功していたときのログと同じです)。

dhcpd: Added new forward map from aaa.test to 10.1.1.100
dhcpd: Added reverse map from 100.1.1.10.in-addr.arpa. to aaa.test

CentOS 6 の dhcpd だと zone を記述していなくても SOA レコードから権威サーバを探して更新先のネームサーバを特定しているっぽいのだけど (nsupdate も同じような動作するよね?)、CentOS 7 の dhcpd だと SOA レコードの検索はせずに、zone で指定されたネームサーバに直接更新を掛けているようでした。

実際、SOA レコードとか NS レコードを適当に書いても更新に成功しました。

dhcpdns も本職ではなく詳しくないので、なにか根本的なところで間違っているのかもしれません。