Project Springfield: クラウド サービスのすべてを F# で構築
本記事は、マイクロソフト本社の .NET Blog の記事を抄訳したものです。【元記事】 Project Springfield: a Cloud Service Built Entirely in F# 2016/12/13 |
今回の記事の執筆者は William Blum (Microsoft Research の Springfield チーム、プリンシパル ソフトウェア エンジニアリング マネージャー) です。
マイクロソフトは今年の始めに、ソフトウェアの潜在的なセキュリティ脆弱性を根絶する最も洗練されたツール Project Springfield のプレビューを発表しました。Project Springfield (英語) はコードのセキュリティ クリティカルなバグを発見するファズ テスト サービスです。
Springfield の素晴らしい点の 1 つは、マイクロソフトの開発プラットフォームである F#、.NET、Azure を基盤としている点です。今回の記事では、Project Springfield、F#、.NET、Azure に関する疑問点や使用方法について取り上げます。
Project Springfield とは
Project Springfield はソフトウェアのセキュリティ クリティカルなバグを発見するために開発したマイクロソフトならではのファズ テスト サービスであり、マイクロソフトのプラクティスやテクノロジをすばやく導入するのに役立ちます。Azure クラウドの能力を活かしながら、マイクロソフトのセキュリティ テスト ツール スイートを通じてセキュリティ テストをスケーリングします。このツールは現在プレビュー段階で提供されています。お試しになる場合は Project Springfield (英語) からぜひサインアップしてください。
Springfield のアーキテクチャについて議論する William Blum ( 右 ) と Cheick Omar Keita ( 左 )
なぜ F# を採用したのか
私たちはこのプロジェクトでなぜ F# を選択したのか。一言で言うと、リリースまでのスピードの速さです。
2015 年、Project Springfield は Microsoft Research NExT (英語) によってスタートされました。当時のエンジニアリング チームはわずか 3 名でしたが、3 か月以内にゼロからサービスを構築し、外部のお客様に提供する、という大きな使命が与えられていました。
開発サイクルがスピードアップし、リリースまでの時間が短縮されたのは、F# の簡潔性、正確性、.NET エコシステム全体との相互運用性のおかげだったと考えています。私たちが感じた F# の大きなメリットは、スクリプト機能と動作するコードのプロトタイプをすばやく作成できるインタラクティブな REPL、代数的データ型、既定で変更不可、パターン マッチ、高階関数、強力な非同期プログラミング モデル、型プロバイダーなどです。
どのように構築したか
F# スクリプトを使用したことで、外部 .NET API をすばやく発見して操作できました。たとえば次のように、F# Interactive を使用して Azure SDK の使い方を学習しました。
上記の F# Interactive のスクリプトでは、指定の Azure サブスクリプションの Azure Virtual Machines がすべて列挙されます。
後の開発プロセスで、修正なしにコンパイルされた最終的なコードに、まったく同じスクリプト コードを簡単に組み込むことができます。
関数型プログラミング
F# は関数型プログラミング言語であるため、コードを関数のスタイルで記述しました。こうすることで多くのテンプレート コードを省くことができました。
たとえば、リストや配列などのコレクションを使用する場合は、Seq モジュールの F# シーケンス (英語) を使用してデータを処理します。F# は関数の引数の部分適用 (英語) に対応しており、関数を第一級引数 (英語) として使用できるため、シンプルで信頼が高くコンポーザブルなコードでデータのシーケンスを処理できます。
C のようなループを使用する場合など、イテレーションや何らかの状態を一時的な変数に保存する必要がないのでシンプルになります。イテレーションが不要なため、配列の範囲外のインデックスにアクセスした場合の例外などイテレーションでよくある問題を回避でき、信頼性が向上します。また、F# のパイプライン演算子 (|>) によって処理を簡潔に作成できます。
F# のパイピング演算子と Sequence モジュールを使用したヒストグラムのソート
上記のスニペットでは、ラムダ関数、関数合成、純粋関数など、F# に既定で実装されている多数の関数型プログラミング機能を使用しています。これらの言語構造は強力であるため、C# などの非関数型言語にも一部組み込まれています。
特に純粋関数はコードベースにおいて最も大きなメリットの 1 つとなっています。純粋関数 (英語) では、関数の出力は入力値によって決まるという決定論の 1 つの形態です。つまり、入力に対して純粋関数は常に同じ出力を返します。さらに、純粋関数は環境に影響しません (グローバル変数やデスク上のファイルなど)。
関数の純粋性という特性をコードベースで維持することで、コードが論理的に明確になりテストがしやすくなりました。純粋関数は互いに影響しないため、並列化も簡単にできます。
F# では、let 束縛の値をすべて既定で変更不可にすることで純粋な関数のコードを簡単に書くことができます。別の言い方をすれば、純粋ではない関数を書くには、あえてそう選択しなければなりません。それが本当に必要な場合 (環境を何らかの形で変更しなければならない場合など)、let mutable キーワードで変更可能なことを明示的に指示するだけで簡単にできます。Springfield では、変更可能な変数は F# のコードベース全体で 5 つしかありません。
簡潔性
上の例は、「プログラムが簡潔になる」という関数型言語のもう 1 つの側面を示しています。上記のプログラムの 4 行のコードは、命令型で記述した場合さらに量が増えます。不自然な例に見えるかもしれませんが、Springfield の規模では、維持がしやすいコードベースが生成されます。
実際、一部のコンポーネントを別の言語から F# に移植すると、この現象を定量化できました。たとえば従来の依存関係を解消するために、Perl スクリプトを 37% 小さい F# プログラムに移植しました。またこれとは別に、1,338 行の PowerShell スクリプトを 489 行の F# (2.7 倍縮小) に移植しました。両方ともコードの量が減ったにもかかわらず、完成した F# プログラムでは、ログ、読みやすさ、信頼性が向上しました (静的な型チェックにもよる)。
正確性
F# が迅速なリリースにつながったもう 1 つの理由は、F# が採用する関数パラダイムがコードの正確性向上に貢献したからです。言語構造が正確性を向上させることを裏付ける例の 1 つが、代数的データ型とパターン マッチの使用です。
その本質を示す例は、データが見つからない場合に F# でそのデータをどのように表現し、処理するかです。主流の言語のほとんどでは、一般的にデータが見つからない場合、特別な null
値で表されます。これには大きな欠点があります。null
はほとんどの型で暗黙であり、データを利用するときに null である可能性をチェックするのを忘れやすいからです。このため実行時の NullReferenceException
エラーなど、信頼性に関する問題やバグが出やすくなります。C# など多くの言語は、オブジェクト値が既定で null 許容型なので、コードベース全体で null をチェックする必要があります。
F# では定義したデータ型は既定で非 null 許容型です。データが見つからないことが想定される場合は、既存の型 'T
を代数的データ型 'T option
(または Option
) にラップします。F# Options (英語) は 2 種類の値があります。none 値はデータがないことを示し、Some v
の v
は有効な型 'T
です。
Option 型自体でデータがない場合を把握することにより、コードで Optional を利用する場合は、コンパイラは Some v
と None
の両方のケースに対応できます。一般的にはパターン マッチと match ... with
構文で行います。次に Springfield のコードベースから例をお見せします。
パターン マッチと型システムを併用すれば、None
やSome
などあらゆるケースを処理できます。
この言語機能だけで、問題となる null をコードベースからほぼ完全に解消し、貴重な時間を節約できました。
明示的な型システム
Option 型は代数的データ型 (英語) の力を示す一例にすぎません。もっと一般に使用される場合、代数的データ型ではシステム内のすべてのデータ構造を簡潔に定義し、正確かつ効率的なコードを作成してそれらのデータ構造を操作できます。たとえば次のように、シンプルな判別された共用体を使用して、Azure でのテスト用にプロビジョニングされている仮想マシンのサイズを定義できます。
また、さらに複雑な構造体を使用して、システムのさまざまなコンポーネント間でやり取りするイベントやメッセージをエンコードできます。
Springfield に送信する各テスト ワークロードでは数千件のメッセージが作成され、サービスのさまざまなコンポーネント間でやり取りされます。強力な F# の型システムのおかげで、こうした複雑な情報も F# Records (英語) や 判別された共用体 (英語) で簡単に表すことができます。
受信したメッセージを型システムを通じて表現すると、その受信したメッセージに対してパターン マッチ (英語) ディスパッチを使用できます。
上記のよいところは、コンパイラがすべてのケースに対応するよう強制する点です。F# の判別された共用体 EventType
に逆シリアル化されたキュー メッセージは、ディスパッチ関数によって処理されることが保証されます。正確性が保証されているので、デバッグに時間をかけずに済みます。F# 型のような機能とパターン マッチの併用は、動作するコードを短時間で完成させるのに大いに役立ちました。
別の例として、信頼性のために、サービス リクエストは有限状態マシンを使用してバックエンドに実装されました。マシンの状態は Azure Queue に保存されるので、障害が発生した場合はそこから再開できます。繰り返しになりますが、F# は次のように状態マシンをきわめて簡潔に定義できます。
Springfield バックエンドで使用している有限状態マシン
JSON によるシリアル化とオープン ソース提供
Springfield では、Json.NET (英語) を利用して JSON メッセージのシリアル化と逆シリアル化を行いました。ところが、F# のデータ型をシリアル化する際の既定の出力は詳細すぎて、私たちのニーズを満たさないことがわかりました。F# データ型をシリアル化する際に Json.NET をラップして拡張する小さなライブラリの FSharpLu.Json (英語) を構築して、Options、代数的データ型、判別された共用体などの F# データ型をさらに簡潔にシリアル化できるようにしました。
たとえば、単純な値の Some [ None; Some 2; Some 3; None; Some 5 ]
は FSharpLu.Json (英語) により [null, 2, 3, null, 5]
にシリアル化されます。FSharpLu.Json なしの場合は、次のようにシリアル化されます。
複雑なデータ型については、先に紹介した Event
型のように、違いはもっと明確になります。たとえば次のイベントを見てみましょう。
これは FSharpLu.Json で次のようにシリアル化されます。
こちらの方が F# の構文を反映しており、既定の Json.NET の書式よりも 47% コンパクトです。
私たちはこのような JSON ユーティリティは F# コミュニティにとって有益だと考え、FSharpLu.Json を GitHub (英語) でオープンソース提供し、NuGet (英語) でリリースしました。
F# の型プロバイダーと Azure
Springfield は完全に Azure を基盤としています。テスト ワークロードを実行するために使用するコンピューティングとネットワークのリソースはすべて、Azure Resource Manager (ARM) を通じて動的にプロビジョニングされます。ARM でリソースを作成するには、2 つの JSON ファイルを作成する必要があります。1 つは作成するすべてのリソース (仮想マシンなど) を定義したテンプレート JSON ファイル、もう 1 つはデプロイメントのカスタマイズ (マシン名など) に使用する値が格納されたパラメーター JSON ファイルです。
Springfield はコンピューティング リソースを動的に配分するため、実行時に JSON パラメーター ファイルを生成する必要があります。これはエラーが頻出するタスクです。F# の型プロバイダー (英語) を使用すれば、生成されたテンプレート パラメーターが有効であることをコンパイル時に静的に検証できます。ARM テンプレートは常に進化しているため、開発やデバッグが大幅にスピードアップします。
FSharp.Data (英語) の JSON 型プロバイダー (英語) を使用すれば、3 行の F# コードで、デプロイメントを Azure に送信するために必要なすべての型をテンプレート パラメーター ファイル (下のスクリーンショットを参照) から自動的に推測できます。
F# Intellisense がテンプレート パラメーター ( 左 ) と対応する ARM テンプレート ( 右 ) から存在しないフィールドをキャッチ
厳密に型指定されたログ、非同期プログラミング、アクティブ パターン
Springfield の構築に F# が便利なもう 1 つの例を示すために、コードベースの別のスニペットを見ていきましょう。次は Azure のリソース グループを削除するために使用する関数です。
厳密に型指定されたログ
上記のコード スニペットを見ると、C/C++ プログラマーは、ログ関数 Trace.info
や Trace.error
の呼び出しが %s
を使用した printf
のような書式であることに気付くはずです。ゲーム プログラマーの John Carmack (英語) は、「Printf の書式文字列エラーは、(ビデオ ゲーム Rage の C/C++) コードベースにおいて null の安全問題に次ぐ大きな問題でした」と言っています。このようなエラーは、printf
関数に渡すパラメーターの数が間違っていたり、入力パラメーターの型が %d
や %s
などの書式指定子とマッチしなかったりすると発生します。
Springfield ではトレース ログを頼りにバグや問題の診断を行うため、ログ関数自体に信頼性の問題が生じることは許されません。F# の強力な型システムのおかげで、書式仕様とパラメーターの不一致はコンパイラにより静的に発見されるため、こうした問題はすべて解決されます。このメリットを活かすために、私たちは厳密に型指定された書式設定モジュール Printf (英語) を使用した独自のトレース ログ ヘルパーを定義しました。ログのロジックは .NET Frameworks の System.Diagnostics.TraceInformation
や Azure SDK の AppInsights など他のログ API にオフロードされます。.
System.Diagnostics.TraceInformation
の厳密に型指定されたラッパーを FSharpLu (英語) ライブラリでオープンソース提供しています。また、AppInsights ラッパーを将来オープン ソース提供することを計画しています。
Microsoft.FSharpLu.TraceLogging で System.Diagnostics 向けに厳密に型指定されたログを実行
非同期プログラミング
Springfield のようなオンライン サービスが高い拡張性を実現するには、非同期コードを利用してハードウェア リソースをさらに活用しなければなりません。このタスクはプログラマーにとって骨の折れる作業ですが、これをより容易にする非同期プログラミングのための言語レベルの抽象化が、主流の言語で最近登場し始めています。
F# は既に 2007 年に、言語レベルの非同期プログラミング モデルを .NET プラットフォーム向けに提供しています。具体的には、非同期ワークフロー (英語) という最先端の非同期サポートが標準で用意されています。
Springfield では IO 束縛コードのほとんどが async{..}
ブロック内にラップされており、let!
演算子を利用して IO 処理が完了するまで非同期で待機します。
たとえば、上記の delete
スニペットでは、let!
を使用して Azure SDK の delete API を非同期的に待機しています。非同期ワークフローは私たちのサービスのいたるところで使用されています。バックエンド イベント処理と REST API はすべて非同期です。
非同期 REST API で Springfield ジョブを送信
F# 非同期プログラミング モデルは F# コア ライブラリでコンピュテーション式 (英語) を使用して実装されています。これはしっかりとした理論的基礎 (英語) に基づいた言語構造で、言語構文をきわめて汎用的に拡張するために使用されます。
F# 非同期プログラミング モデルを使用すれば、非同期コードを記述する C# プログラマーが陥りやすい落とし穴 (英語) の多くは問題になりません。詳細については、C# モデルの非同期と F# モデルの非同期の違い (英語) に関する Tomas Petricek のブログ記事をご覧ください。
アクティブ パターンで非同期例外を処理
.NET の非同期および並列プログラミングの重要な動作の 1 つとして、例外が System.AggregateException
型の例外の下に入れ子になる、またはグループ化されることがある、というのがあります。C# などの .NET 言語では例外処理は例外の型にのみ基づいて決まります。F# の場合、パターン マッチ構造では、複雑な条件で処理したい例外をフィルタリングできます。たとえば、上記スニペットの delete
関数では、パターン マッチとアクティブ パターン (英語) を組み合わせて、収集した例外のフィルタリングを簡潔に行っています。
収集された例外に対してアクティブ パターンを使用
パターン マッチで Azure SDK exception Hyak.Common.CloudException
をフィルタリング
スクリプト言語としての F#
F# には REPL (英語) 環境が用意されており、代替言語として PowerShell など他のスクリプト言語とは別の優れた点を持っています。F# スクリプトは .NET プラットフォーム上で実行されるので既存の Core アセンブリのコードを利用できます。Springfield では、F# スクリプトを使って使用状況の監視やクリーンアップといったメンテナンス作業を行っています。F# スクリプトのもう 1 つの利点は、スクリプト言語としてはレアですが、静的型チェックが行われる点です。このため、実際にデバッグにかかる時間が大幅に短くなります。変数名の打ち間違いなどのケアレス ミスはすぐに F# 向け IDE (Visual Studio、Xamarin Studio、Ionide のプラグイン スイートを搭載した Visual Studio Code) の IntelliSense が拾ってくれます。コードのリファクタリングも簡単に行えます。これは私たちが体験した PowerShell スクリプトの脆弱性とはきわめて対照的です。
F# スクリプトのこれらの機能が、PowerShell に代わり、サービスを構成するいくつかのコンポーネントのスクリプト関連のニーズを満たしてくれたのは大きなメリットでした。
PowerShell は今でもデプロイメントやリソース管理に使用しています。その大きな理由は Azure を基盤としているからであり、また Service Fabric など一部のツールの特定の機能が PowerShell からしか利用できないためです。しかし、可能な場合はできるだけ F# スクリプトを使用しています。
Azure のすべてのリソース グループのリストを作成する Springfield .FSX スクリプト
.NET と Azure でスケーリング
F# は .NET 言語であるため、.NET のエコシステム全体を利用できます。たとえば、私たちは Azure .NET SDK を使用して Resource Manager、Compute、Network、Files Storage、Queues、KeyVault、AppInsights など多くの Azure サービスを利用しています。また、Service Fabric を利用してバックエンド サービスも構築しました。
Springfield で Azure がどのように利用されているかについて、詳細は https://azure.microsoft.com/blog/scaling-up-project-springfield-using-azure (英語) をご覧ください。
コミュニティ ライブラリ
F# のもう 1 つの素晴らしい点として、活発なコミュニティがあります。Springfield では Paket (英語) などで NuGet 依存関係管理を簡易化し、FsCheck (英語) でテストの生成を自動化しているほか、FSharp.Data (英語) の型プロバイダーや Visual Studio Code のクロス プラットフォーム F# エディター Ionide (英語) など多数のオープンソース プロジェクトを活用しています。また、他のプロジェクトにも注目しており、Suave (英語) を将来 Web 関連コンポーネントとして導入することなども検討しています。
また、先述のとおり、FSharpLu (英語) と FSharpLu.Json (英語) いう 2 つの F# ライブラリの提供によってコミュニティに恩返ししています。
Project Springfield の今後
この記事では、Springfield の構築に役立った F# のメリットを大まかに説明しました。私たちがのプロジェクトに F# を選択したのは、これよりも小さな規模のプロジェクトでよい手応えを得たからでした。Springfield の開発を通じてわかったのは、フル機能のオンライン サービスの構築にも F# が使えるということでした。
関数型のパラダイムは業界の主流となっており、それは F#、Scala、Swift、Elixir、Rust といった言語の人気が高まっていることや、C# や Java といった言語にも関数型プログラミング コンストラクトが導入されていることからもわかります。C++ でさえも独自のラムダ関数が導入されています。この人気の理由は、常に変化するユーザーのニーズに対応するためにコードをすばやく進化させなければならない中、F# が関数型パラダイムが保証された正確性と高い表現性という他にはないメリットを持っているからでしょう。.NET の開発者にとって F# は打ってつけの言語です。
最後になりますが、F# は求人にも役立ったことをお伝えしておきます。F# のようにそれほど人気のない言語でコードベースを作成する場合の一番の懸念は、エンジニアリング チームを編成するためのエンジニアの頭数がそろわないことです。しかし、意外にもそうはなりませんでした。理由としてはまず、F# のような関数型プログラミング言語を使用するという点に、多くの優秀なエンジニアが興味を示してくれたことでした。その動機としては、この言語が純粋に好きだという人もいれば、チームの反対にあって今の仕事ではこの言語を使用できないという人もいました。また、新しいプログラミング パラダイムを学ぶことに興味があり、これまでとは違ったやり方に挑戦したいという人もいました。エンジニアの数に悩まされずに済んだもう 1 つの理由は、採用したエンジニアが F# だけでなく、どの言語においても腕の立つ開発者だったことです。私たちはこうして Project Springfield のエンジニア募集を問題なく終えることができました。またそれだけでなく、コードベースで F# を採用すれば、このような才能ある人たち (英語) が集まってくれることも発見できました。
Springfield チームのメンバー : 左から順に Lena Hall 氏、 Patrice Godefroid 氏、 Stas Tishkin 氏、 David Molnar 氏、 Marc Greisen 氏、 William Blum 氏、 Marina Polishchuk 氏
Springfield については、パイプラインの仕事がたくさんあります。その中でもバックエンドを .NET Core (英語) に移植することを検討しており、F# が次の 4.1 リリース (英語) でサポートする予定です。
参照情報
- Springfield Web サイト: https://www.microsoft.com/en-us/springfield (英語)
- Channel 9 のビデオ: https://channel9.msdn.com/Blogs/Seth-Juarez/Security-testing-in-the-cloud-with-F-and-Project-Springfield (英語)
- F# 言語のチュートリアル、Azure での F# の使用ガイド、包括的な F# 言語リファレンス: F# 公式ガイド (英語)、C# および Visual Basic の開発者向け学習用リソース: E ブック「F# For Fun and Profit」(英語)
- FSharpLu ライブラリ: https://github.com/Microsoft/fsharplu (英語)
- FSharpLu.Json ライブラリ: https://github.com/Microsoft/fsharplu/wiki/fsharplu.json (英語)