Files
infra/SETUP.md
2026-04-09 14:36:45 +03:00

17 KiB
Raw Permalink Blame History

StackOps Infrastructure Setup Guide

Полная инструкция для AI-агента по развёртыванию StackOps платформы с нуля на чистом VPS.

Требования

  • VPS с Debian 12+/Ubuntu 22+, минимум 4 CPU / 6GB RAM / 50GB disk
  • Root SSH доступ
  • Домен, направленный на IP сервера (Cloudflare DNS)
  • Docker Desktop на локальной машине (для сборки образов)

Переменные окружения

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

VPS_IP="82.114.226.118"
VPS_USER="root"
DOMAIN="nodeup.ru"
CF_API_TOKEN="<cloudflare api token с Zone:DNS:Edit>"
CF_ZONE_ID="<zone id домена>"
GITEA_ADMIN_USER="stackops"
GITEA_ADMIN_PASS="stackops-admin-2026"
GRAFANA_ADMIN_PASS="stackops-grafana-2026"
STACKOPS_API_TOKEN="dev-secret-token"
ADMIN_EMAIL="admin@stackops.dev"
ADMIN_PASSWORD="admin"

Шаг 1: DNS записи (Cloudflare API)

Создай A-записи для всех поддоменов:

for name in "@" "app" "git" "grafana" "prom" "*.app"; do
  curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records" \
    -H "Authorization: Bearer ${CF_API_TOKEN}" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"A\",\"name\":\"${name}\",\"content\":\"${VPS_IP}\",\"ttl\":1,\"proxied\":false}"
done
Запись Назначение
@ (nodeup.ru) Корневой домен
app StackOps API + UI
git Gitea + Container Registry
grafana Grafana дашборды
prom Prometheus (резерв)
*.app Wildcard для пользовательских приложений

Важно: proxied: false — иначе HTTP-01 ACME challenge не пройдёт.


Шаг 2: Установка k3s

ssh ${VPS_USER}@${VPS_IP} 'apt-get update -qq && apt-get install -y -qq curl git'

ssh ${VPS_USER}@${VPS_IP} 'curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik" sh -'

Traefik отключён — вместо него используется nginx-ingress.

Скопируй kubeconfig локально:

mkdir -p ~/.kube
ssh ${VPS_USER}@${VPS_IP} 'cat /etc/rancher/k3s/k3s.yaml' \
  | sed "s/127.0.0.1/${VPS_IP}/" > ~/.kube/config-nodeup
export KUBECONFIG=~/.kube/config-nodeup

Проверь:

kubectl get nodes  # STATUS: Ready

Шаг 3: Helm репозитории

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo add kubevela https://kubevela.github.io/charts
helm repo add ot-helm https://ot-container-kit.github.io/helm-charts/
helm repo add gitea-charts https://dl.gitea.com/charts/
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

Шаг 4: nginx-ingress

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.kind=DaemonSet \
  --set controller.hostPort.enabled=false \
  --timeout 10m --wait

k3s ServiceLB (Klipper) привязывает порты 80/443 к хосту автоматически. Не используй hostPort.enabled=true — конфликтует с Klipper.

Проверь:

kubectl get pods -n ingress-nginx  # controller Running
curl -s http://${VPS_IP}/ # должен вернуть 404 от nginx

Шаг 5: cert-manager + ClusterIssuer

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true \
  --timeout 10m --wait

Создай ClusterIssuer с HTTP-01 challenge (не DNS-01 — cert-manager из k3s подов может иметь проблемы с доступом к Cloudflare API из-за IPv6 таймаутов):

kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ${ADMIN_EMAIL}
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: nginx
EOF

Проверь:

kubectl get clusterissuer  # READY: True

Важно: Для получения сертификатов Ingress должен содержать аннотацию:

annotations:
  cert-manager.io/cluster-issuer: letsencrypt-prod

toml-converter добавляет её автоматически при tls = true в stackfile.toml.


Шаг 6: KubeVela

