В предыдущей статье я объяснил, как использовать Google Cloud Functions для создания системы брендированного веб-сайта. Сегодня давайте посмотрим, как использовать движок Kubernetes для проведения сквозных тестов.

Интеграция сборки и развертывания вашего веб-сайта в программное обеспечение CI / CD довольно часто вызывает некоторые проблемы.

В нашем случае, в Travix, мы работаем с GoCD для управления нашим CI / CD. Для управления нашей инфраструктурой (CI / CD / front-end production) мы полагаемся на Google Cloud Platform, а точнее на GKE.

Travix - это OTA (Интернет-туристическое агентство), которое управляет до 40 веб-сайтами по всему миру под брендами Cheaptickets, Vayama, Budgetair, Vliegwinkel и Flugladen.

GoCD работает с системой конвейеров. В конвейере может быть один или несколько этапов, в рамках которых может быть одно или несколько заданий.

Чтобы выполнить конвейер, его этапы выполняются агентом. Агент - это работник с заданным набором функций. Каждый этап развертывается в собственном рабочем пространстве. Он выполняет свою работу до конца, после чего завершается. На одном агенте может выполняться множество этапов.

Когда количество конвейеров увеличивается, также увеличивается сложность ваших тестов и время, необходимое для их выполнения. Вы также можете развернуть дополнительную инфраструктуру для своих тестов на своем сервере. Например, для ваших сквозных тестов вы можете развернуть свой собственный Selenium Grid. Однако это делает вашу архитектуру немного более сложной в обслуживании.

1. Контекст

Здесь мы поговорим о интерфейсных e2e-тестах. Допустим, у нас есть 50 фирменных веб-сайтов с общим исходным кодом, у которых есть собственный набор настроек / функций, которые делают их немного уникальными.

Подобно тому, что я описал ранее, если вы хотите добавить несколько тестов в свой поток выпуска, это может стать довольно огромной инфраструктурой, довольно сложной и довольно болезненной в использовании и обслуживании.

Наши тесты e2e написаны на NodeJS с использованием фреймворка webdriverio. Мы используем Babel для небольшого синтаксиса (хипстеры JavaScript). Проект хранится в репозитории Git. Основными пользователями тестов e2e являются наши фронтенд-разработчики. Они разрабатывают пользовательский интерфейс, а затем создают объекты страницы для написания сценариев для тестов.

Каждый раз, когда мы создаем один из 50 веб-сайтов, мы запускаем тесты e2e для этого веб-сайта с использованием Chrome и Firefox. Пока они не пройдут, сайт не будет запущен в рабочую среду. Это означает, что каждый раз, когда я хочу выпустить все веб-сайты, я должен дважды клонировать свой репозиторий, выполняя 2 npm install для каждого сайта. Я так провожу свои тесты e2e в 2 * 50 раз.

100-кратный запуск вашего этапа может привести к некоторым проблемам с вашими агентами (потребление памяти из-за использования среды выполнения Babel, ~ 1 ГБ ОЗУ, доступность ваших агентов). Говоря о времени, 1 этап занимает примерно от 4 до 5 минут. Поэтому нам нужно подождать почти 100 * 4,5 минут, прежде чем мы сможем выпустить все сайты.

Как сократить время, необходимое для запуска наших тестов e2e? Как сделать их более дружественными к памяти? Как снизить сложность нашей инфраструктуры?

Теперь, когда мы представили ситуацию и записали поднятые ею вопросы, давайте на них ответим.

Мы могли бы сократить время выполнения не за счет увеличения количества агентов или их доступной памяти, а за счет использования агента для простого создания задания в нашем кластере Kubernetes.

Задание Kubernetes - это под (один или несколько контейнеров), выполняющий конкретную задачу, ограниченную по времени (в отличие от микросервиса или сервера, которые постоянно работают). Задание выполняется до тех пор, пока оно не завершится в результате сбоя или успеха.

Здесь мы хотим, чтобы одна работа выполняла тесты e2e, предоставляя браузер, когда это необходимо, и сохраняла отчеты из нашей платформы в некотором постоянном хранилище.

2. Давайте поместим вещи в контейнеры

Итак, сначала мы собираемся поместить наши тесты в контейнер. Для этого давайте сначала опишем, как мы выполняем сквозное тестирование. У нас есть линтер для тестирования исходного кода, затем у нас есть компиляция и, наконец, мы можем запускать наши скрипты с NodeJS.

Вот файл Docker, в котором используются многоступенчатые сборки для создания контейнера, и файл JavaScript, тестирующий «наш сайт» с его package.json:

Чтобы создать образ, убедитесь, что вы используете Docker 17.09+ (я использую Docker 17.12.0-ce-mac45, канал Edge). Клонируйте Gist в своей рабочей области, cd в каталог, запустите docker-machine, создайте исходную среду и создайте образ:

