k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

SqaleでPHPアプリケーションのデプロイ時にPHPスクリプトから.htaccessファイルを作成してみたメモ

SqaleでPHPアプリケーションを動かす記事の続きです。

Sqaleではデプロイ完了後にフックさせるpostinstallスクリプトに対応しており、これを使ってCLI版のPHPを呼び出せることが分かりました。

前回の記事 SqaleのPHPアプリケーションでSwiftMailerを使ってメール送信してみたメモ では秘密情報を含む設定ファイルをリポジトリの外に設置して postinstall で ln コマンドを使ってリンクしましたが、postinstall から任意のPHPスクリプトを実行できるのであれば、その中で行うこともできそうです。

また、以前の記事 SqaleのPHPアプリケーションでSilexを動かしてみたメモ に書いた .htaccess がデプロイ対象から除外される件は修正されたのですが、環境に依存する部分が大きい .htaccessリポジトリに含めるのは良くないという考え方もありそうです。

よく引っかかるところでは 2.4から従来の Order, Allow といったディレクティブが Require ディレクティブに置き換えられてることや、例として挙げた FallbackResource ディレクティブも 2.2.16以上からの対応ですし、そもそも Apache じゃなくて nginx かもしれません。

そう考えるうちにスクリプトから設定ファイルを生成するのは悪くない気がしてきたので、試しにPHPスクリプトからの .htaccess 生成に挑戦してみたメモです。

デプロイ用PHPスクリプト

デプロイ時にpostinstallから実行させるPHPスクリプトです。

deplyoment.php

<?php
require_once realpath(__DIR__ . '/../vendor/autoload.php');

$document_root = __DIR__ . DIRECTORY_SEPARATOR . 'public';
$log_file = __DIR__ . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR . 'deplyoment.log';

$messages = array();

// config.json設定ファイルを読み込み、symlinkを作成
$config_file = realpath(__DIR__ . '/../etc/config.json');

if (!$config_file) {
    error_log(sprintf('[%s]設定ファイル "%s" が見つかりません',
        date('Y-m-d H:i:s')
    ) . "\n", 3, $log_file);
    exit;
}

$config_link = __DIR__ . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.json';

$config = json_decode(file_get_contents($config_file));

if (!isset($config->gmail->username)) {
    error_log(sprintf('[%s]設定ファイルにGMailアカウント config.gmail.username が未設定です',
        date('Y-m-d H:i:s')
    ) . "\n", 3, $log_file);
    exit;
}

if (!isset($config->gmail->password)) {
    error_log(sprintf('[%s]設定ファイルにGMailパスワード config.gmail.password が未設定です',
        date('Y-m-d H:i:s')
    ) . "\n", 3, $log_file);
    exit;
}

if (!file_exists($config_link)) {
    symlink($config_file, $config_link);
}


// Apacheバージョンに合わせて.htaccessを作成
exec('httpd -v', $httpd_version);

if (isset($httpd_version[0]) &&
    strpos($httpd_version[0], 'Server version: ') === 0
) {
    $server_software = substr($httpd_version[0], strlen('Server version: '));
    if (preg_match('~\A([^/]+)/([^ ]+)~', $server_software, $matches)) {
        if (isset($matches[1]) && strpos($matches[1], 'Apache') === 0) {
            $contents = version_compare($matches[2], '2.2.16') >= 0 
                ? <<< CONTENTS
FallbackResource /index.php
CONTENTS
                : <<< CONTENTS
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !\.(ico|gif|jpe?g|png)$ [NC]
    RewriteRule ^ index.php [L]
</IfModule>
CONTENTS;
            $htaccess = $document_root . DIRECTORY_SEPARATOR . '.htaccess';
            file_put_contents($htaccess, $contents, LOCK_EX);
            chmod($htaccess, 0600);
            $message = sprintf('[%s]"%s" を作成しました', date('Y-m-d H:i:s'),  $htaccess);
            error_log($message . "\n", 3, $log_file);
            $messages[] = $message;
            $messages[] = '----------';
            $messages[] = $contents;
            $messages[] = '----------';
        }
    }
}


// 結果をメールで送信
$transport = \Swift_SmtpTransport::newInstance()
    ->setHost('smtp.googlemail.com')
    ->setPort(465)
    ->setEncryption('ssl')
    ->setUsername($config->gmail->username)
    ->setPassword($config->gmail->password)
;

$mailer = \Swift_Mailer::newInstance($transport);

$message = \Swift_Message::newInstance()
    ->setSubject(sprintf('[%s]デプロイしました', date('Y-m-d H:i:s')))
    ->setFrom(array('k.holy74@gmail.com' => 'k-holy'))
    ->setTo(array('k.holy74@gmail.com' => 'k-holy'))
    ->setBody(implode("\n", $messages), 'text/plain')
;

try {
    $mailer->send($message);
} catch (\Exception $e) {
    error_log(sprintf('[%s]デプロイしましたが、メールを送信できませんでした',
        date('Y-m-d H:i:s')
    ) . "\n", 3, $log_file);
}

いくつかの処理を行っていますが、順に挙げていくと…

  1. config.json設定ファイルを読み込み、シンボリックリンクを作成する
  2. Apacheのバージョンを調べて、バージョンに合わせた内容の.htaccessファイルを作成する
  3. 作成した.htaccessの内容をswiftmailerで固定のアドレスに送信する

