Swift製WebフレームワークVaporをServer::Starterに対応させてホットデプロイする


Swiftがオープンソース化されLinuxに対応して久しいですが、このアドカレの投稿数を見ても分かる通りServer side swift、いまいち盛り上がってませんね。。。😅

理由は色々あると思いますが、原因の1つとして本番プロダクトに投入できるだけのナレッジが溜まっていないというのがあると思います。

今回はこの悪循環を解決すべく、手始めにいま最も人気のあるSwift製WebFramework「Vapor」をServer::Starterに対応させてホットデプロイ可能にしてやりましょう!!!

GitHub - vapor/vapor: 💧 A server-side Swift web framework.

え? SO_REUSEPORT? ローリングデプロイ?そんな贅沢なものは知りません😉

Hot deployとは?

ダウンタイム無しで新しいバージョンのアプリケーションをデプロイすることです。
デプロイのたびにアプリケーションが止まってしまうのはもちろん良くないので、本番運用するにあたって必要な要件の一つだと考えています。

Server::Starterとは?

Server::Starter - search.cpan.org
サーバーをホットデプロイ可能にするためのCPANモジュールです。

こちらの記事が詳しいです。

PerlだけでなくRuby, Java, Go, Scalaなどの言語でも対応事例を見たことがあり、
自分が所属している会社含め、(特に日本のWeb界隈では)いまでも現役で使われていると思います。

Server::Starterに対応したサーバーを作るには?

READMEを読みながら要件を確認します。

By using Server::Starter it is much easier to write a hot-deployable server. Following are the only requirements a server program to be run under Server::Starter should conform to:

・receive file descriptors to listen to through an environment variable
・perform a graceful shutdown when receiving SIGTERM

  1. start_serverのプロセスが環境変数経由でソケットのファイルディスクリプタを渡してくるので、それに対してaccept(2)を呼ぶ作りにする
  2. start_serverのプロセスに対して(再起動のために)SIGHUPが打たれると、子プロセスにSIGTERMを投げてくるので、処理中のものを処理し終えてから死ぬようにする。つまりgraceful shutdownするようにする。

この2つさえ実装できればServer::Starterに対応したと言えそうです!

参考:

Vaporの仕組み

swift-server/httpで「Swift server API」の策定が進んではいますが、まだリリースされておらず、各フレームワークが独自にサーバーのインターフェースを決めているような現状です。

Vaporにおいては ServerProtocolというprotocolがサーバーが実装すべきprotocolになります。

/// Represents an HTTP server.
public protocol ServerProtocol {
    /// creates a new server
    init(
        hostname: String,
        port: Port,
        _ securityLayer: SecurityLayer
    ) throws

    /// starts the server, using the responder
    /// to respond to accepted requests
    func start(
        _ responder: Responder,
        errors: @escaping ServerErrorHandler
    ) throws
}

これをServerFactoryProtocolを実装したファクトリ経由で作ることができ、このファクトリはアプリケーションの設定として渡すことができます。

/// types conforming to this protocol can be
/// set as the Droplet's `.server`
public protocol ServerFactoryProtocol {
    func makeServer(
        hostname: String,
        port: Port,
        _ securityLayer: SecurityLayer
    ) throws -> ServerProtocol
}
// main.swift
// 設定
let drop = try Droplet(config: config, server: myServerFactory)

環境変数からファイルディスクリプタとポート番号を取り出す

SERVER_STARTER_PORTという環境変数にポート番号=ファイルディスクリプタのような形で入っているのでこれを取り出してあげます。

let env = ProcessInfo().environment

guard let value = env["SERVER_STARTER_PORT"] else {
    fatalError("Server::Starte経由の起動でない")
}
guard let port = value.components(separatedBy: "=")[0]) else {
   fatalError("portが入っていない")
}
guard let fd = value.components(separatedBy: "=")[1] else {
    fatalError("ファイルディスクリプタが入ってない")
}

取り出したファイルディスクリプタ、ポート番号を元にソケットを作る

VaporにはTCPInternetSocketというソケットを表すクラスがあるので、それを作ります。

let config = Sockets.Config.TCP()

// 上で取り出したものをもとにVaporのDescriptorオブジェクトを作る
let descriptor = Descriptor(integerLiteral: fd)

// 下記を参考にResolvedInternetAddressオブジェクトを作る
let resolved: ResolvedInternetAddress = ...

// サーバーソケットGET!!
let socket = try! TCPInternetSocket(descriptor, config, resolved)

ResolvedInternetAddressを作るのがちょっと難しいですが、このあたりの実装を参考 にやってみてください。

あとはこのソケットに対してaccept(2)していく作りにしておけばおkです。

start_serverプロセスからのSIGTERMを拾う

あとはSIGTERMを拾っていい感じに終了してやれば良さそうです!
signal関数を使って直接ハンドルしても良いですが、良さそうなライブラリがあったのでそちらをつかました。

Signals.trap(signal: .term) { signal in
   // 終了フラグを立てるなどの処理
   // あとは良い感じにgraceful shutdown
}

ちなみにSIGPIPEはVapor側で拾ってくれているみたいなので特に設定しなくても大丈夫そうです。

start_server コマンド経由でVaporを実行

Vaporにはvaporコマンドが用意されていて、これ経由でビルドや実行を行うことができます。(実際にはswift packageのラッパーです。)

リリース用にビルドしてあげます。

$ vapor build --release

あとはstart_server 経由でvapor runを実行してあげます。
アプリケーションの起動に数秒かかるので適当に--kill-old-delayを設定してあげて新しいプロセスの立ち上がりを待ってあげます。

$ start_server --port=80 --kill-old-delay=10 vapor run --release

アプリケーションを更新してビルドしたら、start_serverのプロセスにSIGHUPを打ってあげればrestartします。

$ kill -HUP プロセス番号

まとめ

社内のハッカソンでこのネタをやったのでコードはめっちゃ汚いですが、一応レポジトリを貼っておきます。

HDHTTPServer.swift · GitHub

テスト用に作った(Vaporとは関係ない)サーバー単体だとうまくいったのですが、Vapor側ではgraceful shutdownがうまく実装しきれずハッカソンでのデモは失敗しました。。が、方向性は示せたかなと思います😅

まだまだ本番運用するにはいろんな課題をクリアする必要がありますが、1つ1つ前に進めてServer side swiftを盛り上げていきましょう!