LoginSignup
53
28

More than 1 year has passed since last update.

Swift Cloud を触ってみる

Posted at

※本記事は弊社が技術書典 13 で無料配布する同人誌「ゆめみ大技林 '22」の寄稿です。追筆や訂正等がある場合はこの記事で告知します。


意外かもしれませんが、Swift はアップルの手によって生み出されるも、アップルのエコシステムだけしか使えないわけではありません。Swift は汎用プログラミング言語です。その汎用さの証拠として、アップルと全く関係ないサーバサイドでも使えるわけです。

サーバサイド Swift と聞くと、アンテナ貼ってる人なら Vapor は一度は聞いたことあるかもしれません。他にも Kitura や Perfect、そして smoke-framework など、実は Swift のサーバ向けフレームワークがいくつもあります。「よしこれからサーバサイド Swift をやってみよう」と思い立ったら、これらのフレームワークをとりあえず落としてみる人も多いでしょう。

ところがそれらはあくまで構成を含めてサーバをデプロイする前提のフレームワークです。実際にサービスが使えるようになるためには、Heroku なりなんなり、どこかでサーバを立ててリリースしなければなりません。これまでアプリしかリリースしたことがない我々にとっては至難の業と言わないまでも、一つの壁とは言えるでしょう。

そんなあなたにご紹介したのが、この Swift Cloud というサービスです。Swift Cloud を使えば、サーバサイド経験が一切なくても、デプロイに関して一切難なく Swift でサービスをリリースできます。

Swift Cloud とは

一言で言うと、GitHub でホスティングされたサーバサイド Swift のリポジトリーを、自動でビルドしてサーバにデプロイしてくれるサービスです。現在まだ正式リリース前で、興味あればアーリアクセスを申し込んで利用できる段階です。そのため肝心な機能がまだなかったり、公式ドキュメントもほとんど内容が空っぽだったりします。

Vapor などのこれまでのサーバサイド Swift 技術と比べて、Swift Cloud の最大の特徴はやはり「サーバ自体を管理しなくていい」ことでしょう。もちろん Vapor もサーバの設定周りがだいぶ簡単になっていますが、それでもやはり手動で Heroku などのサーバにアップロードしたり、アクセス数に応じてスケール管理したりする必要がありました。この点 Swift Cloud は本質的にはサーバレス技術なので、それらのことを一切関与しなくても大丈夫です。必要なのは main 関数で on IncomingRequest の処理をするだけです。

サーバレス、と聞くと AWS Lambda を思い浮かぶ人もいるではないでしょうか。そうです、Swift Cloud が使っている Fastly の Compute@Edge がまさに AWS Lambda と同じようなものです。ただし AWS Lambda も Fastly Compute@Edge も、結局デプロイに関しては手動でやる必要があり、そこが一つの壁と言えるでしょう。Swift Cloud はまさにそのデプロイの壁まで取っ払ってくれたラッパーサービスのようなものです。我々がやらないといけないのは GitHub にコミットをプッシュしたり PR を作ったりするだけです。あとは Swift Cloud が全部自動でやってくれます。

早速アカウント登録して触ってみる

Swift Cloud の公式 HP にアクセスすると、右上に「Get Early Access」のボタンがあります1。まずこれをクリックしましょう。

Swift Cloud の公式 HP

そしたらユーザ登録画面に入るので、ここで自分の GitHub アカウントを連携すれば、今後のデプロイ作業なども非常に楽になるのでオススメします。

Swift Cloud のユーザ登録画面

アカウント連携が成功したら、いよいよ Swift Cloud のコンソール画面になります。筆者はすでに 2 つサービスをデプロイしたためスクリーンショットではそれらのプロジェクトが表示されていますが、新規ユーザならここのリストは何もないはずです。左上の「+」ボタンからプロジェクトを新規作成できますが、現在まだ GitHub 上で Swift Cloud のプロジェクトがないので、とりあえず認識だけしておくことに留めましょう。

Swift Cloud のコンソール画面

