Шаг 1. Подготовка окружения и загрузка Docker образов

Для этого используем sh скрипт pull‑images‑setup.sh, который выполняет несколько действий: устанавливает k3d и Helm, скачивает необходимые docker образы и создает локальные теги для них, которые будут использоваться через локальный реестр.

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

  • Docker Desktop — понадобится для запуска контейнеров и дальнейшего создания k3s‑кластера через k3d

  • App Installer — предоставляет пакетный менеджер winget, через который скрипт установит k3d и Helm

  • Git Bash — понадобится для запуска shскриптов в Windows.

#!/bin/bash

echo "Установка k3d и Helm..."
winget install k3d
winget install Helm.Helm

echo "Загрузка Docker образов..."
docker pull registry:3
docker pull gitea/gitea:1.25
docker pull jenkins/jenkins:2.554
docker pull maven:3.9.14-eclipse-temurin-21
docker pull eclipse-temurin:21.0.10_7-jdk-jammy
docker pull jenkins/inbound-agent:jdk21
docker pull gcr.io/kaniko-project/executor:v1.24.0

echo "Создание тегов для локального registry..."
docker tag maven:3.9.14-eclipse-temurin-21 localhost:5000/maven:3.9.14-eclipse-temurin-21
docker tag eclipse-temurin:21.0.10_7-jdk-jammy localhost:5000/eclipse-temurin:21.0.10_7-jdk-jammy
docker tag gcr.io/kaniko-project/executor:v1.24.0 localhost:5000/kaniko:v1.24.0

echo "Проверка загруженных образов:"
docker images | grep -E "registry|gitea|jenkins|maven|eclipse-temurin"
  • registry — локальное хранилище Docker‑образов

  • gitea — локальный Git‑сервер

  • jenkins — CI‑сервер

  • maven и eclipse-temurin — окружение для сборки и запуска Java‑приложения

  • jenkins/inbound-agent — базовый образ Jenkins Agent

  • kaniko — инструмент для сборки контейнерных образов без использования docker daemon непосредственно в процессе сборки

Шаг 2. Разворачиваем локальный k3s кластер

Теперь можно переходить к созданию самого кластера.

Для этого используется sh скрипт setup‑cluster.sh. Он создаёт docker сеть и необходимые volumes, разворачивает k3s кластер через k3d, устанавливает Argo CD, запускает сервисы из docker compose и подготавливает образы, которые понадобятся Jenkins.

#!/bin/bash

echo "========================================="
echo "Запуск полной настройки test-prod-cluster"
echo "========================================="

# Создаем сеть
echo "Создание сети test-prod-net..."
docker network create test-prod-net
sleep 5

# Создаём постоянный volume, который будут использовать все агенты
echo "Создание volume который будут использовать все агенты..."
docker volume create build-workspace
# Даём полные права на запись в volume (чтобы jenkins мог писать)
docker run --rm -v build-workspace:/workspace alpine sh -c "chmod 777 /workspace"
sleep 5

# Создаём volumes для кэша
echo "Создание volume для кеша Maven..."
docker volume create maven-cache
# Даём полные права на запись в volume (Maven будет писать сюда)
docker run --rm -v maven-cache:/maven-cache alpine sh -c "chmod 777 /maven-cache"
sleep 5

# Создание кластера k3d
echo "Создание кластера test-prod-cluster..."
k3d cluster create test-prod-cluster \
  --network test-prod-net \
  --api-port 6550 \
  -p "8081:80@loadbalancer" \
  -p "8443:443@loadbalancer" \
  --agents 1 \
  --registry-config registries.yaml

echo "Кластер создан. Ожидание 60 секунд..."
sleep 60

# Замена URL в kubeconfig
echo "Обновление kubeconfig..."
KUBECONFIG_PATH="$HOME/.kube/config"
sed -i 's|https://host\.docker\.internal:6550|https://localhost:6550|g' "$KUBECONFIG_PATH"

# Установка Argo CD
echo "Добавление репозитория Argo CD..."
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
echo "Установка Argo CD..."
kubectl create namespace argocd
helm install argocd argo/argo-cd -n argocd
echo "Установка ArgoCD завершена. Ожидание 60 секунд..."
sleep 60

# Запуск Docker Compose
echo "Запуск Docker Compose..."
docker compose up -d
echo "Запуск Docker Compose завершен. Ожидание 30 секунд..."
sleep 30

# Применение Application
echo "Применение Application..."
kubectl apply -f application.yaml
echo "Применение Application завершено. Ожидание 20 секунд..."
sleep 20

