Concourse CI/CD Meetup Tokyo #6の登壇資料です。
アジェンダ
- Selenium Grid
- Selenium Grid from Concourse
- Selenium Grid from Concourse on Kubernetes
- やってみよう
- Future Work: Selenium Grid from Concourse on Kubernetes on AWS
Selenium Grid
複数のマシンやコンテナにSeleniumのテストを分散並列実行させるためのプロキシ
用語
WebDriver プログラムしたとおりにブラウザを動かすドライバ。プログラムから見れば、ドライバ=ブラウザ。同じマシン・コンテナ内で実際のブラウザの起動やコントロールを行う
RemoteWebDriver 別プロセスで動いているWebDriverを動かすWebDriver
Selenium Node RemoteWebDriver Serverが実行されているマシン、コンテナ
Selenium Grid from Concourse
ConcourseタスクからSeleniumテストの分散・並列実行
メリット
テストの所要時間短縮
複数ブラウザを対象にテスト
BrowserStack、SauceLabsなどの有償サービスを使わなくてもできる)
デメリット
ConcourseとSelenium Gridのクラスタをそれぞれセットアップする手間
Selenium GridのURLがローカルと本番で異なる
⎈ Selenium Grid from Concourse on Kubernetes
Kubernetes上でConcourseとSelenium Gridをホストする
メリット
ConcourseとSelenium Gridのクラスタをそれぞれセットアップ
Selenium GridのURLがローカルと本番で一緒 - ローカル環境でも本番環境でも全く同じパイプライン・タスクでSeleniumテストの分散・並列実行ができる
やってみよう
- ローカルKubernetes(K8S)クラスタ作成
- K8SにSelenium Gridをインストール
- ChromeノードにVNCで接続
- Pythonのreplでテストコードを書く
- pytestでテスト実行
- K8SにConcourseをインストール
- Concourse Job/Taskからテスト実行
- テスト実行の様子を観察
- プロセス並列で実行
- 上級編: いちいちgit commit&pushせずにテストしたい
やってみよう
- ローカルKubernetes(K8S)クラスタ作成
- K8SにSelenium Gridをインストール
- ChromeノードにVNCで接続
- Pythonのreplでテストコードを書く
- pytestでテスト実行
- K8SにConcourseをインストール
- Concourse Job/Taskからテスト実行 ←Concourseの話
- テスト実行の様子を観察
- プロセス並列で実行
- 上級編: いちいちgit commit&pushせずにテストしたい ←Concourseの話
ローカルK8Sクラスタ作成
minikube start --cpus 4 --memory 6144
- CPUは4コア(Concourse Web/Workerでそれぞれ1コア+その他で2コア)
- メモリは多めに6GB確保
- minikubeVMのデフォルトメモリサイズは1024M
- MySQL, Redis, Selenium Grid Hub, Selenium Node
Firefox/Chromeを起動するとだいたい4GBくらい - Concourse Web/Workerでだいたい1GBくらい
Selenium Gridインストール
helm install stable/selenium \
--set chromeDebug.enabled=true \
--set firefoxDebug.enabled=true
--name selenium-grid
- ただし、2017/06/05現在下記PRのマージが必要
- https://github.com/kubernetes/charts/pull/1239
- それまでは自分でHelm Chartをビルド&インストール
helm package . && helm install selenium-0.1.1.tgz --set chromeDebug.enabled=true --set firefoxDebug.enabled=true --name selenium-grid
ChromeノードにVNCで接続
kubectl port-forward --namespace default \
$(kubectl get pods --namespace default \
-l app=selenium-grid-selenium-chrome-debug \
-o jsonpath='{ .items[0].metadata.name }') 5900
macOS標準装備のVNCクライアントで接続
open vnc://127.0.0.1:5900

SeleniumのRemoteWebDriver実行用コンテナ作成
kubectl run selenium-python --image=google/python-hello
export PODNAME=`kubectl get pods --selector="run=selenium-python" --output=template --template="{{with index .items 0}}{{.metadata.name}}{{end}}"`
# 初回はdocker pullに時間がかかるので数分待ってから・・・
kubectl exec --stdin=true --tty=true $PODNAME bash
pip install selenium
python
Pythonのreplでテストコードを書く
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
driver = webdriver.Remote(
command_executor='http://selenium-grid-selenium-hub:4444/wd/hub',
desired_capabilities=getattr(DesiredCapabilities, "CHROME")
)
driver.get("http://google.com")