ここまでできたらもう Swift Cloud を利用する準備ができました。このコンソール画面では他にカスタムドメインを設定したり共同管理のためのチーム設定したりする「ボタン」はありますが、機能自体がまだできていないので、クリックしても「Coming Soon」の文字が表示されるだけなのでスルーしても大丈夫です。

Swift Cloud のサーバサイドプログラムを作ってみる

とりあえず Hello, World! を作ってみる

さて動かすものがないと意味がないので、とりあえず皆さんの好きな方法で新規リポジトリーを作りましょう。筆者は GitHub のウェブ上でリポジトリーを作れば、最初から .gitignore や LICENSE などの情報が自動生成できてとても便利なので好きです。そして作ったらまた自分の git クライアントで落としてきます。

リポジトリーを作ったら、次にターミナルからリポジトリーに移動して、実行できるタイプの Swift Package を作りましょう。

swift package init --type executable

こうすれば初期状態の実行バイナリの Swift Package プロジェクトが作られます。ちなみにデフォルトのパッケージ名およびターゲット名は今のフォルダーの名前になりますが、必要に応じて後からでも変更できます。

初期パッケージを作ったら、次は必要なランタイムフレームワーク「Compute」を入れます。これは Swift Cloud のサーバが実際に動かすランタイムで、必須です。そしてこのフレームワークを入れることによって、対象プラットフォームを macOS 10.15 以上にする必要も出てきます。他にも必要に応じて様々なライブラリーを入れてもいいですが、本節の目的はあくまで Hello, World! なので、特に追加では入れていません。

Package.swift
let package = Package(
    name: "MySwiftCloudRepository",
+    platforms: [.macOS(.v10_15)],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
+        .package(
+            url: "https://github.com/swift-cloud/Compute",
+            from: "1.0.0"
+        ),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .executableTarget(
            name: "MySwiftCloudRepository",
-            dependencies: []),
+            dependencies: [
+                "Compute",
+            ]),

ここまでできたらもう必要な準備はすべて整いました。あとは Hello, World! の実装だけです。ソースフォルダーの下のターゲットフォルダーの下に、唯一の Swift ファイルがあり、中に @main アトリビュートがついてる型があるはずです2。この型にある public static func main() メソッドが実際のエントリーポイント、すなわちこのバイナリを実行するときに最初に来る場所です。初期状態ではこの最低限の宣言しかありませんが、実はこれは asyncthrows もつけられます。実際今回サーバの特性上 async throws をつけてあげる必要があります。