helm install kubevela kubevela/vela-core \
  --namespace vela-system --create-namespace \
  --timeout 8m --wait

Проверь:

kubectl get pods -n vela-system  # vela-core и cluster-gateway Running

Шаг 7: Операторы (CloudNativePG + Redis)

# CloudNativePG (PostgreSQL)
kubectl apply --server-side \
  -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.23/releases/cnpg-1.23.0.yaml

# Redis operator
helm install redis-operator ot-helm/redis-operator \
  --namespace redis-operator --create-namespace \
  --timeout 5m --wait

Шаг 8: KubeVela ComponentDefinitions

Зарегистрируй кастомные типы компонентов для postgres и redis:

kubectl apply -f kubevela/components/

Файлы:

  • kubevela/components/postgres.yaml — обёртка над CloudNativePG Cluster CRD
  • kubevela/components/redis.yaml — обёртка над OT Systems Redis CRD

Проверь:

kubectl get componentdefinitions  # postgres и redis

Шаг 9: Gitea (Git + Container Registry)

helm install gitea gitea-charts/gitea \
  --namespace gitea --create-namespace \
  -f infra/04-gitea/values.yaml \
  --timeout 10m --wait

Ключевые настройки в values.yaml:

  • SQLite (без внешнего PostgreSQL)
  • packages.ENABLED: true — включает Container Registry
  • actions.ENABLED: true — включает Gitea Actions
  • Ingress с cert-manager.io/cluster-issuer и proxy-body-size: 500m (для push больших образов)

Проверь:

curl -sk https://git.${DOMAIN}/  # Gitea login page

Создай репозитории:

for repo in infra stackops test-app; do
  curl -sk -X POST "https://git.${DOMAIN}/api/v1/user/repos" \
    -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}" \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"${repo}\",\"private\":false}"
done

Шаг 10: Настройка k3s для pull из Gitea Registry

ssh ${VPS_USER}@${VPS_IP} "mkdir -p /etc/rancher/k3s && cat > /etc/rancher/k3s/registries.yaml <<EOF
mirrors:
  git.${DOMAIN}:
    endpoint:
      - \"https://git.${DOMAIN}\"
configs:
  \"git.${DOMAIN}\":
    auth:
      username: ${GITEA_ADMIN_USER}
      password: ${GITEA_ADMIN_PASS}
EOF
systemctl restart k3s"

После рестарта k3s подожди ~10 секунд и проверь:

kubectl get nodes  # Ready

Шаг 11: Мониторинг (Prometheus + Grafana + Loki)

# Prometheus + Grafana
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace \
  -f infra/05-monitoring/values-prometheus.yaml \
  --timeout 10m --wait

# Loki + Promtail
helm install loki grafana/loki-stack \
  --namespace monitoring \
  -f infra/05-monitoring/values-loki.yaml \
  --timeout 5m --wait

Проверь:

curl -sk https://grafana.${DOMAIN}/  # 302 → login
# Логин: admin / ${GRAFANA_ADMIN_PASS}

После входа в Grafana добавь Loki datasource:

  • URL: http://loki:3100
  • Тип: Loki

Шаг 12: Сборка и деплой StackOps API

12.1 Собрать Docker образ

cd server/
docker build --platform linux/amd64 \
  -t git.${DOMAIN}/stackops/stackops-api:v1 \
  -f api/Dockerfile .

12.2 Запушить в Gitea Registry

echo "${GITEA_ADMIN_PASS}" | docker login git.${DOMAIN} -u ${GITEA_ADMIN_USER} --password-stdin
docker push git.${DOMAIN}/stackops/stackops-api:v1

12.3 Создать namespace, RBAC, PVC

kubectl apply -f infra/06-stackops/rbac.yaml

Это создаёт:

  • Namespace stackops
  • ServiceAccount stackops-api с ClusterRole stackops-deployer (полный доступ — нужен для kubectl apply в произвольные неймспейсы)
  • PVC stackops-data (1Gi, local-path) для SQLite

