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

Очень кстати, в недавно вышедшем релизе Gitlab 14.1, появился долгожданный функционал хранения Helm-чартов во встроенном Package Registry. Отлично, заодно и разберемся, как его использовать.

Используемые инструменты

KinD

Первый инструмент, о котором пойдет речь, буквально kubernetes-in-docker, позволяет запустить практически полноценный кластер локально на нодах-контейнерах. Под капотом использует kubeadm для настройки узлов и kustomize для слияния предоставленного конфига и сгенерированной внутри конфигурации. Полную документацию можно найти тут. Также по желанию можно установить не только ingress-nginx, манифесты которого для KinD можно найти в его репозитории, но и Ambassador или Contour.

Что примечательно, схема с оригинального сайта не совсем верна, внутри контейнера не используется dockershim, а, в соответствие последним тенденциям, kubelet общается с containerd напрямую через сокет.
Что примечательно, схема с оригинального сайта не совсем верна, внутри контейнера не используется dockershim, а, в соответствие последним тенденциям, kubelet общается с containerd напрямую через сокет.

Еще одним моментом будет отсутствие поддержки сервисов типа LoadBalancer. Поэтому до развернутого приложения нужно ходить по INTERNAL-IP ноды, узнать который можно с помощью kubectl или docker inspect, например:

kubectl get no -o wide
NAME                     STATUS   ROLES     AGE   VERSION    INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION     CONTAINER-RUNTIME
k8s-test-control-plane   Ready    master    36h   v1.19.11   172.18.1.2    <none>        Ubuntu 21.04   5.4.0-80-generic   containerd://1.5.2
k8s-test-worker          Ready    compute   36h   v1.19.11   172.18.1.3    <none>        Ubuntu 21.04   5.4.0-80-generic   containerd://1.5.2

Запущенные контейнеры-ноды помечаются лейбами io.x-k8s.kind.cluster и io.x-k8s.kind.role, в дальнейшем нам это пригодится.

Сейчас и далее предполагается, что локально уже установлен Docker, описывать в этой статье инструкцию его установки сочту излишним. Аналогично поступим и с Helm.

Устанавливаем kind в систему:

curl -Lo ./kind "https://kind.sigs.k8s.io/dl/v0.11.1/kind-$(uname)-amd64"
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

Chart Testing

Второй инструмент - chart-testing, репо и документацию которого можно найти на GitHub. В паре с chart-releaser широко применяется для организации репо Helm-чартов в Github-pages. Оба имеют соответствующие экшены: chart-testing-action и chart-releaser-action.

Инструмент позволит упростить автоматизацию тестирования наших чартов, причем отберет из них только измененные по сравнению с master/main веткой origin. Будучи добавлен в pre-commit хук, не позволит забыть увеличить версию Helm-чарта. Что он нам поможет делать:

  • Прогон линта по нашим свеженаписанным Helm-чартам.

  • Валидацию Chart.yaml.

  • Развертывание Helm-чарта в отдельный сгенерированный неймспейс текущего активного кластера kubernetes (не обязательно KinD).

  • Очистка окружения за собой вне зависимости от успешности развертывания.

Понятно, что все вышеперечисленное можно сделать и с помощью Bash-полотна, python, костыля и пары велосипедов, но мы ведь не за этим сюда пришли, правда? Хотя Bash-полотно все-таки будет.

Установка:

sudo mkdir -p /tmp/ct /etc/ct
curl -sL "https://github.com/helm/chart-testing/releases/download/v3.4.0/chart-testing_3.4.0_linux_amd64.tar.gz" | tar -xvz -C /tmp/ct
sudo mv /tmp/ct/etc/chart_schema.yaml /etc/ct/chart_schema.yaml
sudo mv /tmp/ct/etc/lintconf.yaml /etc/ct/lintconf.yaml
sudo mv /tmp/ct/ct /usr/local/bin/ct
sudo chmod +x /usr/local/bin/ct
rm -rf /tmp/ct

На этом вводное рассмотрение инструментария заканчиваем и переходим к подготовке пайплайна.

Кейс первый: публичный репозиторий и DinD