# Сборка и пуш образа Jenkins агента
echo "Сборка образа Jenkins агента..."
docker build -f Dockerfile.agent -t localhost:5000/jenkins-agent:docker .
docker push localhost:5000/jenkins-agent:docker

# Пуш Maven образа в registry
echo "Пуш Maven образа в registry..."
docker tag maven:3.9.14-eclipse-temurin-21 localhost:5000/maven:3.9.14-eclipse-temurin-21
docker push localhost:5000/maven:3.9.14-eclipse-temurin-21

# Пуш eclipse-temurin образа в registry
echo "Пуш eclipse-temurin образа в registry..."
docker tag eclipse-temurin:21.0.10_7-jdk-jammy localhost:5000/eclipse-temurin:21.0.10_7-jdk-jammy
docker push localhost:5000/eclipse-temurin:21.0.10_7-jdk-jammy

# Пуш Kanico образа в registry
echo "Пуш Kanico образа в registry..."
docker tag gcr.io/kaniko-project/executor:v1.24.0 localhost:5000/kaniko:v1.24.0
docker push localhost:5000/kaniko:v1.24.0

echo "========================================="
echo "Настройка завершена!"
echo "========================================="
echo "Gitea: http://localhost:3000"
echo "Jenkins: http://localhost:8082"
echo "Argo CD: kubectl get pods -n argocd"

После установки Argo CD применяется файл application.yaml:

kubectl apply -f application.yaml

Этот манифест создаёт в Argo CD объект Application. В нём указывается git репозиторий с Helm чартами, путь к нужному chart, целевой кластер и namespace, куда должно быть развернуто приложение.

После этого Argo CD начинает следить за репозиторием helm-charts. Когда Jenkins обновляет в нём тег docker образа и отправляет изменения в gitea, argo cd обнаруживает изменение и синхронизирует состояние приложения в k3s кластере.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: http://gitea:3000/<gitea_user>/helm-charts.git
    path: myapp
    targetRevision: HEAD
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp-prod
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

Работать с кластером можно полностью через kubectl, но для учебного стенда удобно дополнительно использовать OpenLens — графический интерфейс для Kubernetes.

Шаг 3. Подготавливаем репозитории в Gitea

Теперь подготовим Git репозитории, которые будут использоваться в нашем учебном CI/CD процессе.

В качестве локального Git сервера используется Gitea. Она уже запускается в составе окружения и доступна по адресу:

http://localhost:3000

Для дальнейшей работы нам понадобятся два репозитория:

  • test-java-app — исходный код тестового Java‑приложения;

  • helm-charts — helm чарты, которые описывают желаемое состояние приложения в Kubernetes.

Разделение на два репозитория сделано специально. Jenkins будет работать с исходным кодом, собирать docker образ и после успешной сборки обновлять версию образа в helm чарте.

ArgoCD в свою очередь будет следить уже не за репозиторием приложения, а за репозиторием helm-charts. Как только Jenkins изменит там тег образа, ArgoCD увидит изменение и синхронизирует состояние кластера.

После первого запуска Gitea необходимо создать пользователя и затем вручную создать два пустых репозитория:

test-java-app
helm-charts

После этого локальные репозитории можно связать с Gitea и отправить туда код.

Для приложения:

git init
git remote add origin http://localhost:3000/<username>/test-java-app.git
git branch -M main
git add *
git commit -m "first commit"
git push -u origin main

Для Helm чартов:

git init
git remote add origin http://localhost:3000/<username>/helm-charts.git
git branch -M main
git add *
git commit -m "first commit"
git push -u origin main

На этом этапе Gitea становится точкой хранения исходного кода и конфигурации развертывания, а дальше мы подключим к этим репозиториям Jenkins и ArgoCD.

Шаг 4. Запускаем и настраиваем Jenkins

Следующий компонент нашего стенда Jenkins. В этой схеме он будет отвечать за CI часть процесса:

  • получать исходный код приложения из Gitea

  • запускать сборку

  • собирать docker образ

  • отправлять образ в локальный репозиторий

  • обновлять тег образа в репозитории с Helm чартами

Сам Jenkins уже запускается через docker compose внутри нашего sh скрипта, поэтому после выполнения предыдущих шагов его интерфейс должен быть доступен по адресу:

http://localhost:8082

При первом запуске Jenkins попросит ввести начальный пароль администратора. Получить его можно командой (powershell):

docker exec test-prod-jenkins cat /var/jenkins_home/secrets/initialAdminPassword

