ZaleniumをKubernetesで動かして並列テストさせる話

# はじめに

テスト自動化ツールの主力なツールであるSeleniumをラップしたZakeniumをKubernetesで動かした話を載せたいと思います。

> ZaleniumはLineStoreのテスト自動化で言及されています
> [LINE STOREにおけるテスト自動化の取り組み - LINE ENGINEERING](https://engineering.linecorp.com/ja/blog/test-automation-line-store/)

Zaleniumを使おうと思っても意外と情報が個人的に少なく感じたので、どなたかの役に立つと幸いです。
実行環境は環境に書いてあるDocker for Macですが、ベンダーロックインしていない(標準的なKubernetes)ので数箇所(PV,Service等)を変えてもらえばどの環境でも動く想定です。

下記画像のような並列テストを複数Nodeで分散しながら実行できます。

<img width="1373" alt="スクリーンショット 2018-12-05 18.12.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/468db757-ddbb-5116-10d4-178b5282bfca.png">

# 環境

使用した環境は以下です。
・Mac Pro(2018) OS Mojave 10.14.1
・Docker for Mac(Docker Engine 18.09)
・Kubernetes(v1.10.3) ※Docker for Mac付属
※環境のセットアップの手順は省略しています

# 背景

元々は、Javaで作成したアプリケーションのテストでSelenium(Grid)を利用していました。
…が以下の理由からSeleniumからZalaniumを選択しました。

・Seleniumでキャプチャ(画面キャプチャ)を取った際に、望まないタイミングでキャプチャが撮られる。
→テストがエラーで終わった際に、頼りになるキャプチャが正確でないために、泣く泣くテストが落ちる箇所までテスト実行を見守り続けることに…
 
 そこで、画面キャプチャではなくエビデンスとして動画を撮れるZaleniumの検証をKubernetesでしました。
 
# ZaleniumをKubernetesで動かすメリット
 
 Zalenium(Grid):Selenium(Node)が1:Nで複数Nodeが展開されるので、SeleniumNode(Pod)を複数Nodeに負荷分散できるメリットがあります。
 (そのためSeleniumNodeを複数個動かす場合は、複数Nodeを持つKubernetesクラスターが望ましいです)

> この記事ではDocker for MacのKubernetesで検証しているので(Nodeが1つしかない)、Docker-Composeと大差ないですが…
 
# 実際に動かす
 
[Zalenium公式リポジトリ](https://github.com/zalando/zalenium/tree/master/kubernetes)にKubernetes用のyamlが載っています。
中身を見ると、Minikube用に用意されています。
Minikube用というのもあって、何箇所かMinikube依存している箇所もあります(自分の環境にそのまま適用してもきちんと動きませんでした…)。
 
Minikube用を参考にしつつ、標準的なKubernetesで利用できるようにより汎用的に作り変えました。
実際に動かしたソースは[GitHub](https://github.com/govargo/container/tree/master/kubernetes/zalenium)にあげています。
 
```deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: zalenium
 namespace: zalenium
spec:
 replicas: 1
 selector:
    matchLabels:
      app: zalenium
 template:
   metadata:
   labels:
     app: zalenium
     role: grid
   spec:
     containers:
       - name: zalenium
         image: dosel/zalenium:3
         args:
           - start
           - '--desiredContainers'
           - "$(desiredContainers)"
           - '--maxDockerSeleniumContainers'
           - "$(maxDockerSeleniumContainers)"
           - '--sauceLabsEnabled'
           - "$(sauceLabsEnabled)"
           - '--browserStackEnabled'
           - "$(browserStackEnabled)"
           - '--testingBotEnabled'
           - "$(testingBotEnabled)"
           - '--startTunnel'
           - "$(startTunnel)"
           - '--videoRecordingEnabled'
           - "$(videoRecordingEnabled)"
           - '--screenWidth'
           - "$(screenWidth)"
           - '--screenHeight'
           - "$(screenHeight)"
           - '--timeZone'
           - "$(timeZone)"
           - '--sendAnonymousUsageInfo'
           - "$(sendAnonymousUsageInfo)"
           - '--debugEnabled'
           - "$(debugEnabled)"
           - '--logJson'
           - "$(logJson)"
           - '--logbackConfigFilePath'
           - "$(logbackConfigFilePath)"
           - '--seleniumImageName'
           - "$(seleniumImageName)"
           - '--maxTestSessions'
           - "$(maxTestSessions)"
           - '--keepOnlyFailedTests'
           - "$(keepOnlyFailedTests)"
           - '--retentionPeriod'
           - "$(retentionPeriod)"
         envFrom:
          - configMapRef:
              name: zalenium-kubernetes-config
          - configMapRef:
              name: zalenium-config
         resources:
           requests:
             cpu: 250m
             memory: 400Mi
         ports:
          - containerPort: 4444
          - containerPort: 4445
         volumeMounts:
          - name: zalenium-data
            mountPath: /home/seluser/videos
          - name: zalenium-mounted
            mountPath: /tmp/mounted
    volumes:
        - name: zalenium-data
          persistentVolumeClaim:
            claimName: zalenium-data
        - name: zalenium-mounted
          persistentVolumeClaim:
            claimName: zalenium-mounted
    serviceAccountName: zalenium
    serviceAccount: zalenium
```

ServiceのTypeは自身の環境に合わせて変更してください。
(例ではDocker for MacなのでNodePortで外部に露出しています)

```service.yml
apiVersion: v1
kind: Service
metadata:
  name: zalenium
  labels:
    app: zalenium
  namespace: zalenium
spec:
  # Change type according to your choice
  type: NodePort
  ports:
  - name: zalenium
    port: 4444
    protocol: TCP
    targetPort: 4444
  - name: register
    port: 4445
    protocol: TCP
    targetPort: 4445
  selector:
    app: zalenium
    role: grid
```

configMapをKubernetes用でのシステムリソース制限用とZaleniumの挙動変更用の2種設けています。

Kubernetes用

```configMapKubernetes.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: zalenium-kubernetes-config
  namespace: zalenium
data:
  ZALENIUM_KUBERNETES_CPU_REQUEST: "250m"
  ZALENIUM_KUBERNETES_CPU_LIMIT: "500m"
  ZALENIUM_KUBERNETES_MEMORY_REQUEST: "500Mi"
  ZALENIUM_KUBERNETES_MEMORY_LIMIT: "1Gi"
  OVERRIDE_WAIT_TIME: "5m"
```

Zalenium用

```configMapZalenium.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: zalenium-config
  namespace: zalenium
data:
  desiredContainers: '2'
  maxDockerSeleniumContainers: '4'
  sauceLabsEnabled: 'false'
  browserStackEnabled: 'false'
  testingBotEnabled: 'false'
  startTunnel: 'false'
  videoRecordingEnabled: 'true'
  screenWidth: '1900'
  screenHeight: '1880'
  timeZone: 'Asia/Tokyo'
  sendAnonymousUsageInfo: 'true'
  debugEnabled: 'true'
  logJson: 'false'
  logbackConfigFilePath: 'logback.xml'
  seleniumImageName: 'elgalu/selenium'
  maxTestSessions: '1'
  keepOnlyFailedTests: 'false'
  retentionPeriod: '3'
```

Zalenium用のConfigMapに変数が数多く定義されていますが、
Zaleniumプロセスを動かす際のオプションを定義しています。
SeleniumNode数やロギングなどの挙動を変えたい場合は、configMapの値を変更します

```deployment.yml抜粋
         args:
           - start
           - '--desiredContainers'
           - "$(desiredContainers)"
           - '--maxDockerSeleniumContainers'
```

それでは実際に動かします。
(Kubernetesが既に動いている前提で記載しています)

```
$ git clone https://github.com/govargo/container.git
Cloning into 'container'...
remote: Enumerating objects: 26, done.
remote: Counting objects: 100% (26/26), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 26 (delta 4), reused 20 (delta 4), pack-reused 0
Unpacking objects: 100% (26/26), done.

$ cd container/kubernetes/zalenium/

$ kubectl apply -f .
configmap "zalenium-kubernetes-config" created
configmap "zalenium-config" created
deployment.apps "zalenium" created
persistentvolumeclaim "zalenium-data" created
persistentvolumeclaim "zalenium-mounted" created
persistentvolume "zalenium-data" created
persistentvolume "zalenium-mounted" created
service "zalenium" created
serviceaccount "zalenium" created
clusterrole.rbac.authorization.k8s.io "zalenium-role" created
clusterrolebinding.rbac.authorization.k8s.io "zalenium" created

$ kubectl get pods -n zalenium
zalenium-40000-jdvq5       1/1       Running   0          1m
zalenium-40000-xp8tp       1/1       Running   0          1m
zalenium-d57bc759d-54lsr   1/1       Running   0          1m
```

kubectl applyでdeployした後にしばらくすると、Zaleniumが立ち上がります。
「zalenium-d57bc759d-54lsr」がZalenium(Grid)側で、
「zalenium-40000-jdvq5」「zalenium-40000-xp8tp」がSeleniumNode側です。

それでは、Zaleniumの管理コンソールにアクセスします。
ブラウザでアクセスする前に、Zaleniumにアクセスするポートを取得します。

```
$ kubectl get services -n zalenium
NAME       TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)                         AGE
zalenium   NodePort   10.111.104.211   <none>        4444:30299/TCP,4445:31831/TCP   13m
```

Zaleniumの管理コンソールポートの4444に対して、NodePortで30299でポートが開いていることが確認できました。

> NodePortは毎回起動の度にポートが変化します

ブラウザを開いてみます。
URL: http://localhost:30299/grid/console
※ポートは毎回起動の度に変更されます

<img width="1339" alt="スクリーンショット 2018-12-05 17.51.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/2a0f60df-1aff-1c97-1ac1-93308a9103a2.png">

コンソールに立ち上げたSeleniumNode2台が登録されていることが分かりました。

そしてZaleniumでテストが動いている際のライブビューについては、下記のURLから確認できます
URL: http://localhost:30299/grid/admin/live

<img width="1105" alt="スクリーンショット 2018-12-05 14.18.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/a989c99c-283e-e7bc-0fcd-d6436998d022.png">

テストが動いた後の動画記録については、PersistentVolumeに記載しているディレクトリにデータが生成されます。
(本例では/tmp/zalenium/videos配下に動画ファイルが生成されます)

# テストを動かしてみる

Kubernetesで立てたZaleniumに対して、テストを実行してみます。
SeleniumNodeが2台いるので、テストも同時に並列で2件実施してみます。
テストの実行はDocker-Composeで行います。

テスト用のソースも[GitHub](https://github.com/govargo/container/tree/master/docker/ruby/selenium)にあげています。

```sampleTest.rb
require "selenium-webdriver"

HD = ENV['ZALENIUM_HD']

caps = Selenium::WebDriver::Remote::Capabilities.chrome(
  "name" => "sample test",
  "recordVideo" => true
)

driver = Selenium::WebDriver.for(:remote,
   url: "http://#{HD}/wd/hub",
   desired_capabilities: caps
)

driver.get("https://www.google.co.jp/")
driver.quit
```

```docker-compose.yml
version: '3'
services:
  selenium:
    image: ruby:2.3.8
    command: /usr/local/bin/test.sh
    volumes:
      - "./test.sh:/usr/local/bin/test.sh"
      - "./sampleTest.rb:/usr/src/sampleTest.rb"
    environment:
      - "ZALENIUM_HD=192.168.43.36:30299"
```

Zaleniumへの接続先は*ZALENIUM_HD*という環境変数で切り替えています。
※実行PC/サーバーのIP:ポートに適宜変更してください。

テストでは、SeleniumがGoogleのトップページにアクセスして終了します。

```
$ cd ../../docker/ruby/selenium/

# scaleの指定を2にして並列でテストを実行しています
$ docker-compose up --scale selenium=2
Recreating selenium_selenium_1 ... done
Creating selenium_selenium_2   ... done
Attaching to selenium_selenium_1, selenium_selenium_2
selenium_1  | Successfully installed rubyzip-1.2.2
selenium_1  | Building native extensions. This could take a while...
selenium_2  | Successfully installed rubyzip-1.2.2
selenium_2  | Building native extensions. This could take a while...
selenium_2  | Successfully installed ffi-1.9.25
selenium_2  | Successfully installed childprocess-0.9.0
selenium_2  | Successfully installed selenium-webdriver-3.141.0
selenium_2  | 4 gems installed
selenium_1  | Successfully installed ffi-1.9.25
selenium_1  | Successfully installed childprocess-0.9.0
selenium_1  | Successfully installed selenium-webdriver-3.141.0
selenium_1  | 4 gems installed
selenium_selenium_1 exited with code 0
selenium_selenium_2 exited with code 0
```

同時進行で確認すると
<img width="1325" alt="スクリーンショット 2018-12-05 17.52.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/3a6f91cb-54eb-0c89-9e1e-adcf61aa1482.png">
<img width="1311" alt="スクリーンショット 2018-12-05 17.52.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/582a398e-968a-f630-dd5d-a93da102796f.png">
とテストの実行をライブビューで確認できます(便利！)。

撮影保存した動画ファイルは下記のURLからダッシュボードで一覧から確認できます。
URL: http://localhost:30299/dashboard

<img width="1194" alt="スクリーンショット 2018-12-05 20.00.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/698e0ec8-e13b-9ffd-52c9-0294d4d6ca1d.png">

> 何故か今回は動画ファイルが破損していましたが、原因究明まではしていません(何故だ…)

# まとめ

VMやオンプレの環境でSeleniumGridやZaleniumを立てようと思うと、CLIの環境にブラウザを起動する必要が出て、インストール作業が非常に煩雑になると思います。

その点、コンテナだとブラウザのインストールが不要・どの環境でも動きメリットを強く感じます。
またSeleniumNodeが増えても、Podの配置をKubernetes側で制御して配置してくれるので非常に簡単にスケールアウトができるようになって感動しています。

自動テストの環境構築の一助になれば幸いです。

以上