Создаем и клонируем к себе git-репо, можно использовать и уже существующий проект, но для простоты повествования опишу процесс создания “с нуля”. В корне проекта создаем и переходим в директорию charts. Создаем наш первый чарт:

mkdir charts
cd charts
helm create test-chart

В Chart.yaml нужно добавить keywords и maintainers:

keywords:
  - example

maintainers:
  - name: <Your Name>
    email: <mail@example.com>

Сразу стоит уточнить, что проверка мейнтейнеров проводится по наличию пути https://<git-repo-domain>/<maintainer-name>. Отключить её можно в следующем конфиге ct.yaml, добавив validate-maintainers: false.

Зачастую необходимо производить тестирование чарта с какими-либо предопределенными параметрами. Чтобы их передать при раскатке с помощью ct, нужно создать директорию ci в директории конкретного чарта и в ней разместить *-values.yaml, например, ./charts/test-chart/ci/test-values.yaml. Таких файлов может быть несколько, но не указывайте в них пересекающиеся значения, т.к. порядок применения или только некоторые из них выбрать нельзя.

Далее создадим файлы:

ct.yaml - конфиг chart-testing, позволит минимизировать указание ключей при запуске.

ct.yaml
# See https://github.com/helm/chart-testing#configuration
remote: origin
target-branch: main
chart-dirs:
 - charts
helm-extra-args: --timeout=120s

kind-cluster.yaml - конфиг KinD. Кластер kubernetes будет разворачиваться внутри сервиса DinD (docker-in-docker), соответственно, описываем, что api-сервер должен слушать все интерфейсы, патч ClusterConfiguration, где добавляем хостнейм, с которым будем к нему подключаться, тип и количество нод.

Версия kubernetes выбирается через версию docker-имаджа KinD. Доступные варианты перечислены в release notes на GitHub.

Ноды добавляются просто повторением одной и той же строки, например, при указании двух строк - role: worker будет создано две воркер-ноды. Таким же образом можно варьировать и количество мастер-нод.

kind-cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  ipFamily: ipv4
  apiServerAddress: "0.0.0.0"

kubeadmConfigPatchesJSON6902:
  - group: kubeadm.k8s.io
    version: v1beta2
    kind: ClusterConfiguration
    patch: |
      - op: add
        path: /apiServer/certSANs/-
        value: docker

nodes:
- role: control-plane
- role: worker

.gitlab-ci.yml - описание пайплайна. Документацию по синтаксису можно найти здесь. Публичный GitLab “из коробки” предоставляет облачные раннеры с Docker, нам же потребуется настроить запуск сервиса DinD рядом с контейнером, в котором будет бежать выполнение пайплайна. Также и в сами образы, используемые для запуска джоб, в процессе выполнения нужно будет добавить недостающие бинари и конфиги утилит. Конфиг исключительно для примера, никто не запрещает собрать свой образ со всем необходимым для этого пайплайна.

.gitlab-ci.yml
# Объявляем стадии пайплайна
stages:
  - "lint"
  - "chart-test"
  - "chart-release"

variables:
  # Выбираем версию kubectl, соответственно используемой версии kubernetes
  CI_KUBECTL_VER: v1.19.0
  # Объявляем версии Helm, chart-testing и KinD
  CI_HELM_VER: v3.6.3
  CI_CT_VER: v3.4.0
  CI_KIND_VERSION: v0.11.1
  # Часть пути, по которому мы в дальнейшем будем подключать Helm-репо
  CI_HELM_CHANNEL: stable
  # Название создаваемого кластера kubernetes
  CI_KIND_CLUSTER_NAME: k8s-test
  # Для удобства переписываем в переменные имаджи нод с дайджестами
  # в соответствии с используемой версией KinD
  CI_KIND_IMAGE_1_17: 'kindest/node:v1.17.17@sha256:66f1d0d91a88b8a001811e2f1054af60eef3b669a9a74f9b6db871f2f1eeed00'
  CI_KIND_IMAGE_1_18: 'kindest/node:v1.18.19@sha256:7af1492e19b3192a79f606e43c35fb741e520d195f96399284515f077b3b622c'
  CI_KIND_IMAGE_1_19: 'kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729'
  CI_KIND_IMAGE_1_20: 'kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9'
  CI_KIND_IMAGE_1_21: 'kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6'
  # Указываем, какой образ будем использовать в этом пайплайне
  CI_KIND_IMAGE: $CI_KIND_IMAGE_1_19

