add: Перенос контейнера Docker в k3s.

This commit is contained in:
Sergei Erjemin 2025-04-19 19:53:48 +03:00
parent b6b54e136c
commit 3bc414ca98
2 changed files with 670 additions and 0 deletions

View File

@ -14,6 +14,7 @@
* [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.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)

View File

@ -0,0 +1,669 @@
# Перенос контейнера из 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
Переносим данные на узел k3s (в моем случае это opi5plus-1) по SCP:
## Подготовка узла 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/opi/tmp
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/opi/tmp` (как рам, у нас лежит архив с данными 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
#- name: ssh
# port: 22
# targetPort: 222
```
Объяснение:
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
```
У нас уже настроена выдача сертификатов Lets 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 для HTTPS
Настроим IngressRoute для маршрутизации git.cube2.ru через HTTPS с Lets 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: letsencrypt — включает Lets Encrypt для автоматического получения сертификата.
Трафик идёт в тот же Service gitea.
Применим манифест:
```bash
sudo kubectl apply -f ~/k3s/gitea/gitea-ingressroute.yaml
```
Можно удалить временный под:
```bash
sudo kubectl delete pod gitea-init-data -n gitea
```