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

552 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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="<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-записи для всех поддоменов:
```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 - <<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
```
Проверь:
```bash
kubectl get clusterissuer # READY: True
```
**Важно:** Для получения сертификатов Ingress должен содержать аннотацию:
```yaml
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
```
toml-converter добавляет её автоматически при `tls = true` в stackfile.toml.
---
## Шаг 6: KubeVela
```bash
helm install kubevela kubevela/vela-core \
--namespace vela-system --create-namespace \
--timeout 8m --wait
```
Проверь:
```bash
kubectl get pods -n vela-system # vela-core и cluster-gateway Running
```
---
## Шаг 7: Операторы (CloudNativePG + Redis)
```bash
# 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:
```bash
kubectl apply -f kubevela/components/
```
Файлы:
- `kubevela/components/postgres.yaml` — обёртка над CloudNativePG Cluster CRD
- `kubevela/components/redis.yaml` — обёртка над OT Systems Redis CRD
Проверь:
```bash
kubectl get componentdefinitions # postgres и redis
```
---
## Шаг 9: Gitea (Git + Container Registry)
```bash
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 больших образов)
Проверь:
```bash
curl -sk https://git.${DOMAIN}/ # Gitea login page
```
Создай репозитории:
```bash
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
```bash
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 секунд и проверь:
```bash
kubectl get nodes # Ready
```
---
## Шаг 11: Мониторинг (Prometheus + Grafana + Loki)
```bash
# 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
```
Проверь:
```bash
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 образ
```bash
cd server/
docker build --platform linux/amd64 \
-t git.${DOMAIN}/stackops/stackops-api:v1 \
-f api/Dockerfile .
```
### 12.2 Запушить в Gitea Registry
```bash
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
```bash
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 Создать секрет
```bash
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`:
```yaml
image: git.<DOMAIN>/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.<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)
```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": "<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`)
```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.