Selenium Grid 4 の新機能 Dynamic Grid を Kubernetes に構築する

こんにちは。ユーザベース Product Team の old_horizon です。

私達の開発チームでは、E2E テストの実行環境として Zalenium を主に利用しています。
しかし 2020 年 3 月の最終リリースをもって開発が終了しており、将来的にリプレースを検討する必要がありました。

こうした状況の中で、Selenium Grid 4 が 2021 年 10 月にリリースされました。
その新機能の一つである Dynamic Grid が代替手段として有力に思え、まずは概要の理解から始めました。

Dynamic Grid とは

GitHub の docker-selenium に解説があります。

Grid 4 has the ability to start Docker containers on demand, this means that it starts a Docker container in the background for each new session request, the test gets executed there, and when the test completes, the container gets thrown away.

つまり、新規セッションごとに使い捨ての Docker コンテナを用意してテストを実行する機能です。
Zalenium や Selenoid の同機能を参考にしたものと思われます。

技術的な課題

この Dynamic Grid は、現在のところ Docker への構築しかサポートされていません。
すべての Docker コンテナが一つの環境で動作するため、性能向上には垂直スケールが必要になります。

しかし垂直スケールには再起動が必要になるうえ、ハードウェアの物理的限界もあります。
そのため Kubernetes を利用した水平スケール可能な構成を実現したいと考えました。
使い捨ての Docker コンテナを Pod として用意できれば、複数台で構成された各ノードのリソースを有効に利用できます。

公式の Grid では機能が不足しているため、今回は Grid 4 の拡張として独自に実装しました。

Grid 4 の拡張方法

Grid 3 では 独自 Servlet や Proxy を実装して拡張することができましたが、Grid 4 に同等の機構はありません。
そのため実装を読みながら、拡張できそうな箇所を探しました。

これより Selenium Server 4.1.2 のコードを参照しながら解説していきます。

--ext オプション

Selenium Server は、起動時に --ext オプションで指定された jar ファイル内のクラスをロードします。
実装はエントリポイントである Bootstrap クラスに存在します。

これで拡張を含んだ jar ファイルを読み込ませる方法がわかりました。

CliCommand インターフェース

Bootstrap クラスでクラスローダーの準備が整うと、リフレクションで Main クラスが呼び出されます
Main クラスは、ServiceLoader という Java 標準のプラグイン機構で CliCommand インターフェースの実装を取得します

CliCommand インターフェースの実装は Selenium Server で利用可能な各コマンドに対応します。
この仕組みを活用すると、以下の手順で任意のコマンドを拡張できます。

  • 任意のコマンドを継承したクラスを作って拡張する
  • CliCommand#getName をオーバーライドし、起動時に指定するコマンド名を返す
    • 元となったコマンドとの競合を避けるため、別名称にする必要があります
  • com.google.auto.service.AutoService アノテーションをクラスに付与する
    • ServiceLoader が認識するために必要な META-INF/services 配下のファイルを自動生成してくれます
  • 拡張をビルドする
  • --ext オプションで生成された jar を指定しつつ、拡張したコマンドを実行する

TOML 設定ファイルによる実装の切り替え

Grid 4 では TOML 形式の設定ファイル が採用されています。
この設定ファイルで、Selenium Grid を構成する一部コンポーネントの実装を任意のものに差し替えることができます。
4.1.2 で対応しているコンポーネントは以下のとおりです。

各コンポーネントのセクションにおいて implementation キーでクラスの FQCN 文字列を指定します。

拡張を実装する

上記で見つけた機構を使って、Kubernetes に Dynamic Grid を構築する拡張を実装しました。
リポジトリはこちらです。

selenium-k8s-dynamic-grid

Grid 3 と同様に Hub と Node の構成かつ、既存の Zalenium と同様に一つの Pod に必要なアプリケーションを同居させています。
また以前 Zalenium の拡張として実装した、ファイルダウンロードを行う e2e テスト対応と同等の機能も備えました。

Node

  • KubernetesNode
    TOML 設定ファイルで、Node の独自実装であるこのクラスを指定します。
    • create
      リフレクションで呼び出されるファクトリメソッド
    • execute
      ファイルダウンロード対応のためにエンドポイントを追加
  • KubernetesSessionFactory
    • apply
      新規セッション要求時に、WebDriver を実行する使い捨ての Pod を作成します

Hub

  • DynamicGridHub
    CliCommand インターフェースを実装した Hub クラスの拡張です。
    起動時のコマンドに、getName メソッドが返す dynamic-grid-hub を指定します。
    • createHandlers
      ファイルダウンロード対応のためにエンドポイントを追加

Proxy

Hub と Node を一つの Pod で動作させる都合で、OpenResty によるリバースプロキシを間に挟みました。
ルーティングと一部レスポンスの書き換えを行っています。

  • 設定ファイル
    • /wd/hub/session
      レスポンスボディに含まれる Hub の IP が、Kubernetes クラスタ内の IP になってしまう問題への対処
      リクエストの Host ヘッダーの値でホストを置換して返します

動作確認

筆者が動作確認した環境は以下のとおりです。

  • Ubuntu 18.04
  • Docker 20.10.13
  • kind 0.11.1
  • Gauge 1.4.3

事前に kind でローカル環境に Kubernetes クラスタを構築しておきます。

まずは Skaffold を利用して、ビルドから Kubernetes クラスタへのデプロイまでを行います。
リポジトリの kubernetes/skaffoldに移動して、以下のコマンドを実行します。

$ kubectl create ns selenium
$ skaffold run -n selenium

デプロイが完了したら、Grid 4 の Hub にアクセスしてみます。
Internal IP と、NodePort として公開されている Service のポート番号の組み合わせを使います。

$ kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'
172.18.0.2

$ kubectl -n selenium get svc selenium -o jsonpath='{.spec.ports[0].nodePort}'
31968

したがって、この場合は http://172.18.0.2:31968/ が URL になります。
ブラウザで開くと、このように GUI が表示されます。

リポジトリ内の e2e に移動して mvn test を実行して e2e テストを流します。
e2e ではこれらの検証をしています。

  • 要素の操作やアサーションができること
  • Selenium 4 でサポートされた BiDirectional functionality が利用できること
  • ファイルダウンロードを行う e2e テスト向けの機能が利用できること
    • 詳細な仕様は README を参照してください

全件成功することが確認できたら、録画されたビデオを見てみましょう。

(Selenium Grid の URL)/videos/ にアクセスすると、録画されたビデオファイルの一覧が表示されます。
ファイル名には Selenium セッションの ID が使われます。

ビデオファイルをクリックすると、ブラウザ上でそのまま閲覧できます。

おわりに

公式ドキュメントが充実していなくても、しっかりコードを読めばアーキテクチャへの理解が深まることを実感できました。
運用していく中で問題も出てくると思いますが、まずは Selenium 4 への移行の足がかりができたことを嬉しく思います。

ユーザベース Product Team ではエンジニアを積極採用中です。
e2e から始まるテスト駆動開発に興味をお持ちいただけたら、ぜひこちらからご応募ください。

apply.workable.com

Page top