Передовые методы тестирования рулевых диаграмм

Фото Joseph Barrientos on Unsplash

Я рад поделиться с вами подробными сведениями и опытом, которые я получил на пути к поиску хорошего способа написания автоматизированных тестов для диаграмм Helm. В конце этой статьи я представлю вам текущее решение, которое мы используем и которое отвечает всем нашим требованиям.

Я инженер распределенных систем, работаю над проектом Camunda Zeebe, который является частью Camunda Cloud. Меня очень интересуют темы SRE, поэтому я начал поддерживать диаграммы Helm для Camunda Cloud.

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

Как все начиналось

Мы начали с поддерживаемых сообществом диаграмм Helm для Zeebe и связанных с Camunda Cloud инструментов, таких как Tasklist и Operate. У этого проекта была недостаточная поддержка и проблемы со стабильностью.

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

В начале 2022 года мы в Camunda хотели создать несколько новых диаграмм Helm на основе старых. Новые графики Helm должны были быть официально поддержаны Camunda. Чтобы сделать это с чистой совестью, мы хотели добавить несколько автоматизированных тестов к диаграммам.

Предварительные условия

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

  • Kubernetes
  • Helm
  • Golang

Тестирование Helm. В чем проблема?

Тестирование в мире Helm, я бы сказал, не так хорошо развито, как должно быть. Некоторые инструменты существуют, но им не хватает удобства, или они требуют слишком много шаблонного кода. Иногда не совсем понятно, как их использовать или писать.

Некоторые посты на эту тему уже существуют, но их не так много. Например:

  • Тестирование хелмовых диаграмм Kubernetes
  • Лучшие практики тестирования Helm

Нам очень помог этот пост — Automated Testing for Kubernetes and Helm Charts using Terratest.

Здесь рассказывается о том, как тестировать диаграммы Helm с помощью Terratest, о фреймворке для написания тестов для диаграмм Helm и других вещах, связанных с Kubernetes.

Мы провели сравнение Terratest, написания тестов с золотыми файлами (вот статья в блоге о том, почему вы должны их использовать) и использования Chart Testing (CT). Подробности вы можете найти в этом выпуске на GitHub.

Этот выпуск содержит сравнение между инструментами тестирования, а также несколько субъективных полевых отчетов, которые я написал во время тестирования. Это помогло мне принять некоторые решения.

Что и как тестировать

Прежде всего, мы разделили наши тесты на две части, с разными целями и задачами.

  • Шаблонные тесты (юнит-тесты) — проверяют общую структуру.
  • Интеграционные тесты — проверяют, можем ли мы установить диаграммы и использовать их.

Шаблонные тесты

С помощью шаблонных тестов мы хотим проверить общую структуру. Сюда входит соответствие yaml, не меняются ли значения по умолчанию, установлены ли они вообще.

Для тестов шаблонов мы комбинируем как золотые файлы, так и Terratest. Вообще говоря, золотые файлы хранят ожидаемый вывод определенной команды или ответа на определенный запрос. В нашем случае золотые файлы содержат отрисованные манифесты, которые выводятся после запуска шаблона helm. Это позволяет проверить, что значения по умолчанию устанавливаются и изменяются только контролируемым образом, что снижает бремя написания множества тестов.

Если мы хотим проверить конкретные свойства (или условия), мы можем использовать прямые тесты свойств с помощью Terratest. Мы еще вернемся к этому вопросу позже.

Это позволяет нам использовать один инструмент (Terratest) и разделять тесты для каждого манифеста, например, тест для Zeebe statefulset, развертывание шлюза Zeebe и т.д. Тесты могут быть легко запущены через командную строку или IDE, а также CI.

Интеграционные тесты

С помощью интеграционных тестов мы хотим проверить две вещи:

  1. Могут ли графики быть развернуты в Kubernetes и приняты K8s API.
  2. Работают ли сервисы и могут ли они работать друг с другом.

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

