# Kubernetes-оператор

Оператор публикует Service кластера через туннели Tuna. Туннель, домен или TCP-порт описывается ресурсом Kubernetes, а оператор создаёт его в аккаунте Tuna и держит подключение. Кластер сам подключается к Tuna исходящими соединениями, поэтому LoadBalancer, публичный IP и открытые входящие порты не нужны.

> **Не путайте с tuna kubernetes.** Команда [`tuna kubernetes`](https://tuna.am/docs/tunnels/kubernetes.md) даёт временный доступ к Kubernetes API кластера. Оператор решает другую задачу: публикует приложения, которые работают в кластере.

## Ресурсы

| Ресурс                                         | Что делает                                                                                                  | Когда нужен                                  |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| [Tunnel](#tunnel)                              | HTTP- или TCP-туннель к одному Service                                                                      | опубликовать одно приложение или базу данных |
| [Ingress](#ingress) с `ingressClassName: tuna` | маршруты по путям к нескольким Service за одним доменом                                                     | `/api` ведёт в один Service, `/` — в другой  |
| [Domain](#domain)                              | постоянный поддомен или [свой домен](https://tuna.am/docs/tunnels/guides/connect-self-domain.md) в аккаунте | адрес не должен меняться; хост для Ingress   |
| [TCPEndpoint](#tcp)                            | зарезервированный [TCP-порт](https://tuna.am/docs/tunnels/ports.md) с алиасом                               | постоянный порт для TCP-туннеля              |

Tunnel, Domain и TCPEndpoint — ресурсы оператора из группы `k8s.tuna.am/v1alpha1`. Ingress — стандартный ресурс Kubernetes, оператор обслуживает его как Ingress-контроллер.

## Что понадобится

* Kubernetes 1.32 или новее;
* [Nelm](https://github.com/werf/nelm) (рекомендуем) или Helm 4;
* API-ключ с записью в «Эндпоинты и домены» — как его создать, описано ниже.

## API-ключ для оператора

Ключ нужен оператору, чтобы резервировать домены и TCP-порты и выпускать временные токены для туннелей. Всё это — продукт «Эндпоинты и домены», поэтому ключу достаточно права записи в него (скоуп `tunnels:write`).

1. Откройте [API-ключи](https://my.tuna.am/api_keys?lang=ru) в личном кабинете и создайте новый ключ.
2. В блоке «Для чего ключ» выберите «Для API».
3. В поле «Описание» укажите кластер, например `k8s-operator prod`. Заведите отдельный ключ на каждый кластер: тогда доступ одного кластера можно отозвать, не трогая остальные.
4. В поле «Срок действия» выберите «Не ограничено». Набор «Для API» по умолчанию ставит 30 дней, и когда срок истечёт, оператор потеряет доступ к API. Если политика безопасности требует ограниченного срока, оставьте его, но запланируйте замену ключа.
5. В блоке «Доступ к API» выберите «Выборочно» и для «Эндпоинты и домены» нажмите «Запись». У остальных продуктов оставьте «Нет».
6. Создайте ключ и скопируйте его.

Переключатель «Запуск туннелей» включать не нужно. Оператор не подключает туннели своим ключом: для каждого туннеля он выпускает отдельный временный токен.

Ключ с полным доступом к API тоже подойдёт, но откроет оператору мониторинги, секреты и настройки команды, которые ему не нужны.

## Установка

Чарт [`tuna-k8s-operator`](https://code.tuna.am/tuna/-/packages/helm/tuna-k8s-operator) лежит в реестре пакетов `code.tuna.am`. В примерах оператор ставится в namespace `tuna-system`, а ключ передаётся параметром `credentials.apiKey`. Другие способы передать ключ — в разделе [Где хранить ключ](#api-key-storage).

**Nelm**

Nelm сначала показывает план изменений, а потом применяет его:

```shell
export NELM_FEAT_REMOTE_CHARTS=true
nelm repo add tuna https://code.tuna.am/api/packages/tuna/helm
nelm release plan install -n tuna-system -r tuna-operator tuna/tuna-k8s-operator \
  --set credentials.apiKey=<API-ключ>
nelm release install -n tuna-system -r tuna-operator tuna/tuna-k8s-operator \
  --set credentials.apiKey=<API-ключ>

```

**Helm**

```shell
helm repo add tuna https://code.tuna.am/api/packages/tuna/helm
helm repo update
helm install tuna-operator tuna/tuna-k8s-operator \
  --namespace tuna-system --create-namespace \
  --set credentials.apiKey=<API-ключ>

```

Проверьте, что оператор запустился:

```shell
kubectl get pods -n tuna-system

```

В namespace работают два пода: `…-agent-manager` обслуживает Tunnel и Ingress, `…-api-manager` — Domain и TCPEndpoint.

### Где хранить ключ

| Параметр                     | Как работает                                    | Когда выбрать                                                                 |
| ---------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------- |
| `credentials.apiKey`         | чарт сам создаёт Secret с ключом                | пробный запуск, тестовый кластер                                              |
| `credentials.existingSecret` | имя вашего Secret с полем `api-key`             | ключ приходит из External Secrets Operator, Sealed Secrets или создан вручную |
| `extraEnv`                   | переменная `TUNA_API_KEY` от инжектора секретов | Secret в Kubernetes не нужен, ключ берётся прямо из хранилища                 |

Пример `extraEnv` для [Bank-Vaults](https://bank-vaults.dev):

```yaml
podAnnotations:
  vault.security.banzaicloud.io/vault-addr: https://vault.vault.svc:8200
  vault.security.banzaicloud.io/vault-role: tuna-operator
extraEnv:
  - name: TUNA_API_KEY
    value: vault:secret/data/tuna#TUNA_API_KEY

```

### Параметры чарта

Все параметры с описаниями покажет `helm show values tuna/tuna-k8s-operator`. Чаще всего нужны эти:

| Параметр                              | Что задаёт                                                                        |
| ------------------------------------- | --------------------------------------------------------------------------------- |
| `watchNamespace`                      | следить только за одним namespace; по умолчанию оператор следит за всеми          |
| `ingressClass.default`                | сделать класс `tuna` классом по умолчанию для Ingress без `ingressClassName`      |
| `agentManager.allowExternalUpstreams` | разрешить публиковать адреса вне namespace ресурса, см. [Безопасность](#security) |
| `apiManager.replicas`                 | число реплик api-manager; активна одна, остальные в резерве                       |

## Туннель к Service

```yaml
apiVersion: k8s.tuna.am/v1alpha1
kind: Tunnel
metadata:
  name: web
spec:
  protocol: http
  subdomain: my-app
  upstream:
    url: http://web:8080

```

* `protocol` — `http` или `tcp`;
* `subdomain` — поддомен на базовом домене Tuna. Вместо него можно указать `domain` — домен, зарезервированный в аккаунте;
* `upstream.url` — Service из того же namespace и его порт.

Публичный адрес появится в колонке `URL`:

```shell
kubectl get tunnel web

```

### Свой ключ для туннеля

По умолчанию туннель подключается временным токеном, который оператор выпускает своим ключом. Если туннель должен работать под другим ключом, например под ключом команды с её лимитами, положите ключ в Secret того же namespace. Такому ключу нужно только право на запуск туннелей (`tunnels:connect`): при создании выберите набор «Для туннелей».

```shell
kubectl create secret generic team-tuna-api-key --from-literal=api-key=<API-ключ>

```

```yaml
spec:
  apiKeySecretRef:
    name: team-tuna-api-key

```

## Ingress с маршрутами по путям

Хостом Ingress может быть только зарезервированный домен: ресурс Domain или домен из [личного кабинета](https://my.tuna.am/domains?lang=ru). Полное имя домена покажет `kubectl get domain my-app`.

```yaml
apiVersion: k8s.tuna.am/v1alpha1
kind: Domain
metadata:
  name: my-app
spec:
  subdomain: my-app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
spec:
  ingressClassName: tuna
  rules:
    - host: my-app.a.2na.am
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 8080
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  name: http

```

Как это работает:

* на каждый хост создаётся один туннель;
* изменения путей и бэкендов применяются без переподключения;
* префиксы сравниваются по сегментам пути: `/api` подходит для `/api/users`, но не для `/apix`;
* бэкенд получает публичный заголовок `Host`, а также `X-Forwarded-Host` и `X-Forwarded-Proto`.

Ограничения:

* бэкендом может быть только Service;
* правила без хоста и wildcard-хосты пропускаются;
* несколько Ingress на один хост не согласуются между собой.

## Свой домен

```yaml
apiVersion: k8s.tuna.am/v1alpha1
kind: Domain
metadata:
  name: api-example-com
spec:
  domain: api.example.com
  reclaimPolicy: Retain

```

DNS-записи, которые нужно создать у DNS-провайдера, появятся в `status.dnsRecords`:

```shell
kubectl describe domain api-example-com

```

Если проверка DNS не прошла, оператор повторяет её раз в 5 минут. Переключить существующий ресурс между `subdomain` и `domain` нельзя — создайте новый. Что делает `reclaimPolicy`, описано в разделе [Существующие резервации](#reservations).

## TCP-порт

TCPEndpoint резервирует порт, а Tunnel с `protocol: tcp` публикует через него Service:

```yaml
apiVersion: k8s.tuna.am/v1alpha1
kind: TCPEndpoint
metadata:
  name: postgres
spec:
  alias: postgres
---
apiVersion: k8s.tuna.am/v1alpha1
kind: Tunnel
metadata:
  name: postgres
spec:
  protocol: tcp
  portOrAlias: postgres
  upstream:
    url: tcp://postgres:5432

```

Хост и номер порта покажет `kubectl get tcpendpoint postgres`.

Справка по всем полям — `kubectl explain tunnel.spec`, `kubectl explain domain.spec` и `kubectl explain tcpendpoint.spec`. Другие примеры лежат в каталоге `examples/` чарта.

## Существующие резервации

Если домен или алиас уже есть в аккаунте — создан в кабинете или другим кластером, — Domain и TCPEndpoint подхватывают эту резервацию и не падают с ошибкой. У такого ресурса `status.adopted: true`, и оператор никогда не удалит его резервацию.

Для остальных ресурсов действует `reclaimPolicy`:

* `Delete` (по умолчанию) — освободить резервацию при удалении ресурса;
* `Retain` — сохранить резервацию в аккаунте.

Если резервацию удалили в кабинете, оператор создаст её снова.

## Состояние и ошибки

Состояние каждого ресурса — в условии `Ready` и в событиях Kubernetes:

```shell
kubectl describe tunnel web

```

У Ingress условий нет, смотрите его события: `TunnelConnected`, `TunnelStopped`, `RoutesUpdated`, `RuleSkipped`, `BackendSkipped`.

## Безопасность

* **Только свой namespace.** Tunnel и Ingress публикуют только Service своего namespace. IP-адреса, внешние хосты, Service других namespace и Service типа `ExternalName` отклоняются. Параметр `agentManager.allowExternalUpstreams=true` снимает ограничение, но тогда право создавать Tunnel или Ingress в любом namespace означает право опубликовать любой адрес, доступный поду оператора.
* **Токен на туннель.** У каждого туннеля свой временный токен. Он только запускает туннели, без доступа к API, работает с одним протоколом и держит не больше одного туннеля одновременно. Токен отзывается при удалении ресурса. Токены ресурсов, удалённых, пока оператор не работал, отзываются автоматически.
* **Минимум прав в кластере.** Поды работают от непривилегированного пользователя, с корневой ФС только для чтения и без capabilities. Secret и Service читает только agent-manager, и только `get`.
* **Трафик идёт через под.** Туннели работают внутри пода agent-manager: трафик идёт из Tuna через под к Service. У agent-manager одна реплика, поэтому при обновлении оператора туннели переподключаются.

## Обновление

**Nelm**

Выполните те же `nelm release plan install` и `nelm release install` с новой `--chart-version`: CRD обновятся вместе с релизом.

**Helm**

Helm не обновляет CRD, поэтому примените их до обновления чарта:

```shell
helm pull tuna/tuna-k8s-operator --untar --version <версия>
kubectl apply -f tuna-k8s-operator/crds/
helm upgrade tuna-operator tuna/tuna-k8s-operator --namespace tuna-system --reuse-values --version <версия>

```

## Удаление

Сначала удалите ресурсы: у Domain и TCPEndpoint есть финализаторы, и им нужен работающий оператор.

```shell
kubectl delete tunnels,domains,tcpendpoints --all --all-namespaces
nelm release uninstall -n tuna-system -r tuna-operator   # или: helm uninstall tuna-operator -n tuna-system
kubectl delete crd tunnels.k8s.tuna.am domains.k8s.tuna.am tcpendpoints.k8s.tuna.am

```
