# Проксирование внешнего хоста через Traefik (Ingress-контроллер)

У меня в домашней сети есть хост с web-сервисом (audiobookshelf), а стандартные web-порты (80 для HTTP и 443 для HTTPS)
на домашнем роутере перенаправлены в кластер k3S (через keepalived). Таким образом, если прокинуть http-трафик этого
хоста через Traefik, то можно будет получить доступ к этому сервису через доменное имя и SSL-сертификат от
Let’s Encrypt.

Для удобства я поместил все манифесты в один файл (вы можете оформить из и как отдельные файлы). Так как хотелось бы
описать как делаются универсальные манифесты, которые можно использовать для проксирования любого сервиса, то
я заменил в нем конкретные значения на "заглушки. Можно взять этот манифест и просто заменить в нем значения на
свои:

* `<PROXIED-HOST>` -- IP-адрес хоста, где работает сервис, который надо проксировать.
* `<PROXIED-PORT>` -- порт, с которого отвечает сервис.
* `<YOU-DOMAIN-NAME>` -- доменное имя, на которое будет проксировать сервис.
* `<NAME-SPACE>` -- пространство имен кластера, в котором будет создан сервис, маршруты, секреты и все необходимое
  для проксирования. Пространство имен -- это логическая группа ресурсов в кластере Kubernetes, которая позволяет
  организовать и изолировать ресурсы. 
* `<SERVICE-NAME>` -- имя сервиса, который будет проксироваться. Это имя, для простоты, будем использоваться
* и в маршрутах, и сертификатах, и в секрете...

## Пространство имен

Чтобы все было аккуратно и сервисы и поды не путались, создадим пространство имен для проксирования конкретного хоста.
Мо хост относится к <SERVICE-NAME>, поэтому назову пространство имен `<NAME-SPACE>`
Например, `<NAME-SPACE>`.
```bash
sudo ubectl create namespace <NAME-SPACE>
```

Проверяем, что пространство создано:
```bash
sudo kubectl get namespace <NAME-SPACE>
```

Увидим, что пространство создано и активно:
```text
NAME           STATUS   AGE
<NAME-SPACE>   Active   54s
```

## Конфигурация всего

Для удобства я объединил манифесты в один файл (но можно и по отдельности). Создаем единый манифест:
```bash
sudo nano ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
```

Он состоит из следующих частей:
* `Endpoints` -- указывает на конечную точку, в которую будет проксироваться трафик. В данном случае это
  IP-адрес и порт, на который будет проксироваться запрос.
* `Service` -- создает сервис, который будет использоваться для проксирования запросов к `Endpoints`.
* `Certificate` -- создает сертификат для домена `<YOU-DOMAIN-NAME>`, который будет использоваться для
  шифрования трафика. Сертификат запрашивается через `cert-manager`, который автоматически обновляет его по мере
  необходимости.
* `Middleware` -- создает промежуточное ПО, которое будет использоваться для обработки запросов. В данном случае
  это редирект с HTTP на HTTPS и исключение редиректа для ACME challenge (механизма внешней проверки владения доменом
  со стороны Let’s Encrypt).
* `IngressRoute` -- создает маршрут, который будет использоваться для проксирования запросов к сервису.
  В данном случае это маршруты для HTTP и HTTPS, которые будут обрабатывать запросы на домен `<YOU-DOMAIN-NAME>`.
  Также создается маршрут для ACME challenge, который позволяет cert-manager пройти проверку через порт 80.

Вставляем в манифест следующее содержимое (не забудьте заменить `<PROXIED-HOST>`, `<PROXIED-PORT>`, `<YOU-DOMAIN-NAME>`,
 `<NAME-SPACE>` и `<SERVICE-NAME>` на свои значения):