# Описываем небольшие сниппеты, которые легко переиспользовать в нескольких джобах,
# а также улучшают читабельность кода.
.check-and-unshallow: &check-and-unshallow
  - git version
  - |
    if [ -f "$(git rev-parse --git-dir)/shallow" ]; then
        echo "this is a shallow repository";
        git fetch --unshallow --prune --prune-tags --verbose
    else
        echo "not a shallow repository";
        git fetch --prune --prune-tags --verbose
    fi
  - git rev-parse --verify HEAD
  - git rev-list HEAD --count
  - git rev-list HEAD --count --first-parent

.get-kube-binaries: &get-kube-binaries
  - apk add -U wget
  - wget -O /usr/local/bin/kind "https://github.com/kubernetes-sigs/kind/releases/download/${CI_KIND_VERSION}/kind-linux-amd64"
  - chmod +x /usr/local/bin/kind
  - wget -O /usr/local/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/${CI_KUBECTL_VER}/bin/linux/amd64/kubectl"
  - chmod +x /usr/local/bin/kubectl

.install-ct: &install-ct
  - |
    export CT_URL="https://github.com/helm/chart-testing/releases/download/${CI_CT_VER}"
    export CT_TAR_FILE="chart-testing_${CI_CT_VER#v}_linux_amd64.tar.gz"
    echo "install chart-testing ${CI_CT_VER} from \"${CT_URL}/${CT_TAR_FILE}\""
    mkdir -p /tmp/ct /etc/ct
    wget -O "/tmp/${CT_TAR_FILE}" "${CT_URL}/${CT_TAR_FILE}"
    tar -xzvf "/tmp/${CT_TAR_FILE}" -C /tmp/ct
    mv /tmp/ct/etc/chart_schema.yaml /etc/ct/chart_schema.yaml
    mv /tmp/ct/etc/lintconf.yaml /etc/ct/lintconf.yaml
    mv /tmp/ct/ct /usr/bin/ct
    rm -rf /tmp/ct
    ct version

.install-helm: &install-helm
  - |
    export HELM_URL="https://get.helm.sh"
    export HELM_TAR_FILE="helm-${CI_HELM_VER}-linux-amd64.tar.gz"
    echo "install HELM ${CI_HELM_VER} from \"${HELM_URL}/${HELM_TAR_FILE}\""
    mkdir -p /tmp/helm
    wget -O "/tmp/${HELM_TAR_FILE}" "${HELM_URL}/${HELM_TAR_FILE}"
    tar -xzvf "/tmp/${HELM_TAR_FILE}" -C /tmp/helm
    mv /tmp/helm/linux-amd64/helm /usr/bin/helm
    rm -rf /tmp/helm
    chmod +x /usr/bin/helm
    helm version

# Добавляем Package Registry проекта с авторизацией через job-token
# и плагин для push в Helm-репо
.helm-add-project-as-repo: &helm-add-project-as-repo
  - >-
    helm repo add
    --username gitlab-ci-token
    --password "${CI_JOB_TOKEN}"
    "${CI_PROJECT_NAME}"
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/${CI_HELM_CHANNEL}"
  - helm plugin install https://github.com/chartmuseum/helm-push.git
  - helm repo list

# Описываем сами джобы
# Линт. Тут просто берем родной имадж chart-testing, ничего дабавлять не требуется
chart-lint:
  stage: lint
  image: quay.io/helmpack/chart-testing:v3.4.0
  tags:
    - "docker"
  script:
    - *check-and-unshallow
    - ct lint --config ct.yaml
  only:
    - pushes
  except:
    - master
    - main

