Compare commits
13 Commits
f50b509440
...
bf15c4fb7b
Author | SHA1 | Date | |
---|---|---|---|
bf15c4fb7b | |||
effe549f75 | |||
205e8b9c53 | |||
c18a5ffe43 | |||
9e1a414f45 | |||
138bb728fc | |||
dc04658e70 | |||
5560ed2d8e | |||
3b64bc44ac | |||
0b4bc455bf | |||
e2c18fd572 | |||
3bc414ca98 | |||
b6b54e136c |
@@ -11,9 +11,11 @@
|
||||
|
||||
## Kubernetes (k3s/k8s)
|
||||
* [Установка k3s на Orange Pi 5 Plus](raspberry-and-orange-pi/k3s.md)
|
||||
* [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.md) (k3s)
|
||||
* [Под с 3X-UI](kubernetes/k3s-3xui-pod.md) (k3s)
|
||||
* [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.md) в k3s
|
||||
* [Подключение менеджера сертификатов (cert-manager) Let's Encrypt](kubernetes/k3s-lets-encrypt-cert-manager.md) к k3s
|
||||
* [Под с 3X-UI](kubernetes/k3s-3xui-pod.md) в k3s
|
||||
* [Проксирование внешнего хоста через Traefik (Ingress-контроллер)](kubernetes/k3s-proxy.md)
|
||||
* [Перенос контейнера Docker в k3s](kubernetes/k3s-migrating-container-from-docker-to-kubernetes.md)
|
||||
|
||||
## Python
|
||||
* [Устранение проблем при установке Python-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md)
|
||||
|
@@ -321,15 +321,13 @@ Keepalived добавляет 50 к базовому приоритету нод
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Доступ через Ingress Controller по имени домена (http).
|
||||
|
||||
Сейчас web-панель 3x-ui доступна через VIP по порту `2053` по http. _В принципе, так можно и оставить_. Но если мы хотим
|
||||
иметь доступ по https, да еще чтобы это работало через доменное имя, и чтобы k3s автоматически получал и обновлял
|
||||
сертификаты, то можно использовать Ingress-контроллер. Он будет брать трафик с порта VIP, по порту `2055`, через
|
||||
балансировщик svclb-traefik направлять его на Ingress-контроллер Traefik и перенаправлять его на под с 3x-ui (тоже
|
||||
через VIP но уже по порту `2053`).
|
||||
через VIP но уже по порту `2053`). Дополнительно, есть [заметка про настройку Traefik в качестве прокси](k3s-proxy.md).
|
||||
|
||||
#### Манифест для Ingress-контроллера Traefik
|
||||
|
||||
|
198
kubernetes/k3s-affinate.md
Normal file
198
kubernetes/k3s-affinate.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Аффинность узлов в K3s
|
||||
|
||||
Аффинность узлов (node affinity) в K3s позволяет управлять размещением подов на определённых узлах кластера.
|
||||
Это полезно, когда нужно обеспечить запуск приложения на узле с определёнными характеристиками или метками.
|
||||
|
||||
?Привязка узлов может быть и жесткой и не жесткой.
|
||||
|
||||
## Под с предпочтительной привязкой к узлу
|
||||
|
||||
У меня есть [VPN-под на 3x-ui](k3s-3xui-pod.md), и предпочтительно, чтобы он при старте k3s запускался на узле
|
||||
c наивысшем приоритетом в keepalive (у меня это хост `opi5plus-1`), но если нода "упадет" и под 3x-ui "переедет"
|
||||
на другой узел, то keepalive повысит приоритет т ноды При этом возвращать под на исходный узел после его восстановления не обязательно.
|
||||
|
||||
(например, opi5plus-1), но мог переезжать на другие узлы (например, opi5plus-2 или opi5plus-3), если что-то пойдёт не так с основным узлом. При этом возвращать под на исходный узел после его восстановления не обязательно.
|
||||
|
||||
Ответ: Да, это возможно! В Kubernetes для этого используется механизм node affinity (аффинность к узлам). Он позволяет указать "мягкое" предпочтение для запуска пода на определённом узле, но не запрещает поду перемещаться, если нужного узла нет.
|
||||
|
||||
Как настроить node affinity
|
||||
Допустим, ты хочешь, чтобы под запускался на opi5plus-1 по умолчанию, но мог переехать на другие узлы при необходимости.
|
||||
|
||||
Шаги:
|
||||
|
||||
Проверь или добавь метки узлам: Убедись, что твои узлы имеют метки (labels). Например:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
kubectl label nodes opi5plus-1 preferred=true
|
||||
kubectl label nodes opi5plus-2 preferred=false
|
||||
kubectl label nodes opi5plus-3 preferred=false
|
||||
Здесь preferred=true — это метка, которая указывает предпочтительный узел.
|
||||
Обнови манифест деплоя 3x-ui: Добавь секцию affinity в спецификацию пода. Пример:
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: x-ui
|
||||
namespace: x-ui
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: x-ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: x-ui
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 1
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: preferred
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
containers:
|
||||
- name: x-ui
|
||||
image: ghcr.io/mhsanaei/3x-ui:latest
|
||||
# Остальные настройки контейнера...
|
||||
Что здесь происходит:
|
||||
|
||||
preferredDuringSchedulingIgnoredDuringExecution — это "мягкое" правило. Kubernetes постарается запустить под на узле с меткой preferred=true, но если это невозможно (например, узел недоступен), под запустится на любом другом доступном узле.
|
||||
weight: 1 — вес предпочтения. Если у тебя будет несколько правил, можно задавать приоритеты, но для одного правила это не так важно.
|
||||
Как это работает:
|
||||
|
||||
Если узел opi5plus-1 доступен, под запустится там.
|
||||
Если opi5plus-1 упадёт, под автоматически переедет на другой узел (например, opi5plus-2).
|
||||
После восстановления opi5plus-1 под останется на текущем узле и не вернётся автоматически, что соответствует твоему запросу.
|
||||
Как это сочетается с Keepalived?
|
||||
Твой скрипт проверки в Keepalived гарантирует, что виртуальный IP будет следовать за подом 3x-ui. Если ты задашь opi5plus-1 как предпочтительный узел через affinity и дашь ему высокий приоритет в Keepalived (например, 100), то в нормальной ситуации под и IP будут на одном узле. А если под переедет из-за сбоя, Keepalived переключит IP на новый узел благодаря скрипту.
|
||||
|
||||
Итог
|
||||
Скрипт проверки: sudo не нужен, так как Keepalived запускает скрипт от root. Если будут проблемы с доступом, добавь --kubeconfig с путём к файлу конфигурации.
|
||||
Приоритет узла для пода: Используй node affinity с "мягким" предпочтением (preferredDuringSchedulingIgnoredDuringExecution). Это позволит поду запускаться на нужном узле, но свободно перемещаться при сбоях без обязательного возвращения.
|
||||
Твоя идея с приоритетами и "кочеванием" пода отлично реализуема, и связка Keepalived + affinity обеспечит гибкость и надёжность. Удачи с настройкой, если что — пиши! 😊
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Отлично. Ты мне отлично помог и 3x-ui заработал на VIP. Все сработало как ты сказал. Объясни пожалуйста про метки:
|
||||
```
|
||||
kubectl label nodes opi5plus-1 preferred=true
|
||||
kubectl label nodes opi5plus-2 preferred=false
|
||||
kubectl label nodes opi5plus-3 preferred=false
|
||||
```
|
||||
это означает, что все поды будут предпочитать opi5plus-1 или это касается только подов с nodeAffinity?
|
||||
|
||||
|
||||
|
||||
3 / 3
|
||||
|
||||
|
||||
|
||||
Completed
|
||||
42s
|
||||
Thinking
|
||||
Исследую использование меток
|
||||
|
||||
|
||||
Thinking
|
||||
Исследую использование меток
|
||||
Пользователь интересуется метками в K3s и их применением для привязки узлов. Они пометили один узел как "preferred=true", а другие как "preferred=false".
|
||||
Метки в K3s — это пары ключ-значение, прикрепленные к объектам, таким как узлы, поды, сервисы. Они помогают организовать и выбрать подмножества объектов.
|
||||
Привязка узлов позволяет указать, на каких узлах должен размещаться под. Есть два типа: обязательная и предпочтительная.
|
||||
Метка "preferred=true" влияет только на поды с привязкой узлов, ищущими эту метку. Поды без таких правил размещаются без учета этой метки.
|
||||
Все три узла имеют метку "preferred", но с разными значениями: opi5plus-1 — true, opi5plus-2 и opi5plus-3 — false.
|
||||
Если под не имеет привязки узлов, планировщик размещает его на любом узле с нужными ресурсами, игнорируя метку "preferred".
|
||||
Под без nodeSelector или nodeAffinity размещается на любом узле с необходимыми ресурсами. Метка "preferred" важна только для подов, ссылающихся на нее.
|
||||
В K8s, метка "preferred=false" добавляет метку "preferred" со значением "false" на узел. Если метки нет, узел ее не имеет.
|
||||
Под с предпочтительной привязкой сначала пытается разместиться на opi5plus-1, где preferred=true. Если не получится, может быть размещен на opi5plus-2 или opi5plus-3.
|
||||
Метка "preferred=true" влияет только на поды с правилами, ищущими эту метку. Другие поды размещаются по другим критериям.
|
||||
В стандартном K3s нет поведения, которое по умолчанию использует метки, такие как "preferred", для влияния на планирование. Метка важна только для подов, явно ссылающихся на нее в правилах привязки.
|
||||
Другие поды, без ссылок на эту метку, будут размещены без учета "preferred", основываясь на доступности ресурсов.
|
||||
Пример: под без привязки узлов размещается на любом узле, игнорируя метку "preferred". Это подтверждает, что метка влияет только на поды с соответствующими правилами.
|
||||
Если под указывает nodeSelector с preferred="false", он размещается только на узлах, где preferred=false, например, opi5plus-2 или opi5plus-3.
|
||||
Метка "preferred" влияет только на поды с правилами привязки или селектора, ссылающимися на эту метку. Поды без таких правил не затрагиваются.
|
||||
Метка "preferred" важна только для подов, таких как 3x-ui, с установленной предпочтительной привязкой. Другие поды в кластере размещаются по своим спецификациям, если нет похожих правил.
|
||||
Метки на узлах важны только тогда, когда правила планирования пода ссылаются на них. Например, под может исключать узлы, где preferred=true, если указано соответствующее правило.
|
||||
Прямой ответ
|
||||
Метки, такие как preferred=true или preferred=false, влияют только на поды с указанной аффинностью узлов, которые ссылаются на эту метку.
|
||||
Другие поды, без аффинности, размещаются без учёта этих меток, основываясь на доступных ресурсах.
|
||||
Влияние на поды
|
||||
Метки на узлах, вроде preferred, не имеют значения для подов, если те не имеют правил аффинности, ссылающихся на эту метку. Например, под с аффинностью, предпочитающей preferred=true, будет стараться запускаться на opi5plus-1, но может переместиться на другие узлы, если opi5plus-1 недоступен.
|
||||
|
||||
Неожиданный нюанс
|
||||
Интересно, что метки могут использоваться в других правилах, например, для исключения узлов (NotIn), но это требует явного указания в спецификации пода, что выходит за рамки стандартного поведения.
|
||||
|
||||
Отчёт
|
||||
Введение
|
||||
Метки (labels) в K3s — это ключевые пары, прикрепляемые к объектам, таким как узлы, поды и сервисы, для организации и выбора подмножества объектов. В контексте планирования подов метки на узлах используются для влияния на их размещение через механизмы, такие как аффинность узлов (node affinity). Пользователь задал вопрос о влиянии меток preferred=true и preferred=false, установленных на узлы, на все поды или только на те, у которых указана аффинность узлов.
|
||||
|
||||
Анализ влияния меток
|
||||
Метки на узлах, такие как preferred=true или preferred=false, добавляются через команды вроде kubectl label nodes opi5plus-1 preferred=true. Эти метки сами по себе не имеют встроенного значения для планировщика K3s, если поды не ссылаются на них в своих правилах размещения.
|
||||
|
||||
В K3s, основанном на Kubernetes, планировщик подов (scheduler) размещает поды на узлах, учитывая доступные ресурсы (CPU, память) и любые ограничения, указанные в спецификации пода. Метки становятся значимыми только тогда, когда под использует их в правилах nodeSelector, nodeAffinity или nodeAntiAffinity.
|
||||
|
||||
Пользователь настроил метки следующим образом:
|
||||
|
||||
opi5plus-1: preferred=true
|
||||
opi5plus-2: preferred=false
|
||||
opi5plus-3: preferred=false
|
||||
И в манифесте пода 3x-ui указал аффинность:
|
||||
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 1
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: preferred
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
Это означает, что под предпочтительно будет размещён на узле с меткой preferred=true, то есть на opi5plus-1. Если такой узел недоступен, под может быть размещён на opi5plus-2 или opi5plus-3, где preferred=false.
|
||||
|
||||
Влияние на поды без аффинности
|
||||
Поды, у которых не указаны nodeSelector, nodeAffinity или nodeAntiAffinity, размещаются планировщиком K3s без учёта метки preferred. Планировщик выбирает узел на основе:
|
||||
|
||||
Доступных ресурсов.
|
||||
Отсутствия конфликтов (например, taints).
|
||||
Других факторов, таких как анти-аффинность подов или балансировка нагрузки.
|
||||
Таким образом, метки preferred=true и preferred=false не влияют на поды, у которых нет правил, ссылающихся на эту метку. Например, поды Shadowsocks, если они не имеют аффинности, будут размещаться на любом узле без учёта preferred.
|
||||
|
||||
Пример с таблицей
|
||||
Вот таблица, которая иллюстрирует влияние меток на поды:
|
||||
|
||||
Под Имеет аффинность к preferred? Узел предпочтения Возможные узлы при сбое
|
||||
3x-ui Да, preferred=true opi5plus-1 opi5plus-2, opi5plus-3
|
||||
Shadowsocks (без аффинности) Нет Любой доступный узел Любой доступный узел
|
||||
Заключение
|
||||
Метки preferred влияют только на поды с аффинностью, ссылающейся на эту метку, и не затрагивают остальные поды. Это позволяет гибко управлять размещением, сохраняя свободу для других приложений.
|
||||
|
||||
Ключевые цитаты
|
||||
Kubernetes Node Affinity and Anti-Affinity https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
|
||||
K3s Documentation on Node Labels https://grok.com/chat/6a083b91-ea05-4246-9407-b9b25af4b48f#:~:text=K3s%20Documentation%20on%20Node%20Labels
|
212
kubernetes/k3s-lets-encrypt-cert-manager.md
Normal file
212
kubernetes/k3s-lets-encrypt-cert-manager.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Подключение менеджера сертификатов (cert-manager) Let's Encrypt к k3s
|
||||
|
||||
№ Установка cert-manager
|
||||
|
||||
Установим `cert-manager` из официального манифеста GitHub. Это автоматически добавит все необходимые компоненты,
|
||||
включая CRD (Custom Resource Definitions) и контроллеры:
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
|
||||
```
|
||||
|
||||
Это установит cert-manager версии 1.15.3 (предварительно надо проверь актуальную версию на GitHub cert-manager).
|
||||
Компоненты развернутся в namespace cert-manager.
|
||||
|
||||
Проверим установку:
|
||||
```bash
|
||||
kubectl get pods --namespace cert-manager
|
||||
```
|
||||
|
||||
Увидим, что все поды в статусе `Running`:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
cert-manager-778cb8f45-gxtpf 1/1 Running 0 22m
|
||||
cert-manager-cainjector-6d5854cdd8-w4d8x 1/1 Running 0 22m
|
||||
cert-manager-webhook-78b57fb6bb-jljdx 1/1 Running 0 22m
|
||||
```
|
||||
|
||||
Что это за поды:
|
||||
- `cert-manager` — основной контроллер cert-manager, который управляет созданием и обновлением сертификатов.
|
||||
- `cert-manager-cainjector` — контроллер, который автоматически добавляет CA-сертификаты в секреты и ресурсы Kubernetes.
|
||||
- `cert-manager-webhook` — контроллер, который обрабатывает запросы на валидацию и мутацию ресурсов cert-manager.
|
||||
|
||||
Проверим, что Custom Resource Definitions (CRDs) для cert-manager созданы:
|
||||
```bash
|
||||
kubectl get crd | grep cert-manager
|
||||
```
|
||||
|
||||
Должны быть `certificates.cert-manager.io`, `issuers.cert-manager.io`, `clusterissuers.cert-manager.io и другие:
|
||||
```text
|
||||
certificaterequests.cert-manager.io 2025-04-27T16:29:56Z
|
||||
certificates.cert-manager.io 2025-04-27T16:29:56Z
|
||||
challenges.acme.cert-manager.io 2025-04-27T16:29:56Z
|
||||
clusterissuers.cert-manager.io 2025-04-27T16:29:56Z
|
||||
issuers.cert-manager.io 2025-04-27T16:29:56Z
|
||||
orders.acme.cert-manager.io 2025-04-27T16:29:57Z
|
||||
```
|
||||
|
||||
# Создание ClusterIssuer (Эмитент сертификатов для всего кластера)
|
||||
|
||||
Создадим `ClusterIssuer` для Let's Encrypt. Это позволит cert-manager автоматически запрашивать и обновлять сертификаты.
|
||||
Разместим его в отдельном файле `~/k3s/cert-manager/letsencrypt-clusterissuer.yaml`:
|
||||
```bash
|
||||
mkdir -p ~/k3s/cert-manager
|
||||
nano ~/k3s/cert-manager/letsencrypt-clusterissuer.yaml
|
||||
```
|
||||
|
||||
И вставим в него следующий код (не забудьте заменить `<my@email.com>` на свой email):
|
||||
```yaml
|
||||
apiVersion: cert-manager.io/v1 # Версия API для cert-manager, v1 — текущая стабильная.
|
||||
kind: ClusterIssuer # Тип ресурса. ClusterIssuer — глобальный объект для выдачи сертификатов
|
||||
# всему кластеру (в отличие от Issuer, который ограничен namespace).
|
||||
metadata:
|
||||
name: letsencrypt-prod # Имя `ClusterIssuer`, на него будем ссылаться в манифестах (например, в Certificate).
|
||||
spec:
|
||||
acme: # Используется ACME-протокол (Automated Certificate Management Environment) Let's Encrypt.
|
||||
server: https://acme-v02.api.letsencrypt.org/directory # URL сервера Let's Encrypt (боевой).
|
||||
email: <my@email.com> # Логин-клиент Let's Encrypt (раньше так же использовался для уведомлений).
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod # Имя секрета, где будет храниться приватные ключи для ACME-аккаунта.
|
||||
solvers: # Список способов "решения" ACME-задач для подтверждения владения доменом.
|
||||
- http01: # Используется HTTP-01 challenge (подтверждение через HTTP-запросы).
|
||||
ingress:
|
||||
class: traefik # Указывает, что ingress-контроллер Traefik будет обрабатывать HTTP-01 challenge
|
||||
# (создает временные маршруты для `/.well-known/acme-challenge`) для подтверждения
|
||||
# владения доменом.
|
||||
```
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
kubectl apply -f ~/k3s/cert-manager/letsencrypt-clusterissuer.yaml
|
||||
```
|
||||
|
||||
Проверим, что `ClusterIssuer` создан:
|
||||
```bash
|
||||
kubectl get clusterissuer
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY AGE
|
||||
letsencrypt-prod True 27s
|
||||
```
|
||||
|
||||
Проверим статус:
|
||||
```bash
|
||||
kubectl describe clusterissuer letsencrypt-prod
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
...
|
||||
...
|
||||
Status:
|
||||
Acme:
|
||||
Last Private Key Hash: тут-хранится-хэш-приватного-ключа
|
||||
Last Registered Email: <my@email.com>
|
||||
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/2365771147
|
||||
Conditions:
|
||||
Last Transition Time: 2025-04-27T17:00:56Z
|
||||
Message: The ACME account was registered with the ACME server
|
||||
Observed Generation: 1
|
||||
Reason: ACMEAccountRegistered
|
||||
Status: True
|
||||
Type: Ready
|
||||
...
|
||||
```
|
||||
|
||||
Должны быть `Status: True` и `Reason: ACMEAccountRegistered`, что означает, что `ClusterIssuer` успешно зарегистрирован
|
||||
и готов к использованию.
|
||||
|
||||
# Создание сертификата
|
||||
|
||||
Эта общая часть, объясняющая как создавать манифесты для выпуска сертификатов.
|
||||
|
||||
В своем манифесте, например, в манифесте пода, создается блок (данный кусочек взят из манифеста пода
|
||||
[Gitea](k3s-migrating-container-from-docker-to-kubernetes.md) и он находится в пространстве имен `gitea`):
|
||||
```yaml
|
||||
---
|
||||
# Манифест для получения сертификата от Let's Encrypt
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: gitea-tls # Имя сертификата `gitea-tls`
|
||||
namespace: gitea # В пространстве имен `gitea`
|
||||
spec:
|
||||
secretName: gitea-tls # Имя секрета для сертификата
|
||||
dnsNames: # Доменные имена для сертификата...
|
||||
- git.cube2.ru
|
||||
issuerRef: # Эмитент сертификата (Issuer)
|
||||
name: letsencrypt-prod # Имя ClusterIssuer, который отвечает за получение сертификата: `letsencrypt-prod`
|
||||
kind: ClusterIssuer # Тип эмитента
|
||||
```
|
||||
|
||||
После применения манифеста, можно проверить статус сертификата:
|
||||
```bash
|
||||
kubectl get certificate -A
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAMESPACE NAME READY SECRET AGE
|
||||
ab-shelf audiobookshelf-tls False audiobookshelf-tls 4h
|
||||
gitea gitea-tls True gitea-tls 14m
|
||||
```
|
||||
|
||||
Тут можно заметить, что сертификат `audiobookshelf-tls` не готов (False), а `gitea-tls` готов (True).
|
||||
|
||||
Как проверить статус сертификата и причину его неготовности (на примере `audiobookshelf-tls`):
|
||||
```bash
|
||||
kubectl describe certificate audiobookshelf-tls -n ab-shelf
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
Name: audiobookshelf-tls
|
||||
Namespace: ab-shelf
|
||||
...
|
||||
...
|
||||
...
|
||||
Spec:
|
||||
Dns Names:
|
||||
<тут-будет-доменное-имя-для-которого-выдается-сертификат>
|
||||
Issuer Ref:
|
||||
Kind: ClusterIssuer
|
||||
Name: letsencrypt-prod
|
||||
Secret Name: audiobookshelf-tls
|
||||
Status:
|
||||
Conditions:
|
||||
Last Transition Time: 2025-04-27T17:13:32Z
|
||||
Message: The certificate request has failed to complete and will be retried: Failed to wait for order resource "audiobookshelf-tls-1-722111375" to become ready: order is in "errored" state: Failed to create Order: 429 urn:ietf:params:acme:error:rateLimited: too many certificates (5) already issued for this exact set of domains in the last 168h0m0s, retry after 2025-04-28 19:30:09 UTC: see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-exact-set-of-hostnames
|
||||
Observed Generation: 1
|
||||
Reason: Failed
|
||||
Status: False
|
||||
Type: Issuing
|
||||
Last Transition Time: 2025-04-27T17:13:29Z
|
||||
Message: Issuing certificate as Secret does not exist
|
||||
Observed Generation: 1
|
||||
Reason: DoesNotExist
|
||||
Status: False
|
||||
Type: Ready
|
||||
...
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
Как видно, причина неготовности сертификата `audiobookshelf-tls` в том, что уже превышен лимит на количество сертификатов
|
||||
(5) для данного доменного имени за последние 168 часов (7 дней). Указано время после которого можно повторить запрос.
|
||||
|
||||
По идее запрос на повторный сертификат будет отправлен автоматически, но это может произойти спустя несколько часов
|
||||
после разрешенного времени (учтите что время указывается в UTC, делайте поправку по своему локальному времени).
|
||||
|
||||
Чтобы ускорить процесс, можно удалить сертификат и создать его заново (на примере `audiobookshelf-tls`):
|
||||
```bash
|
||||
kubectl delete certificate audiobookshelf-tls -n ab-shelf
|
||||
```
|
||||
|
||||
А затем повторно принять манифест, в котором у вас находится `kind: Certificate`
|
||||
|
||||
| Заметка |
|
||||
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Пока Let's Encrypt не выдал сертификат, Traefik будет работать по SSL (https) на самоподписанном сертификате. Можно открыть анонимное окно браузера, согласится с предупреждениями безопасности и пользоваться сайтом. |
|
||||
| |
|
969
kubernetes/k3s-migrating-container-from-docker-to-kubernetes.md
Normal file
969
kubernetes/k3s-migrating-container-from-docker-to-kubernetes.md
Normal file
@@ -0,0 +1,969 @@
|
||||
# Перенос контейнера из Docker в k3s (на примере Gitea)
|
||||
|
||||
Вот эта самая инструкция, котору вы сейчас читаете размещена на моем персональном сервере Gitea. Раньше она размещалась
|
||||
на домашнем хосте Orange Pi 5 в Docker-контейнере, и проксировалась через nginx c добавлением SSL-сертификата
|
||||
Let's Encrypt. Мне показалось, что зависимость от одного хоста -- опасненько. И я решил перенести Gitea в кластер,
|
||||
в котором у меня несколько узлов, и в случае падения одного из них, Gitea продолжит работать на другом узле. К тому
|
||||
же мне очень хотелось подключить старый Orange Pi 5 тоже к кластеру (ведь для этого нужно установить чистую систему и
|
||||
[перекомпилировать ядро](../raspberry-and-orange-pi/opi5plus-rebuilding-linux-kernel-for-iscsi.md)
|
||||
).
|
||||
|
||||
Я решил задокументировать процесс переноса контейнера из Docker в k3s, тем более Gitea не единственный контейнер,
|
||||
который мне нужно было перенести. Возможно мой опыт вам тоже пригодится.
|
||||
|
||||
## Перенос данных Docker-контейнера Gitea на узел k3s
|
||||
|
||||
На старом хосте Gitea работал в контейнере через docker-compose. Вот её `docker-compose.yaml`:
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
restart: always
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "2222:22"
|
||||
volumes:
|
||||
- /home/orangepi/docker-data/gitea-new:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
```
|
||||
|
||||
Как видим, в контейнере Gitea (с именем `gitea-new`) в качестве базы используется SQLite. Файл базы, конфиги
|
||||
и остальные данные хранятся в каталоге `/data` контейнера, смонтированного во внешний том (volume)
|
||||
`/home/orangepi/docker-data/gitea-new`. Нам нужно будет перенести все эти данные в кластер k3s, в блочное
|
||||
хранилище Longhorn. Для этого создадим архив с данными gitea и перенесем его на один из узлов k3s.
|
||||
|
||||
Чтобы забрать данные из контейнера, нужно остановить его (на хосте с Docker):
|
||||
```bash
|
||||
docker stop docker stop gitea-new
|
||||
```
|
||||
### Архивируем данные gitea
|
||||
|
||||
Находясь на хосте в Docker, cоздадим архив с данными gitea:
|
||||
```bash
|
||||
tar -czf /home/orangepi/gitea-data.tar.gz -C /home/orangepi/docker-data/gitea-new .
|
||||
```
|
||||
|
||||
Проверим, что архив создался:
|
||||
```bash
|
||||
ls -alh /home/orangepi/gitea-data.tar.gz
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
-rw-r--r-- 1 orangepi orangepi 147M Apr 16 18:10 /home/orangepi/gitea-data.tar.gz
|
||||
```
|
||||
|
||||
### Перенесём данные на узел k3s по SCP:
|
||||
|
||||
Находясь на хосте с Docker, перенесем архив с данными gitea на узел k3s (в моем случае это `opi5plus-1`):
|
||||
```bash
|
||||
scp /home/orangepi/gitea-data.tar.gz <USER-NAME>@opi5plus-1:/home/<USER-NAME>/
|
||||
```
|
||||
|
||||
Перейдем на узел `opi5plus-1` k3s и проверим, что архив с данными gitea там есть:
|
||||
```bash
|
||||
ls -alh ~/gitea-data.tar.gz
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
-rw-r--r-- 1 <USER-NAME> <USER-NAME> 147M Apr 16 18:12 /home/<USER-NAME>/gitea-data.tar.gz
|
||||
```
|
||||
|
||||
Архив с данными gitea на месте. Теперь можно переходить к следующему шагу.
|
||||
|
||||
## Подготовка узла k3s
|
||||
|
||||
Создадим пространство имен для gitea в k3s (чтобы все было аккуратно):
|
||||
```bash
|
||||
sudo kubectl create namespace gitea
|
||||
```
|
||||
|
||||
Создаем папку для хранения манифестов gitea:
|
||||
```bash
|
||||
mkdir -p ~/k3s/gitea
|
||||
```
|
||||
|
||||
## Перемещаем файлы и базу данных SQLite в блочное хранилище k3s
|
||||
|
||||
Теперь нам надо перенести данные gitea в k3s в PersistentVolumeClaim (Longhorn). Longhorn -- это блочное хранилище k3s,
|
||||
которое позволяет создавать и управлять блочными томами в кластере для обеспечения высокой доступности
|
||||
и отказоустойчивости. Если узел, на котором находится том, выходит из строя, Longhorn автоматически перемещает том
|
||||
на другой узел и контейнер продолжает работу с томом, как будто ничего не произошло.
|
||||
|
||||
Создадим манифест для PersistentVolumeClaim (PVC) и PersistentVolume (PV):
|
||||
```bash
|
||||
nano ~/k3s/gitea/longhorn-pvc.yaml
|
||||
```
|
||||
|
||||
Вставляем в него следующее содержимое:
|
||||
```yaml
|
||||
piVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: gitea-pvc # Имя PVC-хранилища
|
||||
namespace: gitea # Пространство имен gitea
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce # Режим доступа к PVC -- чтение и запись
|
||||
storageClassName: longhorn # Используем Longhorn как класс хранения
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi # Размер под данные Gitea 10 Гб (максимальный объем)
|
||||
```
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/longhorn-pvc.yaml
|
||||
```
|
||||
|
||||
Проверим, что PVC создан и доступен:
|
||||
```bash
|
||||
sudo kubectl get pvc -n gitea -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE VOLUMEMODE
|
||||
gitea-pvc Bound pvc-5a562c67-89b2-48e2-97a6-25035b90729a 10Gi RWO longhorn <unset> 20h Filesystem
|
||||
```
|
||||
|
||||
Создадим временный под `gitea-init-data` для переноса данных gitea с хост-узла в хранилище Longhorn:
|
||||
```bash
|
||||
nano ~/k3s/gitea/gitea-init-data.yaml
|
||||
```
|
||||
|
||||
Манифест временного пода:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: gitea-init-data
|
||||
namespace: gitea
|
||||
spec:
|
||||
nodeName: opi5plus-1 # Привязка к хосту opi5plus-1 (на этом узле у нас лежит архив с данными gitea из docker)
|
||||
containers:
|
||||
- name: init-data
|
||||
image: alpine:latest
|
||||
command: ["/bin/sh", "-c", "tar -xzf /mnt/gitea-data.tar.gz -C /data && chmod -R 777 /data && ls -la /data && sleep 3600"]
|
||||
volumeMounts:
|
||||
- name: gitea-data
|
||||
mountPath: /data
|
||||
- name: tmp-data
|
||||
mountPath: /mnt
|
||||
volumes:
|
||||
- name: gitea-data
|
||||
persistentVolumeClaim:
|
||||
claimName: gitea-pvc
|
||||
- name: tmp-data
|
||||
hostPath:
|
||||
path: /home/<USER-NAME>
|
||||
type: Directory # Указываем, что это папка
|
||||
restartPolicy: Never
|
||||
```
|
||||
|
||||
Что тут происходит:
|
||||
- `metadata` — задаёт имя пода `gitea-init-data` и привязывает его к пространству имен `gitea`.
|
||||
- `nodeName` — гарантирует запуск на нужной ноде (в данном случае `opi5plus-1`).
|
||||
- `containers` — задаёт контейнер с именем `init-data`, который использует образ `alpine:latest`.
|
||||
- `command` — выполняет команду в контейнере:
|
||||
- `tar -xzf /mnt/gitea-data.tar.gz -C /data` — распаковывает архив в `/data`.
|
||||
- `chmod -R 777 /data` — задаёт права на папку `/data`.
|
||||
- `ls -la /data` — выводит содержимое `/data` в логи.
|
||||
- `sleep 3600` — держит под живым 1 час, чтобы ты можно было зайти в sh и проверить, что всё распаковалось.
|
||||
- `volumeMounts` — монтирует два тома:
|
||||
- том и именем `gitea-data` монтируем PVC в `/data`.
|
||||
- том и именем `tmp-data` монтирует временную папку на хосте в `/mnt`.
|
||||
- `volumes` — определяет расположение томов:
|
||||
- том `gitea-data` — размещается в PersistentVolumeClaim (PVC) хранилища `gitea-pvc`, которое мы создали ранее.
|
||||
- том `tmp-data` — размещается в каталоге хоста `/home/<USER-NAME>` (как рам, у нас лежит архив с данными gitea
|
||||
из docker).
|
||||
- `restartPolicy: Never` — под не будет перезапускаться, если завершится.
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/gitea-init-data.yaml
|
||||
```
|
||||
|
||||
Проверим, что под создан и работает:
|
||||
```bash
|
||||
sudo kubectl get pod -n gitea -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
gitea-init-data 1/1 Running 0 4m 10.42.2.64 opi5plus-1 <none> <none>
|
||||
```
|
||||
|
||||
Проверим логи пода:
|
||||
```bash
|
||||
sudo kubectl logs -n gitea gitea-init-data
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
total 36
|
||||
drwxrwxrwx 6 1000 1000 4096 Apr 16 15:09 .
|
||||
drwxr-xr-x 1 root root 4096 Apr 17 13:01 ..
|
||||
drwxrwxrwx 5 root root 4096 Apr 16 15:09 git
|
||||
drwxrwxrwx 18 root root 4096 Apr 17 13:02 gitea
|
||||
drwxrwxrwx 2 root root 16384 Apr 16 15:27 lost+found
|
||||
drwxrwxrwx 2 root root 4096 Apr 17 13:01 ssh
|
||||
```
|
||||
|
||||
Как видим, данные благополучно распаковались в `/data` внутри пода, и это Longhorn PVC `gitea-pvc`. Можно также "зайти"
|
||||
в под и посмотреть, что там внутри:
|
||||
```bash
|
||||
sudo kubectl exec -it -n gitea gitea-init-data -- /bin/sh
|
||||
```
|
||||
|
||||
Внутри пода дать команду, например:
|
||||
```bash
|
||||
ls -l /data/gitea/gitea.db
|
||||
```
|
||||
|
||||
И убедиться, что данные gitea распакованы. Увидим что-то вроде:
|
||||
```text
|
||||
-rwxrwxrwx 1 root root 2555904 Apr 16 15:09 /data/gitea/gitea.db
|
||||
```
|
||||
|
||||
База SQLite gitea.db на месте. Выходим из пода:
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
|
||||
## Создание пода gitea и подключение к нему хранилища
|
||||
|
||||
Теперь нужно создать под с Gitea, подключить к нему PVC `gitea-pvc` и проверить, что данные подцепились.
|
||||
|
||||
### Настройка Аффинити -- предпочтительные узлы
|
||||
|
||||
В моем кластере k3s на OrangePi 5 Plus несколько узлов работают на nVME SSD, а некоторые на eMMC. Накопители
|
||||
SSD быстрее (если интересно, то вот [заметка о тестировании производительности дискового накопителя](../raspberry-and-orange-pi/measuring-performance-storage-devices.md))
|
||||
и потому если контейнер с Gitea будет работать на SSD, то он будет работать быстрее. Как настроить предпочтение узлов
|
||||
описано в [заметке о аффинити](k3s-affinate.md), поэтому кратко: присваиваем узлам метки, например `disk=ssd`:
|
||||
```bash
|
||||
sudo kubectl label nodes opi5plus-1 disk=ssd
|
||||
sudo kubectl label nodes opi5plus-3 disk=ssd
|
||||
```
|
||||
|
||||
Проверяем, что метки добавлены:
|
||||
```bash
|
||||
sudo kubectl get nodes --show-labels | grep "disk=ssd"
|
||||
```
|
||||
|
||||
Будут показаны только узлы с меткой `disk=ssd`. У каждого узда очень много меток.
|
||||
|
||||
### Создание манифеста для развертывания Gitea
|
||||
|
||||
Создадим манифест для развертывания Gitea (deployment):
|
||||
```bash
|
||||
nano ~/k3s/gitea/gitea-deployment.yaml
|
||||
```
|
||||
|
||||
Вставляем в него следующее содержимое:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # определяем тип ресурса -- Deployment (развертывание)
|
||||
metadata: # определяем метаданные развертывания
|
||||
name: gitea # имя развертывания `gitea`
|
||||
namespace: gitea # в пространстве имен `gitea`
|
||||
labels:
|
||||
app: gitea
|
||||
spec:
|
||||
replicas: 1 # количество реплик (подов) в кластере -- 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitea
|
||||
spec:
|
||||
affinity: # определяем аффинити (предпочтения) для узлов
|
||||
nodeAffinity: # аффинити для узлов
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100 # вес предпочтения -- 100
|
||||
preference: # предпочтение
|
||||
matchExpressions: # выражения для соответствия
|
||||
- key: disk # метка узла `disk`...
|
||||
operator: In # ...оператор `In` (входит в множество)...
|
||||
values: # ...значений...
|
||||
- ssd # ...значение `ssd` (т.е. узлы с SSD)
|
||||
containers: # определяем контейнеры, которые будут запущены в поде
|
||||
- name: gitea # имя контейнера `gitea`
|
||||
image: gitea/gitea:latest # образ
|
||||
# env: # переменные окружения
|
||||
# - name: USER_UID # идентификатор пользователя
|
||||
# value: "1000"
|
||||
# - name: USER_GID # идентификатор группы
|
||||
# value: "1000"
|
||||
ports: # определяем порты, которые будут открыты в контейнере
|
||||
- containerPort: 3000 # порт 3000
|
||||
name: http # будет именоваться 'http' (используется в Gitea для веб-интерфейса)
|
||||
- containerPort: 22 # порт 22
|
||||
name: ssh # будет именоваться 'ssh' (используется в Gitea для SSH-доступа)
|
||||
volumeMounts: # монтируем тома в контейнер
|
||||
- name: gitea-data # том с именем 'gitea-data'...
|
||||
mountPath: /data # в каталог '/data' внутри контейнера
|
||||
- name: timezone # том 'timezone'...
|
||||
mountPath: /etc/timezone # в каталог '/etc/timezone' внутри контейнера
|
||||
readOnly: true # только для чтения
|
||||
- name: localtime # том 'localtime'...
|
||||
mountPath: /etc/localtime # в каталог '/etc/localtime' внутри контейнера
|
||||
readOnly: true # только для чтения
|
||||
volumes: # определяем тома, которые будут использоваться в поде
|
||||
- name: gitea-data # том с именем 'gitea-data'...
|
||||
persistentVolumeClaim: # используем PersistentVolumeClaim (PVC Longhorn)
|
||||
claimName: gitea-pvc # имя PVC 'gitea-pvc'
|
||||
- name: timezone # том 'timezone'...
|
||||
hostPath: # использует каталог (или файл) на хосте узла
|
||||
path: /etc/timezone # путь к файлу '/etc/timezone' на хосте
|
||||
- name: localtime # том 'localtime'...
|
||||
hostPath: # использует каталог (или файл) на хосте узла
|
||||
path: /etc/localtime # путь к файлу '/etc/localtime' на хосте
|
||||
```
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/gitea-deployment.yaml
|
||||
```
|
||||
|
||||
Проверим, что под создан и работает:
|
||||
```bash
|
||||
sudo kubectl get pod -n gitea -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
gitea-55dfcf4dd9-d99nw 1/1 Running 0 74s 10.42.1.94 opi5plus-3 <none> <none>
|
||||
gitea-init-data 0/1 Completed 0 2h 10.42.2.64 opi5plus-1 <none> <none>
|
||||
```
|
||||
|
||||
Как видим:
|
||||
* под `gitea` работает и запущен на узде `opi5plus-3` (там где у нас SSD). В принципе, аффинити не гарантирует запуск
|
||||
пода на узле с SSD, это только предпочтение. Но в данном случае предпочтение сработало.
|
||||
* под `gitea-init-data` завершился. Это нормально, так как он был создан только для переноса данных gitea в Longhorn
|
||||
PVC `gitea-pvc`, и должен быть "жив" только 1 час (времени, которое мы указали в `sleep 3600` в манифесте пода).
|
||||
|
||||
Проверим логи пода `gitea`:
|
||||
```bash
|
||||
sudo kubectl logs -n gitea deployment/gitea
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
Server listening on :: port 22.
|
||||
Server listening on 0.0.0.0 port 22.
|
||||
2025/04/18 17:42:33 cmd/web.go:253:runWeb() [I] Starting Gitea on PID: 17
|
||||
2025/04/18 17:42:33 cmd/web.go:112:showWebStartupMessage() [I] Gitea version: 1.23.7 built with GNU Make 4.4.1, go1.23.8 : bindata, timetzdata, sqlite, sqlite_unlock_notify
|
||||
2025/04/18 17:42:33 cmd/web.go:113:showWebStartupMessage() [I] * RunMode: prod
|
||||
2025/04/18 17:42:33 cmd/web.go:114:showWebStartupMessage() [I] * AppPath: /usr/local/bin/gitea
|
||||
2025/04/18 17:42:33 cmd/web.go:115:showWebStartupMessage() [I] * WorkPath: /data/gitea
|
||||
2025/04/18 17:42:33 cmd/web.go:116:showWebStartupMessage() [I] * CustomPath: /data/gitea
|
||||
2025/04/18 17:42:33 cmd/web.go:117:showWebStartupMessage() [I] * ConfigFile: /data/gitea/conf/app.ini
|
||||
...
|
||||
...
|
||||
...
|
||||
2025/04/18 17:42:34 cmd/web.go:205:serveInstalled() [I] PING DATABASE sqlite3
|
||||
...
|
||||
...
|
||||
...
|
||||
2025/04/18 17:42:39 cmd/web.go:315:listen() [I] Listen: http://0.0.0.0:3000
|
||||
2025/04/18 17:42:39 cmd/web.go:319:listen() [I] AppURL(ROOT_URL): https://git.cube2.ru/
|
||||
2025/04/18 17:42:39 cmd/web.go:322:listen() [I] LFS server enabled
|
||||
2025/04/18 17:42:39 ...s/graceful/server.go:50:NewServer() [I] Starting new Web server: tcp:0.0.0.0:3000 on PID: 17
|
||||
```
|
||||
|
||||
Как видим, Gitea запустилась, порт 3000 (HTTP) и 22 (SSH) открыты, тома подключены, база данных SQLite на месте, права
|
||||
на папку `/data` установлены. Всё работает.
|
||||
|
||||
Можно устроить более глубокую проверку, зайдя внутр пода и посмотреть как там всё устроено:
|
||||
```bash
|
||||
sudo kubectl exec -n gitea -it deployment/gitea -- /bin/bash
|
||||
```
|
||||
|
||||
Внутри пода проверим, что PVC `gitea-pvc` подключен и данные gitea и база на месте:
|
||||
```bash
|
||||
ls -la /data
|
||||
ls -l /data/gitea/gitea.db
|
||||
```
|
||||
|
||||
Проверим открытые порты в поде:
|
||||
```bash
|
||||
netstat -tuln
|
||||
```
|
||||
|
||||
Увидим что порты слушаются:
|
||||
```text
|
||||
Active Internet connections (only servers)
|
||||
Proto Recv-Q Send-Q Local Address Foreign Address State
|
||||
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
|
||||
tcp 0 0 :::3000 :::* LISTEN
|
||||
tcp 0 0 :::22 :::* LISTEN
|
||||
```
|
||||
|
||||
Проверим, что web-интерфейс Gitea доступен:
|
||||
```bash
|
||||
curl -v http://localhost:3000
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
Host localhost:3000 was resolved.
|
||||
* IPv6: ::1
|
||||
* IPv4: 127.0.0.1
|
||||
* Trying [::1]:3000...
|
||||
* Connected to localhost (::1) port 3000
|
||||
* using HTTP/1.x
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:3000
|
||||
> User-Agent: curl/8.12.1
|
||||
> Accept: */*
|
||||
>
|
||||
* Request completely sent off
|
||||
< HTTP/1.1 303 See Other
|
||||
< Cache-Control: max-age=0, private, must-revalidate, no-transform
|
||||
< Content-Type: text/html; charset=utf-8
|
||||
...
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
Как видим, web-интерфейс Gitea доступен внутри пода по http на порту 3000. Все работает. Можно выходить из пода:
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
|
||||
## Создание сервиса и IngressRoute для доступа к Gitea снаружи
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
nano ~/k3s/gitea/gitea-service.yaml
|
||||
```
|
||||
|
||||
Вставляем в него следующее содержимое:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
spec:
|
||||
selector:
|
||||
app: gitea
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
#- name: ssh
|
||||
# port: 22
|
||||
# targetPort: 22
|
||||
# protocol: TCP
|
||||
```
|
||||
Объяснение:
|
||||
|
||||
selector: app: gitea — находит поды из Deployment gitea.
|
||||
port: 80 — внешний порт сервиса (Traefik будет слать трафик сюда).
|
||||
targetPort: 3000 — порт контейнера Gitea.
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/gitea-service.yaml
|
||||
```
|
||||
|
||||
sudo kubectl get svc -n gitea -o wide
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
|
||||
gitea ClusterIP 10.43.211.8 <none> 80/TCP 115s app=gitea
|
||||
|
||||
nano ~/k3s/gitea/https-redirect-middleware.yaml
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: https-redirect
|
||||
namespace: gitea
|
||||
spec:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
```
|
||||
|
||||
Объяснение:
|
||||
|
||||
redirectScheme: scheme: https — перенаправляет запросы на HTTPS.
|
||||
permanent: true — возвращает 301 (постоянный редирект) для SEO и кэширования.
|
||||
Размещаем в gitea, чтобы не затрагивать другие сервисы.
|
||||
|
||||
Применить:
|
||||
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/https-redirect-middleware.yaml
|
||||
```
|
||||
|
||||
### Подключаем сертификат Let's Encrypt
|
||||
|
||||
У нас уже настроена выдача сертификатов Let’s Encrypt в подах cert-managerб cert-manager-cainjector и
|
||||
cert-manager-webhook, в пронстве имен cert-manager. Это нестандартный способ (см. [заметку о cert-manager](k3s-cert-manager.md)).
|
||||
|
||||
Проверим, что cert-manager работает:
|
||||
```bash
|
||||
sudo kubectl get pods -n cert-manager
|
||||
```
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
cert-manager-64478b89d5-p4msl 1/1 Running 2 (5d13h ago) 19d 10.42.1.55 opi5plus-3 <none> <none>
|
||||
cert-manager-cainjector-65559df4ff-t7rj4 1/1 Running 5 (5d13h ago) 19d 10.42.1.54 opi5plus-3 <none> <none>
|
||||
cert-manager-webhook-544c988c49-zxdxc 1/1 Running 0 19d 10.42.1.56 opi5plus-3 <none> <none>
|
||||
```
|
||||
|
||||
Поды `cert-manager`, `cainjector`, `webhook` должны быть **Running**.
|
||||
|
||||
Проверим наличие ClusterIssuer:
|
||||
```bash
|
||||
sudo kubectl get clusterissuer -A -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY STATUS AGE
|
||||
letsencrypt-prod True The ACME account was registered with the ACME server 19d
|
||||
```
|
||||
|
||||
Проверим, что работает и Let's Encrypt знает о нас:
|
||||
```bash
|
||||
sudo kubectl describe clusterissuer letsencrypt-prod
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
Name: letsencrypt-prod
|
||||
Namespace:
|
||||
Labels: <none>
|
||||
Annotations: <none>
|
||||
API Version: cert-manager.io/v1
|
||||
Kind: ClusterIssuer
|
||||
...
|
||||
...
|
||||
...
|
||||
Status:
|
||||
Acme:
|
||||
Last Private Key Hash: тут-будет-хэш-вашего-ключа=
|
||||
Last Registered Email: тут-будет-ваш-email
|
||||
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/тут-будет-id-вашего-аккаунта
|
||||
Conditions:
|
||||
...
|
||||
...
|
||||
Status: True
|
||||
Type: Ready
|
||||
Events: <none>
|
||||
```
|
||||
|
||||
Важно чтобы `Status: Conditions: Ready:` был `True`.
|
||||
|
||||
|
||||
|
||||
Создадим манифест для получения сертификата Let's Encrypt:
|
||||
```bash
|
||||
nano ~/k3s/gitea/gitea-certificate.yaml
|
||||
```
|
||||
|
||||
и вставим в него следующее содержимое:
|
||||
```yaml
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: gitea-tls
|
||||
namespace: gitea
|
||||
spec:
|
||||
secretName: gitea-tls
|
||||
dnsNames:
|
||||
- git.cube2.ru
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
```
|
||||
|
||||
secretName: gitea-tls: Сертификат сохраняется в Secret gitea-tls в gitea.
|
||||
dnsNames: Домен git.cube2.ru.
|
||||
issuerRef: Ссылается на ClusterIssuer letsencrypt-prod.
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/gitea-certificate.yaml
|
||||
```
|
||||
|
||||
Проверим, что секрет создан:
|
||||
```bash
|
||||
sudo kubectl get secret -n gitea gitea-tls -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME TYPE DATA AGE
|
||||
gitea-tls kubernetes.io/tls 2 46s
|
||||
```
|
||||
|
||||
Проверим, что сертификат выдан:
|
||||
```bash
|
||||
sudo kubectl describe certificate -n gitea gitea-tls
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
Name: gitea-tls
|
||||
Namespace: gitea
|
||||
...
|
||||
...
|
||||
...
|
||||
Spec:
|
||||
Dns Names:
|
||||
тут-будет-ваш-домен
|
||||
...
|
||||
...
|
||||
Status:
|
||||
Conditions:
|
||||
Last Transition Time: тут-будет-дата-время-выдачи-сертификата
|
||||
Message: Certificate is up to date and has not expired
|
||||
Observed Generation: 1
|
||||
Reason: Ready
|
||||
Status: True
|
||||
Type: Ready
|
||||
Not After: тут-будет-дата-время-окончания-действия-сертификата
|
||||
Not Before: тут-будет-дата-время-начала-действия-сертификата
|
||||
Renewal Time: тут-будет-дата-время-ближайшего-обновления-сертификата
|
||||
Revision: 1
|
||||
Events:
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
Ожидается `Status: True` в `Conditions`, свежие даты в `Not After` и `Not Before` и сообщение `Certificate is
|
||||
up to date and has not expired` в `Message`.
|
||||
|
||||
|
||||
## Создание IngressRoute для доступа к Gitea
|
||||
|
||||
Создать IngressRoute для HTTPS
|
||||
Настроим IngressRoute для маршрутизации git.cube2.ru через HTTPS с Let’s Encrypt и редиректом HTTP.
|
||||
```bash
|
||||
nano ~/k3s/gitea/gitea-ingressroute.yaml
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: gitea-http
|
||||
namespace: gitea
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
routes:
|
||||
- match: Host("git.cube2.ru")
|
||||
kind: Rule
|
||||
services:
|
||||
- name: gitea
|
||||
port: 80
|
||||
middlewares:
|
||||
- name: https-redirect
|
||||
---
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: gitea-https
|
||||
namespace: gitea
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host("git.cube2.ru")
|
||||
kind: Rule
|
||||
services:
|
||||
- name: gitea
|
||||
port: 80
|
||||
tls:
|
||||
secretName: gitea-tls
|
||||
# Если у вас стандартная настройка Traefik, то раскомментируйте следующую строку (а строку выше закомментируйте)
|
||||
# certResolver: letsencrypt
|
||||
```
|
||||
Объяснение:
|
||||
* Первая IngressRoute (`gitea-http`):
|
||||
* `entryPoints: web` — слушает порт 80 (HTTP).
|
||||
* `match: Host("git.cube2.ru")` — обрабатывает запросы к git.cube2.ru.
|
||||
* `services: gitea` — направляет трафик в Service gitea (порт 80 → 3000).
|
||||
* `middlewares: https-redirect` — редирект на HTTPS.
|
||||
* Вторая IngressRoute (gitea-https):
|
||||
* `entryPoints: websecure` — слушает порт 443 (HTTPS).
|
||||
* `tls: certResolver: gitea-tls` — включает Let’s Encrypt для автоматического получения сертификата через
|
||||
cert-manager в секрет gitea-tls. Трафик идёт в тот же Service gitea.
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/gitea-ingressroute.yaml
|
||||
```
|
||||
|
||||
Все долно работать. Проверим, что IngressRoute создан:
|
||||
|
||||
## Подключение SSH к Gitea (опционально)
|
||||
|
||||
Кроме http-доступа и идентификации по логину, паролю и токенами, к к Gitea можно подключаться по SSH ([git://]). Выше
|
||||
мы не настраивали SSH, так как Gitea я использовал как демонстрацию переноса Docker-контейнера в под K3s.
|
||||
|
||||
Порт 22 (SSH) на узлах K3s уже занят, поэтому нам нужно перенастроить его на другой порт. Например, 2222.
|
||||
|
||||
Для настройки SSH нужно:
|
||||
- Обновить Service для Gitea, чтобы он включал порт 22 (SSH).
|
||||
- Настроить Traefik для порта 2222
|
||||
- Добавить IngressRouteTCP для Gitea (маршрутизация порта 22 изнутри пода а 2222 порт снаружи).
|
||||
- Изменить Traefik Service для доступа по порту 2222
|
||||
|
||||
### Обновим Service для Gitea
|
||||
|
||||
Вносим изменения в `~/k3s/gitea/gitea-service.yaml`, добавив в конце (этот блок был закомментирован):
|
||||
```yaml
|
||||
- port: 22 # SSH, добавить
|
||||
targetPort: 22
|
||||
protocol: TCP
|
||||
name: ssh
|
||||
```
|
||||
|
||||
Применим изменения:
|
||||
```bash
|
||||
kubectl apply -f ~/k3s/gitea/gitea-service.yaml
|
||||
````
|
||||
|
||||
Теперь Traefik сможет маршрутизировать SSH-трафик. Проверим, что сервис обновился:
|
||||
```bash
|
||||
sudo kubectl get svc -n gitea -o wide
|
||||
```
|
||||
|
||||
Увидим что теперь в сервисе gitea есть порт 22:
|
||||
```text
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
|
||||
gitea ClusterIP 10.43.211.8 <none> 80/TCP,22/TCP 5h app=gitea
|
||||
```
|
||||
|
||||
### Настроим Traefik для порта 2222
|
||||
|
||||
Traefik должен слушать порт 2222. У меня Traefik настроен через `Helm values` в `HelmChartConfig`, через дополнительные
|
||||
параметры `additionalArguments` . Чтобы Traefik начал слушать порт 2222, добавим новый entryPoint с именем `ssh` в
|
||||
`~/k3s/traefik/traefik-config.yaml`. Добавим в конец файла: `- --entrypoints.ssh.address=:2222`. Полностью манифест
|
||||
у меня выглядит так:
|
||||
```yaml
|
||||
aapiVersion: helm.cattle.io/v1
|
||||
kind: HelmChartConfig
|
||||
metadata:
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
spec:
|
||||
valuesContent: |-
|
||||
additionalArguments:
|
||||
- --entrypoints.web-custom.address=:2055 # Слушаем HTTP на 2055 (для web-панели 3x-ui)
|
||||
- --entrypoints.ssh.address=:2222 # Слушаем TCP на 2222 (для SSH)
|
||||
- --log.level=DEBUG
|
||||
```
|
||||
|
||||
Применим изменения и перезапустим Traefik чтобы изменения вступили в силу:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/traefik/traefik-config.yaml
|
||||
sudo kubectl rollout restart deployment -n kube-system traefik
|
||||
```
|
||||
|
||||
Проверим, что Traefik перезапустился:
|
||||
```bash
|
||||
sudo kubectl get pod -n kube-system -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде (время жизни пода Traefik небольшое, так как он недавно перезапустился):
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
...
|
||||
...
|
||||
traefik-6b96fd9d85-8txb9 1/1 Running 0 119s 10.42.0.93 opi5plus-2 <none> <none>
|
||||
...
|
||||
```
|
||||
|
||||
### Добавим IngressRouteTCP для Gitea
|
||||
|
||||
Чтобы маршрутизировать трафик с порта 2222 на Service Gitea (порт 22), создадим манифест IngressRouteTCP:
|
||||
```bash
|
||||
nano ~/k3s/gitea/gitea-ingressroute-tcp.yaml
|
||||
```
|
||||
|
||||
И вставим в него следующее содержимое:
|
||||
```yaml
|
||||
# IngressRouteTCP для маршрутизации SSH через Traefik
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: gitea-ssh
|
||||
namespace: gitea
|
||||
spec:
|
||||
entryPoints:
|
||||
- ssh # Соответствует entryPoint в Traefik
|
||||
routes:
|
||||
- match: HostSNI(`*`) # Для TCP без SNI
|
||||
services:
|
||||
- name: gitea
|
||||
port: 22 # Порт Service
|
||||
```
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/gitea/gitea-ingressroute-tcp.yaml
|
||||
```
|
||||
|
||||
Проверим, что IngressRouteTCP создан:
|
||||
```bash
|
||||
sudo kubectl get ingressroutetcp -n gitea
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME AGE
|
||||
gitea-ssh 16s
|
||||
```
|
||||
### Изменим Traefik Service для доступа к SSH по порту 2222 через балансировщик
|
||||
|
||||
Если у вас уже есть манифест Traefik Service (например `~/k3s/traefik/traefik-service.yaml`), то нужно добавить в него
|
||||
обработку порта:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
...
|
||||
...
|
||||
spec:
|
||||
ports:
|
||||
...
|
||||
...
|
||||
...
|
||||
- name: ssh # Имя порта (должно соответствовать имени в IngressRouteTCP и entryPoint)
|
||||
port: 2222 # Внешний порт
|
||||
targetPort: 2222 # Порт Traefik
|
||||
protocol: TCP # Протокол
|
||||
```
|
||||
|
||||
И, само собой, применить манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/traefik/traefik-service.yaml
|
||||
```
|
||||
|
||||
Если у вас нет манифеста Traefik Service, то можно отредактировать его непосредственно в кластере через `kubectl edit`
|
||||
(будьте готовы редактировать в **vim**):
|
||||
```bash
|
||||
sudo kubectl edit svc -n kube-system traefik -o yaml
|
||||
```
|
||||
|
||||
Важно: значение `nodePort` для SSH не нужно указывать, так как оно будет сгенерировано автоматически сразу после
|
||||
сохранения и выхода из редактора (из vim выходите через `<esc:>wq`).
|
||||
|
||||
Проверим, что Service обновился:
|
||||
```bash
|
||||
sudo kubectl get svc -n kube-system traefik
|
||||
```
|
||||
|
||||
Увидим что-то вроде (орт 2222 добавился в Service Traefik):
|
||||
```text
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
traefik LoadBalancer 10.43.164.48 192.168.1.26,192.168.1.27,192.168.1.28 80:32171/TCP,443:30329/TCP,9000:31317/TCP,2055:31675/TCP,2222:31963/TCP 6d
|
||||
```
|
||||
|
||||
Проверим, что порт 2222 доступен на VIP-адресе keepalived (не забудьте заменить `<VIP>` на ваш Virtual IP):
|
||||
```bash
|
||||
nc -zv <VIP> 2222
|
||||
```
|
||||
|
||||
Увидим, что порт 2222 доступен:
|
||||
```text
|
||||
Connection to <VIP> port 2222 [tcp/rockwell-csp2] succeeded!
|
||||
```
|
||||
|
||||
SSH в Gitea по порту 2222 должен работать. Проверим, что SSH доступен. Создадим SSH-ключ:
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "ваше-имя-в-gitea-или-что-угодно"
|
||||
```
|
||||
|
||||
Возьмём публичный SSH-ключ `cat ~/.ssh/id_ed25519.pub` и добавим через web-интерфейс Gitea (_Settings → SSH / GPG Keys_
|
||||
или _Настройки → SSH / GPG ключи_). После добавления ключа, можно подключиться к Gitea по SSH:
|
||||
```bash
|
||||
ssh -p 2222 git@<VIP>
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
PTY allocation request failed on channel 0
|
||||
Hi there, ваше-имя-в-gitea-или-что-угодно! You've successfully authenticated with the key named Apple Mac Mini M4, but Gitea does not provide shell access.
|
||||
If this is unexpected, please log in with password and setup Gitea under another user.
|
||||
Connection to <VIP> closed.
|
||||
```
|
||||
|
||||
**ВСЕ!!** Можно настроить переадресацию внешнего 22 порта на домашнем роутере на 2222 порт VIP-адреса, и работать
|
||||
со всеми репозиториями Gitea по SSH как обычно. Для большинства роутеров при обращении по доменому имени gitea тоже
|
||||
будет работать по 22 порту.
|
||||
|
||||
## Удаление временного пода и архива
|
||||
|
||||
Можно удалить временный под, который мы использовали для переноса данных gitea в блочное хранилище Longhorn PVC:
|
||||
```bash
|
||||
sudo kubectl delete pod gitea-init-data -n gitea
|
||||
```
|
||||
|
||||
И удалить временный архив с данными gitea которые мы перенесли из Docker-контейнера и положили в корень домашнего
|
||||
каталога. Теперь данные gitea уже в Longhorn PVC и арихив не нужен:
|
||||
```bash
|
||||
sudo rm ~/gitea-data.tar.gz
|
||||
```
|
||||
|
||||
## Выводы
|
||||
|
||||
### Плюсы
|
||||
|
||||
Размещение Gitea в K3s с Longhorn и Traefik повысит отказоустойчивость и доступность. Выход из строя любого узла
|
||||
не приведёт к потере данных, так как Longhorn хранит данные в репликах на других узлах, а Traefik обеспечивает
|
||||
автоматическую маршрутизацию трафика.
|
||||
|
||||
### Минусы
|
||||
|
||||
Сложность и число абстракций при доступе клиентов в Gitea возросло, и Gitea стала работать на 50-100 миллисекунд
|
||||
медленнее (в зависимости от нагрузки на узлы). Если посмотреть метрики:
|
||||
```bash
|
||||
time curl -s -o /dev/null https://git.cube2.ru
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
real 0m0.233s
|
||||
user 0m0.124s
|
||||
sys 0m0.015s
|
||||
```
|
||||
|
||||
Где **233 мс** — суммарное время запроса (включая DNS, соединение, TLS и ответ Gitea). Для моей локальной
|
||||
сети (1 Гбит/с) стало медленнее, чем ожидалось (~50-100 мс для Docker с Nginx). Но для цепочки K3s с Traefik, Longhorn
|
||||
и keepalived — это разумно.
|
||||
|
||||
#### Основные компоненты задержки:
|
||||
|
||||
| Причина | Docker + Nginx | K3s + Traefik + Longhorn + Keepalived |
|
||||
|-----------------------------|-------------------|-------------------------------------------------|
|
||||
| DNS | 20-50 мс. | 20-50 мс. |
|
||||
| Web-сервер + TLS | Nginx — 20-40 мс. | Traefik — 30-50 мс. |
|
||||
| Маршрутизация | 10 мс. | IngressRoute + Service + Keepalived — 20-60 мс. |
|
||||
| Доступ к базе данных SQLite | 10 мс. | Longhorn PVC — 20-50 мс. |
|
||||
| | | |
|
||||
| **Итого:** | **60-110 мс** | **90-210 мс** |
|
@@ -1,100 +1,263 @@
|
||||
# Проксирование внешнего хоста через Traefik (Ingress-контроллер)
|
||||
|
||||
У меня в домашней сети есть хост с сервисом (audiobookshelf), то удобно его прокинуть через Traefik, чтобы не открывать
|
||||
на внешний IP лишних адресов, портов, использовать домен и включить SSL, и управлять всем этим через из единой
|
||||
точки.
|
||||
У меня в домашней сети есть хост с 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>` -- имя сервиса, который будет проксироваться. Это имя, для простоты, будем использоваться
|
||||
* и в маршрутах, и сертификатах, и в секрете...
|
||||
|
||||
## Пространство имен
|
||||
|
||||
Чтобы все было аккуратно и серисы и поды не путались, создадим пространство имен для проксируемого сервиса.
|
||||
Например, `ab-shelf`.
|
||||
Чтобы все было аккуратно и сервисы и поды не путались, создадим пространство имен для проксирования конкретного хоста.
|
||||
Мо хост относится к <SERVICE-NAME>, поэтому назову пространство имен `<NAME-SPACE>`
|
||||
Например, `<NAME-SPACE>`.
|
||||
```bash
|
||||
sudo ubectl create namespace ab-shelf
|
||||
sudo ubectl create namespace <NAME-SPACE>
|
||||
```
|
||||
|
||||
Проверяем, что пространство создано:
|
||||
```bash
|
||||
sudo kubectl get namespace ab-shelf
|
||||
sudo kubectl get namespace <NAME-SPACE>
|
||||
```
|
||||
|
||||
Увидим, что пространство создано и активно:
|
||||
```text
|
||||
NAME STATUS AGE
|
||||
ab-shelf Active 54s
|
||||
NAME STATUS AGE
|
||||
<NAME-SPACE> Active 54s
|
||||
```
|
||||
|
||||
## Конфигурация IngressRoute, Service и Endpoints
|
||||
## Конфигурация всего
|
||||
|
||||
Для удобства я объединил манифесты в один файл (но можно и по отдельности). Создаем единый манифетст:
|
||||
Для удобства я объединил манифесты в один файл (но можно и по отдельности). Создаем единый манифест:
|
||||
```bash
|
||||
sudo nano ~/k3s/audiobookshelf/audiobookshelf.yaml
|
||||
sudo nano ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
|
||||
```
|
||||
|
||||
И вставляем в него следующее содержимое (не забудь заменить `<PROXIED-HOST>`, `<PROXIED-PORT>` и `<YOU-DOMAIN-NAME>`
|
||||
на свои значения... пространство имен `ab-shelf` и имя сервиса `audiobookshelf` можно тоже поменять на свое, если
|
||||
у вас другой сервис):
|
||||
Он состоит из следующих частей:
|
||||
* `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 для внешнего хоста
|
||||
# Endpoints для внешнего хоста <SERVICE-NAME>
|
||||
# Задаёт IP и порт внешнего сервера, так как <SERVICE-NAME> внешний хост для k3s
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: audiobookshelf
|
||||
namespace: ab-shelf # пространство, например
|
||||
subsets:
|
||||
name: <SERVICE-NAME>
|
||||
namespace: <NAME-SPACE> # Namespace для <SERVICE-NAME>
|
||||
subsets: # Прямо в корне, без spec
|
||||
- addresses:
|
||||
- ip: <PROXIED-HOST>
|
||||
- ip: <PROXIED-HOST> # IP Synology, где работает <SERVICE-NAME>
|
||||
ports:
|
||||
- port: <PROXIED-PORT>
|
||||
- port: <PROXIED-PORT> # Порт Synology (HTTP)
|
||||
protocol: TCP
|
||||
---
|
||||
|
||||
# Service для проксируемого хоста (<PROXIED-HOST>:<PROXIED-PORT>)
|
||||
---
|
||||
# Service для маршрутизации трафика от Traefik к внешнему хосту
|
||||
# Связывает IngressRoute с Endpoints
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: audiobookshelf
|
||||
namespace: ab-shelf # пространство, например
|
||||
name: <SERVICE-NAME>
|
||||
namespace: <NAME-SPACE>
|
||||
spec:
|
||||
ports:
|
||||
- port: <PROXIED-PORT>
|
||||
targetPort: <PROXIED-PORT>
|
||||
- port: <PROXIED-PORT> # Порт сервиса, на который Traefik отправляет трафик
|
||||
targetPort: <PROXIED-PORT> # Порт на Synology
|
||||
protocol: TCP
|
||||
---
|
||||
|
||||
# IngressRoute
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
---
|
||||
# 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: audiobookshelf
|
||||
namespace: ab-shelf # пространство, например
|
||||
name: <SERVICE-NAME>-http
|
||||
namespace: <NAME-SPACE>
|
||||
spec:
|
||||
entryPoints:
|
||||
- web-custom # ендпоинт, который "слушает" порт 2055
|
||||
- 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: audiobookshelf
|
||||
- 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
|
||||
```
|
||||
|
||||
Что тут происходит:
|
||||
* `Endpoints` -- указываем, что сервис будет проксировать запросы на внешний хост `<PROXIED-HOST>`
|
||||
и порт `<PROXIED-PORT>`. Ендпоинт -- это конечная точка, к которой будет проксироваться запрос. В данном случае
|
||||
это IP-адрес и порт, но обычно это имя пода, на который отправляются и который отвечает на запросы.
|
||||
* `Service` -- создаем сервис, который будет использоваться для проксирования запросов к `Endpoints`. Сервис -- это
|
||||
абстракция, которая позволяет упрощать доступ к подам. Он может использоваться для балансировки нагрузки между
|
||||
несколькими потоками, которые обрабатывают запросы. В данном случае мы создаем сервис, который будет проксировать
|
||||
запросы к `Endpoints` (внешнему хосту).
|
||||
* `IngressRoute` -- создаем маршрут, который будет проксировать запросы на домен `<YOU-DOMAIN-NAME>`
|
||||
к сервису `audiobookshelf` в пространстве `ab-shelf`. Маршрут -- это правило, которое определяет, как обрабатывать
|
||||
запросы, поступающие на определенный адрес. В данном случае мы создаем маршрут, который будет направлять запросы
|
||||
на домен `<YOU-DOMAIN-NAME>` к сервису `audiobookshelf` в пространстве `ab-shelf`. Внутри маршрута мы указываем,
|
||||
что запросы должны обрабатываться сервисом `audiobookshelf` на порту `<PROXIED-PORT>`.
|
||||
В данном случае мы используем `web-custom` как точку входа, которая будет слушать порт 2055.
|
||||
**ВАЖНО**: В манифесте используется 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
|
||||
```
|
@@ -41,7 +41,7 @@ sudo /usr/local/bin/k3s-agent-uninstall.sh # На воркере
|
||||
|
||||
Некоторые требования к узлам:
|
||||
* На всех Orange Pi установлена одинаковая версия Ubuntu (например, 22.04 или 24.04).
|
||||
* Статические IP-адреса узлов (или зрезервированные под MAC-адреса IP в DHCP).
|
||||
* Статические IP-адреса узлов (или зарезервированные под MAC-адреса IP в DHCP).
|
||||
* На уздах открыты порты 6443 (для API), 2379-2380 (для etcd) и 10250 (для kubelet).
|
||||
|
||||
|
||||
@@ -664,5 +664,63 @@ SSH-тоннель с помощью `autossh` и упаковкой UDP-тра
|
||||
|
||||
|
||||
|
||||
-----
|
||||
Cертификаты
|
||||
sudo kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.1/cert-manager.yaml
|
||||
|
||||
PVC Longhorn
|
||||
sudo kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml
|
||||
|
||||
Установить Helm (менеджер пакетов для Kubernetes):
|
||||
|
||||
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
|
||||
Добавить репозиторий Traefik в Helm:
|
||||
helm repo add traefik https://helm.traefik.io/traefik
|
||||
|
||||
Обновить репозитории:
|
||||
helm repo update
|
||||
|
||||
mkdir ~/.kube/config
|
||||
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
|
||||
|
||||
sudo chown opi:opi ~/.kube
|
||||
chmod 755 ~/.kube
|
||||
sudo chown opi:opi ~/.kube/config
|
||||
chmod 755 ~/.kube/config
|
||||
sudo chown opi:opi ~/.kube/config/k3s.yaml
|
||||
chmod 755 ~/.kube/config/k3s.yaml
|
||||
|
||||
или
|
||||
chmod 755 ~/.kube ~/.kube/config
|
||||
chmod 755 ~/.kube ~/.kube
|
||||
chmod 600 ~/.kube/config/k3s.yaml
|
||||
|
||||
ls -ld ~/.kube ~/.kube/config ~/.kube/config/k3s.yaml
|
||||
|
||||
```text
|
||||
drwxr-xr-x 4 opi opi 4096 Apr 26 11:32 /home/opi/.kube
|
||||
drwxr-xr-x 2 opi opi 4096 Apr 26 11:09 /home/opi/.kube/config
|
||||
-rw------- 1 opi opi 2961 Apr 26 11:09 /home/opi/.kube/config/k3s.yaml
|
||||
```
|
||||
|
||||
Установить переменную KUBECONFIG:
|
||||
```bash
|
||||
export KUBECONFIG=~/.kube/config/k3s.yaml
|
||||
```
|
||||
|
||||
И добавь её в `~/.bashrc` для постоянства:
|
||||
```bash
|
||||
echo 'export KUBECONFIG=~/.kube/config/k3s.yaml' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
|
||||
Установить Traefik:
|
||||
```bash
|
||||
helm install traefik traefik/traefik -n kube-system --set service.type=LoadBalancer
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user