```yaml
# Endpoints для внешнего хоста <SERVICE-NAME>
# Задаёт IP и порт внешнего сервера, так как <SERVICE-NAME> внешний хост для k3s
apiVersion: v1
kind: Endpoints
metadata:
  name: <SERVICE-NAME>
  namespace: <NAME-SPACE>  # Namespace для <SERVICE-NAME>
subsets:  # Прямо в корне, без spec
  - addresses:
      - ip: <PROXIED-HOST>  # IP Synology, где работает <SERVICE-NAME>
    ports:
      - port: <PROXIED-PORT>  # Порт Synology (HTTP)
        protocol: TCP

---
# Service для маршрутизации трафика от Traefik к внешнему хосту
# Связывает IngressRoute с Endpoints
apiVersion: v1
kind: Service
metadata:
  name: <SERVICE-NAME>
  namespace: <NAME-SPACE>
spec:
  ports:
    - port: <PROXIED-PORT>  # Порт сервиса, на который Traefik отправляет трафик
      targetPort: <PROXIED-PORT>  # Порт на Synology
      protocol: TCP

---
# Middleware для редиректа HTTP → HTTPS
# Применяется к HTTP-запросам для перенаправления на HTTPS
apiVersion: traefik.io/v1alpha1               # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: https-redirect
  namespace: <NAME-SPACE>
spec:
  redirectScheme:
    scheme: https  # Перенаправлять на HTTPS
    permanent: true  # Код 301 (постоянный редирект)

---
# Middleware для исключения редиректа на ACME challenge
# Позволяет Let’s Encrypt проверять /.well-known/acme-challenge без HTTPS
apiVersion: traefik.io/v1alpha1               # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: no-https-redirect
  namespace: <NAME-SPACE>
spec:
  stripPrefix:
    prefixes:
      - /.well-known/acme-challenge  # Убрать префикс для ACME

---
# IngressRoute для HTTP (порт 80) с редиректом на HTTPS
# Обрабатывает HTTP-запросы и перенаправляет их
apiVersion: traefik.io/v1alpha1               # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: <SERVICE-NAME>-http
  namespace: <NAME-SPACE>
spec:
  entryPoints:
    - web  # Порт 80 (стандартный HTTP)
  routes:
    - match: Host("<YOU-DOMAIN-NAME>")  # Запросы к <YOU-DOMAIN-NAME>
      kind: Rule
      services:
        - name: <SERVICE-NAME>  # Сервис <SERVICE-NAME>
          port: <PROXIED-PORT>  # Порт сервиса
      middlewares:
        - name: https-redirect  # Редирект на HTTPS

---
# IngressRoute для HTTPS (порт 443)
# Обрабатывает HTTPS-запросы с TLS
apiVersion: traefik.io/v1alpha1               # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: <SERVICE-NAME>-https
  namespace: <NAME-SPACE>
spec:
  entryPoints:
    - websecure  # Порт 443 (HTTPS)
  routes:
    - match: Host("<YOU-DOMAIN-NAME>")
      kind: Rule
      services:
        - name: <SERVICE-NAME>
          port: <PROXIED-PORT>
  tls:
    secretName: <SERVICE-NAME>-tls  # Сертификат от cert-manager

---
# IngressRoute для HTTP-01 challenge (Let’s Encrypt)
# Позволяет cert-manager пройти проверку через порт 80
apiVersion: traefik.io/v1alpha1               # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1    # старая версия Traefik 
kind: IngressRoute
metadata:
  name: <SERVICE-NAME>-acme
  namespace: <NAME-SPACE>
spec:
  entryPoints:
    - web  # Порт 80
  routes:
    - match: Host("<YOU-DOMAIN-NAME>") && PathPrefix("/.well-known/acme-challenge")
      kind: Rule
      services:
        - name: <SERVICE-NAME>
          port: <PROXIED-PORT>
      middlewares:
        - name: no-https-redirect  # Не редиректить ACME

---
# Certificate для TLS-сертификата от Let’s Encrypt
# Запрашивает сертификат для <YOU-DOMAIN-NAME> через cert-manager
# ВАЖНО: cert-manager должен быть установлен в кластере
# ВАЖНО: если манифесты принимаются (apply) последовательно, то манифест с сертификатом должен быть последним для
#        избежания исчерпания лимитов Let’s Encrypt (пять запросов в неделю)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: <SERVICE-NAME>-tls
  namespace: <NAME-SPACE>
spec:
  secretName: <SERVICE-NAME>-tls  # Имя секрета для сертификата
  dnsNames:
    - <YOU-DOMAIN-NAME>  # Домен для сертификата
  issuerRef:
    name: letsencrypt-prod  # ClusterIssuer для Let’s Encrypt
    kind: ClusterIssuer
```

**ВАЖНО**: В манифесте используется letsencrypt-prod для получения сертификата от Let’s Encrypt. Это нестандартный 
ClusterIssuer cert-manager, создание которого описано в [документации](https://cert-manager.io/docs/usage/ingress/#tls-termination)
и [отдельной инструкции](k3s-custom-container-deployment.md#создание-clusterissuer)
(возможно, вам нужно будет создать его отдельно). Если вы используете другой ClusterIssuer, то замените letsencrypt-prod
на имя вашего ClusterIssuer в секции `issuerRef` в манифесте.

## Применяем манифест
```bash
sudo kubectl apply -f ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
```

Проверяем, что все ресурсы создались:
```bash
sudo kubectl get all -n <NAME-SPACE>
sudo kubectl get ingressroute -n <NAME-SPACE>
sudo kubectl get middleware -n <NAME-SPACE>
sudo kubectl get service -n <NAME-SPACE>
sudo kubectl get endpoints -n <NAME-SPACE>
```

Увидим что-то вроде:
```text
NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)               AGE
service/<SERVICE-NAME>   ClusterIP   10.43.152.59   <none>        <PROXIED-PORT>/TCP   3h
```

Для IngressRoute:
```text
NAME                   AGE
<SERVICE-NAME>         3h
<SERVICE-NAME>-acme    1h
<SERVICE-NAME>-http    1h
<SERVICE-NAME>-https   1h
```

Для Middleware:
```text
NAME                AGE
https-redirect      1h
no-https-redirect   1h
```

Для Service:
```text
NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)              AGE
<SERVICE-NAME>   ClusterIP   10.43.152.59   <none>        <PROXIED-PORT>/TCP   3h
```

Для Endpoints:
```text
NAME             ENDPOINTS                       AGE
<SERVICE-NAME>   <PROXIED-HOST>:<PROXIED-PORT>   3h
```

Проверяем, что сертификат создан:
```bash
sudo kubectl describe certificate -n <NAME-SPACE> <SERVICE-NAME>-tls
sudo kubectl get secret -n <NAME-SPACE>  <SERVICE-NAME>-tls
```