# Здесь добавляем все необходимые бинари, поднимаем KinD
# и разворачиваем в него тестируемые чарты
chart-test:
  stage: chart-test
  image: docker:20.10-git
  variables:
    # Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
  # Сервис DinD нам не нужен в остальных джобах, поэтому укажем его здесь,
  # чтобы не замедлять весь пайплайн
  services:
    - name: docker:20.10-dind
      alias: docker
  tags:
    - "docker"
  script:
    - *check-and-unshallow
    - apk add -U wget
    # Добавляем недостающие бинари
    - *get-kube-binaries
    - *install-ct
    - *install-helm
    # разворачиваем KinD
    - >-
      kind create cluster
      --name ${CI_KIND_CLUSTER_NAME}
      --image ${CI_KIND_IMAGE}
      --config=kind-cluster.yaml
      --wait 5m
    # Правим kubeconfig, чтобы указанный хост api-server соответствовал хосту DinD,
    # который, в свою очередь, мы ранее указали в патче ClusterConfiguration
    - sed -i -E -e 's/127\.0\.0\.1|0\.0\.0\.0/docker/g' "$HOME/.kube/config"
    # Разворачиваем наши чарты
    - ct install --config ct.yaml
  after_script:
    # не обязательно, т.к. сервис все равно будет погашен с завершением джобы
    - kind delete cluster --name k8s-test
  only:
    - pushes
  except:
    - master
    - main

# Упаковываем чарты и публикуем их в Package Registry
chart-release:
  stage: chart-release
  image: quay.io/helmpack/chart-testing:v3.4.0
  tags:
    - "docker"
  script:
    - *check-and-unshallow
    - apk add jq yq
    - *helm-add-project-as-repo
    # используем доработанный скрипт из экшена chart-releaser
    - >-
      ./gitlab-cr.sh
      --charts-dir charts
      --charts-repo-url "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/${CI_HELM_CHANNEL}"
      --repo "${CI_PROJECT_NAME}"
  only:
    - master
    - main

Зачем тут unshallow? Chart-testing просматривает историю гит для поиска измененных чартов, а GitLab по умолчанию делает shallow clone с глубиной 50 коммитов, что дает вероятность его падения, когда он пытается просмотреть больше. Это поведение можно настроить через clone, а не fetch или через git depth, на вкус и цвет, так сказать.

Канал Helm-repo лучше использовать один, например, stable (название на самом деле можно выбрать любое) или же минимальное их разнообразие. Дело в том, что в интерфейсе GitLab определить какой чарт из какого канала можно примерно никак и разнообразие нейминга в этом случае приведет только к увеличению хаоса. В целом работает аналогично Chartmuseum и Helm-repo в GitHub-pages. Получить и распарсить индекс можно по пути "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/${CI_HELM_CHANNEL}/index.yaml"

Также обращу внимание, что поиск изменений происходит при сравнении текущего HEAD и указанного в конфиге ct target-branch в origin. Это еще один повод не пушить в мастер, т.к. в таком случае внутри пайплайна сравнивать будет не с чем. 

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

gitlab-cr.sh Скрипт упаковки и публикации чарта мы подсмотрим в оригинальном GitHub экшене. К сожалению, chart-releaser с GitLab пока не работает, поэтому перепишем на использование helm для поиска версии чарта по Helm-repo, а затем упаковки и публикации, если текущей версии найдено не будет:

gitlab-cr.sh
#!/usr/bin/env bash

# Copyright The Helm Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -o errexit
set -o nounset
set -o pipefail

needy_tools=("jq" "yq" "helm")

show_help() {
cat << EOF
Usage: $(basename "$0") <options>
    -h, --help               Display help
    -d, --charts-dir         The charts directory (default: charts)
    -u, --charts-repo-url    The Gitlab helm package registry URL (default: "<CI_API_V4_URL>/projects/<CI_PROJECT_ID>/packages/helm/stable")
    -r, --repo               The repo name
EOF
}

