はじめに
git pull だけのデプロイに限界を感じて、composer install の自動化をシェルスクリプトで書いたことがあった。
自分なりにうまく書けたと思ってPRを出したら、レビューで色々指摘をもらい、最終的に「それ Deployer がやってることとほぼ同じでは?」と言われた。
移行自体はまだ途中なんだけど、この「自作 → 指摘 → ツール検討」という過程で得た気づきが結構あったのでまとめておく。
自分で書いてみて分かったこと
最初に書いたのは、composer install の前に vendor/ をそのままバックアップする方式(A案)だった。シンプルで分かりやすいし、これで十分だろうと思っていた。
ただ、冷静に考えると composer install がvendorを直接書き換えている最中に、そのvendorを参照しているPHPプロセスがいたらまずい。途中で失敗したらvendorが壊れた状態になる。
そこで、新しいディレクトリに composer install して、完了したらsymlinkで切り替える方式(B案)に設計を変えた。
これなら composer install 中も旧vendorで動き続けるし、切り替えは一瞬。失敗しても新ディレクトリを消すだけで済む。
B案はうまくできたと思ってPRを出した。が、レビューで複数の指摘を受けた。
- 「切り替え直後に旧リリース削除すると、実行中のリクエストが壊れない?」
-
「composerのコマンド、本番環境向けオプションつけてる?」 —
--no-devで開発用パッケージを除外、--optimize-autoloaderでクラスマップ最適化、--no-interactionで対話プロンプト抑制。本番では必須だが、ローカルと同じ感覚でcomposer installだけ書いていた - 「これ、Deployer がやってることとほぼ同じでは?」
OPCacheについては今回の対象サーバでは考慮して対処していた。ただ、旧リリース削除のタイミングやcomposerオプションは深く考えていなかった。そして何より「対象サーバでは対処済み」と「横展開しても大丈夫」は別の問題だという視点が抜けていた。
自作してみたおかげでデプロイの仕組み自体は理解できたけど、成熟したツールが既に解決している問題は思った以上に多かった。
既存ツールを知る — 車輪の再発明を避ける
デプロイツールは成熟した選択肢がいくつもある。Deployer(PHP)、Capistrano(Ruby)、Ansistrano(Ansible)など。
これらのツールには、自作スクリプトでは見落としがちな機能が最初から組み込まれている。
- 排他ロック: 同時に複数のデプロイが走らないようにする
- クリーンアップ: 古いリリースの自動削除(保持世代数を指定できる)
- 並列デプロイ: 複数サーバに同時にデプロイ
- ロールバック: コマンド一つで前のリリースに戻せる
- dry-run: 実際に実行せず、何が行われるか確認できる
自分の場合、PRレビューで「Deployer使えば?」と言われて初めてその存在を知った。知らないものは検討のしようがない。日頃から同じ領域のツールをざっと把握しておくのは大事だと感じた。
Before/After を明確にする
デプロイ先はEC2で、GitHub ActionsからSSM RunCommand経由でデプロイしていた。移行を始める前に、今のフローと移行後のフローを図にして比較するのが大事だった。
Before:
After:
Before/Afterを並べてみると、いくつかのことが整理できた。
- 何が変わるのか: 接続方式(RunCommand → Session Manager)とデプロイ実行方式(git pull → Deployer)の2つが同時に変わる。変更点が2つあるなら、切り分けて段階的に進めるべきだと気づけた
- 何が増えるのか: Afterではsymlink切り替え・OPCacheクリア・旧リリース削除のステップが増えている。これは手動でやるのか、ツールが自動でやるのかを明確にする必要がある
-
なぜ移行したいのか: アトミックデプロイ、ロールバック、
composer installの自動化。動機を書き出すと「本当に移行が必要か」の判断材料にもなる
図にする前は漠然と「Deployer入れたい」と思っていたけど、描いてみると考慮すべき範囲が見えてくる。
段階的に移行する(Phase分け)
Before/Afterの図で「接続方式とデプロイ実行方式の2つが同時に変わる」と分かったので、これを段階的に進めることにした。
| Phase | やること | ゴール |
|---|---|---|
| 1 | 接続基盤の整備 | 新しい接続方式でSSH接続できる |
| 2 | 1台で検証 | 1台で dep deploy が動く |
| 3 | 全サーバ移行 | 全台 Deployer 化、旧方式廃止 |
まずPhase 1で接続だけ確認する。これが通らなければ何も始まらない。
Phase 2で1台だけ実際にDeployerでデプロイしてみる。dry-runで事前確認もできる。
# 実際に実行せず、何が行われるか確認
dep deploy worker-1 --dry-run -vvv
Phase 3で全台に展開するが、旧方式との 並行運用期間 を設けるのが大事。最低1週間くらいは両方動く状態にしておいて、問題がなければ旧方式を廃止する。
「いきなり全部切り替えて戻せなくなった」が一番怖いので、慎重にやりすぎるくらいでちょうどいい。
接続方式の選定 — 既存インフラを活かす
デプロイツールを導入するとき、サーバへの接続方式もセットで考える必要がある。Deployer のようなツールはSSH接続が前提なので、「どうやってSSHするか」を決めないと始まらない。
選択肢としては直接SSH、踏み台サーバ経由、VPN経由などがあるが、大事なのは 既に使っているインフラを活かせないか を最初に考えること。
自分の場合、既にSSM RunCommandでデプロイしていたので、EC2にSSM Agentが入っていてIAMロールも設定済みだった。この既存インフラを使って、SSM Session Managerのトンネル越しにSSH接続する方法を選んだ。
この方式だとPort 22をインターネットに開ける必要がなく、IAMでアクセス制御もできる。新しいインフラを一から構築するよりも、既存の仕組みに乗るほうがハードルが低いし、運用の実績もある。
ツールのバージョン制約を最初に確認する
これは地味だけどかなり重要なポイント。
PHP 7.4の環境でDeployerを導入しようとしたとき、最新のDeployer 8.xはPHP 8.1以上が必要だった。使えない。
| Deployer バージョン | 必要な PHP |
|---|---|
| 8.x(最新) | PHP 8.1 以上 |
| 7.x | PHP 7.3 以上 |
OPCacheクリアに便利な cachetool も、最新版はPHP 8.0以上を要求する。
# Deployer 7.x なら PHP 7.4 でも使える
composer require deployer/deployer:^7.0 --dev
OPCache — 対処済みでも横展開で抜ける
今回の対象サーバではOPCacheのことを考慮して設計していた。ただ、PRレビューを通じて気づいたのは、これが「サーバごとに確認すべきポイント」だということ。今の対象サーバでは対処できていても、将来別のサーバに横展開するときに同じ考慮が抜けると事故になる。
PHPはソースコードをバイトコードにコンパイルして実行する。OPCacheはこのバイトコードをメモリにキャッシュして高速化する仕組み。
問題は、symlinkを切り替えても php-fpmのワーカープロセスは旧バイトコードをキャッシュに持ったまま 動いていること。
新しいクラス定義と旧バイトコードが混在して、「Class not found」のようなエラーが起きる可能性がある。
php-fpm reload で解決する
php-fpm をreloadすると、新しいワーカープロセスが起動して新しいOPCacheで動き始める。既存のワーカーは処理中のリクエストを完了してからグレースフルに終了する。ただし、systemctl reload 自体はシグナルを送って即座に返るので、旧ワーカーの終了完了を待つわけではない。
// Deployer のカスタムタスクとして定義
desc('Reload php-fpm to clear OPCache');
task('php-fpm:reload', function () {
run('sudo /usr/bin/systemctl reload php-fpm');
});
// symlink切り替えの直後に実行
after('deploy:symlink', 'php-fpm:reload');
旧リリース削除のタイミング
もう一つの落とし穴が、旧リリースをいつ削除するかという問題。
危険: symlink 切り替え直後に即削除
安全: php-fpm reload 後に削除 + keep_releases で複数世代保持
ポイントは、reload はシグナルを送るだけで旧ワーカーの終了完了は待たないということ。つまり cleanup の時点でまだ旧ワーカーが生きている可能性はある。
ここで効いてくるのが keep_releases の設定。たとえば keep_releases: 3 なら、cleanup で消えるのは4世代以上前のリリースだけで、直前のリリース(releases/2)はまだ残っている。旧ワーカーがグレースフルに終了するまでの間、参照先のファイルは消えない。
Deployer を使う場合、タスクの実行順序はこうなる。
reload と keep_releases の組み合わせで安全性を確保する設計になっている。この仕組みを理解せずに keep_releases: 1 にしてしまうと、直前のリリースが即削除されて危険なので注意。
権限まわりで忘れがちなこと
デプロイの移行では、新しい接続経路に合わせて権限を整理する必要がある。見落としがちだった2つのポイントを挙げる。
接続先を制限する
新しい接続方式を導入するときは、「どのサーバに接続できるか」を明示的に絞っておく。自分の場合はSSM経由だったのでIAMポリシーでインスタンスIDを指定して制限した。直接SSHなら踏み台の接続先制限やauthorized_keysの管理、VPN経由なら接続可能なネットワークセグメントの制限など、方式に応じた絞り方がある。
ポイントは「デプロイに必要なサーバだけに接続できる」状態を作ること。
Deployer を使うなら sudoers の NOPASSWD が必要
Deployer はSSH経由で非対話的にコマンドを実行する。つまり、パスワード入力のプロンプトが出せない。デプロイタスクの中で sudo を使うコマンドがあると、NOPASSWD を設定していない限りそこで止まって失敗する。
php-fpm reload のように sudo が必要なコマンドがある場合は、sudoers でそのコマンドだけ NOPASSWD にしておく。
# /etc/sudoers.d/ に専用ファイルを作成
deploy-user ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload php-fpm
全コマンドに NOPASSWD を付けるのではなく、Deployer のタスクで実際に使うコマンドだけに限定するのがポイント。
まとめ
デプロイ方法の移行は「ツールを入れ替えるだけ」では終わらない。
- Before/Afterを図にする: 変更点の数と範囲が見える。段階的に進める判断材料になる
- 段階的移行: いきなり全台切り替えない、並行運用期間を設ける
- 接続方式: 既存インフラを活かせないか最初に考える
- バージョン制約: ツールが自分の環境で動くか設計前に確認
- OPCache: symlinkデプロイではキャッシュクリアが必須。横展開時にも忘れずに
- 削除タイミング: reload の非同期性を理解し、keep_releases と組み合わせる
- 権限設計: 接続先の制限とsudoersのNOPASSWDを忘れない
自作スクリプトを書いてレビューで指摘をもらったおかげで、これらのポイントに気づけた。最初からDeployerを導入していたら、仕組みを理解しないまま使っていたかもしれない。回り道だったけど、結果的にはよかったと思う。
移行はまだ途中だけど、検証環境で1台試してから広げていく予定。事前に考慮すべきポイントを洗い出せたので、実際の移行はスムーズにいくはず。