クラウドMac miniにSwift Vaporをデプロイ:launchd常駐とゼロダウンタイム更新

CI/CD ·約 11 分

クラウドMac miniにSwift Vaporをデプロイ:launchd常駐とゼロダウンタイム更新

先月、チームのMacBook Proが「間に合わせのバックエンド」として使われていました。iOSアプリのローカルAPIデバッグ用にSwift Vaporサービスを動かし、swift runで起動した後はターミナルウィンドウを閉じることもできない状態でした。ある日、システムの自動更新で再起動がかかり、サービスは何の警告もなく半日ほど落ちたままになっていました。QAの同僚はAPIに接続できず、自分のネットワークが悪いのかと疑っていたほどです。こうした「人力デーモン」的な運用は、常時稼働のクラウドMac miniを借りたタイミングで必ず改めるべきです。本稿では、Vaporサービスをターミナルからlaunchd常駐へ移行し、さらにゼロダウンタイムでのリリースまで実現する一連の流れを記録します。

シナリオ:なぜVaporサービスをクラウドMac miniに置くのか

Swift Vaporは「iOSクライアント向けの軽量バックエンド」としてよく使われます。仮ログイン、プッシュ通知のコールバック、計測データ受信、社内管理画面など、規模は大きくないものの24時間365日の稼働が求められ、しかも実機のmacOS環境が必須(APNs証明書の結合テストや、swift buildのネイティブ依存関係が必要な場合もある)というケースです。クラウドMac miniは物理機を専有し、仮想マシンではないため、「ローカルマシンは常時起動しておけない」「クラウドのLinuxではmacOS専用の依存関係が動かない」というギャップをちょうど埋めてくれます。ただし、ベアメタルを専有した瞬間、プロセス管理の責任もすべて自分に移ります——プラットフォームがクラッシュしたプロセスを自動再起動してくれる管理層はなく、launchdを自分で組み立てる必要があります。

環境準備:ツールチェーンとポート設計

ログイン後、まずツールチェーンのバージョンを確認し、ローカル開発環境とクラウド側の差異によるビルド結果の挙動違いを防ぎます:

swift --version
xcode-select -p
mkdir -p ~/apps/vapor-api/releases
mkdir -p ~/apps/vapor-api/logs

ポート設計では、後のゼロダウンタイム切り替えに備えて2つのポートを確保します:

用途 ポート 説明
本番メインポート 8080 Nginxが外部に公開する現行アクティブバージョン
カナリア/新バージョンポート 8081 新しいビルドをまずこのポートでセルフチェック
内部ヘルスチェック 8080/8081 /healthz Vaporのカスタムルート、ビルドバージョン番号を返す

/healthzルートはGitのコミットの短縮ハッシュを直接返すようにするのがおすすめです。リリース時に目視で正しいバージョンに切り替わったか確認でき、プロセスが本当に再起動されたかを推測する必要がなくなります。

launchdでnohupを置き換え、常駐サービスにする

~/Library/LaunchAgents/com.m4rent.vaporapi.plistを次のように記述します:

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.m4rent.vaporapi</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/deploy/apps/vapor-api/current/Run</string>
    <string>serve</string>
    <string>--hostname</string>
    <string>127.0.0.1</string>
    <string>--port</string>
    <string>8080</string>
  </array>
  <key>WorkingDirectory</key>
  <string>/Users/deploy/apps/vapor-api/current</string>
  <key>KeepAlive</key>
  <true/>
  <key>RunAtLoad</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/Users/deploy/apps/vapor-api/logs/stdout.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/deploy/apps/vapor-api/logs/stderr.log</string>
</dict>
</plist>

ロードと動作確認:

launchctl load ~/Library/LaunchAgents/com.m4rent.vaporapi.plist
launchctl list | grep vaporapi
curl -s localhost:8080/healthz

plistの主要フィールド解説

KeepAlivetrueに設定すると、プロセスが異常終了した場合にlaunchdが自動的に再起動してくれるため、別途デーモンスクリプトを書く必要はありません。ただし設定ファイル自体に誤りがあり起動直後にクラッシュする場合、launchdは「秒単位での再起動ループ」に陥ることがあります。この場合、ログを見るよりlaunchctl listの再起動回数を確認する方が問題の特定が速いです。RunAtLoadは起動時/ログイン時の自動起動を保証し、クラウドMac miniの計画停止なしの運用スタイルと組み合わせれば、基本的に手動で起動フローに介入する必要はなくなります。WorkingDirectoryは必ず明示的に記述してください。省略すると相対パスの設定ファイルやログパスが、launchdのデフォルト作業ディレクトリが想定と異なるために誤って読み込まれてしまいます。