main() {
    local charts_dir=charts
    local repo=
    local charts_repo_url=

    parse_command_line "$@"

    for tool in "${needy_tools[@]}"; do
        assert_tools "$tool"
    done

    local repo_root
    repo_root=$(git rev-parse --show-toplevel)
    pushd "$repo_root" > /dev/null

    echo 'Looking up latest tag...'
    local latest_tag
    latest_tag=$(lookup_latest_tag)

    echo "Discovering changed charts since '$latest_tag'..."
    local changed_charts=()
    readarray -t changed_charts <<< "$(lookup_changed_charts "$latest_tag")"

    if [[ -n "${changed_charts[*]}" ]]; then

        rm -rf .cr-release-packages
        mkdir -p .cr-release-packages

        rm -rf .cr-index
        mkdir -p .cr-index

        for chart in "${changed_charts[@]}"; do
            if [[ -d "$chart" ]]; then
                package_chart "$chart"
            else
                echo "Chart '$chart' no longer exists in repo. Skipping it..."
            fi
        done

        release_charts "$repo"
    else
        echo "Nothing to do. No chart changes detected."
    fi

    popd > /dev/null
}

parse_command_line() {
    while :; do
        case "${1:-}" in
            -h|--help)
                show_help
                exit
                ;;
            -d|--charts-dir)
                if [[ -n "${2:-}" ]]; then
                    charts_dir="$2"
                    shift
                else
                    echo "ERROR: '-d|--charts-dir' cannot be empty." >&2
                    show_help
                    exit 1
                fi
                ;;
            -u|--charts-repo-url)
                if [[ -n "${2:-}" ]]; then
                    charts_repo_url="$2"
                    shift
                else
                    echo "ERROR: '-u|--charts-repo-url' cannot be empty." >&2
                    show_help
                    exit 1
                fi
                ;;
            -r|--repo)
                if [[ -n "${2:-}" ]]; then
                    repo="$2"
                    shift
                else
                    echo "ERROR: '--repo' cannot be empty." >&2
                    show_help
                    exit 1
                fi
                ;;
            *)
                break
                ;;
        esac

        shift
    done

    if [[ -z "$repo" ]]; then
        echo "ERROR: '-r|--repo' is required." >&2
        show_help
        exit 1
    fi

    if [[ -z "$charts_repo_url" ]]; then
        charts_repo_url="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/stable"
    fi
}

lookup_latest_tag() {
    git fetch --tags > /dev/null 2>&1

    if ! git describe --tags --abbrev=0 2> /dev/null; then
        git rev-list --max-parents=0 --first-parent HEAD
    fi
}

filter_charts() {
    while read -r chart; do
        [[ ! -d "$chart" ]] && continue
        local file="$chart/Chart.yaml"
        if [[ -f "$file" ]]; then
            echo "$chart"
        else
            echo "WARNING: $file is missing, assuming that '$chart' is not a Helm chart. Skipping." 1>&2
        fi
    done
}

lookup_changed_charts() {
    local commit="$1"

    local changed_files
    changed_files=$(git diff --find-renames --name-only "$commit" -- "$charts_dir")

    local depth=$(( $(tr "/" "\n" <<< "$charts_dir" | sed '/^\(\.\)*$/d' | wc -l) + 1 ))
    local fields="1-${depth}"

    cut -d '/' -f "$fields" <<< "$changed_files" | uniq | filter_charts
}

lookup_chart_in_repo_by_version() {
    local chart="$1"
    local chart_version
    chart_version="$(yq r "${chart}/Chart.yaml" 'version')"
    local chart_name
    chart_name="$(yq r "${chart}/Chart.yaml" 'name')"

    helm search repo "${repo}/${chart_name}" --version "$chart_version" -o json | jq -r '.[].version'
}

package_chart() {
    local chart="$1"
    local chart_version_in_repo
    chart_version_in_repo="$(lookup_chart_in_repo_by_version "$chart")"
    local args=("$chart" --destination .cr-release-packages)

    if [[ -z "$chart_version_in_repo" ]]; then
        echo "Packaging chart '$chart'..."
        helm package "${args[@]}"
    else
        echo "$chart with version $chart_version_in_repo already exist ​in repo. Skipping..."
    fi
}

