# 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 на локальной машине (для сборки образов) ## Переменные окружения Перед началом определи эти переменные — они используются во всех шагах: ```bash VPS_IP="82.114.226.118" VPS_USER="root" DOMAIN="nodeup.ru" CF_API_TOKEN="" CF_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-записи для всех поддоменов: ```bash 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 ```bash 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 локально: ```bash 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 ``` Проверь: ```bash kubectl get nodes # STATUS: Ready ``` --- ## Шаг 3: Helm репозитории ```bash 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 ```bash 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. Проверь: ```bash kubectl get pods -n ingress-nginx # controller Running curl -s http://${VPS_IP}/ # должен вернуть 404 от nginx ``` --- ## Шаг 5: cert-manager + ClusterIssuer ```bash 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 таймаутов): ```bash kubectl apply -f - < /etc/rancher/k3s/registries.yaml </stackops/stackops-api:v1 ``` Также добавь env vars для логина: ```bash 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} ``` Проверь: ```bash 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 ```bash # Сначала создай 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: ```yaml stringData: token: "${RUNNER_TOKEN}" ``` ### 13.3 Задеплоить runner ```bash 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 джобов Проверь: ```bash 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 Создать проект ```bash 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 в корне репозитория ```toml [app] name = "my-app" image = "git./stackops/my-app:latest" replicas = 1 port = 8080 [ingress] host = "my-app.app." path = "/" tls = true # ← автоматически добавит cert-manager аннотацию [dependencies.postgres] version = "15" storage = "1Gi" [dependencies.redis] version = "7" storage = "1Gi" ``` ### 14.3 Задеплоить (StackOps клонирует из git) ```bash curl -sk -X POST https://app.${DOMAIN}/stackops.v1.StackOpsService/DeployProject \ -H "Authorization: Bearer ${STACKOPS_API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"projectId": ""}' ``` 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`) ```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. -u stackops --password-stdin docker build --platform linux/amd64 -t git./stackops/my-app:${{ gitea.sha }} . docker tag git./stackops/my-app:${{ gitea.sha }} git./stackops/my-app:latest docker push git./stackops/my-app:${{ gitea.sha }} docker push git./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.` | StackOps API + UI | email/password → token | | `https://git.` | Gitea + Registry | GITEA_ADMIN_USER / GITEA_ADMIN_PASS | | `https://grafana.` | Grafana | admin / GRAFANA_ADMIN_PASS | | `https://.app.` | Пользовательские приложения | — | 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.