古のPHP5.6プロジェクトをdeployerでデプロイする

## はじめに

まだまだ現役なPHP5.6、デプロイ方法に困ることありますよね（たぶんない）
ということでPHP製の[Deployer](https://deployer.org/)を使ってデプロイする方法をご紹介します。

いい感じの前提環境は以下のとおりです。

- PHP 5.6
- CentOS 6.x
- AWS EC2
- Bastionインスタンスからデプロイを実行する
- デプロイ対象は複数インスタンス
- インスタンス利用用途ごとに異なる追加処理を行う

この記事はEOLを迎えた古の環境をなんとか生きながらえる試行錯誤のためです。
良い子の皆さんは決してこんな古いバージョンを使わないようにしましょう。
また、ここで紹介するデプロイコードはDeployer 4.x以外の環境だと動きません。
おとなしく最新版のドキュメントを読んで対応してください。

## Deployerをセットアップする

それではサクッとインストールしてみましょう。
現時点の最新バージョンは7.xですが、もちろんPHP5.6には対応していません。
諦めてギリギリ対応する最終版である4.xを使いましょう。

```shell
curl -LO https://deployer.org/releases/v4.3.4/deployer.phar
chmod +x deployer.phar
mv deployer.phar /usr/bin/dep
```

以降のステップで困ったら公式の情報を参照しましょう。
古いバージョンだとあまり紹介記事がないのとドキュメント整備状況が最新版と比べると弱いので、ソースコードを読みながら進めると幸せになれます。

- [4.x ドキュメント](https://github.com/deployphp/docs/tree/4.x)
- [4.x ソースコード](https://github.com/deployphp/deployer/tree/4.x)

## デプロイ用のコードを作成する

プロジェクトのルートにデプロイ設定ファイルを作っていきましょう。

```php
<?php
namespace Deployer;

// 基本になるデプロイレシピを読み込む
require 'recipe/common.php';

// アプリケーション名
set('application', 'Application Name');
// リポジトリの場所
set('repository', 'git@github.com:/pathto/repository');
// デプロイ先ディレクトリ
set('deploy_path', '/pathto/deploy');
// SSH接続の種別（Nativeが強く推奨されている）
set('ssh_type', 'native');
// CentOS 6.x系だと標準でmultiplexingに対応していないのでfalse
// OpenSSH 5.5以降に更新することで使えるようになる（バージョンアップ可能なら強く推奨）
set('ssh_multiplexing', false);
// gitコマンドにTTYを割り当てる（必要に応じてONにする）
set('git_tty', true);
// ------------------------------------------------------------
```

ひとまず設定はこんな感じでしょうか。
環境が古いと `ssh_multiplexing` が使えないのはハマりがちですね。
これが設定できないことで接続オーバーヘッドがかなりかかるのが痛いです。

## デプロイ先インスタンスを設定しよう

```php:/deploy/init.php
<?php
namespace Deployer;

use Deployer\Exception\ConfigurationException;

// 起動中インスタンスのすべてのタグ情報を取得する
// 設定されたタグ情報をもとにデプロイ対象を決めるため
function getInstances()
{
    $runnings = json_decode(`aws ec2 describe-instances --filters Name=instance-state-name,Values=running --query Reservations[].Instances[].InstanceId`, true);
    $tags = json_decode(`aws ec2 describe-tags --filters Name=resource-type,Values=instance --query Tags[]`, true);
    $instances = [];
    // インスタンスごとのタグ情報を取得する
    foreach ($tags as $tag) {
        $id = $tag['ResourceId'];
        $key = $tag['Key'];
        $value = $tag['Value'];
        // 起動中でない場合は追加しない
        if (!in_array($id, $runnings)) continue;
        $instances[$id][$key] = $value;
    }
    // サーバ名順にソートする
    uasort($instances, function($a, $b){
        if ($a['Name'] == $b['Name']) return 0;
        return $a['Name'] > $b['Name'];
    });
    return $instances;
}

// ローカルホストのIPアドレスを取得する
$environments = [];
$localhost = `curl 169.254.169.254/latest/meta-data/instance-id/`;
// ローカルホストのPHP_ENVを取得する
$localenv = getenv('PHP_ENV');
// インスタンス一覧を取得する
$instances = getInstances();
if (!$instances) throw new ConfigurationException('サーバが一台もない');
foreach ($instances as $iname => $instance) {
    // 異なるEnvのサーバは対象にしない
    if ($instance['Environment'] !== $localenv) continue;
    server($instance['Name'], $instance['Name'])
        ->user('USER_ID')
        ->identityFile('PUBLIC_KEY', 'PRIVATE_KEY');
    foreach ($instance['Targets'] as $target) {
        $environments[$target][] = $instance;
    }
}
```

EC2インスタンスのタグを元にデプロイ対象サーバを設定しています。
簡便にタグ情報を使っていますが、他にもいろんな方法があると思います。
以下のようなタグが設定されています。

- Environment: 環境名（production|staging|development）
- Targets: デプロイターゲットをカンマ区切りで複数記述したもの

Targetsでは、特定のサーバのみに行うデプロイ処理のグループをカンマ区切りで定義しました。
たとえばwebサーバには「web」、バッチサーバには「batch」という名前をつけています。

## リリース作戦とリリースディレクトリの説明

リリースディレクトリは以下のような構成になります。

```shell
# 仮にデプロイ先を /var/www 配下と仮定する

# デプロイのルートディレクトリ
/var/www
# リリース時に実ファイルが格納されるディレクトリ
# デフォルトでこの下に連番のリビジョンディレクトリが作成される
/var/www/releases
/var/www/releases/1
/var/www/releases/2
# デプロイが終わるまで最新リビジョンディレクトリがシンボリックリンクされる
# デプロイの最後にcurrentにリネームされる（途中でこけたら残る）
/var/www/release
# デプロイが終わった際に最新リビジョンディレクトリにシンボリックリンクされる
/var/www/current
```

今回のリリース時の作戦ですが、以下のようにしたいと思います。

1. インストールやビルドはローカルリポジトリ上で行う
2. 各サーバにrsyncでデプロイする
3. 各サーバで必要な再起動処理などを行う

## ビルドを設定する

```php:/deploy/tasks.php
<?php
namespace Deployer;

task('composer:install', function () {
    runLocally('composer install --prefer-dist --no-interaction');
})->once()->setPrivate();

task('npm:ci', function () {
    runLocally('npm ci');
})->once()->setPrivate();

task('npm:build', function () {
    runLocally('npm run build');
})->once()->setPrivate();

// Webサーバのphp-fpmを再起動する
task('restart:web', function () {
    run('sudo svc -h /service/php-fpm-web');
})->onlyOn($environments['web']);

// Batchサーバのphp-fpmを再起動する
task('restart:batch', function () {
    run('sudo svc -h /service/php-fpm-batch');
})->onlyOn($environments['batch']);

// ---------------------------------------------------

// 共通デプロイ処理を切り出して書いてみる
task('deploy:common', [
    'composer:install',
    'npm:ci',
    'npm:build',
])->setPrivate();

// 全体のデプロイ処理を記述する
task('deploy', [
    'deploy:common',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'rsync',
    'deploy:symlink',
    'deploy:unlock',
    'restart:web',
    'restart:batch',
    'success'
])->desc('Deploy!!!');

```

デプロイの個々のタスクは `task('name', callback)` として作成します。
- `task()->once()` は、ローカル環境で動作するタスクであると定義します。
- `task()->setPrivate()` は、該当タスクだけどCLIから実行させない設定です。
- `task()->onlyOn()` は、ここで指定したサーバのみコマンドを実行します。

1つのタスクに複数タスクを紐付ける（グループ化する）ことができます。
その場合は `task('name', ['task1', 'task2',...])` のようにします。
グループタスクに `once()` などをつけられないので注意してください。

タスク内のコマンドは `run()` または `runLocally()` で実行します。

## おや？rsyncが動かないぞ

そうです。
Deployer 4.xにはrsyncするためのレシピがありません。
以下からダウンロードしていい感じに書き換えてあげましょう。

https://github.com/deployphp/deployer/blob/master/contrib/rsync.php

## さあデプロイを実行しよう

以下のコマンドを実行してみましょう。
最大限にverboseしながらデプロイコマンドが流れていくことがわかります。いえい。
このコマンドでは `task('deploy', [...])` が実行されてます。

```
dep deploy -vvv
```

## おわりに

Deployerは楽しいですよね。

あまり古い環境（言語・OS・フレームワークなど）を使い続けるとセキュリティの懸念が高いのはもちろん、取り得る選択肢がどんどん狭まっていくことがネックになるなと改めて感じました。便利なツールは動かない、自前でなんとかしなきゃいけない、そんな思いをしないためには、ぜひがんばって環境をバージョンアップしましょう（死）