12.4 Создать секрет

kubectl create secret generic stackops-secrets \
  --namespace stackops \
  --from-literal=api-token=${STACKOPS_API_TOKEN} \
  --dry-run=client -o yaml | kubectl apply -f -

12.5 Задеплоить

Перед деплоем обнови image в infra/06-stackops/deployment.yaml:

image: git.<DOMAIN>/stackops/stackops-api:v1

Также добавь env vars для логина:

kubectl apply -f infra/06-stackops/deployment.yaml
kubectl set env deployment/stackops-api -n stackops \
  STACKOPS_ADMIN_EMAIL=${ADMIN_EMAIL} \
  STACKOPS_ADMIN_PASSWORD=${ADMIN_PASSWORD}

Проверь:

kubectl rollout status deployment/stackops-api -n stackops
curl -sk https://app.${DOMAIN}/  # StackOps UI (login page)
curl -sk -X POST https://app.${DOMAIN}/api/login \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"${ADMIN_EMAIL}\",\"password\":\"${ADMIN_PASSWORD}\"}"
# → {"token":"..."}

Шаг 13: Gitea Actions Runner (CI/CD)

13.1 Получить registration token

# Сначала создай API token для Gitea
GITEA_API_TOKEN=$(curl -sk -X POST \
  "https://git.${DOMAIN}/api/v1/users/${GITEA_ADMIN_USER}/tokens" \
  -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-token","scopes":["all"]}' | python3 -c "import sys,json; print(json.load(sys.stdin)['sha1'])")

# Получи registration token
RUNNER_TOKEN=$(curl -sk \
  "https://git.${DOMAIN}/api/v1/admin/runners/registration-token" \
  -H "Authorization: token ${GITEA_API_TOKEN}" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

13.2 Обновить токен в манифесте

В infra/08-gitea-runner/runner.yaml замени токен в Secret:

stringData:
  token: "${RUNNER_TOKEN}"

13.3 Задеплоить runner

kubectl apply -f infra/08-gitea-runner/runner.yaml

Runner содержит два контейнера:

  • runner — gitea/act_runner, подключается к Gitea по in-cluster DNS (gitea-http.gitea.svc.cluster.local:3000)
  • dind — Docker-in-Docker сайдкар (privileged) для docker build / docker push внутри CI джобов

Проверь:

kubectl get pods -n gitea-runner  # 2/2 Running
kubectl logs deployment/gitea-runner -n gitea-runner -c runner --tail=5
# → "Runner registered successfully", "Starting runner daemon"

Шаг 14: Деплой приложения через StackOps

14.1 Создать проект

curl -sk -X POST https://app.${DOMAIN}/stackops.v1.StackOpsService/CreateProject \
  -H "Authorization: Bearer ${STACKOPS_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-app",
    "gitUrl": "https://git.'${DOMAIN}'/stackops/my-app.git",
    "gitBranch": "main",
    "namespace": "my-app"
  }'

Сохрани id из ответа.

14.2 stackfile.toml в корне репозитория

[app]
name     = "my-app"
image    = "git.<DOMAIN>/stackops/my-app:latest"
replicas = 1
port     = 8080

[ingress]
host = "my-app.app.<DOMAIN>"
path = "/"
tls  = true            # ← автоматически добавит cert-manager аннотацию

[dependencies.postgres]
version = "15"
storage = "1Gi"

[dependencies.redis]
version = "7"
storage = "1Gi"

14.3 Задеплоить (StackOps клонирует из git)

curl -sk -X POST https://app.${DOMAIN}/stackops.v1.StackOpsService/DeployProject \
  -H "Authorization: Bearer ${STACKOPS_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"projectId": "<project-id>"}'

StackOps сделает:

  1. git clone репозитория
  2. Прочитает stackfile.toml
  3. Сконвертирует в KubeVela Application YAML
  4. kubectl create namespace + kubectl apply
  5. KubeVela развернёт: PostgreSQL → Redis → App → Ingress + TLS

14.4 CI/CD workflow (.gitea/workflows/ci.yaml)

name: Build and Deploy
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: docker:27
    steps:
      - uses: actions/checkout@v4
      - name: Build and push
        run: |
          echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.<DOMAIN> -u stackops --password-stdin
          docker build --platform linux/amd64 -t git.<DOMAIN>/stackops/my-app:${{ gitea.sha }} .
          docker tag git.<DOMAIN>/stackops/my-app:${{ gitea.sha }} git.<DOMAIN>/stackops/my-app:latest
          docker push git.<DOMAIN>/stackops/my-app:${{ gitea.sha }}
          docker push git.<DOMAIN>/stackops/my-app:latest
      - name: Deploy
        run: |
          apk add --no-cache curl
          curl -sf -X POST http://stackops-api.stackops.svc.cluster.local:8080/stackops.v1.StackOpsService/DeployProject \
            -H "Authorization: Bearer ${{ secrets.STACKOPS_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{"projectId":"${{ secrets.PROJECT_ID }}"}'

Secrets в настройках репозитория (Settings > Actions > Secrets):

  • REGISTRY_PASSWORD — пароль от Gitea
  • STACKOPS_TOKEN — API токен StackOps
  • PROJECT_ID — ID проекта из шага 14.1

Результат

После выполнения всех шагов будет работать:

URL Сервис Credentials
https://app.<DOMAIN> StackOps API + UI email/password → token
https://git.<DOMAIN> Gitea + Registry GITEA_ADMIN_USER / GITEA_ADMIN_PASS
https://grafana.<DOMAIN> Grafana admin / GRAFANA_ADMIN_PASS
https://<app>.app.<DOMAIN> Пользовательские приложения

Namespaces в кластере:

  • ingress-nginx — ingress controller
  • cert-manager — автоматические TLS сертификаты
  • vela-system — KubeVela OAM engine
  • cnpg-system — CloudNativePG оператор
  • redis-operator — Redis оператор
  • gitea — Gitea + Valkey
  • gitea-runner — CI/CD runner (DinD)
  • monitoring — Prometheus + Grafana + Loki + Promtail
  • stackops — StackOps API + SQLite

Flow деплоя:

git push → Gitea Actions → docker build → push registry → StackOps API
  → git clone → toml-converter → KubeVela Application → k8s
  → CloudNativePG (postgres) + Redis operator + Deployment + Ingress + TLS

Известные нюансы

  1. Первый деплой на чистом VPS медленный — k3s тянет все образы с нуля (5-10 мин на helm install). Используй --timeout 10m на helm install.

  2. nginx-ingress + k3s Klipper — не включай hostPort.enabled=true, будет конфликт портов. Klipper ServiceLB сам привяжет 80/443.

  3. cert-manager DNS-01 vs HTTP-01 — используй HTTP-01. DNS-01 через Cloudflare API может таймаутить из k3s подов (IPv6 → fallback → timeout в Go HTTP client cert-manager'а).

  4. Gitea Container Registry — нужна аннотация nginx.ingress.kubernetes.io/proxy-body-size: 500m на Gitea ingress, иначе docker push больших образов вернёт 413.

  5. k3s registry auth — настраивается через /etc/rancher/k3s/registries.yaml, после изменения нужен systemctl restart k3s.

  6. KubeVela gateway trait + cert-manager — toml-converter автоматически добавляет cert-manager.io/cluster-issuer: letsencrypt-prod на ингресс при tls = true в stackfile.toml.

  7. StackOps API нуждается в git — Docker образ должен содержать git в runtime (для git clone при деплое из репозитория).

  8. Gitea Actions runner — registration token одноразовый. При пересоздании runner нужно получить новый token через API.

  9. IPv6 — если на VPS нет рабочего IPv6, отключи: sysctl -w net.ipv6.conf.all.disable_ipv6=1. Иначе некоторые Go HTTP клиенты могут таймаутить пытаясь подключиться по IPv6.