DeployerでPreBuildするときのハマリポイント

## はじめに

[Deployer](https://deployer.org/)を使って複数台のEC2インスタンスにデプロイするRecipeを高速化させるときのハマりポイントについての備忘録です。

Deployerについてある程度の知識があり、同じ悩みを抱える方を対象としております。

本来、ドキュメントにも書いてあるので、簡単にできるはずなので、こうすれば良いんだよっていうアドバイスをいただけると感謝感激です:bow:

## やりたいこと

EC2上に複数台に配置しているLaravel + Vue.jsのアプリケーションのデプロイがとても遅かったので、そのデプロイを高速化することです。

## デプロイ改善前の状況

殆どがDeployerが用意している[Recipes](https://deployer.org/recipes/rollbar)を使用しており、カスタムしていたのは `release_path` などの設定を動的にオーバライドしている処理と、Gulpくらいでした。

デプロイ方法は、CIからデプロイサーバに対してEC2 Run CommandでDeployerのコマンドを実行する方法です。

使用しているDeployerのRecipeはLaravelとYarn、後はカスタムでGulpコマンドを順番に実行している程度です。
Recipeの各Taskを複数のサーバで一台ずつ実行するデプロイ方式になっていました。

一台ずつデプロイしていた理由としては、一回のTask実行でかなりCPU使用率が上昇するので、同時にTaskを実行してサービス運用に影響を与えないためです。

特にCPU使用率が高かったTaskは、 `composer install`, `yarn install`, `gulp`の3つのTaskでした。

実際に動いているサーバ上で、 `composer` や `yarn` を実行していればCPU使用率が上がって遅くなるのは明らかですね。

また、一台ずつ直列実行している事も遅い原因ではあるけど、CPUが上がってしまうので仕方なく一台ずつ実行しているというのが改善前の状況でした。

## 改善後の構成

デプロイ時にCPUが上がってしまうことを課題として、デプロイ前にデプロイサーバで一度だけBuildして、Build後に各サーバに並列で配信するという方式を取ることで解決することにしました。

Pre Buildに関しては、Deployerのドキュメントにも記載されていたので、一般的なやり方のようです。

> If you have a lot of servers where are you going to deploy your application, or you are going to use a CI server, it's better to build your release on one server and then upload files to the application servers.  
by https://deployer.org/docs/advanced/deploy-strategies

## ハマリポイント
早速実装に入ったのですが、ドキュメント通りに書いても中々上手く動きませんでした。
その中のハマリポイントを残しておきます。

### 1. Taskをローカル実行させると、変数が使用できない!?

Deployerのドキュメントに従って、以下の記述をしたのですが、 `local()` をつけてローカル実行させると、 `Segmentation fault` が起こってしまいます。

```php
task('build', function () {
    cd('{{ release_path }}/laravel');
    run('composer install');
    run('npm install');
    run('npm run build');
    // ...
})->local();
```

どうやら、Deployer内部で再帰処理があるらしく、無限呼び出しをして `Segmentation fault` になっているようでした。

このIssueに関連しているっぽいのですが、これは単純に無限再帰呼出ししてるだけなので、若干違うかもです。
https://github.com/deployphp/deployer/issues/1377

#### 対応策

これは正攻法では、どうやっても解決できなかったので、仕方なく別の方法を取ることにしました。
Deployerではデプロイ先のHost情報を別ファイルにYamlで記述することができ、その中で `local: true` の記述を混ぜておくとローカル実行させる事ができます。

そこで、ローカル用のHostを記述しておき、 `local()` を使用せずに、 `onHosts()` を使用してビルドはローカル用のHostのみで実行するようします。

このように書くと、ローカル実行のTask内でも変数を参照できるようになりました。

### 2. `onHosts()`, `local()` は `function` でないと動かない...?

これは多分仕様なので、Deployerが悪いわけではないのですが、知らないと詰まってしまうポイントでした。

DeployerではTaskを配列で書く方法とFunctionで書く2種類の方法を取ることができます。

以下のような感じですね。

```php
// local()は無効
task('hoge', [
    'deploy:prepare',
    'deploy:lock',
    'deploy:vendor',
    ...
])->local();

// local()は有効
task('fuga', function() {
    invoke('deploy:prepare');
    invoke('deploy:lock');
    invoke('deploy:vendor');
})->local();
```

これを知らないと、配列の書き方をしているとローカル実行したいのにされないという現状に頭を悩まされる事になります。

### 3. docker上で動かす時は `setfacl` が `Operation not supported` になる!?

これはDockerの仕様らしく、ファイルのアクセス・コントロールリストを設定する `setfacl` が `Operation not supported` というエラーを吐いてデプロイが終了してしまいます。
(Deployerでは、デフォルトの `wriable_mode` が `setfacl` を使用するようになっている。)

Docker上でDeployerを動かしたことがある人は、 `deploy:writable` が動かないという経験をお持ちのはず。

mobyのIssueにも上がっているので、これは対応を待ったほうが良さそうですね。
https://github.com/moby/moby/issues/32915

#### 対応策

1. Dockerの `storage-driver` を変更する  
    こちらの[Stack Overflowの投稿](https://stackoverflow.com/questions/22714885/how-to-use-setfacl-within-a-docker-container)によると、dockerのstorage-driverを `devicemapper` に変更すれば `setfacl` が使用できるようになるとのこと。

    Docker for Macを使っていたので、  `Preference->deamon->advanced` からstorage-driverをdevicemapperに変更してみたのだが、dockerがクラッシュしてしまったので、断念する事に。

2. `writable_mode` を `chmod` に変更する  
    Deployerの `writable_mode` のデフォルトが `setfacl` を使うようになっているだけなので、ここを `chmod` などに変えてあげれば問題無く動く。

    しかし、このやり方の問題点は、本番のデプロイの `writable_mode` は `setfacl` のままなので、本番とテストのデプロイコマンドに乖離が出てしまうことです。テストの用としてDocker上でDeployerを実行していたため、今回はこの方法は取らないことにしました。

3. vagrantを使う  
    この問題はDocker上で起こっていることなので、意固地にDockerを使うのではなくて、仮想マシンを使うことで解決することにしました。
    幸い、サーバ構成のプロビジョニングはansibleで自動化可能だったので、こちらを用いてテスト環境を再度作成して、デプロイすることで、 `setfacl` を使用可能になりました。


## まとめ

色々悩まされた挙句、Deployerのコードも読み、時間もおもったよりかかってしまいましたが、Pre Buildを実現させる事ができました。
しかし、保守性の観点から見ると、RecipeがPHPに特化していることと、 `Segmentation fault` が起こるなどまだ不安定な部分もあるようなので、このままDeployerを使い続けること自体を見直す必要もありそうです。

今回はデプロイ速度改善のインパクトとコストを考えて、Deployerを引き続き使用するという選択を取りましたが、個人的には、デプロイ先がEC2なのであれば、[Code Build](https://aws.amazon.com/jp/codebuild/)や[Code Deploy](https://aws.amazon.com/jp/codedeploy/)を使うか、[Beanstalk](https://aws.amazon.com/jp/elasticbeanstalk/) を使う方が保守の面や、デプロイ先との相性、今後の拡張性の観点から最適だと思います。

今後リソースが空いたときにでも移行して、記事を書こうと思います。