SREで実践しているリグレッションテスト自動化について(Rewriteモジュール編)

はじめに

こんにちは。価格.com基盤システム部のSRE担当の橋本と申します。

当社では、サステナビリティで掲げている取り組みのなかで「Kakaku Developer Meetup」という社内勉強会を実施しております。
※社内勉強会の様子は、こちらに写真が掲載されておりますので、ご参照ください。

先日、Kakaku Developer Meetupの3回目が開催されました。その時のテーマが「品質保証(QA)、テストについて」で、私の方ではSREチームで取り組んでいる「リグレッションテスト自動化の導入」について登壇いたしました。
登壇でエンジニアリングの実例として、Webで用いられるRewriteモジュールの設定ファイル変更に関する自動テスト化の話をしたのですが、時間の関係で詳細な説明ができなかったため、この場をお借りして実際に取り組んだ内容を記します。

Rewrite設定ファイル構成維持に関する問題について

価格.comでは2023/11現在、以下のRewriteモジュールを利用しています。

価格.comのように、長年サービスを維持しているWebシステムでのRewriteモジュール運用において問題になりやすいことの1つに「メンテナンス性の低下」が挙げれらます。
私自身がRewrtieモジュールの設定ファイルの担当になった際も、この問題に直面しました。具体的には、以下の通りです。

  • ルールが複雑化されていて、後の設定変更時の影響範囲が不明になり、変更しにくい状態に陥っていた。
  • 古い定義が残っていて、本当に必要なルールの判別が困難になっていた。(ドキュメントや変更管理の不備・不足、という問題)
  • 具体的なテスト方式が無い、または設定当時のテスト内容(パターン)が残っていない。

この技術的負債を抱えたまま運用することは非常に危険で、SREの目的であるシステム安定稼働から逸脱していることを認識しました。そのため、これらの問題を解決するためには「リグレッションテストの導入」が必要不可欠であると考えました。導入することで、設定変更がRewrite定義全体に与える影響を確実に把握できることで、問題を未然に解決できることが期待できるためです。
導入する上で意識したのは「誰でも簡単に安定したテストが実行できる状態にすること」です。せっかくテスト方式が確立できても、テストの実行が簡単でなければ誰も使わなくなって、最終的にその仕組み自体が負の遺産になるため、です。
そのため、テスト実施の敷居を低くするために、最終的にCI側での自動テスト化を計画して実行に移りました。

【補足】
URLRewriteには、テストパターンGUIツールが標準で提供されていますが、以下の点で今回の問題を解決することは難しいと考えます。

  • テスト実施が、IISManagerを利用できる環境に限定される。
  • 手動テストになるため、時間経過により実施内容が陳腐化しやすい。(例えば、証跡が残りづらい)

CIを用いた自動テスト化の取り込みについて

続いて、実際に今回の自動テスト導入に向けて取り組んだことを記します。

1.既存ルールのパターン整理

まずは、兎にも角にも既存ルールの理解が最初になります。
ルールはトータル数百種類存在していて、1つ1つのルールの意味や目的について、解析を行ないました。 ルールの中には、理解すること自体困難であるものが含まれていたため難航しましたが、開発環境での動作確認(=色々なアクセスログを解析して実際にHTTPリクエストを送信して呼び出されるアプリケーションを確認)を少しずつ進めて、何とか一通り理解できました。(これが結果、最も大変なフェーズでした…。)

例えば、以下のURLRewriteルールの場合。

<rule name="Sample Rewrite Test" enabled="true" stopProcessing="true">
    <match url="^([^/]+/[^/]+/)rewrite_sample/([0-9]{4}|[a-z][0-9]{3})/$" />
    <action type="Rewrite" url="sample.html?dir={R:1}&amp;paramcd={R:2}" appendQueryString="false" />
</rule>

上記ルールの場合、以下のようにRequest URL(入力)ごとに、Rewriteモジュールが期待する結果をRewrite Path(出力)として、一覧化=データ化しました。

Request URL Rewrite Path 備考
/hoge/fuga/rewrite_sample/1234/ /sample.html?dir=hoge/fuga/&paramcd=1234 リライトパラメータ渡しの設定({R:1}、{R:2})、{R:2}は数文字のみ
/1/2/rewrite_sample/z999/?demo=test /sample.html?dir=1/2/&paramcd=z999 クエリの内容を引き渡さない設定(appendQueryString=false)、{R:2}はアルファベット含む