$> git clone https://gist.github.com/1e4518f09a4d28b32d78bcce1411c6e3.git gist
$> cd gist
$> docker-machine start
$> eval $(docker-machine env)
$> docker build -t my-repository-name/e2etest .

Вот он, ваш контейнер построен. Чтобы протестировать наши скрипты в Chrome, давайте смонтируем образ, содержащий браузер Chrome. Для этого мы будем использовать официальный автономный образ Chrome от SeleniumHQ.

Запустите эту команду в своем терминале:

$> docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome

Мы запускаем образ, выполняющий Chrome как демон. Вы можете получить доступ к браузеру через порт 4444, используя IP-адрес вашего Docker-компьютера (выполните docker-machine ip в своем терминале, чтобы увидеть IP-адрес).

Теперь давайте запустим наши тесты на нашей машине:

$> docker run -e BROWSER=chrome -e SELENIUM_HOST=$(docker-machine ip) -e SELENIUM_PORT=4444 -t my-repository-name/e2etest
Title is: WebdriverIO at DuckDuckGo

Как видите, когда мы запускаем контейнер в неинтерактивном режиме, он выполняет наши сквозные тесты.

Давайте посмотрим, как управлять отчетами. Мы добавляем в наши сценарии e2e вызов метода saveScreenshot Webdriver.io, чтобы создать снимок экрана из браузера страницы, которую мы тестируем. Мы также выводим некоторые подробности в каталог report.

Теперь ваш сценарий должен выглядеть так:

Теперь, когда мы выводим журналы и создаем снимок экрана в каталоге report, давайте подключим том к образу Docker для хранения файлов.

Мы будем использовать крошечный скрипт с использованием fs.watch. Каждый раз, когда файл изменяется, сценарий помещает его в Google Cloud Storage Bucket. Таким образом, у нас есть постоянное хранилище для наших отчетов.

Вот небольшой скрипт, который просматривает файлы и загружает их в корзину со своим файлом Docker и его package.json.

Клонируйте Gist в своем рабочем пространстве, создайте контейнер.

$> git clone https://gist.github.com/3efff749b08f0b95ca0fa907a7c56597.git gist-catcher
$> cd gist-catcher
$> docker build -t my-repository-name/catcher .

Когда это будет сделано, создайте каталог report в той же папке.

Теперь давайте запустим контейнер в терминале и воспользуемся другим терминалом для запуска тестов e2e.

Вы должны увидеть что-то эквивалентное:

Терминал 1

$> docker run -e BROWSER=chrome -e SELENIUM_HOST=$(docker-machine ip) -e SELENIUM_PORT=4444 -v /path/to/workspace/report-catcher/report:/var/app/report -t my-repository-name/e2etest
Title is: WebdriverIO at DuckDuckGo

Терминал 2

$> docker run -v $PWD/report:/var/app/report -t my-repository-name/catcher
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
screenshot.jpg change
screenshot.jpg change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change
chrome.2018-01-15T23-34-04.1.log change

Контейнеры catcher и e2etest связаны с помощью тома $PWD/report.

3. Развертывание наших контейнеров в Kubernetes

Теперь, когда у нас все запущено на моем компьютере, давайте развернем контейнеры в кластере Kubernetes.

Предположим, что у вас уже есть кластер Kubernetes. Кроме того, мы предполагаем, как и мы, что вы работаете с облачной платформой Google и имеете достаточные учетные данные для создания учетной записи службы приложений, которая может вести запись в сегменте облачного хранилища Google. На следующих шагах потребуется также доступ к репозиторию Docker для отправки образов.

Для этого сделайте следующее:

$> docker push my-repository-name/e2etest
$> docker push my-repository-name/catcher

Начнем манифест Kubernetes.

Сначала мы создаем заданное пространство имен для запуска наших заданий и объявляем основной контейнер нашего задания.

apiVersion: v1
kind: Namespace
metadata:
  name: my-end-to-end-tests
---
apiVersion: batch/v1
kind: Job
metadata:
  name: e2e-chrome-mysite-1
  namespace: my-end-to-end-tests
spec:
  parallelism: 1
  completions: 1
  backoffLimit: 3
  activeDeadlineSeconds: 20
  template:
    metadata:
      labels:
        app: "e2e-chrome-mysite-1"
        run: "1"
        version: "1"
    spec:
      restartPolicy: Never
      containers:
      - name: chrome
        image: my-repository-name/container-e2e
        env:
        - name: "TARGET_URL"
          value: "http://www.mywebsite.com"
        - name: "BROWSER"
          value: "chrome"
        - name: "SELENIUM_HOST"
          value: "127.0.0.1"
        - name: "SELENIUM_PORT"
          value: "4444"