release_charts() {
    local repo="$1"

    echo 'Releasing charts...'
    for f in .cr-release-packages/*.tgz
    do
        [ -e "$f" ] || break
        helm push "$f" "$repo"
    done
}

assert_tools() {
    local tool="$1"

    command -v "$tool" >/dev/null 2>&1 || {
        echo "ERROR: ${tool} is not installed." >&2
        exit 1
        }
}

main "$@"

Готово. Коммитим, пушим в develop и наблюдаем, как пайплайн становится зеленым, после чего мерджим нашу ветку в master(main) и по завершению пайплайна получаем тарболл чарта в Package Registry.

Добавляем Helm-repo (подставьте свой ID проекта):

helm repo add habr-test https://gitlab.com/api/v4/projects/<project_ID>/packages/helm/stable
"habr-test" has been added to your repositories

Проверяем, что тестовый чарт опубликован:

helm search repo habr-test/test-chart  
NAME                	CHART VERSION	APP VERSION	DESCRIPTION                
habr-test/test-chart	0.1.0        	1.16.0     	A Helm chart for Kubernetes

Кейс второй: раннер с монтированием сокета Docker

Сразу оговорюсь, что монтирование сокета с хоста в контейнеры, создаваемые раннером не лучшее решение в плане безопасности, но пока что необходимо, например, если вы пользуетесь werf. В таком случае запустить DinD будет как минимум не самой тривиальной задачей, т.к. сокет монтируется и в контейнеры сервисов.

Тем не менее задача легко решаема, поскольку контейнеры KinD в этом случае будут подниматься в Docker хоста, где запущен раннер, а интерфейс docker0 доступен изнутри контейнера и практически всегда имеет адрес 172.17.0.1. Как и в случае с сервисом DinD мы добавим этот адрес через патч ClusterConfiguration в kind-cluster.yaml:

kubeadmConfigPatchesJSON6902:
  - group: kubeadm.k8s.io
    version: v1beta2
    kind: ClusterConfiguration
    patch: |
      - op: add
        path: /apiServer/certSANs/-
        value: 172.17.0.1

И sed’ом по ходу выполнения пайплайна заменим 0.0.0.0 в кубконфиге на адрес интерфейса docker0: sed -i -E -e 's/0\.0\.0\.0/172\.17\.0\.1/g' "$HOME/.kube/config"

Кроме того, нужно позаботиться и об удалении контейнеров KinD в случае отмены задания GitLab, т.к. контейнер задания будет просто убит kill без выполнения after_script. Подробнее с сутью проблемы можно ознакомиться в этом issue. В этом нам помогут лейбы, которые KinD ставит на запускаемые контейнеры нод. Сперва добавляем CI_JOB_ID в переменную названия кластера, затем, перед запуском кластера, создаем контейнер “мертвой руки”, который после некоторого таймаута будет завершать контейнеры с лейбой io.x-k8s.kind.cluster=job-${CI_JOB_ID}.

Пример пайплайна для работы с docker0
# Объявляем стадии пайплайна
stages:
  - "lint"
  - "chart-test"
  - "chart-release"

variables:
  # Выбираем версию kubectl, соответственно используемой версии kubernetes
  CI_KUBECTL_VER: v1.19.0
  # Объявляем версии Helm, chart-testing и KinD
  CI_HELM_VER: v3.6.3
  CI_CT_VER: v3.4.0
  CI_KIND_VERSION: v0.11.1
  # Часть пути, по которому мы в дальнейшем будем подключать Helm-репо
  CI_HELM_CHANNEL: stable
  # Название создаваемого кластера kubernetes
  CI_KIND_CLUSTER_NAME: job-${CI_JOB_ID}
  # Название контейнера отложенного удаления кластера
  CI_KIND_REAPER_NAME: job-${CI_JOB_ID}-reaper
  # Для удобства переписываем в переменные имаджи нод с дайджестами
  # в соответствии с используемой версией KinD
  CI_KIND_IMAGE_1_17: 'kindest/node:v1.17.17@sha256:66f1d0d91a88b8a001811e2f1054af60eef3b669a9a74f9b6db871f2f1eeed00'
  CI_KIND_IMAGE_1_18: 'kindest/node:v1.18.19@sha256:7af1492e19b3192a79f606e43c35fb741e520d195f96399284515f077b3b622c'
  CI_KIND_IMAGE_1_19: 'kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729'
  CI_KIND_IMAGE_1_20: 'kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9'
  CI_KIND_IMAGE_1_21: 'kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6'
  # Указываем, какой образ будем использовать в этом пайплайне
  CI_KIND_IMAGE: $CI_KIND_IMAGE_1_19


# Описываем небольшие сниппеты, которые легко переиспользовать в нескольких джобах,
# а также улучшают читабельность кода.
.check-and-unshallow: &check-and-unshallow
  - git version
  - |
    if [ -f "$(git rev-parse --git-dir)/shallow" ]; then
        echo "this is a shallow repository";
        git fetch --unshallow --prune --prune-tags --verbose
    else
        echo "not a shallow repository";
        git fetch --prune --prune-tags --verbose
    fi
  - git rev-parse --verify HEAD
  - git rev-list HEAD --count
  - git rev-list HEAD --count --first-parent

.get-kube-binaries: &get-kube-binaries
  - wget -nv -O /usr/local/bin/kind "https://github.com/kubernetes-sigs/kind/releases/download/${CI_KIND_VERSION}/kind-linux-amd64"
  - chmod +x /usr/local/bin/kind
  - wget -nv -O /usr/local/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/${CI_KUBECTL_VER}/bin/linux/amd64/kubectl"
  - chmod +x /usr/local/bin/kubectl

.install-ct: &install-ct
  - |
    export CT_URL="https://github.com/helm/chart-testing/releases/download/${CI_CT_VER}"
    export CT_TAR_FILE="chart-testing_${CI_CT_VER#v}_linux_amd64.tar.gz"
    echo "install chart-testing ${CI_CT_VER} from \"${CT_URL}/${CT_TAR_FILE}\""
    mkdir -p /tmp/ct /etc/ct
    wget -nv -O "/tmp/${CT_TAR_FILE}" "${CT_URL}/${CT_TAR_FILE}"
    tar -xzvf "/tmp/${CT_TAR_FILE}" -C /tmp/ct
    mv /tmp/ct/etc/chart_schema.yaml /etc/ct/chart_schema.yaml
    mv /tmp/ct/etc/lintconf.yaml /etc/ct/lintconf.yaml
    mv /tmp/ct/ct /usr/bin/ct
    rm -rf /tmp/ct
    ct version

.install-helm: &install-helm
  - |
    export HELM_URL="https://get.helm.sh"
    export HELM_TAR_FILE="helm-${CI_HELM_VER}-linux-amd64.tar.gz"
    echo "install HELM ${CI_HELM_VER} from \"${HELM_URL}/${HELM_TAR_FILE}\""
    mkdir -p /tmp/helm
    wget -nv -O "/tmp/${HELM_TAR_FILE}" "${HELM_URL}/${HELM_TAR_FILE}"
    tar -xzvf "/tmp/${HELM_TAR_FILE}" -C /tmp/helm
    mv /tmp/helm/linux-amd64/helm /usr/bin/helm
    rm -rf /tmp/helm
    chmod +x /usr/bin/helm
    helm version

# Добавляем Package Registry проекта с авторизацией через job-token
# и плагин для push в Helm-репо
.helm-add-project-as-repo: &helm-add-project-as-repo
  - >-
    helm repo add
    --username gitlab-ci-token
    --password "${CI_JOB_TOKEN}"
    "${CI_PROJECT_NAME}"
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/${CI_HELM_CHANNEL}"
  - helm plugin install https://github.com/chartmuseum/helm-push.git
  - helm repo list

# Создаем контейнер отложенной очистки
.create-kind-reaper: &create-kind-reaper
  - >-
    docker run --rm -d --name ${CI_KIND_REAPER_NAME}
    -e CI_KIND_CLUSTER_NAME=${CI_KIND_CLUSTER_NAME}
    -e CI_REAPER_SLEEP=${CI_REAPER_SLEEP:-300}
    -v /var/run/docker.sock:/var/run/docker.sock
    docker:20.10
    sh -c
    'sleep ${CI_REAPER_SLEEP};
    docker rm -fv $(docker ps -aq --filter label=io.x-k8s.kind.cluster=${CI_KIND_CLUSTER_NAME})'

# Описываем сами джобы
# Линт. Тут просто берем родной имадж chart-testing, ничего дабавлять не требуется
chart-lint:
  stage: lint
  image: quay.io/helmpack/chart-testing:v3.4.0
  tags:
    - "docker"
  script:
    - *check-and-unshallow
    - ct lint --config ct.yaml
  only:
    - pushes
  except:
    - master
    - main

# Здесь добавляем все необходимые бинари, поднимаем KinD
# и разворачиваем в него тестируемые чарты
chart-test:
  stage: chart-test
  image: docker:20.10-git
  tags:
    - "docker"
  script:
    - *check-and-unshallow
    - apk add -U wget
    # Добавляем недостающие бинари
    - *get-kube-binaries
    - *install-ct
    - *install-helm
    # запускаем рипер
    - *create-kind-reaper
    # разворачиваем KinD
    - >-
      kind create cluster
      --name ${CI_KIND_CLUSTER_NAME}
      --image ${CI_KIND_IMAGE}
      --config=kind-cluster.yaml
      --wait 5m
    # Правим kubeconfig, чтобы указанный хост api-server соответствовал хосту docker0,
    # адрес которого, в свою очередь, мы ранее указали в патче ClusterConfiguration
    - sed -i -E -e 's/0\.0\.0\.0/172\.17\.0\.1/g' "$HOME/.kube/config"
    # Разворачиваем наши чарты
    - ct install --config ct.yaml
  after_script:
    # удаляем кластер KinD
    - kind delete cluster --name ${CI_KIND_CLUSTER_NAME}
    # удаляем контейнер отложенной очистки
    - docker rm -fv ${CI_KIND_REAPER_NAME}
  only:
    - pushes
  except:
    - master
    - main

# Упаковываем чарты и публикуем их в Package Registry
chart-release:
  stage: chart-release
  image: quay.io/helmpack/chart-testing:v3.4.0
  tags:
    - "docker"
  script:
    - *check-and-unshallow
    - apk add jq yq
    - *helm-add-project-as-repo
    # используем доработанный скрипт из экшена chart-releaser
    - >-
      ./gitlab-cr.sh
      --charts-dir charts
      --charts-repo-url "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/${CI_HELM_CHANNEL}"
      --repo "${CI_PROJECT_NAME}"
  only:
    - master
    - main

Для подключения к приватному Helm-репо на pull будет достаточно прав read_api персонального токена, но намного правильнее использовать для этого deploy token с правами read_package_registry. Аналогичный пример подключения Helm-репо с авторизацией токеном задания GitLab можно найти в сниппете "helm-add-project-as-repo".

Заключение

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

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

Комментарии (4)


  1. trak
    24.08.2021 23:38

    Простите, а KinD от k8s чем отличается?

    P.S. Спасибо за статью!


    1. vainkop
      25.08.2021 00:06
      +3

      Kind - это Kubernetes IN Docker

      K8s это сокращённое название Kubernetes, и обычно не означает какого-то конкретного дистрибутива Kubernetes.

      Для Kubernetes в докере рекомендую K3D https://k3d.io/

      Если не планируете локально запускать несколько кластеров, что может K3D, то хватит и K3S https://k3s.io/ управляется systemd, один бинарник, легковесный и полностью поддерживает Kubernetes api, т.к. CNCF certified Kubernetes.

      curl -sfL https://get.k3s.io | sh -
      # Check for Ready node, takes maybe 30 seconds
      k3s kubectl get node


      1. trak
        25.08.2021 08:48

        Я конечно идиотский вопрос задал. Имел в виду k3s и k3d :)


        1. shurup
          25.08.2021 16:29
          +1

          k3s — дистрибутив от Rancher, название которое сделали «меньше», чем K8s, чтобы подчеркнуть его легковесность и простоту (пусть и с меньшим функционалом).

          Помимо собственно дистрибутива существует также k3d — утилита, управляющая узлами k3s, каждый из которых помещён в контейнер Docker.

          Из свежего сравнения подобных решений (включая упомянутый kind), по соседству, в том же хабе ;-)