ではどう作ってあげればいいでしょう。百聞は一見に如かずなので、早速実装を見てみましょう。

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
+import Compute
+
@main
public struct MySwiftCloudRepository {
-    public private(set) var text = "Hello, World!"
+    public private(set) static var text = "Hello, World!"

-    public static func main() {
-        print(MySwiftCloudRepository().text)
+    public static func main() async throws {
+        try await onIncomingRequest(handler)
    }

+    static func handler(
+        req: IncomingRequest,
+        res: OutgoingResponse
+    ) async throws {
+        try await res
+            .status(.ok)
+            .send(text)
+    }
+
}

では上記の差分を一つずつ見ながら解説していきます。まず先ほどのパッケージの宣言で追加した Compute というフレームワークをインポートします。

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
+import Compute

そして Swift Cloud、厳密には Compute というランタイムではユーザからリクエストを受けた時(ユーザが Swift Cloud の URL にアクセスして API を叩いた時と思えばわかりやすいかもしれません)の処理を、onIncomingRequest というメソッドで設定してあげます。このメソッドにはリクエストデータ IncomingRequest とレスポンスオブジェクト OutgoingResponse がクロージャの引数として渡されてくるので、我々の仕事はこれらを使って実際のレスポンスの返し方を定義します。

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
   public static func main() {
-        print(MySwiftCloudRepository().text)
+        try await onIncomingRequest(handler)
    }
+
+    static func handler(
+        req: IncomingRequest,
+        res: OutgoingResponse
+    ) async throws {
+        // ここで実際のレスポンスの返し方を定義します
+    }

ここで onIncomingRequest の引数クロージャの定義が長くなり見づらくなりがちなので、先に handler というメソッドを作り、onIncomingRequest メソッドで直接この handler メソッドを引数として渡してあげれば、ソースコードがだいぶ読みやすくなります。ところで気づいた人もいると思いますが、onIncomingRequesttry await で呼び出されています。つまり呼び出し元の main メソッドも async throws で宣言しなくてはならないですね。

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
-   public static func main() {
+   public static func main() async throws {

さて最後、"Hello, World!" の文字列はどう返せばいいでしょうか。初期プロジェクトではすでにこの文字列があるので、せっかくだからこれをそのまま流用したいですね。また、レスポンスを返すのに、先ほど紹介したこの OutgoingResponse オブジェクトを使います。レスポンスには最低限 HTTP Status Code と内容が含まれていれば OK です。というわけでこれらを実装していきます:

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
@main
public struct MySwiftCloudRepository {
-    public private(set) var text = "Hello, World!"
+    public private(set) static var text = "Hello, World!"
// ...
    static func handler(
        req: IncomingRequest,
        res: OutgoingResponse
    ) async throws {
-        // ここで実際のレスポンスの返し方を定義します
+        try await res
+            .status(.ok)
+            .send(text)
    }

我々の関心としてどんなリクエストが来たかには興味がないので、req を完全に無視して、res というレスポンスオブジェクトを操作だけします。まずは .status(.ok) で正常の HTTP Status Code を返します。次に .send を呼び出して、その引数の内容を渡します。引数の内容はすでに初期で定義された text の文字列です。ただしこの text は元々はインスタンスプロパティーなので、タイププロパティーにするために static もつけてあげる必要があります。気をつけないといけないのは、send は必ず最後にやる必要があります。この send メソッドによってメソッドチェーンが終了します3

ここまで作ったら GitHub の main ブランチにプッシュしましょう。そして Swift Cloud のコンソール画面から、前節で軽く紹介したようにプロジェクトを新規作成して、このリポジトリー選択画面からいま作ったリポジトリーを選びましょう。

Swift Cloud のリポジトリー選択画面

次にプロジェクトの情報を入れてあげます。プロジェクト名は自動で入れられているので、Executable Target Name に実行ターゲット名を入れて、Swift Version の Swift のバージョンが正しいかどうか確認して、間違っていれば変更して、Create Project をクリックすればプロジェクトが生成されます。

Swift Cloud のプロジェクト生成画面

最後にプロジェクトの設定を入れる画面がありますが、これはすべて空欄のままで問題ありませんので、そのまま下の Deploy Project をクリックしましょう。

Swift Cloud のプロジェクト設定画面

これで自動で初回デプロイされますので、成功したらブラウザーのデフォルトのドメインでアクセスすれば Hello, World! の文字が表示されるでしょう。

筆者のデプロイ済みの Swift Cloud のプロジェクト

ここまでできたら、もう成功です。

リクエストの内容を処理してみる

バックエンドが Hello, World! だけ表示しても何の意味がないですよね。大抵の場合、我々はサーバで何かしら処理をしてもらいたいわけです。そしてそのために我々もサーバに何かしらの情報を送ります。例えば World ではなく、実際の誰かに挨拶したい時に、その人の名前を送ったら挨拶してくれる処理を考えてみましょう。その場合入力のデータはこうなるでしょう:

{
    "name": "星野恵瑠"
}

この場合、iOS 側から API を呼び出すときに、このようにリクエストを投げるじゃないでしょうか。

let jsonContent = [
    "name": "星野恵瑠",
]
var request = // リクエストを生成
request.httpBody = try JSONEncoder().encode(jsonContent)
let (data, response) = try await URLSession.shared.data(for: request)
// レスポンスに対する処理

もちろん jsonContent を独自の型で定義する人もいるかと思いますが、ここで重要なのはサーバに渡したいデータを httpBody にセットする行為4です。さて、サーバサイドの API を実装している我々は、どうやってこの渡された httpBody を取得すればいいかというと、実は Compute においてこれは非常に簡単に取得できます。前節の Hello, World! の出力で我々は IncomingRequest を完全に無視しましたが、実はこの IncomingRequestbody プロパティーを経由すればすぐにでも取得できます。辞書型のままで取得しても問題なければこのように書けばいいです:

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
    static func handler(
        req: IncomingRequest,
        res: OutgoingResponse
    ) async throws {
+        let jsonBody = try await request.body.jsonObject()
        try await res
            .status(.ok)
            .send(text)
    }

これで我々は [String: Sendable] 辞書型の jsonBody を取得できました。もちろん辞書型でなく、きちんとした定義された型で取得したい場合もあるでしょう、その場合は request.body.decode(MyType.self) を使えばいいです。また今回の場合は必要ないですが、例えば日付などのデコーディングで独自のデコーダーを使いたい場面もあるでしょう、その場合は更に decode(MyType.self, decoder: myJSONDecoder) のように、後ろにデコーダーのインスタンスを渡せばいいです。

さてせっかくこれで取得できたので、あとはこれを処理して、ちゃんとした Markdown 文字列を返せばいいですね。

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
    static func handler(
        req: IncomingRequest,
        res: OutgoingResponse
    ) async throws {
        let jsonBody = try await request.body.jsonObject()
+        let name = jsonBody["name"] as? String ?? ""
+        let result = "Hello, \(name)!"
        try await res
            .status(.ok)
-            .send(text)
+            .send(result)
    }

もちろんこの場合、我々は httpBody で入力データ渡している以上、すでにブラウザで出力結果を見たいのではなく、iOS 端末で出力結果をそのまま取得したいわけなので、文字列ではなく JSON オブジェクトで返したいことも多いでしょう。この場合ももちろん、JSON データに変換して send するのも可能です:

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
    static func handler(
        req: IncomingRequest,
        res: OutgoingResponse
    ) async throws {
        let jsonBody = try await request.body.jsonObject()
        let name = jsonBody["name"] as? String ?? ""
        let result = "Hello, \(name)!"
+        let resultJSON = [
+            "greeting": result,
+        ]
+        let resultData = try JSONEncoder().encode(resultJSON)
        try await res
            .status(.ok)
-            .send(result)
+            .send(resultData)
    }

そして実際に API 動作確認ソフトでリクエスト投げてみて、そのレスポンスを確認してみましょう。例えば Insomnia でしたらこのように表示されるでしょう。

Insomnia で動作確認してみる

ルーティング処理をしてみる

実際の API は大抵エンドポイントが複数あります。それらのエンドポイントの切り分け処理がルーティング処理です。

Swift Cloud は Vapor と同じようなルーティングスタイルを使うので、すでに Vapor の経験がある方ならすぐに馴染めるではないかと思います。ルーティング処理をするには、まず Router を作る必要があります。

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
   public static func main() {
-        try await onIncomingRequest(handler)
+        try await onIncomingRequest(router.run)
    }
    
+    static let router = Router()

ルーターを作ったら、次に実際にどのようにルーティング処理をしてほしいか実装していきます。ここでひとまず前節で作った挨拶の処理を、/greeting にアクセスしたら返すようにしてみましょう。データを返すだけなので、HTTP リクエストメソッドは GET でいいでしょう5。この場合、ルーターの設定はこのように書けばいいです:

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
    static let router = Router()
+        .get("/greeting") { req, res in
+            // 挨拶の処理
+        }

GET リクエストメソッドを使うので、メソッドチェーンで Router() の後ろに .get を使えば一つ定義できます。引数として我々が希望するエンドポイント、すなわち今回の場合 "/greeting" を入れてあげれば、https://ドメイン/greetingGET でアクセスするときにこれから書く処理が実行されます。そして肝心の処理の実装について、すでに気づいた方もいるかもしれませんが、get メソッドの後ろにクロージャーがあり、更に req 及び res がクロージャーの引数として存在しています。この処理がまさに前節までの handler の実装と同じフォーマットですね。なので次は前節に書いた挨拶の処理をそのまま持って来れば OK です:

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
    static let router = Router()
        .get("/greeting") { req, res in
-            // 挨拶の処理
+            let jsonBody = try await request.body.jsonObject()
+            let name = jsonBody["name"] as? String ?? ""
+            let result = "Hello, \(name)!"
+            let resultJSON = [
+                "greeting": result,
+            ]
+            let resultData = try JSONEncoder().encode(resultJSON)
+            try await res
+                .status(.ok)
+                .send(resultData)
        }

さて、せっかくルーティング処理を入れてみたので、greeting ではなく、普通にトップページ、つまり https://ドメイン にアクセスした時は、我々が最初に実装していた Hello, World! を画面に表示するようにしてみましょう。すでにもっと難しい名前付き挨拶の処理も実装できたから、この処理はもう楽勝ですね:

Sources/MySwiftCloudRepository/MySwiftCloudRepository.swift
    static let router = Router()
+        .get("") { req, res in
+            try await res
+                .status(.ok)
+                .send("Hello, World!")
+        }
        .get("/greeting") { req, res in
// ...

これでトップページにアクセスすれば Hello, World! のテキストが表示され、さらに /greeting にアクセスして name が含まれる JSON オブジェクトを Body として渡せば、{ "greeting": "Hello, \(name)!" } の JSON オブジェクトが返されます。

Insomnia でトップページにアクセスしてみる

Insomnia で greeting エンドポイントにアクセスしてみる

後書き

Swift Cloud を利用してできる最低限のことについて書いてみましたが、いかがでしたでしょうか。もちろん Swift Cloud でできることはこれだけではありません。ルーティングでクエリやパスコンポーネントを使った処理から、ヘッダーや様々なリクエストメソッドについての機能など、実に幅広いことが可能です。そしてなんといってもこれらは全て手軽に試してみれるのです。

ただし残念ながら、Swift Cloud は現状まだ正式リリースしておらざ、公式ドキュメントすらまともに整備されていない状況です。さらに致命的な問題として、現在ローカルでの動作確認の方法もまだありませんので、いちいち GitHub に上げて PR 作って Swift Cloud のテスト環境でしか動作確認できないです。

もしとりあえずサーバサイド Swift を試してみたいが、バックエンドについての知識がほとんどなく、そしてちゃんとしたプロダクトではなく、とりあえず色々試してみたいだけでしたら、Swift Cloud は非常にいい選択肢になると思いますので、興味ある方はぜひ試してみてください。

  1. 執筆した 2022 年 8 月 20 日現在の情報です。また本記事内の他の当該サービスの情報やスクリーンショットも、断りなければこの段階のものです。

  2. 執筆時の最新の安定版 5.6.1 での環境です。Swift のバージョンによって初期ファイルの構成が違う可能性があります。

  3. OutgoingResponse のあり方としては、HTTP のリクエストの処理がすべて終わったら、接続の終了処理を呼び出す必要があります。そのため、大体の場合は必ず返さないといけないはずの内容を送信したら自動的に接続の終了を行う実装が send メソッド内にまとめられています。接続が終了したらそれ以上やることがないので、メソッドチェーンを終わらせるために send メソッドには戻り値があえてつけられていないと考えられます。他に、何の内容も送らずにいきなり終了させる close メソッドもあり、同じく戻り値がないためメソッドチェーンの終了として使えます。逆に、HTTP Status Code を設定する status メソッドや、ヘッダーを設定する header メソッドもありますが、これらはすべて Self を返すので、メソッドチェーンの途中でも活用できます。

  4. もちろんこれ以外に直接 URL にクエリを埋め込むなど、別の方法もあります。

  5. HTTP リクエストメソッドは GET 以外にも、POSTPUT などのメソッドが規格として定義されています。これらがどう言う時に使うべきかは本記事の解説範囲ではないので、気になる方はぜひ HTTP リクエストメソッドについて検索してみましょう。

53
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
28