Мы (пытаемся) ограничить нашу работу запуском только один раз в кластере, установив parallelism на 1 и completion на 1. Я имею в виду try, потому что может случиться так, что модуль будет запущен дважды. Мы также ограничиваем количество backoffLimit и activeDeadlineSeconds, чтобы избежать бесконечного цикла при попытке запустить модуль.

SELENIUM_HOST установлен на 127.0.0.1. Автономный браузер будет выполнять ту же работу, поэтому все контейнеры используют один и тот же IP-адрес.

Мы должны создать секрет, необходимый для доступа к API GCP. Он нужен для нашего контейнера sidecar, который отправляет отчеты в Google Cloud Storage Bucket. Как и то, что мы делали локально, мы также разделяем том с предыдущим контейнером.

После объявления пространства имен добавьте секретное объявление:

---
apiVersion: v1
kind: Secret
metadata:
  name: e2e-chrome-mysite-1-secrets
  namespace: my-end-to-end-tests
  labels:
    app: e2e-chrome-mysite-1
type: Opaque
data:
  gcloud.json: "my-base64-encoded-file"
---

Добавьте следующее в конце предыдущего объявления контейнера:

        volumeMounts:
          - name: reports
            mountPath: /var/app/reports

Объявим наш новый контейнер:

        - name: catcher
          image: my-repository-name/catcher
          env:
          - name: "VOLUME_PATH"
            value: "/var/app/report"
          - name: "GCLOUD_APPLICATION_CREDIENTIALS"
            value: "/home/.config/gcloud/auth.json"
          - name: "GCLOUD_BUCKET_NAME"
            value: "my-bucket"
          - name: "GCLOUD_PROJECT_NAME"
            value: "my-project"
          volumeMounts:
          - name: reports
            mountPath: /var/app/reports
          - name: secrets
            mountPath: /home/.config/gcloud

А теперь объявляем свои объемы:

      volumes:
        # extended memory for the browser
        - name: extended-mem
          hostPath:
            path: /dev/shm
        # shared volume where we write the report from the main container.
        - name: reports
          emptyDir: {}
        # secret volume containing the app credentials for the side container pushing to GCP
        - name: secrets
          secret:
            secretName: e2e-chrome-mysite-1-secrets

Как вы уже заметили, я добавил еще один том /dev/shm. Он используется для расширения памяти последнего необходимого нам контейнера sidecar - браузера.

Браузер может появиться откуда угодно, как только ваш кластер сможет вытащить изображение.

Подобно тому, что я написал ранее, мы будем использовать стандартный образ SeleniumHQ Docker для автономного Chrome.

Добавьте эти строки после объявления контейнера-улавливателя:

        - name: chrome-browser
          image: selenium/standalone-chrome:3.8.1-erbium
          ports:
          - containerPort: 4444
          env:
          - name: SCREEN_WIDTH
            value: "1680"
          - name: SCREEN_HEIGHT
            value: "1050"
          volumeMounts:
            - mountPath: /dev/shm
              name: extended-mem

Мы предоставляем контейнеру объем /dev/shm, чтобы уменьшить вероятность сбоя браузера.

Вы можете найти полный манифест здесь.

Мы готовы развернуть свою работу. Давайте запустим в терминале следующие команды:

$> kubectl apply -f ./kubernetes.yaml

Если вы запустите kubectl get ns, вы увидите, что ваше пространство имен было правильно создано.

Давайте теперь посмотрим, запустился ли модуль:

$> kubectl get po -n my-end-to-end-tests -w

Вы должны увидеть статус своей работы.

Чтобы просмотреть журналы, выполните следующую команду:

$> kubectl logs e2e-chrome-mysite-1-rand1 chrome -n my-end-to-end-tests

Вы можете увидеть журналы, поступающие из каждого контейнера, просто поиграйте со вторым параметром kubectl logs, значение может быть chrome, chrome-browser или catcher.

Когда ваш основной контейнер завершится, вы можете узнать его статус и код выхода, используя следующий маленький скрипт bash:

Чтобы пойти дальше, вы можете автоматизировать все шаги, которые мы прошли для запуска задания и его завершения в вашем CI.

Здесь мы сделали все вручную (запуск задания, завершение задания). Однако сегодня вы можете найти некоторые инструменты для управления и мониторинга ваших заданий Kubernetes, такие как Azure Brigade или Apache Airflow.

Кроме того, поскольку мы отправляем наши отчеты в корзину Google Cloud Storage, вы можете легко реализовать их анализ с помощью функции Google Cloud. Исходя из этого, вы можете создать собственную систему оповещений с помощью Slack.

Но, несмотря на все это, вы должны выбрать, что вы хотите делать :)

Наслаждаться !