これはフェンリル デザインとテクノロジー Advent Calendar2024 23日目の記事です。
こんにちは!NILTOチームでインフラエンジニアをしている太田です。
先月、NILTOの開発の中で、FastlyというCDN1を導入しました。Fastlyでは、エッジサーバー2で動くプログラムをRustというプログラミング言語で書くことができます。
個人的にも(そして、おそらく会社的にも)初めてRustを業務で利用した事例ですので、少しでもその経験をアウトプットしておきたいと考え、今回の記事のテーマとしました。…とはいうものの、趣味で個人的にRustを書いていたこともあり、それを業務で活かせるのが嬉しかったという面も多分にあります(^_^)
本記事では、開発を進める中で利用したFastlyおよびRust/Cargo3の機能や工夫した点などを、以下の3つの視点から書いていきます。
- Cargo Workspaceの利用
- ユニットテストの実装
- タスクランナー(just)の利用
内容に入る前に、記事執筆時点でのフォルダ構成(抜粋)を参考のために載せておきます。
fastly_compute_edge_src ├── .cargo │ └── config.toml ├── config_store_data │ └── <Config Storeに格納するデータ> ├── crates │ ├── <FastlyサービスA> │ │ ├── src │ │ │ └── <Rustのソースコード> │ │ ├── Cargo.toml │ │ ├── fastly.toml │ │ └── ... │ ├── <FastlyサービスB> │ ├── <FastlyサービスX> │ ├── ... │ └── shared ├── kv_store_seed_data │ └── <KV Storeに格納する初期データ> ├── local_test_data │ └── <開発者の端末でユニットテストをする時に使用するデータ> ├── target │ └── <Rustのビルド成果物> ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── README.md └── rust-toolchain.toml
Cargo Workspaceの利用
RustのパッケージマネージャーであるCargoにはWorkspaceという機能があり、一つのGitリポジトリで複数のRustパッケージを管理することができます。開発においては、Fastlyサービス4ごとのパッケージと、全サービス共通で利用するライブラリ用のパッケージを作り、crates
ディレクトリにまとめました5。
Cargo Workspaceを使ったのは、一言でいえば、管理の手間を省きたかったからです。Fastlyでの開発に限りませんが、外部のライブラリ6に頼らずに、全てのコードを自分で実装することはほとんどありません。Cargo Workspaceでは、共通で使う外部ライブラリのバージョンを全パッケージで統一し、1ヶ所で管理できるため、バージョン管理の手間が減ります。また、後々外部ライブラリのアップデートをする機会が来た時も、1ヶ所を変えるだけで全パッケージに反映されるのでお手軽です7。
ユニットテストの実装
今回の開発では、FastlyのドキュメントのUnit testingを参照しつつ、ユニットテスト8を実装しました。
上記のリンク先を見ると書いてあるように、Rustに標準で用意されているユニットテストの仕組みがそのまま利用できるわけではなく、追加のセットアップが必要でした。また、Config Store、KV Store、Secret Storeのようなデータストアを開発者の端末で再現する方法を調査したり、設定ファイルのfastly.tomlの書き方を調査したりと、実際にユニットテストが実行できるまでは結構時間と手間がかかった印象です。しかし、一度やってしまえば、あとはファイルをGitで管理できるので楽になりました。
一方、Rust側からのアプローチとしては、rstestを利用したパラメータ化テスト9に取り組みました。パラメータ化テストを書くことで、通常のユニットテストに比べてコードの重複が減り、スッキリ見やすくなったと感じます。具体的なテストコードとしては、以下のようになります。(サンプルコードであり、実際に動いているコードとは異なります)
// テストしたい関数 fn is_v1_api_path_access(url_path: &str) -> bool { url_path.starts_with("/v1/") } // テストコード #[cfg(test)] mod tests { use super::*; use rstest::rstest; #[rstest] #[case("/v1/contents", true)] #[case("/v1/contents?model=blog&limit=1", true)] #[case("/v1/contents/0000000000", true)] #[case("/v1/contents/0000000001?lang=ja&depth=2", true)] #[case("/v2/contents", false)] #[case("/v2/contents?model=blog&limit=1", false)] #[case("/v2/contents/0000000000", false)] #[case("/v2/contents/0000000001?lang=ja&depth=2", false)] fn test_is_v1_api_path_access(#[case] url_path: &str, #[case] expected: bool) { assert_eq!(is_v1_api_path_access(url_path), expected); } }
ユニットテストを実装することで、実際にFastlyサービスをデプロイ10しなくても、簡単・手軽に動作確認ができます。ソースコードを変更した時も、既存のユニットテストが全て通っていれば、少なくとも変更していない部分については正常に動くことがわかります。あとは何より、「テストは通っているんだ」ということは、ひとりの人間として安心感が持てます(o^_^o)
タスクランナー(just)の利用
Node.jsでの開発に携わったことがあれば、npm scriptsを使って開発作業におけるコマンドを実行するのは日常のことだったかと思います。Rustではこういった標準のタスクランナー11は今のところないため、justというRust製のタスクランナーを導入しました。
justでは、Justfile
というファイルにタスクを定義して、just <タスク名>
というコマンドでタスクを実行します。また、just -l
というコマンドを実行すると、定義したタスクの一覧が表示できます。他にも、タスク定義に[group(“<グループ名>”)]
を使ってグループを記載し、タスク定義のすぐ上に#
で始まるコメントを記載しておくと、just -l
の実行時に以下のように表示を整えてくれる機能があったりします。
$ just -l Available recipes: [Fastlyサービス A] serve-service-a # service-aのローカル検証用サーバを起動する build-service-a # service-aのPackageをビルドする test-service-a *args # service-aのユニットテストを実行する deploy-develop-service-a # develop環境のservice-aのPackageをデプロイする deploy-staging-service-a # staging環境のservice-aのPackageをデプロイする [Fastlyサービス B] serve-service-b # service-bのローカル検証用サーバを起動する build-service-b # service-bのPackageをビルドする test-service-b *args # service-bのユニットテストを実行する deploy-develop-service-b # develop環境のservice-bのPackageをデプロイする deploy-staging-service-b # staging環境のservice-bのPackageをデプロイする [shared library] test-shared *args # sharedライブラリのユニットテストを実行する [common] test-all # 全てのユニットテストを実行する deploy-develop-services # 全サービスをdevelop環境にデプロイする deploy-staging-services # 全サービスをstaging環境にデプロイする
「長くて面倒なコマンドが含まれる、開発作業の実行が一発のコマンドでできるようになって気持ちいい」のはもちろんですが、「タスク一覧に表示するグループやコマンドの説明が記載できる」のが、すぐに短期記憶が消えてしまう私にとっては刺さりましたね!
その他、1つのタスクにWindows/Mac/Linuxそれぞれ別々の処理を定義できたり、Moduleという仕組みでファイルを分割して管理できたりと機能が盛りだくさんなので、プライベートでも大変お世話になっているツールです。
おわりに
ここまでお読みいただき、ありがとうございました!
本記事で触れた部分以外にも書きたいことはある12のですが、今回はここまでにしたいと思います。
FastlyとRustという組み合わせは、日本語の情報がまだまだ少なく13、苦労することも多いです。ですがその分、新しくてちょっと変わったことができていて、個人的にはとても楽しくお仕事させていただいてます。
本記事の内容が、少しでもみなさまの参考になれば幸いです。
それでは、少し早いですが……メリークリスマス!
- Contents Delivery Networkの略称で、Webサイトのコンテンツ(テキストファイル・画像ファイルなど)を、ユーザーに高速に配信するための仕組みです。↩
- 世界中に分散して配置されたサーバー群のことです。アクセスしてきたユーザーに近いサーバーが選択される仕組みがあるので、ユーザーに近い場所からコンテンツを配信できます。↩
- Rustをインストールすると標準でついてくる、パッケージマネージャーです。Node.jsでいうところのnpmや、Pythonでいうところのpipと同じような役割です。↩
- Fastlyの設定の定義(どのオリジンサーバーに接続するか、どのソースコードを実行するか、など)をひとまとめにして管理する単位です。サービスごとに各種設定を定義することができます。↩
- そもそもFastlyサービスごとにRustパッケージを分ける必要があるのか、という点も考えられますね。確かに、1つのパッケージ内でFastlyサービスごとにバイナリクレートを作り、共通ライブラリはライブラリクレートを作る、というやり方もありそうです。fastly.tomlのフォーマットを見ていると、なんとなくサービスごとに1つのfastly.tomlをおく前提なのかなと感じて、今回は別パッケージに分けました。今後改善の余地はあるかもしれません。↩
- プログラミング時に利用するライブラリの分類については、過去の記事に書いていますので、参照いただけるとより理解できるかと思います。↩
- アップデート自体はお手軽ですが、影響範囲が全パッケージに及ぶので、ライブラリに破壊的な変更があった時の修正は大変になるかもしれません。このあたりは、外部ライブラリアップデートの重要性・緊急性との相談になりそうです。↩
- プログラムを構成する最小単位の部品(ユニット)が正しく動いているかを確認するためのテストのことです。テストコードを書いた後、コマンドを実行することで、テストを実施します。↩
- Parameterized Test。同じテストのロジックに対して、異なる入力値で繰り返し実行するテスト手法のことです。↩
- ここでは、ソースコードをFastlyサービスに反映して、実際に動かせるようにすることを示します。↩
- 開発において何度も実行する作業や複雑な処理を自動化して、開発効率を上げるためのツールです。自動化したい作業を「タスク」として定義しておくことで、毎回手動で実行するよりも作業が効率化できます。↩
- Terraform/Terragruntを使ってFastlyとGoogle Cloudにまたがってリソースを管理している話とかを書きたいです。↩
- なんと、Fastly Computeの一人アドベントカレンダーをしている方がいらっしゃいました……参考にします!↩