-
サーバエコシステムでのSwiftの詳細
Swiftはサーバアプリの記述に適している言語であり、Appleのさまざまなクラウド製品の重要なサービスをサポートしています。このセッションでは、ツールを紹介し、Swiftサーバパッケージエコシステムについて詳しく説明するとともに、データベースとのインタラクションを行う方法とアプリにオブザーバビリティを追加する方法をデモを交えて解説します。
関連する章
- 0:00 - Introduction
- 0:13 - Agenda
- 0:27 - Meet Swift on Server
- 2:30 - Build a service
- 3:46 - Swift OpenAPI generator
- 5:42 - Database drivers
- 10:53 - Observability
- 15:19 - Explore the ecosystem
- 16:12 - Wrap up
リソース
関連ビデオ
WWDC23
-
ダウンロード
こんにちは Appleの Swift on ServerチームのFranzです 本セッションではサーバエコシステムでの Swiftの詳細について見ていきます まず Swiftがサーバアプリの開発に 非常に適した言語である理由を説明します 次に 人気のあるエコシステムの パッケージを使ってサービスを構築します
最後に エコシステムの仕組みと どこで学ぶことができるかを説明します
ではまず Swiftがサーバアプリに適している理由から 始めましょう
Swiftは ガベージコレクションの代わりに 自動参照カウントを行うため C言語と同等のパフォーマンスが得られ メモリ消費も低減されます そのため 予測可能なリソース消費量と 高速な起動時間が求められる 最新のクラウドサービスに 最適です
Swiftは表現力と安全性に優れた言語であり コンパイル時に様々なバグを排除するため デベロッパは 堅牢で信頼性の高い 分散システムを記述できます 強力な型付けやオプショナル メモリの安全性といった機能により Swiftサービスでは クラッシュやセキュリティの脆弱性が 生じにくくなっています
クラウドサービスでは 高度な並列の ワークロード処理が多く生じます デベロッパは Swiftの 優れた並列処理機能により 拡張性と応答性に優れた サーバアプリを記述しつつ データ競合が原因の 一般的なバグの発生源を排除できます
このような特性を備えたSwiftは サーバアプリ記述の優れた選択肢です Swiftは実際 Appleのクラウドサービスの 重要機能の多くに使われています iCloudキーチェーン 写真 メモなどです ほかの使用例としては App Storeの処理パイプライン SharePlayでのファイル共有があります 最新のプライベートクラウド コンピューティングサービスも Swift on Serverで構築されています
Appleのサービス全体では Swift on Serverを 使用するアプリケーションが 毎秒数百万のリクエストを 処理しています
Appleのプラットフォーム以外では Swiftを最も早期から使っていたのは サーバエコシステムです 実際 Swift Serverワークグループは Swiftのオープンソース化から わずか1年後の2016年に設立されました これは最も古いワークグループです
ワークグループは Swift on Serverを 使用している企業を代表するメンバーと エコシステムの個人の貢献者で 構成されています サーバアプリの開発とデプロイにおける Swift利用の促進が主な活動目的です ワークグループの責務は サーバコミュニティの ニーズに対応するための 取り組みの定義と優先順位づけや 作業の重複の削減 互換性の向上 ベストプラクティスの促進のための パッケージのインキュベーションプロセスの 推進などです
同グループはまた サーバエコシステムからのフィードバックを Swiftプロジェクトの 各グループに伝達します 次に サーバエコシステムの人気の パッケージを使って サービスを構築します 同僚と私は今年 多数のイベントに参加する予定です 計画を整理するために 誰がどのイベントに参加しているかを 追跡するイベントサービスを実装します サービスでは2つの操作をサポートします 1つは全イベントをリストし 誰がどのイベントに参加するか表示します オクトーバーフェストにも必ず行きたいので 新しいイベントを作成する もう1つの操作も必要です
Swiftパッケージの作業では 各種エディタを使用できます XcodeやVS Code Neovimのほか 言語サーバプロトコルを サポートするその他のエディタも使えます このデモでは パッケージでの作業にVS Codeを使います デモでは 画面下の VS Codeの組み込みターミナルを使って サービスからの出力を確認し サービスにリクエストを送信します イベントサービスを開始するための パッケージは作成済みです 確認してみましょう
パッケージはOpenAPI Generatorに依存し OpenAPIのサーバトランスポートとして Vaporを使います
1つはEventAPIターゲットで OpenAPIGeneratorプラグインが 構成されています
もう1つは EventServiceの executableTargetで サービスの実装を含みます
Swift OpenAPI Generatorにより サービスをYAMLで文書化し サーバ用と クライアント用のコードを生成できます OpenAPIを初めて使う方や 復習したい方は 昨年の 「Meet Swift OpenAPI Generator」を ぜひご覧ください OpenAPIのドキュメントを見てみましょう
2つ目の操作は createEventというpostメソッドです
作成が成功したかどうかに応じて 201または400のステータスコードを返します
次に アプリケーションの OpenAPI VaporTransportを作成します
次に サービスのインスタンスを作成し transportに登録します
最後に Vaporアプリを実行します このアプリは HTTPサーバを起動し 外部からの接続をリッスンします
listEventsメソッドは ハードコードされたイベントの配列を返します
createEventメソッドは 現在 未実装のステータスコードを 返しています
これにより サービスがビルドされ デバッガがアタッチされます ターミナルの下部に サーバが起動したことが表示されます
別のターミナルでcurlを使って サービスに照会し 全イベントを表示できるようになりました
ハードコーディングされたイベントの リストを含む JSON配列が返されました
しかし 新しいイベントを動的に追加し データベースで永続化させたいので データベースドライバを見てみます オープンソースのエコシステムには PostgreSQL MySQL Cassandra MongoDBなど 様々なデータベースドライバがあります
今回は永続化のために Postgresデータベースを使用します PostgresNIOは VaporとAppleが保守する Postgres用の オープンソースデータベースドライバです PostgresClientは PostgresNIO 1.21の新機能です PostgresClientはまったく新しい 非同期のインターフェイスを提供し 構造化並列処理を活用する 接続プールが組み込まれているため データベースにおける 断続的なネットワーク障害に対する 耐性があります さらに 接続プールは スループットを向上させるために クエリを複数の接続に分散するとともに クエリ高速化のために 接続をウォームアップさせます
PostgresNIOを使って データベースに EventServiceを接続しましょう
最初に PostgresNIOへの依存関係を パッケージに追加し サービスにインポートします 次に PostgresClientを使用して listEventsメソッドで データベースへのクエリを行います 最後に createEventメソッドを実装して 新しいイベントをデータベースに挿入します まず パッケージマニフェストに PostgresNIOへの 依存関係を追加します
次に EventServiceターゲットに 依存関係を追加します
これで ServiceにPostgresNIOを サービスにインポートできます
次に サービスに PostgresClientプロパティを追加します
クライアントにより listEventsメソッドで データベースへのクエリを行います
このクエリメソッドは 行のAsyncSequenceを返します ハードコードされたイベントのリストを 置換するために 行を反復処理し フィールドをデコードして 各行のイベントを作成します
クエリメソッドから返された AsyncSequenceは データベースから自動的に 行を事前に取得して パフォーマンスを高速化します
サービスを再実行する前に PostgresClientサービスを作成して サービスに渡す必要があります
まず PostgresClientを作成し すでにローカルで起動している データベースに接続します
クライアントを起動するために runメソッドを呼び出し 現在のタスクを 終了時点まで 引き継がせます VaporアプリとPostgresClientの両方を 同時に実行するために タスクグループを使います 破棄するタスクグループを作成し PostgresClientを実行する 子タスクを追加します
再実行ボタンを押すと 現行のプロセスが停止し サービスが再ビルドされ 再実行されます
ターミナル下部に実行中の表示が出ます 再度全イベントを表示しましょう
データベースが空のようです 新しいイベントをデータベースに 追加するために createEventメソッドを実装します
まず 入力を切り替えて JSONイベントを抽出する必要があります
次に データベースへのクエリを実行し 新しいイベントを挿入します
このコードを見て一部の人には警告がなるかもしれません ほかの言語では SQLインジェクション脆弱性の 一般的な攻撃経路だからです 文字列のように見えますが これは文字列ではなく Swiftの文字列補間機能を使用して 文字列クエリを 値がバインドされた パラメータ化クエリに変換しているので SQLインジェクション攻撃から 完全に保護されます コードの安全性を確保しつつ 使いやすさを追求する Swiftの取り組みの好例と言えます
サービスが再開したら curlを使って2つのイベントを作成します
イベント作成は成功したようです 全イベントを再び表示し データベースに保存されているか 確認してみましょう
すべてのイベントが データベースに保存されています Gusがメッセージで 友達を連れてきたいと言い 彼の名前で別のイベントエントリを 追加するよう頼んでいます Gusのために イベントをもう1つ作成しましょう
Gusの名前で 別のイベントエントリを追加する際 問題が生じたようです ターミナルの下部に 長いエラーメッセージがありますが このエラーからは 問題を正確に把握できません 既知の情報は 処理が未完了であることと スローされたエラーの型が PSQLErrorであることだけです PSQLErrorの説明では 意図的に詳細情報を省略します テーブルのスキーマなどの データベース情報の漏洩を 防ぐためです このような場合に トラブルシューティングに役立つのが サービスへの オブザーバビリティの追加です オブザーバビリティの3つの主な要素は ログ メトリックス トレースです ログにより サービスの実行内容を正確に把握して 問題のトラブルシューティングを行いつつ 詳細を調査できます メトリックスを利用すれば 迅速に サービスの健全性の概要を把握できます ログとメトリックスは 単一のサービスの状況の把握に有効ですが 現代のクラウドシステムは多くの場合 分散システムの集合体です トレースはこうしたシステムで役立ちます リクエストが辿った システム内の経路がわかるためです
以上3つの要素をすべて網羅した SwiftエコシステムのAPIパッケージでは オブザーバビリティイベントを コードで出力できます
listEventsメソッドを インスツルメントする方法を確認しましょう
まず swift-logを使い 新しいlistEventsリクエストの処理の 開始時のログを出力します swift-logは ログメッセージに メタデータを追加して構造化ログを提供し トラブルシューティングのための コンテキストを追加します
次に swift-metricsの カウンターを追加します これをリクエストごとに増加させ サービスが処理したリクエスト数を追跡します
最後に swift-distributed-tracingを追加し データベースクエリの前後に スパンを作成します これは システム全体でのリクエストの トラブルシューティング用です Swiftの分散トレースの仕組みの 詳細については 昨年のセッションをご覧ください 「Beyond the basics of structured concurrency」です listEventsメソッドを ログ メトリックス トレースで インスツルメントしました 使用したAPIは オブザーバビリティの バックエンドに非依存で データの送信先は サービスの作成者が選択します ログ メトリックス 分散トレースのための 多数のバックエンドを Swift on Serverエコシステムは 提供しています バックエンドの選択は 3つのライブラリの ブートストラップメソッドを 呼び出すことで実行します ブートストラップは 実行可能ファイル内でのみ実行され オブザーバビリティイベントの保持のため 可能な限り早期に実行する必要があります またブートストラップの実行順は 最初にLoggingSystem 次にMetricsSystem 最後にInstrumentationSystemが 推奨されます メトリックスと インスツルメンテーションのシステムが ステータスログを 出力することがあるためです
わずか2行のコードで ログをターミナルに メトリックスをPrometheusに トレースをOpen Telemetryに出力できました createEventメソッドにログ作成を追加して Gusの名前で別のイベントを 追加しようとした時 何が問題だったのか確認しましょう
まずswift-logを パッケージと EventServiceターゲットの依存関係として 追加します
次に サービスに Loggingモジュールをインポートします
クエリメソッドはクエリ実行時に 何らかの問題が発生した場合 PSQLErrorをスローします
Postgresサーバが送信した エラーメッセージを含む ログイベントを出力できるよう ロガーを作成しましょう
次に エラーメッセージを抽出し ログを出力します PSQLErrorでは 問題の原因の詳細情報が serverInfoプロパティに 含まれています
最後に データベースへのイベント追加時に 問題が発生したことを示すために badRequestレスポンスを返します
サービスを再実行して エラーの詳細を取得できるか 確認しましょう
デフォルトでは swift-logはターミナルにログを 出力しますが これはアプリのデバッグに最適です 同じcurlコマンドを実行し イベントを再作成します
今回は同じエラーが発生しませんでした badRequestステータスコードを 返したためです
ターミナルの下部に ログメッセージが表示されています エラーメッセージの メタデータフィールドによると 原因は重複キー違反です このデータベースのテーブルで 許可されるエントリは 名前 日付 出席者の組み合わせに対し 1つのみです
ログの追加が 具体的な問題の解決に役立ちました あとで同僚に修正してもらいます
サービスの構築に使用できる Swift on Serverエコシステムのライブラリを 手短にご紹介しました 様々な用途向けのライブラリが ほかにも多数あります 例えば ネットワーキング データベースドライバ オブザーバビリティ メッセージストリーミングなど これらはほんの一部です ほかのライブラリを探したい場合 swift.orgのPackagesセクションの Serverのカテゴリをご確認ください swiftのPackagesのインデックスから ほかのサーバライブラリも探せます
パッケージ検索のもう1つのリソースは Swift Serverワークグループの インキュベーションリストです ワークグループは パッケージの育成プロセスを推進し 安定した強固なエコシステムを築いています
育成プロセスにあるパッケージは SandboxからIncubatingへ そしてGraduatedへと 成熟度に応じて移行します
各レベルの要件は パッケージの 本番リリースに向けた準備状況と 使用状況に基づきます 育成されたパッケージは swift.orgで確認できます
Swift on Serverのエコシステムの セッションはいかがでしたか Swiftがサーバアプリに 適した言語である理由と Appleのクラウドサービスの重要機能の 多くでの利用状況を説明しました パッケージの概要と Swift Serverワークグループの 健全なエコシステムの構築への貢献を ご紹介しました ご視聴ありがとうございました オクトーバーフェストで会いましょう
-
-
3:23 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), ] ), ] )
-
4:05 - EventService openapi.yaml
openapi: "3.1.0" info: title: "EventService" version: "1.0.0" servers: - url: "https://localhost:8080/api" description: "Example service deployment." paths: /events: get: operationId: "listEvents" responses: "200": description: "A success response with all events." content: application/json: schema: type: "array" items: $ref: "#/components/schemas/Event" post: operationId: "createEvent" requestBody: description: "The event to create." required: true content: application/json: schema: $ref: '#/components/schemas/Event' responses: '201': description: "A success indicating the event was created." '400': description: "A failure indicating the event wasn't created." components: schemas: Event: type: "object" description: "An event." properties: name: type: "string" description: "The event's name." date: type: "string" format: "date" description: "The day of the event." attendee: type: "string" description: "The name of the person attending the event." required: - "name" - "date" - "attendee"
-
4:35 - EventService initial implementation
import OpenAPIRuntime import OpenAPIVapor import Vapor import EventAPI @main struct Service { static func main() async throws { let application = try await Vapor.Application.make() let transport = VaporTransport(routesBuilder: application) let service = Service() try service.registerHandlers( on: transport, serverURL: URL(string: "/api")! ) try await application.execute() } } extension Service: APIProtocol { func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let events: [Components.Schemas.Event] = [ .init(name: "Server-Side Swift Conference", date: "26.09.2024", attendee: "Gus"), .init(name: "Oktoberfest", date: "21.09.2024", attendee: "Werner"), ] return .ok(.init(body: .json(events))) } func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { return .undocumented(statusCode: 501, .init()) } }
-
6:56 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), .package( url: "https://github.com/vapor/postgres-nio", from: "1.19.1" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), .product( name: "PostgresNIO", package: "postgres-nio" ), ] ), ] )
-
7:08 - Implementing the listEvents method
import OpenAPIRuntime import OpenAPIVapor import Vapor import EventAPI import PostgresNIO @main struct Service { let postgresClient: PostgresClient static func main() async throws { let application = try await Vapor.Application.make() let transport = VaporTransport(routesBuilder: application) let postgresClient = PostgresClient( configuration: .init( host: "localhost", username: "postgres", password: nil, database: nil, tls: .disable ) ) let service = Service(postgresClient: postgresClient) try service.registerHandlers( on: transport, serverURL: URL(string: "/api")! ) try await withThrowingDiscardingTaskGroup { group in group.addTask { await postgresClient.run() } group.addTask { try await application.execute() } } } } extension Service: APIProtocol { func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let rows = try await self.postgresClient.query("SELECT name, date, attendee FROM events") var events = [Components.Schemas.Event]() for try await (name, date, attendee) in rows.decode((String, String, String).self) { events.append(.init(name: name, date: date, attendee: attendee)) } return .ok(.init(body: .json(events))) } func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { return .undocumented(statusCode: 501, .init()) } }
-
9:02 - Implementing the createEvent method
func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { switch input.body { case .json(let event): try await self.postgresClient.query( """ INSERT INTO events (name, date, attendee) VALUES (\(event.name), \(event.date), \(event.attendee)) """ ) return .created(.init()) } }
-
11:34 - Instrumenting the listEvents method
func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let logger = Logger(label: "ListEvents") logger.info("Handling request", metadata: ["operation": "\(Operations.listEvents.id)"]) Counter(label: "list.events.counter").increment() return try await withSpan("database query") { span in let rows = try await postgresClient.query("SELECT name, date, attendee FROM events") return try await .ok(.init(body: .json(decodeEvents(rows)))) } }
-
13:14 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), .package( url: "https://github.com/vapor/postgres-nio", from: "1.19.1" ), .package( url: "https://github.com/apple/swift-log", from: "1.5.4" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), .product( name: "PostgresNIO", package: "postgres-nio" ), .product( name: "Logging", package: "swift-log" ), ] ), ] )
-
13:38 - Adding logging to the createEvent method
func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { switch input.body { case .json(let event): do { try await self.postgresClient.query( """ INSERT INTO events (name, date, attendee) VALUES (\(event.name), \(event.date), \(event.attendee)) """ ) return .created(.init()) } catch let error as PSQLError { let logger = Logger(label: "CreateEvent") if let message = error.serverInfo?[.message] { logger.info( "Failed to create event", metadata: ["error.message": "\(message)"] ) } return .badRequest(.init()) } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。