После ввода пароля Jenkins предложит установить плагины. Для учебного стенда можно сначала выбрать стандартный набор, а затем дополнительно установить плагины, которые понадобятся для работы с docker:

  • Docker— для создания временных Jenkins Agent через Docker

  • CloudBees Docker Build and Publish— для работы со сборкой и публикацией docker образов

После установки плагинов Jenkins можно перезапустить из интерфейса.

Далее проверим базовые параметры Jenkins:

System Configuration → System → Jenkins Location

В поле Jenkins URL для нашего локального стенда используется внутреннее имя контейнера:

http://jenkins:8080

Это важно: из браузера мы открываем Jenkins через localhost:8082, но другие контейнеры внутри Docker сети обращаются к нему по имени сервиса jenkins.

Добавляем полномочия(credentials) для Gitea

Теперь нужно дать Jenkins доступ к нашим репозиториям в Gitea.

В дальнейшем Jenkins будет использовать эти полномочия для двух задач:

  • клонирования репозитория с исходным кодом приложения

  • отправки изменений в репозиторий helm-charts

В интерфейсе Jenkins переходим:

Настройки Jenkins → Credentials → Global → Add Credentials

В качестве типа credentials выбираем: Username with password

И указываем данные пользователя Gitea:

Username: <ваш пользователь Gitea>
Password: <пароль пользователя Gitea>
ID: gitea-credentials

Поле ID здесь особенно важно. В нашем pipeline полномочия будут запрашиваться именно по ID.

Настраиваем Docker Cloud в Jenkins

Теперь настроим окружение, в котором Jenkins будет выполнять наши сборки.

В этой конфигурации Jenkins использует Docker Cloud. Его задача — динамически создавать временные Jenkins Agent контейнеры для выполнения pipeline. После завершения сборки такой агент удаляется.

Переходим в: Настройки Jenkins → Clouds → New cloud → Type Docker

Name: my-docker-cloud
Docker Cloud details -> Docker Host URI: tcp://host.docker.internal:2375 (включен в Desktop)
Enabled: true

Адрес host.docker.internal позволяет контейнеру Jenkins обращаться к Docker Engine, который работает на Windows хосте через Docker Desktop. Порт 2375 используется для подключения к Docker daemon по TCP. Для нашего локального учебного стенда это упрощает взаимодействие Jenkins с Docker.

После этого добавляем шаблон Docker Agent для Kaniko‑сборки.

В разделе Docker Agent templates создаём новый шаблон со следующими параметрами:

Labels: kaniko
Enabled: true
Name: kaniko-agent
Docker Image: localhost:5000/jenkins-agent:docker

Раздел Container settings:

Mounts: type=volume,src=build-workspace,dst=/workspace
Network: test-prod-net
Remote File System Root: /home/jenkins
Connect method: Connect with JNLP
Pull strategy: Pull all images every time

Нажми Apply внизу, потом Save

После сохранения шаблона Jenkins сможет динамически создавать временный kaniko-agent для каждой сборки, а после завершения pipeline агент будет удаляться.

Создаём pipeline для Java приложения: сборка через Kaniko

Теперь, когда Jenkins умеет создавать временный агент с лейбломkaniko, можно перейти к самому pipeline.

Создаём новую задачу: New Item → Pipeline

В качестве имени можно использовать: build-java-app-kaniko

Далее выбираем: Definition: Pipeline script

Definition: Pipeline script

Наш pipeline будет выполнять несколько последовательных действий:

  1. создавать рабочую директорию

  2. клонировать исходный код приложения из Gitea

  3. собирать Docker‑образ с помощью Kaniko

  4. отправлять образ в локальный registry

  5. обновлять тег образа в Helm‑чарте

  6. отправлять изменения обратно в Gitea

Важно: перед запуском замените <gitea_user> на имя своего пользователя в Gitea.

Скрипт:

pipeline {
    agent { label 'kaniko' }

    environment {
        IMAGE_TAG = "registry:5000/test-java-app:${BUILD_NUMBER}"
        GIT_REPO = "http://gitea:3000/<gitea_user>/test-java-app.git"
        GIT_CREDENTIALS = "gitea-credentials"
        HELM_REPO = "http://gitea:3000/<gitea_user>/helm-charts.git"
    }

    stages {
        stage('Prepare workspace') {
            steps {
                sh """
                    rm -rf /workspace/repo
                    mkdir -p /workspace/repo
                """
            }
        }

        stage('Checkout') {
            steps {
                echo "Клонируем репозиторий приложения..."
                dir('/workspace/repo') {
                    git branch: 'main',
                        url: "${GIT_REPO}",
                        credentialsId: "${GIT_CREDENTIALS}"
                }
            }
        }

        stage('Build with Kaniko') {
            steps {
                sh """
                    export DOCKER_HOST=tcp://host.docker.internal:2375
                    docker run \\
                      -v build-workspace:/workspace \\
                      -v maven-cache:/root/.m2 \\
                      --network test-prod-net \\
                      localhost:5000/kaniko:v1.24.0 \\
                      --dockerfile /workspace/repo/Dockerfile \\
                      --destination ${IMAGE_TAG} \\
                      --context dir:///workspace/repo \\
                      --insecure \\
                      --insecure-pull
                """
            }
        }

        stage('Update Helm Chart') {
            steps {
                echo "Обновляем тег в helm чартах..."
                withCredentials([usernamePassword(
                    credentialsId: 'gitea-credentials',
                    usernameVariable: 'GIT_USER',
                    passwordVariable: 'GIT_PASS'
                )]) {
                    sh """
                        git clone ${HELM_REPO} helm-charts
                        cd helm-charts
                        sed -i "s/tag:.*/tag: ${BUILD_NUMBER}/" myapp/values.yaml
                        git config user.email "jenkins@local"
                        git config user.name "Jenkins CI"
                        git add myapp/values.yaml
                        git commit -m "Update app version to ${BUILD_NUMBER}"
                        git push http://${GIT_USER}:${GIT_PASS}@gitea:3000/<gitea_user>/helm-charts.git main
                    """
                }
            }
        }

        stage('Verify') {
            steps {
                echo "Готово! Образ ${IMAGE_TAG} запушен, helm чарты обновлены"
                echo "ArgoCD подхватит изменения автоматически"
            }
        }
    }

    post {
        always {
            echo "Очищаем ресурсы Docker..."
            script {
                sh """
                    export DOCKER_HOST=tcp://host.docker.internal:2375
                    docker ps -a \
                      --filter "ancestor=localhost:5000/kaniko:v1.24.0" \
                      --filter "status=exited" -q | xargs -r docker rm
                """
            }
        }
    }
}

После сохранения pipeline запускаем его через Build Now.

Если всё настроено правильно, Jenkins соберёт новый образ, отправит его в локальный registry, обновит тег в репозитории helm-charts, а ArgoCD подхватит изменения и применит новую версию приложения в кластере.

Удаление учебного стенда

После экспериментов окружение можно удалить с помощью отдельного скрипта delete-k3d-helm.sh.

Он удаляет установленный Argo CD и его namespace, удаляет Helm‑репозиторий Argo CD, удаляет локальные k3d кластеры, а также деинсталлирует Helm и k3d.

Внимание: скрипт предназначен для полного удаления учебного окружения. Команда k3d cluster delete --all удаляет все k3d‑кластеры на компьютере, а не только созданный в рамках этой статьи. Кроме того, скрипт удаляет установленные через winget Helm и k3d. Если эти инструменты или другие k3d кластеры используются вами для других проектов, не запускайте скрипт целиком.

#!/bin/sh

echo "=== Удаление Argo CD ==="
# Удаление Argo CD (если установлен)
helm uninstall argocd -n argocd 2>/dev/null || echo "Argo CD не найден или уже удален"
# Удаление namespace Argo CD
kubectl delete namespace argocd 2>/dev/null || echo "Namespace argocd не найден"

echo "=== Удаление репозитория Helm ==="
# Удаление репозитория Argo CD из Helm
helm repo remove argo 2>/dev/null || echo "Репозиторий argo не найден"

echo "=== Удаление Helm ==="
# Удаление Helm через winget
winget uninstall Helm.Helm 2>/dev/null || echo "Helm не найден"

echo "=== Удаление k3d ==="
# Удаление всех кластеров k3d
k3d cluster delete --all 2>/dev/null || echo "Нет кластеров k3d для удаления"
# Удаление k3d через winget
winget uninstall k3d 2>/dev/null || echo "k3d не найден"

echo "=== Очистка конфигурации kubectl ==="
# Очистка контекста kubectl (опционально)
kubectl config unset current-context 2>/dev/null || echo "Контекст не установлен"
kubectl config delete-cluster k3d-* 2>/dev/null || echo "Кластеры k3d не найдены в конфиге"
kubectl config delete-context k3d-* 2>/dev/null || echo "Контексты k3d не найдены"

echo "=== Откат завершен ==="

Ссылка на репозиторий: https://github.com/Evgeni-86/LabClusterV1