assert "google" in driver.page_source
driver.close()
pytestでテスト実行
pip install -U pytest
import time
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities im
port DesiredCapabilities
def test_chrome():
driver = webdriver.Remote(
command_executor='http://selenium-grid-selen
ium-hub:4444/wd/hub',
desired_capabilities=getattr(DesiredCapabili
ties, "CHROME")
)
driver.get("http://google.com")
time.sleep(5)
assert "google" in driver.page_source
driver.close()
pytest
K8SにConcourseをインストール
helm install stable/concourse --name concourse
export POD_NAME=$(kubectl get pods --namespace default -l "app=concourse-web" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use Concourse"
kubectl port-forward --namespace default $POD_NAME 8080:8080
open http://127.0.0.1:8080/
Concourseパイプラインをつくる
jobs:
- name: e2e
plan:
- task: pytest-selenium
config:
platform: linux
image_resource:
type: docker-image
source: {repository: google/python-hello}
run:
path: bash
args:
- -exc
- |
apt-get update -y
apt-get install curl -y
pip install selenium
pip install -U pytest
cat << EOS > e2e_test.py
import time
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
def test_chrome():
driver = webdriver.Remote(
command_executor='http://selenium-grid-selenium-hub:4444/wd/hub',
desired_capabilities=getattr(DesiredCapabilities, "CHROME")
)
driver.get("http://google.com")
time.sleep(5)
assert "google" in driver.page_source
driver.close()
EOS
pytest
$ fly login -t k8s -c http://127.0.0.1:8080 -u concourse -p concourse
$ fly -t k8s set-pipeline -p e2e -c e2e.yaml
$ fly -t k8s unpause-pipeline -p e2e
パイプライン感なし
Concourse Job/Taskからテスト実行
[+]ボタンをクリックするか、fly -t k8s trigger-job -j e2e/e2e
を実行

Concourse Job/Taskからテスト実行
pytest実行中。RemoteWebDriver経由でSelenium NodeのChromeが動いているはず。

テスト実行の様子を観察
実際、VNCクライアントで見てみるとChromeが動いていることがわかる。

しばらく待つとテストが通る。

プロセス並列で実行
pytest-xdistを使う。
pip install --upgrade setuptools
pip install pytest-xdist
...
pytest -n <プロセス数>
※ setuptoolsをアップグレードしないと-nオプションが認識されない。pythonあるある?
上級編: いちいちgit commit&pushせずにテストしたい
resource_types:
- name: kube-broker
type: docker-image
source:
# Replace the url
repository: mumoshu/concourse-kube-broker-resource
resources:
- name: test-sources
type: kube-broker
source:
configmap: foobar
path: /app
k8s_ca: {{k8s_ca_base64}}
k8s_service_account_token: {{k8s_service_account_token_base64}}
jobs:
- name: e2e
plan:
- get: test-sources
- task: pytest-selenium
config:
inputs:
- name: test-sources
platform: linux
image_resource:
type: docker-image
source: {repository: google/python-hello}
run:
path: bash
args:
- -exc
- |
apt-get update -y
apt-get install curl -y
pip install selenium
pip install -U pytest
cd test-sources
pytest
以下のようなconfigmapで、resourceのinputをとってくる元podを指定
$ kubectl get configmap foobar -o yaml
apiVersion: v1
data:
kubernetes.pod.name: selenium-python-2632905994-7ck0z
kind: ConfigMap
metadata:
creationTimestamp: 2017-06-06T04:31:48Z
name: foobar
namespace: default
resourceVersion: "75860"
selfLink: /api/v1/namespaces/default/configmaps/foobar
uid: 0e436817-4a71-11e7-affc-080027d5f559
Concourse ResourceからK8S APIにアクセスするために利用するトークンを取得
token_name=$(kubectl get secret | grep default| awk '{ print $1 }')
SERVICE_ACCOUNT_TOKEN=$(kubectl get secret $token_name -o jsonpath={.data.token})
トークンとクラスタのCAをパラメータにパイプラインを作成
fly -t k8s set-pipeline -p e2e-2 -c e2e-2.yml -v k8s_ca_base64=$(cat $HOME/.minikube/ca.crt | base64) -v k8s_service_account_token_base64=$SERVICE_ACCOUNT_TOKEN
fly -t k8s unpause-pipeline -p e2e-2
kube-broker-resource
concourse-smuggler-resourceというadhocなconcourseリソースつくるメタなConcourseリソースをベースに作成
smuggler.yml
# filter_raw_request: true
commands:
check: |
KUBE_URL=${SMUGGLER_k8s_api_endpoint_url:-https://kubernetes}
NAMESPACE=${SMUGGLER_k8s_namespace:-default}
KUBECTL="/usr/local/bin/kubectl --server=$KUBE_URL --namespace=$NAMESPACE"
# configure SSL Certs if available
if [[ "$KUBE_URL" =~ https.* ]]; then
KUBE_CA_BASE64="${SMUGGLER_k8s_ca}"
KUBE_SERVICE_ACCOUNT_TOKEN_BASE64=${SMUGGLER_k8s_service_account_token}
CA_PATH="/root/.kube/ca.pem"
mkdir -p /root/.kube
echo "$KUBE_CA_BASE64" | base64 -d > $CA_PATH
KUBE_SERVICE_ACCOUNT_TOKEN=$(echo "$KUBE_SERVICE_ACCOUNT_TOKEN_BASE64" | base64 -d)
KUBECTL="$KUBECTL --certificate-authority=$CA_PATH --token=$KUBE_SERVICE_ACCOUNT_TOKEN"
fi
$KUBECTL get configmap ${SMUGGLER_configmap} >/dev/null
$KUBECTL get configmaps ${SMUGGLER_configmap} -o json \
| jq -r '.data["kubernetes.pod.name"]' > kubernetes-pod-name
kube_pod_name=$(cat kubernetes-pod-name)
if [ "$kube_pod_name" == "" ]; then
echo No kubernetes pod named $kube_pod_name exists 1>&2
exit 1
fi
mkdir -p /copied
$KUBECTL cp $kube_pod_name:${SMUGGLER_path} /copied${SMUGGLER_path} 1>&2
find /copied${SMUGGLER_path} -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | cut -d' ' -f 1 > current-version
current_version=$(cat current-version)
if [ "$current_version" != "${SMUGGLER_VERSION_ID:-}" ]; then
echo "$current_version" >> ${SMUGGLER_OUTPUT_DIR}/versions
fi
in: |
KUBE_URL=${SMUGGLER_k8s_api_endpoint_url:-https://kubernetes}
NAMESPACE=${SMUGGLER_k8s_namespace:-default}
KUBECTL="/usr/local/bin/kubectl --server=$KUBE_URL --namespace=$NAMESPACE"
# configure SSL Certs if available
if [[ "$KUBE_URL" =~ https.* ]]; then
KUBE_CA_BASE64="${SMUGGLER_k8s_ca}"
KUBE_SERVICE_ACCOUNT_TOKEN_BASE64=${SMUGGLER_k8s_service_account_token}
CA_PATH="/root/.kube/ca.pem"
mkdir -p /root/.kube
echo "$KUBE_CA_BASE64" | base64 -d > $CA_PATH
KUBE_SERVICE_ACCOUNT_TOKEN=$(echo "$KUBE_SERVICE_ACCOUNT_TOKEN_BASE64" | base64 -d)
KUBECTL="$KUBECTL --certificate-authority=$CA_PATH --token=$KUBE_SERVICE_ACCOUNT_TOKEN"
fi
$KUBECTL get configmap ${SMUGGLER_configmap} >/dev/null
$KUBECTL get configmaps ${SMUGGLER_configmap} -o json \
| jq -r '.data["kubernetes.pod.name"]' > kubernetes-pod-name
kube_pod_name=$(cat kubernetes-pod-name)
if [ "$kube_pod_name" == "" ]; then
echo No kubernetes pod named $kube_pod_name exists 1>&2
exit 1
fi
$KUBECTL cp $kube_pod_name:${SMUGGLER_path} ${SMUGGLER_DESTINATION_DIR} 1>&2
Future Work: Selenium from Concourse on Kubernetes on AWS
- AWS Device Farm --> Browser --> Temporary Endpoint --> (VPN -->) Target Webapp
-
AWS Device Farm iOS、Androidデバイスをリモートで時間貸ししてくれるサービス
- ただし、VPCにはつなげられない、VPNも使えないという制限がある。リモートで借りたデバイスはインターネットにしかアクセスできないということ。
- 課題 「まだインターネットに公開したくない、開発中Webアプリ」をDevice Farmでどうやって安全にテストする?
- 案: テスト対象のWebアプリに、インターネットからアクセス可能な一時URLを発行する
- テスト対象のWebアプリがローカルマシンにある場合は、VPNを使ってインターネットからのアクセスをローカルK8S内で動いているWebアプリにフォワードする
- 構成 Device --> 一時URL --> テスト環境K8S --> kube-openvpn --> ローカル環境K8S --> テスト対象Webアプリ
- メリット スマフォからのE2Eテスト用パイプラインさえもローカルで動かせる! DevProd Parity!
参考リンク
まとめ
- ConcourseでSeleniumの分散・並列テスト実行
- ローカルマシンでも本番サーバでも全く同じパイプラインでテストできる
- Concourse on K8Sならコードをいちいちpushしなくてもパイプラインをテストできる
自己紹介 & おわり
twitter/github/slack.k8s.io: @mumoshu (むもしゅ)
Primary maintainer of kubernetes-incubator/kube-aws
Fin