Строим простой локальный CI/CD на Windows: k3s + Jenkins + Gitea + ArgoCD

—

от автора

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

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

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

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

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

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

#!/bin/bashecho "Установка k3d и Helm..."winget install k3dwinget install Helm.Helmecho "Загрузка Docker образов..."docker pull registry:3docker pull gitea/gitea:1.25docker pull jenkins/jenkins:2.554docker pull maven:3.9.14-eclipse-temurin-21docker pull eclipse-temurin:21.0.10_7-jdk-jammydocker pull jenkins/inbound-agent:jdk21docker pull gcr.io/kaniko-project/executor:v1.24.0echo "Создание тегов для локального registry..."docker tag maven:3.9.14-eclipse-temurin-21 localhost:5000/maven:3.9.14-eclipse-temurin-21docker tag eclipse-temurin:21.0.10_7-jdk-jammy localhost:5000/eclipse-temurin:21.0.10_7-jdk-jammydocker tag gcr.io/kaniko-project/executor:v1.24.0 localhost:5000/kaniko:v1.24.0echo "Проверка загруженных образов:"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/bashecho "========================================="echo "Запуск полной настройки test-prod-cluster"echo "========================================="# Создаем сетьecho "Создание сети test-prod-net..."docker network create test-prod-netsleep 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# Создание кластера k3decho "Создание кластера 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.yamlecho "Кластер создан. Ожидание 60 секунд..."sleep 60# Замена URL в kubeconfigecho "Обновление kubeconfig..."KUBECONFIG_PATH="$HOME/.kube/config"sed -i 's|https://host\.docker\.internal:6550|https://localhost:6550|g' "$KUBECONFIG_PATH"# Установка Argo CDecho "Добавление репозитория Argo CD..."helm repo add argo https://argoproj.github.io/argo-helmhelm repo updateecho "Установка Argo CD..."kubectl create namespace argocdhelm install argocd argo/argo-cd -n argocdecho "Установка ArgoCD завершена. Ожидание 60 секунд..."sleep 60# Запуск Docker Composeecho "Запуск Docker Compose..."docker compose up -decho "Запуск Docker Compose завершен. Ожидание 30 секунд..."sleep 30# Применение Applicationecho "Применение Application..."kubectl apply -f application.yamlecho "Применение 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 образа в registryecho "Пуш Maven образа в registry..."docker tag maven:3.9.14-eclipse-temurin-21 localhost:5000/maven:3.9.14-eclipse-temurin-21docker push localhost:5000/maven:3.9.14-eclipse-temurin-21# Пуш eclipse-temurin образа в registryecho "Пуш eclipse-temurin образа в registry..."docker tag eclipse-temurin:21.0.10_7-jdk-jammy localhost:5000/eclipse-temurin:21.0.10_7-jdk-jammydocker push localhost:5000/eclipse-temurin:21.0.10_7-jdk-jammy# Пуш Kanico образа в registryecho "Пуш Kanico образа в registry..."docker tag gcr.io/kaniko-project/executor:v1.24.0 localhost:5000/kaniko:v1.24.0docker push localhost:5000/kaniko:v1.24.0echo "========================================="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/v1alpha1kind: Applicationmetadata:  name: myapp  namespace: argocdspec:  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-apphelm-charts

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

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

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

Для Helm чартов:

git initgit remote add origin http://localhost:3000/<username>/helm-charts.gitgit branch -M maingit 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-cloudDocker 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: kanikoEnabled: trueName: kaniko-agentDocker Image: localhost:5000/jenkins-agent:docker

Раздел Container settings:

Mounts: type=volume,src=build-workspace,dst=/workspaceNetwork: test-prod-netRemote File System Root: /home/jenkinsConnect method: Connect with JNLPPull 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/shecho "=== Удаление Argo CD ==="# Удаление Argo CD (если установлен)helm uninstall argocd -n argocd 2>/dev/null || echo "Argo CD не найден или уже удален"# Удаление namespace Argo CDkubectl delete namespace argocd 2>/dev/null || echo "Namespace argocd не найден"echo "=== Удаление репозитория Helm ==="# Удаление репозитория Argo CD из Helmhelm repo remove argo 2>/dev/null || echo "Репозиторий argo не найден"echo "=== Удаление Helm ==="# Удаление Helm через wingetwinget uninstall Helm.Helm 2>/dev/null || echo "Helm не найден"echo "=== Удаление k3d ==="# Удаление всех кластеров k3dk3d cluster delete --all 2>/dev/null || echo "Нет кластеров k3d для удаления"# Удаление k3d через wingetwinget 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

ссылка на оригинал статьи https://habr.com/ru/articles/1093586/