Итак, чтобы развернуть это, вот потенциальные случаи неудач, которые мы можем найти с помощью таких тестов:

  1. Спецификации, которые находятся в неправильном месте (выглядят как правильный yaml), но не принимаются K8s API.
  2. Сервисы, которые не готовы из-за ошибок конфигурации, и они не могут связаться друг с другом.

Первый случай мы могли бы решить с помощью других инструментов, которые проверяют манифесты на основе K8s API, но не второй.

Для написания интеграционных тестов мы попробовали инструмент Chart Testing и Terratest. Мы выбрали Terratest, а не Chart Testing. Если вы хотите узнать, почему, прочтите следующий раздел, в противном случае вы можете просто пропустить его.

Тестирование диаграмм

При попытке написать тесты с помощью Chart Testing мы столкнулись с несколькими проблемами, которые сделали инструмент сложным в использовании, а тесты — сложными в сопровождении.

Например, возможности настройки процесса тестирования кажутся довольно ограниченными — доступные опции см. в документации CT Install. В частности, на этапе установки Helm наши тесты развертывают множество компонентов (Elasticsearch, Zeebe), которым требуется время, чтобы стать готовыми. Однако Chart Testing по умолчанию завершает работу через три минуты, и мы не нашли способа настроить этот параметр. Таким образом, мы так и не смогли запустить успешный тест с помощью ct CLI.

Еще одной болезненной точкой был способ доставки, выполнения тестов и, в конечном счете, предоставления результатов. Инструмент Chart Testing, проще говоря, обернут в Helm CLI, что означает, что он будет выполнять команды helm install и helm test. Для выполнения с помощью команды helm test тесты должны быть настроены и развернуты как часть диаграммы Helm. Это означает, что тесты должны быть встроены в образ Docker, что может быть не очень удобно, а также необходимо изменить схему Helm, чтобы она поставлялась с дополнительными настройками тестов.

Если тесты не работают в CI и вы хотите воспроизвести это, вам понадобится локальный ct CLI, и запустите ct install, чтобы переразвернуть всю диаграмму Helm и запустить тесты. Когда тесты терпят неудачу, печатаются полные журналы всех контейнеров, что может быть большим объемом данных для проверки. Мы обнаружили, что было трудно выполнять итерации в тестах и довольно сложно отлаживать их в случае сбоя.

Все вышеперечисленные причины подтолкнули нас к использованию Terratest (см. следующий раздел) для написания тестов. Преимущество здесь в том, что у нас есть один инструмент для обоих (модульных и ИТ-тестов) и больше контроля над ними. Это облегчает запуск и отладку тестов. В целом, тесты также довольно просты в написании, а сбои легко понять.

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

Тесты диаграмм Helm на практике

В следующем разделе я хотел бы представить, как мы используем Terratest и как выглядят наши новые тесты для графиков Helm.

Тест золотых файлов

Мы написали базовый тест, который отображает заданные шаблоны Helm и сравнивает их с золотыми файлами. Золотые файлы могут быть сгенерированы с помощью отдельного флага. Золотые файлы отслеживаются в git, что позволяет нам легко увидеть изменения через git diff. Это означает, что если мы изменим какие-либо параметры по умолчанию, мы сможем непосредственно увидеть результирующие отрисованные манифесты. Эти тесты гарантируют, что шаблоны диаграмм Helm отображаются правильно, а вывод шаблонов изменяется контролируемым образом.

Золотая база

package golden

import (
    "flag"
    "io/ioutil"

    "regexp"

    "github.com/gruntwork-io/terratest/modules/helm"
    "github.com/gruntwork-io/terratest/modules/k8s"
    "github.com/stretchr/testify/suite"
)

var update = flag.Bool("update-golden", false, "update golden test output files")

type TemplateGoldenTest struct {
    suite.Suite
    ChartPath string
    Release string
    Namespace string
    GoldenFileName string
    Templates []string
    SetValues map[string]string
}

