diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..2567988 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,551 @@ +# 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.