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

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

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

<img width="400" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/14846/88523a85-a788-8c38-f7f8-f8969b68c9fd.png">

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

[GitHub - vapor/vapor: 💧 A server-side Swift web framework.](https://github.com/vapor/vapor)

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

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

## Server::Starterとは？

 [Server::Starter - search.cpan.org](http://search.cpan.org/~kazuho/Server-Starter-0.33/lib/Server/Starter.pm)
サーバーをホットデプロイ可能にするためのCPANモジュールです。

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

- [Server::Starterから学ぶhot deployの仕組み - $shibayu36->blog;](http://blog.shibayu36.org/entry/2012/05/07/201556)

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に対応した**と言えそうです！

参考: 


- [Server::Starterに対応するとはどういうことか - limitusus’s diary](http://limitusus.hatenablog.com/entry/20131225/1387993119)
- [「Server::Starterに対応するとはどういうことか」の補足 - sonots:blog](http://blog.livedoor.jp/sonots/archives/40248661.html)
- [Server::Starter を Java で利用する方法。または、System.inheritedChannel() の挙動について - tokuhirom’s blog](http://blog.64p.org/entry/2015/05/27/175336)


## Vaporの仕組み

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

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

```swift
/// 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`を実装したファクトリ経由で作ることができ、このファクトリはアプリケーションの設定として渡すことができます。

```swift
/// 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
}
```

```swift
// main.swift
// 設定
let drop = try Droplet(config: config, server: myServerFactory)
```


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

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

```swift
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`というソケットを表すクラスがあるので、それを作ります。

```swift
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`を作るのがちょっと難しいですが、[このあたりの実装を参考](https://github.com/vapor/sockets/blob/master/Sources/Sockets/Address/Address.swift#L177-L188) にやってみてください。

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

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

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

- [GitHub - IBM-Swift/BlueSignals: Generic Cross Platform Signal Handler](https://github.com/IBM-Swift/BlueSignals)

```swift
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](https://github.com/HDHTTPServer)

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


![https://user-images.githubusercontent.com/1257116/33343019-556614a0-d4c7-11e7-854e-f8faedb70a6e.gif](https://user-images.githubusercontent.com/1257116/33343019-556614a0-d4c7-11e7-854e-f8faedb70a6e.gif)


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