func (s *TemplateGoldenTest) TestContainerGoldenTestDefaults() {
    options := &helm.Options{
        KubectlOptions: k8s.NewKubectlOptions("", "", s.Namespace),
        SetValues: s.SetValues,
    }
    output := helm.RenderTemplate(s.T(), options, s.ChartPath, s.Release, s.Templates)
    regex := regexp.MustCompile(`s+helm.sh/chart:s+.*`)
    bytes := regex.ReplaceAll([]byte(output), []byte(""))
    output = string(bytes)

    goldenFile := "golden/" + s.GoldenFileName + ".golden.yaml"

    if *update {
        err := ioutil.WriteFile(goldenFile, bytes, 0644)
        s.Require().NoError(err, "Golden file was not writable")
    }

    expected, err := ioutil.ReadFile(goldenFile)

    // then
    s.Require().NoError(err, "Golden file doesn't exist or was not readable")
    s.Require().Equal(string(expected), output)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Тест базы позволяет нам легко добавлять/писать новые тесты золотого файла для каждого из наших подшаблонов. Например, у нас есть следующий тест для подграфика Zeebe:

package zeebe

import (
    "path/filepath"
    "strings"
    "testing"

    "camunda-cloud-helm/charts/ccsm-helm/test/golden"

    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/stretchr/testify/require"
    "github.com/stretchr/testify/suite"
)

func TestGoldenDefaultsTemplate(t *testing.T) {
    t.Parallel()

    chartPath, err := filepath.Abs("../../")
    require.NoError(t, err)
    templateNames := []string{"service", "serviceaccount", "statefulset", "configmap"}

    for _, name := range templateNames {
        suite.Run(t, &golden.TemplateGoldenTest{
            ChartPath: chartPath,
            Release: "ccsm-helm-test",
            Namespace: "ccsm-helm-" + strings.ToLower(random.UniqueId()),
            GoldenFileName: name,
            Templates: []string{"charts/zeebe/templates/" + name + ".yaml"},
        })
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Здесь мы тестируем ресурсы Zeebe: service, serviceaccount, statefulset и confimap со значениями по умолчанию против золотых значений. Вот золотые файлы.

Тест свойств:

Как описано выше, иногда мы хотим протестировать определенные свойства, например, условия в наших шаблонах. Здесь проще написать специальные тесты Terratest.

Мы делаем это для каждого манифеста, например, statefulset , и называем его statefulset_test.go.

В таком тестовом файле go у нас есть базовая структура, которая выглядит следующим образом:

type statefulSetTest struct {
    suite.Suite
    chartPath string
    release string
    namespace string
    templates []string
}

func TestStatefulSetTemplate(t *testing.T) {
    t.Parallel()

    chartPath, err := filepath.Abs("../../")
    require.NoError(t, err)

    suite.Run(t, &statefulSetTest{
        chartPath: chartPath,
        release: "ccsm-helm-test",
        namespace: "ccsm-helm-" + strings.ToLower(random.UniqueId()),
        templates: []string{"charts/zeebe/templates/statefulset.yaml"},
    })
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

spec:
      {{- if .Values.priorityClassName }}
      priorityClassName: {{ .Values.priorityClassName | quote }}
      {{- end }}
Вход в полноэкранный режим Выход из полноэкранного режима

Тогда мы можем легко добавить такие тесты в файл statefulset_test.go. Это будет выглядеть следующим образом:

func (s *statefulSetTest) TestContainerSetPriorityClassName() {
    // given
    options := &helm.Options{
        SetValues: map[string]string{
            "zeebe.priorityClassName": "PRIO",
        },
        KubectlOptions: k8s.NewKubectlOptions("", "", s.namespace),
    }

    // when
    output := helm.RenderTemplate(s.T(), options, s.chartPath, s.release, s.templates)
    var statefulSet v1.StatefulSet
    helm.UnmarshalK8SYaml(s.T(), output, &statefulSet)

    // then
    s.Require().Equal("PRIO", statefulSet.Spec.Template.Spec.PriorityClassName)
}
Войти в полноэкранный режим Выход из полноэкранного режима

В этом тесте мы устанавливаем priorityClassName в пользовательское значение, например «PRIO», отрисовываем шаблон и проверяем, что объект (statefulset) содержит это значение.

Интеграционный тест

Terratest позволяет нам писать не только шаблонные тесты, но и настоящие интеграционные тесты. Это означает, что мы можем получить доступ к кластеру Kubernetes, создать пространства имен, установить диаграмму Helm и проверить определенные свойства.

Я представлю здесь только базовую настройку, поскольку в противном случае она зашла бы слишком далеко. Если вам интересно, как выглядит наш интеграционный тест, посмотрите здесь. Здесь мы настраиваем пространства имен, устанавливаем диаграммы Helm и тестируем каждую развернутую службу.

Базовая настройка:

//go:build integration
// +build integration

package integration

import (
    "os"
    "path/filepath"
    "strings"
    "time"

    "context"
    "testing"

    "github.com/gruntwork-io/terratest/modules/helm"
    "github.com/gruntwork-io/terratest/modules/k8s"
    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/stretchr/testify/require"
    "github.com/stretchr/testify/suite"
    v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type integrationTest struct {
    suite.Suite
    chartPath string
    release string
    namespace string
    kubeOptions *k8s.KubectlOptions
}

func TestIntegration(t *testing.T) {
    chartPath, err := filepath.Abs("../../")
    require.NoError(t, err)

    namespace := createNamespaceName()
    kubeOptions := k8s.NewKubectlOptions("gke_<project>_europe-west1-b_<project-name>", "", namespace)

    suite.Run(t, &integrationTest{
        chartPath: chartPath,
        release: "zeebe-cluster-helm-it",
        namespace: namespace,
        kubeOptions: kubeOptions,
    })
}
Вход в полноэкранный режим Выход из полноэкранного режима

Подобно приведенному выше тесту свойств, у нас есть некоторая базовая структура, которая позволяет нам писать интеграционные тесты. Это настройка тестовой среды. Она позволяет нам указать целевой кластер Kubernetes через kubeOptions.

Для того чтобы отделить интеграционные тесты от обычных модульных тестов, мы используем теги go build. Первые строки выше определяют тег integration, который позволяет нам запускать тесты только через go test -tags integration ./…/integration.

Мы создаем имя пространства имен Kubernetes либо случайным образом (используя хелпер из Terratest ), либо на основе git-коммита, если он запускается как действие GitHub. К этому мы вернемся позже.

func truncateString(str string, num int) string {
   shortenStr := str
   if len(str) > num {
      shortenStr = str[0:num]
   }
   return shortenStr
}

func createNamespaceName() string {
   // if triggered by a github action the environment variable is set
   // we use it to better identify the test
   commitSHA, exist := os.LookupEnv("GITHUB_SHA")
   namespace := "ccsm-helm-"
   if !exist {
      namespace += strings.ToLower(random.UniqueId())
   } else {
      namespace += commitSHA
   }

   // max namespace length is 63 characters
   // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
   return truncateString(namespace, 63)
}
Перейдите в полноэкранный режим Выход из полноэкранного режима

Набор Go testify позволяет запускать функции до и после теста, которые мы используем для создания и удаления пространства имен.

func (s *integrationTest) SetupTest() {
   k8s.CreateNamespace(s.T(), s.kubeOptions, s.namespace)
}

func (s *integrationTest) TearDownTest() {
   k8s.DeleteNamespace(s.T(), s.kubeOptions, s.namespace)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Пример интеграционного теста довольно прост: мы устанавливаем диаграммы Helm со значениями по умолчанию и ждем, пока все капсулы станут доступны. Для этого мы можем использовать некоторые помощники, которые Terratest предлагает, например, здесь.

func (s *integrationTest) TestServicesEnd2End() {
   // given
   options := &helm.Options{
      KubectlOptions: s.kubeOptions,
   }

   // when
   helm.Install(s.T(), options, s.chartPath, s.release)

   // then
   // await that all ccsm related pods become ready
   pods := k8s.ListPods(s.T(), s.kubeOptions, v1.ListOptions{LabelSelector: "app=camunda-cloud-self-managed"})

   for _, pod := range pods {
      k8s.WaitUntilPodAvailable(s.T(), s.kubeOptions, pod.Name, 100, 1*time.Second)
   }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Как написано выше, наш реальный интеграционный тест намного сложнее, но это должно дать вам хорошее представление о том, что вы можете сделать. Поскольку Terratest написан на go, это позволило нам написать все наши тесты на go, использовать такие механики, как build tags, и использовать библиотеки go, такие как testify. Terratest позволяет легко получить доступ к API Kubernetes, выполнить команды Helm, такие как install, и проверить результат. Я очень ценю многословность, поскольку при запуске тестов шаблоны Helm также выводятся в стандартный вид, что помогает их отлаживать. После внедрения интеграционных тестов мы были вполне удовлетворены результатом и подходом к кодированию тестов, который контрастирует с наличием отдельной абстракции вокруг тестов, как это было бы при использовании инструмента Chart Testing.

После создания таких интеграционных тестов мы, конечно же, захотели их автоматизировать. Мы сделали это с помощью действий GitHub (см. следующий раздел).

Автоматизация

Как было написано выше, мы автоматизируем наши тесты с помощью GitHub Actions. Для обычных тестов это довольно просто, вы можете найти здесь пример того, как мы запускаем наши обычные шаблонные тесты.

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

Следуйте этому руководству, чтобы настроить необходимую федерацию идентификаторов рабочей нагрузки. Это рекомендуемый способ аутентификации на ресурсах Google Cloud извне и замена старого использования ключей учетных записей служб. Объединение идентификаторов рабочей нагрузки позволяет получить доступ к ресурсам напрямую, используя недолговечный маркер доступа, и устраняет бремя обслуживания и безопасности, связанное с ключами учетных записей служб.

После настройки федерации идентификаторов рабочих процессов ее использование в действиях GitHub довольно просто.

В качестве примера мы используем следующее в нашем действии GitHub:

# Add "id-token" with the intended permissions.
permissions:
  contents: 'read'
  id-token: 'write'

steps:
- uses: actions/checkout@v3
- id: 'auth'
  name: 'Authenticate to Google Cloud'
  uses: 'google-github-actions/auth@v0'
  with:
    workload_identity_provider: ‘<Workload Identity Provider resource name>’
    service_account:  ‘<service-account-name>@<project-id>.iam.gserviceaccount.com’
- id: 'get-credentials'
  name: 'Get GKE credentials'
  uses: 'google-github-actions/get-gke-credentials@v0'
  with:
    cluster_name: ‘<cluster-name>’
    location: 'europe-west1-b'
# The KUBECONFIG env var is automatically exported and picked up by kubectl.
- id: 'check-credentials'
  name: 'Check credentials'
  run: 'kubectl auth can-i create deployment'
Войти в полноэкранный режим Выйти из полноэкранного режима

Это основано на примерах google-github-actions/auth и google-github-actions/get-gke-credentials. Проверка учетных данных — это последний шаг, позволяющий проверить, достаточно ли у нас прав для создания развертывания, что необходимо для наших интеграционных тестов.

После этого нужно просто установить Helm и зайти в свой контейнер действий GitHub. Чтобы запустить интеграционный тест, можно выполнить go-тест с тегом integration build (описанным выше). Для этого мы используем Makefile . Посмотрите на полный вариант действия на GitHub.

Последние слова

Сейчас мы вполне довольны новым подходом и тестами. Написание таких тестов позволило нам обнаружить несколько проблем на наших диаграммах Helm, что весьма приятно. Их весело писать и выполнять (шаблонные тесты довольно быстрые), и это всегда дает нам хорошую обратную связь.

Запуск шаблонных тестов в GoLand

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

Я надеюсь помочь вам с помощью этих знаний и приведенных выше примеров. Не стесняйтесь связаться со мной или написать в твиттере, если у вас есть какие-либо мысли, которыми вы хотите поделиться, или лучшие идеи о том, как тестировать Helm charts 🙂

Спасибо Ahmed AbouZaid, Jonathan Ballet и Brittany des Vignes за обзор этой заметки.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *