552 lines
17 KiB
Markdown
552 lines
17 KiB
Markdown
# 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.
|