さくらのクラウドのAPIにrunnを導入してみた

この記事は2025年1月24日(金)に行われた社内勉強会での発表をさくナレ編集部で記事化したものです。
はじめに
さくらインターネットの野村孔命です。クラウド事業本部 クラウドサービス部 API開発チームに所属していて、業務としてはさくらのクラウドのAPI開発をしたりとか、認証認可システム(IAM)の開発に注力しています。他にはお客様の行動ログを収集する基盤であるイベントログというシステムの開発などもしています。格闘ゲームが大好きで、最近はストリートファイター6を中心にやっています。
今日はまずrunnの簡単な紹介をさせていただいて、その後、さくらのクラウドのAPIにrunnを導入した事例を紹介していきます。runnは非常に多彩な機能を持っているので、実際に作ったシナリオを皆さんと一緒にコードを見ていきながら説明していった方がわかりやすいかなと思い、そのような順序で説明していきます。
runnの簡単な紹介
それではまずrunnの簡単な紹介からしていきます。
runnとは
まずオペレーション自動化(Runbook Automation)というジャンルがあります。Runbookというのは、コンピューターシステムとネットワークに対して管理者やオペレーターが実施する定型の手順や操作をまとめたものです。
runnはここに対するソリューションの一つとして開発されたもので、実際には以下のようなユースケースに限定してツールが開発されています。まず一つ目がAPIのシナリオテストです。他にもローカルやリモートへの任意のコマンドを利用したテストであったり、データベース操作などのオペレーションの実行と記録をしてくれるのがrunnの主な機能になっています。特にAPIシナリオテストを行うテスティングツールとしての役割が一番大きいかなと思っていて、ここに着目して今回さくらのクラウドに導入したという形です。
runnの特徴
runnの特徴としては、まずシナリオベースのテストが記述しやすいというのがあります。上図に記述例を示します。このYAML形式の定義を見てもらったらわかると思いますが、stepsと呼ばれる部分が実際のオペレーション内容になります。GitHub Actionsのように書くことができます。この例ではサーバーを作るというリクエストを書いているのですが、ここはOpenAPIっぽく記述することができます。こういう点を踏襲しているのが非常にわかりやすいというか、エンジニアとしては馴染みのある記法なので記述がしやすいなと感じました。
テストの記述自体の構成もシンプルになっています。まず最初にRunnerというものを設定しています。Runnerというはrunnの中で使いたい機能を指しています。例えばHTTPリクエストを送りたかったらHTTP Runnerを呼び出してとか、gRPCを実行したかったらgRPC Runnerとか、DB操作をしたければDB Runnerを呼び出すという形になっています。その後に変数を設定する部分があって、上記の例のようにtokenとして変数を使って宣言することができます。これをsteps内で呼び出すこともできます。その後のstepsに、実際にどういう処理でテストをやってほしいのかを書いていくという風になっています。
この他に特徴として挙げられるのが、シンプルなワークフローエンジンとしての機能があります。例えばステップのループの実行やリトライとか、レスポンスの結果を変数として保持することもできます。条件によるステップの制御や、他のシナリオファイルを読み込んで再利用したりとかいったこともできるので、非常に便利な機能を持っていると言えます。それからrunnはGoで書かれているOSSなので、Goのテストヘルパーとして組み込むこともできます。また、Goで書いてあるのでシングルバイナリでCIフレンドリーに利用できるというメリットも開発者は謳っています。
さくらのクラウドのAPIへの導入
それでは、このrunnをさくらのクラウドのAPIに導入してみた事例を、実際に作成したシナリオをもとに紹介していきたいと思います。
さくらのクラウドで想定したシナリオ
さくらのクラウドで想定したシナリオを説明します。
まずはIaaSとしての基本的な機能の動作保証からスタートしたいと思います。そこでまずサーバを正常に作って消せるかというところからいきたいということで、サーバのライフサイクルというシナリオを作りました。動作としては、サーバとディスクを作成し、ディスクをサーバに接続して電源をONしてOFFして、最後にサーバとディスクを消していくという、サーバができてから削除されるまでの一連の流れをAPIベースで表現したものになっています。
その後にスイッチのライフサイクルなども作ってみたり、他にもいろいろ書いていますが、とりあえずはこのあたりが最初に取り組んだものになります。
本記事ではサーバのライフサイクルのシナリオテストを使って、runnは便利だなと私が感じた機能を皆さんに紹介していけたらと思います。
変数設定
サーバライフサイクルのシナリオを記述したコードの先頭部分を上図に示します。
1行目のdescは「サーバのライフサイクルのシナリオ」と書いてありますが、これは現在何のテストをやっているかが標準出力に表示されるというだけの機能です。
その後のvars:以下に変数の定義がたくさん並んでいるのですが、この変数定義に関してちょっと推しのポイントがあります。例えば3行目には
endpoint: ${BASE_URL}
と書いてありますが、ここは環境変数に定義してある内容を展開して利用することができます。これが結構便利です。例えばAPIを使うのであればトークンが必要になりますが、そのトークンを環境変数に切り出してリポジトリにコミットしなくてよいので非常に便利に使えます。
サーバ作成+作ったサーバのIDを変数にバインド
次はいよいよステップの定義です。まずステップに名前をつけることができます。上記の例ではcreateServerという名前をつけています。ハイフン(-)にして名前をつけないステップとして定義することもできます。次行のdescにステップの説明を書きます。ここではサーバ作成のAPIを作るのでそのように記載しています。
その次からが実質的な内容になりますが、まず4行目にincludeというディレクティブがあります。includeというのはシナリオを外部から呼び出すrunnの機能で、これが結構便利です。ここではサーバを作成するAPIをたたくというシナリオを外部に切り出していて、それを呼び出しています。
呼び出している内容がスライドの右側にあります。runnersの次にreqと書いてありますが、これはHTTPリクエストをするためのrunnerです。reqの値がparent.vars.endpointと書いてありますが、これは呼び出された側に宣言されていた変数の中のendpointを読み込む形になります。つまり呼び出した側で定義した変数を流し込めるような形になっています。よって、どんな変数が設定されていればよいかをきちんと設計しておけば、こういうふうに使い回しやすいシナリオを定義することができます。
続いて右側の6行目に"if: included"という記述があります。これは、このファイル自体もシナリオとして認識されてしまうので、runnでまとめて実行するときにこれも読み込まれて単体で動こうとしてしまいます。そこで"if: included"というのを明示的に書いておくと、これはinclude専用なので実行しないというように解釈してくれます。つまり実行時に呼び込まれないようなシナリオであることを宣言するためのものですね。
こちらのファイルのstepsとしては、これはAPIの呼び出しの定義なのですが、/serverというendpointに対してpostメソッドで、headersとbodyに書かれた内容を送ってくださいという風に読むことができます。そしてその次の行にあるbindという機能を使って、responseという変数にcurrent.res(このAPIリクエストを実際に行ったときのレスポンスの値)をバインドしてくださいという指示をしています。そして、このresponseを呼び出し元の方で参照することができます。
説明の順序が前後しますが、右側のプログラムの15行目にあるvars.requestも、呼び出し元のプログラムの7行目にあるrequest行の内容を読み込んでいます。で、そのrequest行を見ると、リクエストするための内容をまた別のファイルとして定義して呼び出しているような形になっています。ここは後でまた出てくるので説明します。
これでレスポンスの内容が返ってくるのはわかりました。これに対するテストとしてはレスポンスとして何が返ってきたかという中身をきちんとチェックしたいので、それは左側プログラム9行目のtest:から始まる青色で囲まれた部分で実施します。内容としてはまずステータスコードとして何が返ってきたのかを見ています。ここでは201が返ってきたらOKとしています。それから、作られたサーバのプランがリクエストした内容と一致しているかを見ています。一致しなかった場合はもうこのステップでこのテストが失敗に終わるという形になります。
その後、17行目にまたbindが出てきます。runnでは、ステップの名前ベースで一つ前のステップの値を取ってきてくださいというような書き方ができるのですが、記述がすごく長くなってしまうので(上記のプログラム例ではsteps.createServer.response.body.Server.ID)、それをここではserverIdという変数にバインドしておいて、それを後で使い回すことで呼び出しを簡略化するようなこともできます。
ディスク作成+サーバ接続
ではプログラムの続きを見ていきましょう。先ほどのプログラムではサーバを作ったので、次はディスクを作ってサーバに接続するところをシナリオとして書いています。
まず左側のプログラムを見てもらいたいのですが、ここは先ほどと似たような形ですね。今度はディスクのシナリオを外に分離していて、それを4行目で呼び出す形になっています。そして前の章で、リクエストのボディの内容をファイルとして分離しておいて呼び出すことができるという話をしましたが、ここでも左側プログラムの6行目で同じことをしています。呼び出している内容が右側のプログラムになります。単純なJSON構造になっています。この中でも4行目に記述したように、runbook側(左側のプログラム)に記述した変数を呼び出しています。Goテンプレートが内部で使われているのでGoテンプレートの変数展開に従って書かないといけないというちょっとした癖がありますが、こういう形で呼び出すことができます。
ここでポイントとして見ていきたいのがオレンジで囲った部分(13-15行目)で、前半のプログラムでサーバIDを簡略化してバインドした話をしたと思いますが、それをここで参照しています。つまり前のリクエストで作成したサーバIDをこういう形で変数として組み込んでボディに含めるということも可能になっています。これもかなり便利ですね。このテンプレート自体も使い回しができます。
左側プログラムの後半は前の章で説明した内容とだいたい同じで、14行目でわかるようにディスクのIDも先ほどと同様にバインドしています。
サーバの接続については、ディスクを作るときにこのサーバに接続してくださいというのも明示的に記述します。右側プログラムの13-15行目がそれに該当します。これはさくらのクラウドのAPIの仕様です。
ディスク作成完了を待つ
ディスクを作ると言いましたが、ディスクはリクエストを受け付けてから非同期で作ります。ですので、ディスクが作成されて利用可能になるのを待つ必要があります。そういうのもrunnだと結構書きやすいです。
例示したプログラムのようにcheckDiskStatusというステップを定義します。プログラムの中にloopというのがあります。このループの読み方なんですが、current.response.body.Disk.Availabilityがavailableという状態で返ってくるようになるまで、インターバル10秒で20回実行してくださいというように書いています。ここでディスクが利用可能になったかどうかを見ている形になります。指定した回数を失敗するとこのテストは失敗になります。
そして、実際にディスクが利用可能になったかをチェックするためのリクエスト内容が、またincludeで外から呼び出している形になっています。test行以下にテスト内容が書いてあります。最後にdumpという行がありますが、ここでは標準出力にデバッグ情報みたいな形で出力しています。
電源ONとサーバの電源状態の確認
ディスクがサーバに接続されて、いよいよ電源をONにする段階になりました。というところで今度は電源をONにしましょう。コードを上図に示します。こちらも電源をONにするためのシナリオを外部から呼び出して、その下にテストのコードがあります。電源ONも非同期で行われるので、checkServerPowerOnのところに書いたようにloopで待って、loopで待つ間にたたくAPIの内容をincludeの中から呼び出しています。
紹介したrunnの便利機能まとめ
ここまで、実際の書き心地なども知ってもらいたかったのでコードを交えて説明してきましたが、実際どういう機能があったのかをまとめたのが上のスライドになります。
まず環境変数を展開して変数にバインドできるという話がありました。これによってシークレットやシナリオによって可変になるような値を外部に分離することができます。次に、適宜任意の変数にバインドして使い回すことができます。呼び出しの階層が深くなってくると呼び出すのが面倒臭くなるので、適宜簡単な変数としてバインドし直せるのは結構便利かなと思いました。
それから、シナリオを別ファイルに定義して呼び出せるところも結構便利な機能です。ただし使い方を間違えると大変になってしまうというのもちょっとあったりしますが、シナリオの再利用性が高い場合にはすごく便利に利用できます。あと、先ほど見てもらったようにAPIレスポンスのテストも結構容易にすることができて、リクエストボディを別ファイルに分離して変数を展開することもできます。これもシナリオによってリクエストボディの内容を変えたいというようなときに、変数化しておくとすごく便利に使い回せるという感じですね。最後に、非同期でリクエストが処理される場合にも、ループを使うことで処理結果を待ち、OKになったかどうかを確認するという、このような処理も非常に直感的に書ける形になっているのでやはり便利かと思いました。
おわりに
この記事ではrunnの特徴を簡単に紹介して、さくらのクラウドのAPIに導入したところを説明しました。実際に作成したシナリオをベースにしてrunnの便利機能を紹介しました。今回はAPIのテストにフォーカスを絞って説明させていただきましたが、runnは他にもいろいろな機能があるので、この記事を読んで興味が出た方はぜひ触ってみてください。