前回swiftmailerによるメール送信フォームで利用した設定ファイルをそのまま利用し、postinstallスクリプトでlnコマンドにより行っていたシンボリックリンク作成は symlink()関数 で行うように変更します。

シンボリックリンクの作成はWindowsとLinuxでは全くコマンドが異なりますが、これなら(Windows Vista, Windows Server 2008 以降ですが)同じ記述で実現できます。

.htaccessの作成では、httpd -v コマンドの実行結果からApacheのバージョンを判定して、2.2.16以上であれば mod_dir の FallbackResource ディレクティブを使い、そうでなければ mod_rewrite 用の記述で、フロントコントローラのスクリプトを指定しています。

exec()関数はWindowsでもLinuxと同じように使えますが、インストール先の httpd.exe にパスが通っていないといけません。

swiftmailerの処理は前回と同じですが、メール送信に失敗した場合を考慮して、失敗した場合はログを残すようにしています。

ローカルでデプロイ用PHPスクリプトを実行する

複雑な処理をPHPで書けるということ自体が(私の場合)大きな利点ですが、PHP-CLIが使える開発環境でも当然このスクリプトは実行できるわけで、事前にテストできるのも嬉しいところです。

ローカルの状態はこんな感じです。*付きがGitリポジトリの管理対象です。

vendor/
etc/
    config.json
demo/
   *.gitignore
    builtin-server.lnk
   *composer.json
   *composer.lock
    composer.phar
   *deployment.php
   *postinstall
   *config/
       *.gitkeep
        config.json
   *log/
       *.gitkeep
   *public/
        .htaccess
       *.undeletable
       *favicon.ico
       *index.php
        router.php

.gitignoreはこんな感じです。

composer.phar
*.lnk
public/router.php
public/.htaccess
config/config.json
log/*.log

ログファイルの作成に log ディレクトリが必要なのですが、ログファイルは除外しています。

前回までリポジトリに含めていた .htaccess は git rm で明示的に削除します。

スクリプトから作成するconfig.json のシンボリックリンク.htaccessを削除した上で、ローカルで実行してみます。

$ php -f deployment.php

シンボリックリンク config/config.json および public/.htaccess ファイルが作成され、ログファイル log/deployment.log には以下のような内容が記録されました。

[2013-03-26 11:38:49]"C:\Users\k_horii\Dropbox\Documents\Projects\sqale\demo\public\.htaccess" を作成しました

GMailにも以下のようなメールが届きました。

[2013-03-26 11:38:49]"C:\Users\k_horii\Dropbox\Documents\Projects\sqale\demo\public\.htaccess" を作成しました
----------
FallbackResource /index.php
----------

デプロイ用スクリプト単体ではちゃんと動作しているようです。

postinstallからデプロイ用PHPスクリプトを実行する

postinstallからはphpを呼んでスクリプトを実行させるだけになります。

前回に追記した composer update コマンドについてはそのまま残しています。

postinstall

#!/bin/sh

set -e
/home/sqale/.phpenv/shims/php /home/sqale/bin/composer.phar update -d /home/sqale/current/
/home/sqale/.phpenv/shims/php -f /home/sqale/current/deployment.php

この状態でコミットを push したところ、無事に動いてくれました。

$ ls /home/sqale/current/config -la|grep config.json
lrwxrwxrwx 1 sqale sqale  27  3月 26 12:06 2013 config.json -> /home/sqale/etc/config.json

$ ls /home/sqale/current/public -la|grep .htaccess
-rw------- 1 sqale sqale   27  3月 26 12:06 2013 .htaccess

$ tail /home/sqale/current/log/deplyoment.log
[2013-03-26 12:06:33]"/home/sqale/current/public/.htaccess" を作成しました

以下のように、.htaccessの内容が書かれたメールも受信できています。

[2013-03-26 12:06:33]"/home/sqale/current/public/.htaccess" を作成しました
----------
FallbackResource /index.php
----------

デプロイ時に何をするか

今回はPHPでデプロイ時に実行するスクリプトを書いて、秘密情報を含んだ設定ファイルへのシンボリックリンク作成と、環境に応じた.htaccessの作成、それらのログとメール送信を行いました。

Sqale のPHPアプリケーション環境は Apache + FastCGI + PHP-FPM ということですが、PHP5.3から導入された user_ini を生成するのはどうでしょう。

user_ini(デフォルト設定のファイル名は".user.ini")は CGI/FastCGI SAPI でのみ有効な設定ファイルで、ini_set() 関数では変更できない PHP_INI_PERDIR レベルのディレクティブを設定できます。

従来 PHP_INI_PERDIR レベルのディレクティブの設定は .htaccessphp_flag や php_value で記述していましたが、これを動的に生成することで、Apache以外のWebサーバやPHPのバージョンが異なる環境にも移植しやすくなるのではと思いました。

また、今回はスクリプトのコード内で設定ファイルを生成していますが、事前に複数の環境に合わせた設定ファイルを用意しておいて、読み替えるような方法が良いのかも。

まだ自動デプロイ環境に慣れていないので、これから勉強していきます…。