ゼロダウンタイムリリース:ダブルポート+Nginxリバースプロキシ切り替え

新バージョンをリリースする際、currentディレクトリを直接上書きして同じプロセスを再起動してはいけません。それでは必ず数秒間のサービス空白が発生します。代わりに「新ポートで新プロセスを起動し、ヘルスチェック確認後にトラフィックを切り替える」方式を使います:

cp -R releases/build-2026-07-12 releases/build-2026-07-12-verify
sed -i '' 's/8080/8081/' com.m4rent.vaporapi-staging.plist
launchctl load com.m4rent.vaporapi-staging.plist
curl -s localhost:8081/healthz

/healthzが期待するコミットハッシュを返したら、Nginxのupstreamを切り替えます:

upstream vapor_api {
    server 127.0.0.1:8081;
}

nginx -s reloadは設定を再読み込みするだけで、既に確立された接続を中断しません。旧8080プロセスは既存のリクエストを処理し終えてから、launchctl unloadで優雅にオフラインにします。

トラフィックを切り替える前に旧ポートを停止してはいけません——ヘルスチェックが通ることは、新バージョンが実際の負荷下で問題ないことを保証しません。最低でも数分の観察時間を確保しておけば、異常を検知した際に1コマンドで旧upstreamに戻す方が、事後のロールバックよりはるかに速いです。

ログとモニタリング:ディスク容量の圧迫を避ける

launchdがリダイレクトするstdout.log/stderr.logは、デフォルトでは自動ローテーションされません。長期稼働のサービスでは数ヶ月で数GBに膨らむこともあります。システム標準のnewsyslogにルールを1行追加します:

/Users/deploy/apps/vapor-api/logs/stdout.log  deploy:staff  644  7  10240  *  N

このルールは、ログが10MBを超えたらローテーションし、最大7世代の履歴ファイルを保持することを意味します。クラウドMac miniのSSD容量は固定なので、ログが肥大化すると直接影響を受けるのはビルド成果物やスナップショットの容量です。週に一度du -sh ~/apps/vapor-api/logsで増加傾向を確認することをおすすめします。

トラブルシューティングチェックリスト

  • 環境変数が失われる:launchdが起動するプロセスは、.zshrcexportした変数を継承しません。データベース接続文字列などの設定はplistのEnvironmentVariables辞書に書くか、独立した.envファイルをコード側で明示的に読み込むようにしてください。
  • ポート占有が検出できない:launchctl loadが「サービスは既に存在する」と返すのに接続できない場合、通常は前回の異常終了によりゾンビ化したplistの記録が残っています。まずlaunchctl removeを実行し、その後再度loadしてください。
  • KeepAliveの無限ループ:設定ファイルのパス誤りでプロセスが即座に終了する場合、KeepAliveによって無限に再起動が繰り返され、CPU使用率が瞬時に急上昇します。まずlaunchctl unloadで止血し、その後で原因を調査してください。
  • リリーススクリプトにロールバックポイントがない:currentディレクトリはシンボリックリンクとして具体的なreleases/build-*ディレクトリを指すようにしておくことをおすすめします。ロールバックは、そのリンクを前のディレクトリに戻してreloadするだけです。上書き型のリリースは避けてください。

よくある質問

nohupで起動するのとlaunchdを使うのは何が違いますか?

nohupはターミナルから切り離すだけで、プロセスが落ちても再起動されません。launchdはmacOSのシステムレベルの常駐管理機能で、KeepAliveによる自動再起動や起動時の自動実行を設定できるため、長期レンタルするMac miniには適しています。

ゼロダウンタイム更新には必ず2つのポートが必要ですか?

同じポートで再起動すると新プロセスが立ち上がるまでの数秒間は接続が失敗します。別ポートで新バージョンを起動しヘルスチェック後にNginxのアップストリームを切り替えることで、旧プロセスは既存接続を処理しつつ新規リクエストは新バージョンに流せます。

データベースも同じMac miniに置くべきですか?

小規模なら同一機上のPostgreSQLやSQLiteと日次スナップショットで十分ですが、データ量が多い場合や複数インスタンスで共有する場合は、独立した管理型データベースに接続しMac miniはアプリ層に専念させるのが安全です。

専有Mac miniで検証する

日単位で借りられ、root権限付き、数分で利用開始。まずは試してから継続利用を判断できます。

今すぐ注文