2.Rewriteリグレッションテスト方式の検討

ルール解析する一方で、最終的に自動テスト化に向けて、機械的なテスト方式の設計を行ないました。 結果として、解析していく作業中に思いついたのですが、利用しているRewriteモジュールの仕様をテストで利用することを考えました。
それは、「IISLogに出力されるRewrite後のURLが期待するものであることを照合する」 です。 利用しているRewriteモジュールのIISLogに出力される「cs-uri-stem」と「cs-uri-query」については、それぞれ以下のように出力されます。

  • ISAPI_Rewriteモジュールは、Rewrite後のURLがデフォルトで出力される。
  • URL Rewriteモジュールは、「logRewrittenUrl="true"」を指定することRewrite後のURLが出力される。(参考

この仕様を利用することで安定した自動テストを実現することを考えました。結果、以下の処理フローでのテスト設計を行ないました。

  1. テスト対象のHTTPリクエストを発行
  2. HTTPリクエストの内容がIISLogに出力される「cs-uri-stem」と「cs-uri-query」を取得
  3. 「cs-uri-stem」と「cs-uri-query」の内容が期待するURLであること(=Rewrite定義が正しく実行されていること)を評価する

このあたりのテストは、詳細な評価・ロジックが実現できるようにMSTest(C#)で実装することにしました。
それぞれのフェーズにおいてC#で実装することは比較的容易です。IISLogの取得も価格.comで導入している可視化環境のElasticsearchからAPIで取得することにしました。

MSTestについても、dotnet testコマンドを利用したく(※後述に説明)、.NET6で実装しました。

3.テスト対象の構造データ化

MSTestを用いたテスト設計ができたので、次は、テストを行なう上でのテストデータの準備です。
前述の「既存ルールのパターン整理」で整理した一覧を元に、今回はJSON形式のデータに書き換えしました。
JSON形式を採用したのは、将来的に他のプログラミング言語やプラットフォームが変更になっても、データ構造自体は別のプラットフォームでも利用できるため、です。

下図が、作成したテストデータのイメージになります。

テストデータの利用について簡単に説明します。

  • 「Request URL」は、実際にIISに対してHTTPリクエストを送信するURLで利用
  • Rewrite Path」は、MSTestのAssertAssert.AreEqual()にてIISLogの当該項目と比較する値で利用

Rewrite Path」の各項目は、MSTest(C#)で下記のようなイメージで取り扱います。
テストデータと実際のテスト評価が分離されているため、変更時のメンテナンス性の向上が期待できます。

// JSONファイルのデータを展開
var path = json.result.path;
var query = json.result.query;
var statusCode = json.result.statusCode;

//Assertで評価
//iislog=ElasticsearchからAPIで取得したIISLog情報が格納されている
Assert.AreEqual(path, iislog.cs_uri_stem);
Assert.AreEqual(query, iislog.cs_uri_query);
Assert.AreEqual(statusCode, iislog.sc_status)

リグレッションテストを行なうためのテストデータ作成ですが実際問題、テスト対象が数百種類あることから手動で作成するのは(時間や精神的に)無理があります。そのため、設計したデータ構造のJSONファイル作成プログラムを準備して、テストデータを自動生成しました。生成後の内容チェックして問題ある箇所を微修正して、最終的なテストデータ.jsonファイルとして作成完了(※サービスごとに管理できるように分割)しました。
(今回は省略していますが、実際はRedirectにも対応するデータ構造になっています。)

このテストデータファイルですが、最終的にはRewrite定義ファイルのリポジトリ内で管理するようにしました。これには主に以下の理由があります。

  • プルリクエスト時のレビューの容易性
    あるRewriteの定義を変更する際には、セットで対するテストデータファイルも修正することになります。その修正内容がセットで同じプルリクエストに含まれるようになるため、同一リポジトリにしておいた方がレビュー担当者がチェックしやすいため、です。

  • バージョン管理の一貫性 これは設定ファイルとテストデータファイルの変更が、同一commit/revertで管理できるため、です。もし設定ファイルとテストデータファイルが異なるリポジトリで管理されていたら、commit/revertが論理的に分離されているために、場合によっては矛盾が生じることになるため、その問題が回避されるようになります。

4.CI導入によるテスト自動化

テスト自動化するにあたり、昨今ではCIツールの利用は必須だと思います。
価格.comでは現在GitHub Actionsをメインで利用しています。実は以前からJenkinsを用いたリグレッションテストは実施していたのですが、最近GitHub Actionsで実行できるように移行しました。

下図が、現在のGitHub Actionsを用いたテスト自動化の全体構成になります。

今回の自動テストについては、リグレッションテストを毎日行なうことで、以下のようなworkflow(.yml)を作成しました。

  1. scheduleトリガーで開始する。
  2. 開始後、リグレッションテスト環境の準備を行なう。(テストデータ準備、MSTest準備)
  3. 準備完了後、dotnet testコマンドを呼び出し、MSTestを実行する。

以下、GitHub Actionsで実行するworkflow(.yml)の例になります。

name: Schedule Rewrite Regression Test
on:
  workflow_dispatch:
    # 動作確認のためGUI実行を可能にしておく
  schedule:
    # GitHub ActionsではUTC指定(下記は日本時間(JST)で23:00実行指定の例)
    - cron: '0 14 * * *'
jobs:
  preparation:
    # テスト実行前の準備、
    # GitHub Actions実行環境にて、テストデータやMSTestの準備を実施
    steps:
      # (詳細は略)
  execute:
    # テストツール呼び出しでのテスト実行(マトリクス指定で全量実施)
    name: Execute Test (${{ matrix.test_file }})
    needs: [ preparation ]
    strategy:
      # matrix途中でテスト失敗しても全パターンを実行する目的で、fail-fastにfalseを指定
      fail-fast: false
      matrix:
        test_file:
          # (例)各テストデータファイルの全量テストをするための指定
          - AAAAA/RewriteTest1.json
          - BBBBB/RewriteTest2.json
          - CCCCC/RewriteTest3.json
    steps:
      - name: Execute rewrite regression test
        # dotnet test 実行
        # .runsettingsに定義した任意パラメータ「TestPatternFile」を外部から指定して、matrix.test_fileを入力ファイルパスとして渡す。
        run: |
          dotnet test XXXXX.csproj --settings temp.runsettings -- TestRunParameters.Parameter(name=\"TestPatternFile\", value=\"${{ matrix.test_file }}\")

上記workflowについて、幾つかポイントを挙げておきます。

  • 毎日実行するために、scheduleを実行トリガーで利用します。
  • 複数の環境に対してテストを実施するため、マトリクス定義することで全量テストを行なっています。
  • GitHub Actionsの仕様上、マトリクス実行の途中でエラーになった際、後続のstepはすべてスキップされます。その結果テスト実行が不十分になるため、fail-fastでfalse指定することで、そのスキップを回避するようにしています。

また、「dotnet testコマンド」を用いた理由ですが、仕様として.runsettingsにある任意のパラメータを外部から注入する仕組みが準備されています。これを用いることでメンテナンス性の向上が期待できるためです。(参考

リグレッションテスト(自動化)の効果

最後に、リグレッションテスト(自動テスト化)導入後の効果について簡単に記します。

  • 積極的に設定ファイル変更ができるようになった
    これは当初の期待通りの成果になります。 自動化されたテストで簡単に評価できることは非常に大きく、変更時に予期せぬ影響がわかるようになったため、障害を未然に防いだケースも実際にあります。

  • 不要なルールを炙りだすことができた
    例えば、Rewrite先のアプリケーションが撤去済みだった場合は、結果ルール自体を削除して整理できました。 不要なルールが残っていることは、リクエストのたびに不必要なルール評価を行なっていたことであるため、微々たるものかもしれませんが、パフォーマンス改善にも繋がっていると思います。

  • 情報共有がやり易くなった
    例えばですが、実際のRewrite対象のリクエストURLがテストデータにあるため、他担当者とルールに関する説明がし易くなりました。正規表現の理由は文書化が難しいと思いますが、テストデータの内容がある種のドキュメント扱いができるため、スムースに調整できるようになりました。

さいごに

今回はリグレッションテスト自動化の導入による運用改善ということで書きましたが、実際にSREチームでは関わるOS・ミドルウェア・プラットフォームも多岐にわたります。もし、価格.comといった大規模サービスを通じて多種多様なアーキテクチャへの関わりにご興味をお持ちになりましたら、ぜひ、以下のページをご覧頂ければ幸いです。

https://kakaku.com/info/recruit/