Установка Kubernetes на домашнем сервере с помощью K3s

Kate

Administrator
Команда форума

Но зачем​


Знаю, о чем вы думаете — Kubernetes? На домашнем сервере? Кто может быть настолько сумасшедшим? Что ж, раньше я согласился бы, однако недавно кое-что изменило мое мнение.


Я начал работать в небольшом стартапе, в котором нет DevOps разработчиков со знанием Kubernetes (в дальнейшем K8s), и даже будучи старым ненавистником K8s из-за его громоздкости, был вынужден признать, что мне не хватает его программного подхода к деплойментам и доступу к подам. Также должен признать, что азарт от укрощения настолько навороченного зверя давно будоражит меня. И вообще, K8s захватывает мир — так что лишние знания не навредят.


Я все еще не большой фанат K8s, однако с Docker всё плохо, и его проект Swarm давно мертв; Nomad ненамного лучше (или не на 100% бесплатен, так как некоторые функции находятся за «корпоративной» стеной платного доступа), а Mesos не набрал критической массы. Все это, к сожалению, делает K8s последней оставшейся технологией оркестрации контейнеров производственного уровня. Не воспринимайте это как похвалу — мы знаем, что в IT успех иногда не равняется качеству (см. Window в 1995 году). И, как я уже сказал, он слишком громоздкий, но недавние улучшения инструментария значительно упростили работу с ним.


Причина, по которой я буду использовать его для своего личного сервера, в основном сводится к воспроизводимости. В моей текущей системе запущено около 35 контейнеров для множества сервисов, таких как wiki, сервер потоковой передачи музыки Airsonic, MinIO хранилище, совместимое с S3 API, и много чего еще, плюс сервера Samba и NFS, к которым обращается Kodi на моем Shield TV и четыре рабочих ПК/ноутбука дома.


Уже почти 5 лет я довольствовался запуском всего этого на OpenMediaVault, однако темпы его развития замедлились, и, будучи основанным на Debian, он страдает от проблемы «релизов». Каждый раз, когда выходит новый выпуск Debian, что-то неизбежно ломается на некоторое время. Я жил с этим с Debian 8 или 9, но недавний 11-й выпуск изрядно все поломал, поэтому настало время перемен. Я также подозреваю, что замедление развития OpenMediaVault связано с увеличившейся популярностью K8s среди «типичных» владельцев NAS, если судить по количество «easy» шаблонов K8s на посвященных ему Discord-серверах и Github. Доверие к шаблонам, использующим что попало для решения задачи, — не мой стиль, и если я доверяю чему-то управление своим домашним сервером, то должен непременно понимать что к чему.


Еще мне не нужно убивать уйму времени на обслуживание — обновления автоматизированы, и я редко что-то настраиваю после изначальной установки. На данный момент идет 161-й день аптайма! Однако воспроизведение моей системы было бы по большей части ручной работенкой. Переустановить OpenMediaVault, добавить плагин ZFS, импортировать мой 4-дисковый пул ZFS, настроить Samba и NFS, переустановить Portainer, заново импортировать все мои docker-compose файлы… это уже перебор. K8s же управляет состоянием кластера, поэтому (теоретически) можно просто переустановить мой сервер, добавить поддержку ZFS, импортировать пул, запустить скрипт, который воссоздает все деплойменты, и вуаля! В теории.


Минуточку. Если вы совершенно новичок — что вообще такое Kubernetes?


Краткий обзор Kubernetes​


Kubernetes (по-гречески «кормчий») — это продукт для оркестрации контейнеров, изначально созданный в Google. Однако они не часто используют его внутри компании, что подтверждает теорию о том, что это тщательно продуманный Троянский конь, гарантирующий, что ни одна конкурирующая компания никогда не бросит им вызов в будущем, потому что конкуренты будут тратить все свое время на управление этой штукой (это, как известно, сложно).


В двух словах, вы устанавливаете его на сервере или, что более вероятно, на кластере, а затем развертываете на нем различные типы рабочих нагрузок. Он заботится о создании контейнеров, их масштабировании, создании пространства имен, управлении сетевыми правами доступа, и тому подобное. В основном вы взаимодействуете с ним путем написания YAML файлов и их применения к кластеру, обычно при помощи инструмента командной строки под названием kubectl, который проверяет и преобразует YAML в полезную нагрузку JSON, которая затем отправляется в REST API эндпоинт кластера.


В K8s есть много концепций, однако я остановлюсь на основных:


  • Под (Pod), основная рабочая единица, которая грубо говоря представляет собой один контейнер или набор контейнеров. Поды гарантированно присутствуют на одном и том же узле кластера. K8s назначает подам IP-адреса, поэтому вам не нужно управлять ими. Контейнеры внутри пода доступны друг для друга, но не контейнеры, запущенные на других подах. Вам не следует напрямую управлять подами, это работа Сервисов.
  • Сервисы (Services) являются точками входа в наборы подов и упрощают управление ими как единым целым. Они не управляют подами напрямую (вместо этого используют ReplicaSets), но в большинстве случаев вам даже не нужно знать, что это такое. Сервисы идентифицируют поды, которыми они управляют, с помощью меток.
  • Метки (Labels). К каждому объекту в K8s можно прикрепить метаданные. Метки — это одна из их форм, аннотации — другая. Большинство действий в K8s можно ограничить с помощью селектора, указывающего на определенные метки.
  • Тома (Volumes), как и в Docker, соединяют контейнеры с хранилищем. На работе у вас будет S3 или что-то подобное с аналогичными гарантиями, но для домашнего сервера мы будем использовать тип тома hostPath, который непосредственно сопоставляется с папками на сервере. В большинстве случаев K8s немного усложняет это — для фактического доступа вам необходимо объявить Persistent Volume (PV) и PersistentVolumeClaim (PVC). Вы можете ограничиться hostPath в конфигурации развертывания, однако PVS и PVCS дадут вам больше контроля над использованием тома.
  • Конфигурации развертывания (deployments) являются своего рода главными рабочими единицами. Они объявляют, какой Docker-образ использовать, какие сервисы являются частью развертывания, какие тома монтировать и какие порты экспортировать, и заботятся о дополнительных вопросах безопасности.
  • ConfigMaps — это место, где хранятся данные конфигурации в форме ключ-значение. Среду для развертывания можно взять из ConfigMap — полностью или с определенными ключами.
  • Ingress. Без этого ваши поды заработают, но будут отрезаны от внешнего мира. В этой статье используются nginx ingresses.
  • Jobs и CronJobs — это разовые или периодические рабочие нагрузки, которые можно выполнять.

Существует еще несколько концепций, которые необходимо освоить, сторонние инструменты могут расширить кластер K8s с помощью пользовательских объектов под названием CRD. Официальная документация — хорошее место, где можно узнать больше. На данный момент все это поможет нам пройти долгий путь к эффективному рабочему примеру.


Давайте сделаем это!​


Шаг 1 — установка Linux​


Для начала я рекомендую использовать VirtualBox (он бесплатный) и установить базовую виртуальную машину Debian 11 без рабочего стола, просто с запущенным OpenSSH. Должно работать и с другими дистрибутивами, но большая часть тестирования проходила на Debian. В будущем я планирую перейти на Arch, чтобы избежать «проблемы с релизами», но хорошего понемножку. После освоения настройки виртуальной машины, переход на физический сервер не должен представлять проблемы.


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


2ea4cf222ace9bd0b2c3add1dc3384b3.png


Клонирование виртуальной машины


Убедитесь, что сетевой адаптер вашей клонированной виртуальной машины установлен на Bridged и имеет тот же MAC-адрес, что и основная виртуальная машина, чтобы все время получать один и тот же IP-адрес. Это также упростит проброс портов на вашем домашнем маршрутизаторе.


8f0b3ba08fd28fdb8a27ffad8810372c.png


Установка MAC-адреса для сетевого адаптера виртуальной машины


Убедитесь, что следующие порты на вашем домашнем маршрутизаторе проброшены на IP-адрес виртуальной машины:


  • 80/TCP http
  • 443/TCP https

Если вы не находитесь в той же локальной сети, что и виртуальная машина, или используйте удаленный сервер (DigitalOcean, Amazon и т.д.), то также пробросьте следующие порты:


  • 22/TCP ssh
  • 6443/TCP K8s API
  • 10250/UDP kubelet

Прежде чем продолжить, убедитесь, что добавили свой SSH-ключ на сервер и получаете приглашение командной строки c root правами без запроса пароля, когда подключаетесь к нему по SSH. Если добавить:


Host k3s
User root
Hostname <your VM or server's IP>

в ваш файл .ssh/config, то при команде ssh k3s вы должны получить вышеупомянутое приглашение с правами root.


Также следует установить kubectl. Я рекомендую плагин asdf.


Шаг 2 — установка k3s​


Полноценный Kubernetes является слишком комплексным и требует больших ресурсов, поэтому мы будем использовать облегченную альтернативу под названием K3s, гибкое single-binary решение, на 100 % совместимое с обычными K8s.


Чтобы установить K3s и взаимодействовать с нашим сервером, я буду использовать Makefile (старая школа — мой стиль). Вверху несколько переменных, которые вам нужно указать:


# set your host IP and name
HOST_IP=192.168.1.60
HOST=k3s
# do not change the next line
KUBECTL=kubectl --kubeconfig ~/.kube/k3s-vm-config

С IP все понятно, HOST— это метка сервера в файле .ssh/config, как указано выше. Использовать её проще, чем user@HOST_IP, но не стесняйтесь изменять файл Makefile по своему усмотрению. Назначение переменной KUBECTL прояснится, как только мы установим K3s. Добавьте в Makefile следующую цель:


k3s_install:
ssh ${HOST} 'export INSTALL_K3S_EXEC=" --no-deploy servicelb --no-deploy traefik"; \
curl -sfL https://get.k3s.io | sh -'
scp ${HOST}:/etc/rancher/k3s/k3s.yaml .
sed -r 's/(\b[0-9]{1,3}\.){3}[0-9]{1,3}\b'/"${HOST_IP}"/ k3s.yaml > ~/.kube/k3s-vm-config && rm k3s.yaml


ОК, здесь нужно кое-что прояснить. В первой строке происходит подключение к серверу по Ssh и установка K3s с пропуском нескольких компонентов.


  • servicelb нам не нужна балансировка нагрузки на одном сервере
  • traefik мы будем использовать nginx для ingresses, поэтому нет необходимости устанавливать этот ingress-контроллер

Во второй строке происходит копирование с сервера файла k3s.yaml, который создается после установки и включает сертификат для связи с его API. Третья строка заменяет в локальной копии IP-адрес 127.0.0.1 в конфигурации сервера IP-адресом сервера и копирует файл в директорию .kube вашей директории $HOME (убедитесь, что она существует). Именно здесь kubectl найдет его, так как мы явно установили переменную KUBECTL в Makefile для этого файла.


Ожидаемый вывод:


ssh k3s 'export INSTALL_K3S_EXEC=" --no-deploy servicelb --no-deploy traefik"; \
curl -sfL https://get.k3s.io | sh -'
[INFO] Finding release for channel stable
[INFO] Using v1.21.7+k3s1 as release
[INFO] Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.21.7+k3s1/sha256sum-amd64.txt
[INFO] Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.21.7+k3s1/k3s
[INFO] Verifying binary download
[INFO] Installing k3s to /usr/local/bin/k3s
[INFO] Skipping installation of SELinux RPM
[INFO] Creating /usr/local/bin/kubectl symlink to k3s
[INFO] Creating /usr/local/bin/crictl symlink to k3s
[INFO] Creating /usr/local/bin/ctr symlink to k3s
[INFO] Creating killall script /usr/local/bin/k3s-killall.sh
[INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO] systemd: Creating service file /etc/systemd/system/k3s.service
[INFO] systemd: Enabling k3s unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
[INFO] systemd: Starting k3s
scp k3s:/etc/rancher/k3s/k3s.yaml .
k3s.yaml 100% 2957 13.1MB/s 00:00
sed -r 's/(\b[0-9]{1,3}\.){3}[0-9]{1,3}\b'/"YOUR HOST IP HERE"/ k3s.yaml > ~/.kube/k3s-vm-config && rm k3s.yaml


Я предполагаю, что в вашем дистрибутиве, как и в большинстве других, sed установлен. Чтобы проверить, что все работает, простая команда kubectl --kubeconfig ~/.kube/k3s-vm-config get nodes должна вывести:


NAME STATUS ROLES AGE VERSION
k3s-vm Ready control-plane,master 2m4s v1.21.7+k3s1

Наш кластер K8s теперь готов к приему рабочих нагрузок!


Шаг 2.5 Клиенты (опционально)​


Если вы хотите иметь приятный пользовательский интерфейс для взаимодействия с


K8s, есть два варианта:


  • k9s (CLI) Мне очень нравится, с ним легко работать, идеально подходит для удаленных систем.

a51fa7f983f8cb70ae6580687ea8b2ab.png


k9s


  • Lens (GUI) Недавно перешел на него, здесь мне нравятся интегрированные метрики

582b2378a2bb0165805db8068afb5036.png


Lens


Эти программы должны найти наши настройки кластера в ~/.kube.


Шаг 3 — nginx ingress, Let’s Encrypt и хранилище​


Следующая цель в нашем Makefile устанавливает nginx ingress-контроллер и менеджер сертификатов Let’s Encrypt, чтобы наши деплойменты могли иметь валидные сертификаты TLS (бесплатно!). Также там есть класс хранилища по умолчанию, чтобы наши нагрузки без установленного класса использовали дефолтный.


base:
${KUBECTL} apply -f k8s/ingress-nginx-v1.0.4.yml
${KUBECTL} wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=60s
${KUBECTL} apply -f k8s/cert-manager-v1.0.4.yaml
@echo
@echo "waiting for cert-manager pods to be ready... "
${KUBECTL} wait --namespace=cert-manager --for=condition=ready pod --all --timeout=60s
${KUBECTL} apply -f k8s/lets-encrypt-staging.yml
${KUBECTL} apply -f k8s/lets-encrypt-prod.yml


Найти используемые мной файлы можно тут. Nginx ingress YAML получен отсюда, но с одной модификацией на строке 323:


dnsPolicy: ClusterFirstWithHostNet
hostNetwork: true

таким образом мы можем правильно использовать DNS для нашего случая с одним сервером. Более подробная информация здесь.


Файл cert-manager слишком большой, чтобы его можно было полностью просмотреть, не стесняйтесь обращаться к документации по нему. Для выдачи сертификатов Let's Encrypt нам понадобиться определенный объект ClusterIssuer. Мы будем использовать два, один для staging API и один для production. Используйте staging issuer для экспериментов, так как в этом случае нет ограничений на скорость выдачи сертификатов, однако имейте в виду, что сертификаты будут недействительными. Обязательно замените адрес электронной почты в обоих issuers на свой собственный.


# k8s/lets-encrypt-staging.yml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
namespace: cert-manager
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: YOUR.EMAIL@DOMAIN.TLD
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx

# k8s/lets-encrypt-prod.yml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
namespace: cert-manager
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: YOUR.EMAIL@DOMAIN.TLD
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx

Если бы мы выполнили все инструкции kubectl apply одну за другой, процесс, вероятно, завершился бы неудачей, так как нам нужно переходить к cert-менеджеру уже с готовым ingress-контроллером. С этой целью в kubectl есть удобная подкоманда wait, которая может принимать условия и метки (помните их?) и останавливает процесс до тех пор, пока не будут готовы необходимые компоненты. Рассмотрим подробнее отрывок из примера выше:


${KUBECTL} wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=60s

Здесь происходит ожидание в течение 60 секунд, пока все поды, соответствующие селектору app.kubernetes.io/component=controller, не станут иметь состояние ready. Если истечет время ожидания, Makefile остановится. Однако не беспокойтесь, если в какой-то из целей возникнет ошибка, так как все они являются идемпотентными. В этом случае можно запустить make base несколько раз, и если в кластере уже есть определения, они просто останутся неизменными. Попробуйте!


Шаг 4 — Portrainer​


Мне все еще очень нравится, когда Portainer управляет моим сервером, и, по удачному стечению обстоятельств, он поддерживает как K8s, так и Docker. Давайте постепенно перейдем к соответствующим частям файла YAML:


---
apiVersion: v1
kind: Namespace
metadata:
name: portainer

Достаточно просто, Portainer определяет собственное пространство имен.


---
apiVersion: v1
kind: PersistentVolume
metadata:
labels:
type: local
name: portainer-pv
spec:
storageClassName: local-storage
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/zpool/volumes/portainer/claim"
---
# Source: portainer/templates/pvc.yaml
kind: "PersistentVolumeClaim"
apiVersion: "v1"
metadata:
name: portainer
namespace: portainer
annotations:
volume.alpha.kubernetes.io/storage-class: "generic"
labels:
io.portainer.kubernetes.application.stack: portainer
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "1Gi"

Этот том (и связанный с ним claim), где Portainer хранит свою конфигурацию. Обратите внимание, что в объявление PersistentVolume можно включить nodeAffinity, чтобы соответствовать имени хоста сервера (или виртуальной машины). Я пока не нашел способа сделать это лучше.


---
# Source: portainer/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: portainer
namespace: portainer
labels:
io.portainer.kubernetes.application.stack: portainer
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
spec:
type: NodePort
ports:
- port: 9000
targetPort: 9000
protocol: TCP
name: http
nodePort: 30777
- port: 9443
targetPort: 9443
protocol: TCP
name: https
nodePort: 30779
- port: 30776
targetPort: 30776
protocol: TCP
name: edge
nodePort: 30776
selector:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer

Здесь мы видим определение сервиса. Обратите внимание, как указаны порты (наш ingress будет использовать только один из них). Теперь перейдем к конфигурации развертывания.


---
# Source: portainer/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: portainer
namespace: portainer
labels:
io.portainer.kubernetes.application.stack: portainer
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
spec:
replicas: 1
strategy:
type: "Recreate"
selector:
matchLabels:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
template:
metadata:
labels:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
spec:
nodeSelector:
{}
serviceAccountName: portainer-sa-clusteradmin
volumes:
- name: portainer-pv
persistentVolumeClaim:
claimName: portainer
containers:
- name: portainer
image: "portainer/portainer-ce:latest"
imagePullPolicy: Always
args:
- '--tunnel-port=30776'
volumeMounts:
- name: portainer-pv
mountPath: /data
ports:
- name: http
containerPort: 9000
protocol: TCP
- name: https
containerPort: 9443
protocol: TCP
- name: tcp-edge
containerPort: 8000
protocol: TCP
livenessProbe:
httpGet:
path: /
port: 9443
scheme: HTTPS
readinessProbe:
httpGet:
path: /
port: 9443
scheme: HTTPS
resources:
{}

Большую часть этого файла занимают метки метаданных, это то, что связывает все вместе. Мы видим монтирование тома, используемый Doker-образ, порты, а также определения проб readiness и liveness. Они используются в K8s для определения того, готовы ли поды, а также работают и реагируют ли они соответственно.


---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portainer-ingress
namespace: portainer
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
rules:
- host: portainer.domain.tld
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: portainer
port:
name: http
tls:
- hosts:
- portainer.domain.tld
secretName: portainer-staging-secret-tls

Наконец, ingress, который сопоставляет фактическое доменное имя с этим сервисом. Убедитесь, что у вас есть домен, указывающий на IP-адрес вашего сервера, так как распознаватель вызовов Let's Encrypt зависит от его доступности извне. В нашем случае потребуются записи, указывающие на ваш IP-адрес для domain.tld и *.domain.tld.


Обратите внимание, как мы получаем сертификат — нам нужно добавить в ingress аннотацию cert-manager.io/cluster-issuer : letsencrypt-staging (или prod) и ключ tls с именем хоста и именем секрета, в котором будет храниться ключ TLS. Если сертификат вам не нужен, просто удалите аннотацию и ключ tls.


Kustomize

Итак, вот файлы, необходимые для развертывания Portainer:


# stacks/portainer/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- portainer.yaml

# stacks/portainer/portainer.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: portainer
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: portainer-sa-clusteradmin
namespace: portainer
labels:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
---
apiVersion: v1
kind: PersistentVolume
metadata:
labels:
type: local
name: portainer-pv
spec:
storageClassName: local-storage
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/zpool/volumes/portainer/claim"
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k3s-vm
---
# Source: portainer/templates/pvc.yaml
kind: "PersistentVolumeClaim"
apiVersion: "v1"
metadata:
name: portainer
namespace: portainer
annotations:
volume.alpha.kubernetes.io/storage-class: "generic"
labels:
io.portainer.kubernetes.application.stack: portainer
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "1Gi"
---
# Source: portainer/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: portainer
labels:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
namespace: portainer
name: portainer-sa-clusteradmin
---
# Source: portainer/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: portainer
namespace: portainer
labels:
io.portainer.kubernetes.application.stack: portainer
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
spec:
type: NodePort
ports:
- port: 9000
targetPort: 9000
protocol: TCP
name: http
nodePort: 30777
- port: 9443
targetPort: 9443
protocol: TCP
name: https
nodePort: 30779
- port: 30776
targetPort: 30776
protocol: TCP
name: edge
nodePort: 30776
selector:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
---
# Source: portainer/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: portainer
namespace: portainer
labels:
io.portainer.kubernetes.application.stack: portainer
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
app.kubernetes.io/version: "ce-latest-ee-2.10.0"
spec:
replicas: 1
strategy:
type: "Recreate"
selector:
matchLabels:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
template:
metadata:
labels:
app.kubernetes.io/name: portainer
app.kubernetes.io/instance: portainer
spec:
nodeSelector:
{}
serviceAccountName: portainer-sa-clusteradmin
volumes:
- name: portainer-pv
persistentVolumeClaim:
claimName: portainer
containers:
- name: portainer
image: "portainer/portainer-ce:latest"
imagePullPolicy: Always
args:
- '--tunnel-port=30776'
volumeMounts:
- name: portainer-pv
mountPath: /data
ports:
- name: http
containerPort: 9000
protocol: TCP
- name: https
containerPort: 9443
protocol: TCP
- name: tcp-edge
containerPort: 8000
protocol: TCP
livenessProbe:
httpGet:
path: /
port: 9443
scheme: HTTPS
readinessProbe:
httpGet:
path: /
port: 9443
scheme: HTTPS
resources:
{}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portainer-ingress
namespace: portainer
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
rules:
- host: portainer.domain.tld
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: portainer
port:
name: http
tls:
- hosts:
- portainer.domain.tld
secretName: portainer-staging-secret-tls

Цель в Makefike:


portainer:
${KUBECTL} apply -k stacks/portainer

Ожидаемый вывод:


> make portainer
kubectl --kubeconfig ~/.kube/k3s-vm-config apply -k stacks/portainer
namespace/portainer created
serviceaccount/portainer-sa-clusteradmin created
clusterrolebinding.rbac.authorization.k8s.io/portainer created
service/portainer created
persistentvolume/portainer-pv created
persistentvolumeclaim/portainer created
deployment.apps/portainer created
ingress.networking.k8s.io/portainer-ingress created

Так как она идемпотентна, то при повторном запуске вы должны увидеть следующие:


> make portainer
kubectl --kubeconfig ~/.kube/k3s-vm-config apply -k stacks/portainer
namespace/portainer unchanged
serviceaccount/portainer-sa-clusteradmin unchanged
clusterrolebinding.rbac.authorization.k8s.io/portainer unchanged
service/portainer unchanged
persistentvolume/portainer-pv unchanged
persistentvolumeclaim/portainer unchanged
deployment.apps/portainer configured
ingress.networking.k8s.io/portainer-ingress unchanged

Шаг 5 — Samba share​


Запустить сервер Samba в кластере очень просто. Вот наши файлы YAML:


# stacks/samba/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

secretGenerator:
- name: smbcredentials
envs:
- auth.env

resources:
- deployment.yaml
- service.yaml

Здесь у нас kustomization со множеством файлов. Когда мы применяем apply -k к директории, в которой находится этот файл, все они объединятся в один.


Сервис достаточно простой:


# stacks/samba/service.yaml
apiVersion: v1
kind: Service
metadata:
name: smb-server
spec:
ports:
- port: 445
protocol: TCP
name: smb
selector:
app: smb-server

Конфигурация развертывания тоже:


# stacks/samba/deployment.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: smb-server
spec:
replicas: 1
selector:
matchLabels:
app: smb-server
strategy:
type: Recreate
template:
metadata:
name: smb-server
labels:
app: smb-server
spec:
volumes:
- name: smb-volume
hostPath:
path: /zpool/shares/smb
type: DirectoryOrCreate
containers:
- name: smb-server
image: dperson/samba
args: [
"-u",
"$(USERNAME1);$(PASSWORD1)",
"-u",
"$(USERNAME2);$(PASSWORD2)",
"-s",
# name;path;browsable;read-only;guest-allowed;users;admins;writelist;comment
"share;/smbshare/;yes;no;no;all;$(USERNAME1);;mainshare",
"-p"
]
env:
- name: PERMISSIONS
value: "0777"
- name: USERNAME1
valueFrom:
secretKeyRef:
name: smbcredentials
key: username1
- name: PASSWORD1
valueFrom:
secretKeyRef:
name: smbcredentials
key: password1
- name: USERNAME2
valueFrom:
secretKeyRef:
name: smbcredentials
key: username2
- name: PASSWORD2
valueFrom:
secretKeyRef:
name: smbcredentials
key: password2
volumeMounts:
- mountPath: /smbshare
name: smb-volume
ports:
- containerPort: 445
hostPort: 445

Обратите внимания, что здесь вместо PV и PVC мы используем hostPath. Устанавливаем его type как DirectoryOrCreate, чтобы каталог был создан в случае его отсутствия.


Мы используем docker-образ dperson/samba, который позволяет настраивать пользователей и общие ресурсы на лету. Здесь я указываю один общий ресурс с двумя пользователями (с USERNAME1 в качестве администратора общего ресурса). Пользователи и пароли берутся из простого файла env:


# stacks/samba/auth.env
username1=alice
password1=foo

username2=bob
password2=bar

Цель в Makefile:


samba:
${KUBECTL} apply -k stacks/samba

Ожидаемый результат:


> make samba
kubectl --kubeconfig ~/.kube/k3s-vm-config apply -k stacks/samba
secret/smbcredentials-59k7fh7dhm created
service/smb-server created
deployment.apps/smb-server created

Шаг 6 — BookStack​


В качестве примера использования Kompose для преобразования docker-compose.yaml в файлы K8s воспользуемся отличным wiki-приложением BookStack.


Это мой оригинальный docker-compose файл для BookStack:


version: '2'
services:
mysql:
image: mysql:5.7.33
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=bookstack
- MYSQL_USER=bookstack
- MYSQL_PASSWORD=secret
volumes:
- mysql-data:/var/lib/mysql
ports:
- 3306:3306

bookstack:
image: solidnerd/bookstack:21.05.2
depends_on:
- mysql
environment:
- DB_HOST=mysql:3306
- DB_DATABASE=bookstack
- DB_USERNAME=bookstack
- DB_PASSWORD=secret
volumes:
- uploads:/var/www/bookstack/public/uploads
- storage-uploads:/var/www/bookstack/storage/uploads
ports:
- "8080:8080"

volumes:
mysql-data:
uploads:
storage-uploads:

Использовать Kompose просто:


> kompose convert -f bookstack-original-compose.yaml
WARN Unsupported root level volumes key - ignoring
WARN Unsupported depends_on key - ignoring
INFO Kubernetes file "bookstack-service.yaml" created
INFO Kubernetes file "mysql-service.yaml" created
INFO Kubernetes file "bookstack-deployment.yaml" created
INFO Kubernetes file "uploads-persistentvolumeclaim.yaml" created
INFO Kubernetes file "storage-uploads-persistentvolumeclaim.yaml" created
INFO Kubernetes file "mysql-deployment.yaml" created
INFO Kubernetes file "mysql-data-persistentvolumeclaim.yaml" created

Облом, нам сразу же говорят, что наши тома и использование depends_on не поддерживаются. Но их достаточно легко исправить. В интересах краткости и не делая эту статью длиннее, я просто опубликую окончательный результат с некоторыми примечаниями.


# stacks/bookstack/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- bookstack-build.yaml

# stacks/bookstack/bookstack-build.yaml
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: bookstack
name: bookstack
spec:
ports:
- name: bookstack-port
port: 10000
targetPort: 8080
- name: bookstack-db-port
port: 10001
targetPort: 3306
selector:
io.kompose.service: bookstack
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: bookstack-storage-uploads-pv
spec:
capacity:
storage: 5Gi
hostPath:
path: >-
/zpool/volumes/bookstack/storage-uploads
type: DirectoryOrCreate
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-path
volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
io.kompose.service: bookstack-storage-uploads-pvc
name: bookstack-storage-uploads-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-path
volumeName: bookstack-storage-uploads-pv
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: bookstack-uploads-pv
spec:
capacity:
storage: 5Gi
hostPath:
path: >-
/zpool/volumes/bookstack/uploads
type: DirectoryOrCreate
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-path
volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
io.kompose.service: bookstack-uploads-pvc
name: bookstack-uploads-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-path
volumeName: bookstack-uploads-pv
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: bookstack-mysql-data-pv
spec:
capacity:
storage: 5Gi
hostPath:
path: >-
/zpool/volumes/bookstack/mysql-data
type: DirectoryOrCreate
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-path
volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
io.kompose.service: bookstack-mysql-data-pvc
name: bookstack-mysql-data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-path
volumeName: bookstack-mysql-data-pv
---
apiVersion: v1
kind: ConfigMap
metadata:
name: bookstack-config
namespace: default
data:
DB_DATABASE: bookstack
DB_HOST: bookstack:10001
DB_PASSWORD: secret
DB_USERNAME: bookstack
APP_URL: https://bookstack.domain.tld
MAIL_DRIVER: smtp
MAIL_ENCRYPTION: SSL
MAIL_FROM: user@domain.tld
MAIL_HOST: smtp.domain.tld
MAIL_PASSWORD: vewyvewysecretpassword
MAIL_PORT: "465"
MAIL_USERNAME: user@domain.tld
---
apiVersion: v1
kind: ConfigMap
metadata:
name: bookstack-mysql-config
namespace: default
data:
MYSQL_DATABASE: bookstack
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: secret
MYSQL_USER: bookstack
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
io.kompose.service: bookstack
name: bookstack
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: bookstack
strategy:
type: Recreate
template:
metadata:
labels:
io.kompose.service: bookstack
spec:
containers:
- name: bookstack
image: reddexx/bookstack:21112
securityContext:
allowPrivilegeEscalation: false
envFrom:
- configMapRef:
name: bookstack-config
ports:
- containerPort: 8080
volumeMounts:
- name: bookstack-uploads-pv
mountPath: /var/www/bookstack/public/uploads
- name: bookstack-storage-uploads-pv
mountPath: /var/www/bookstack/storage/uploads
- name: mysql
image: mysql:5.7.33
envFrom:
- configMapRef:
name: bookstack-mysql-config
ports:
- containerPort: 3306
volumeMounts:
- mountPath: /var/lib/mysql
name: bookstack-mysql-data-pv
volumes:
- name: bookstack-uploads-pv
persistentVolumeClaim:
claimName: bookstack-uploads-pvc
- name: bookstack-storage-uploads-pv
persistentVolumeClaim:
claimName: bookstack-storage-uploads-pvc
- name: bookstack-mysql-data-pv
persistentVolumeClaim:
claimName: bookstack-mysql-data-pvc
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bookstack-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
rules:
- host: bookstack.domain.tld
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bookstack
port:
name: bookstack-port
tls:
- hosts:
- bookstack.domain.tld
secretName: bookstack-staging-secret-tls

Kompose преобразует оба контейнера внутри файла docker-compose в сервисы, однако я превратил их в один сервис.


Обратите внимание, как config map содержит всю конфигурацию приложения, а затем вводится в конфигурацию развертывание с помощью:


envFrom:
- configMapRef:
name: bookstack-config

Сегмент chown в Makefile связан с особенностью установки docker-образа BookStack. У большинства образов этой проблемы нет, однако PHP-образы ею славятся. Без надлежащих прав для директории на сервере загрузка в wiki не будет работать. Но в нашем Makefile это учитывается:


bookstack:
${KUBECTL} apply -k stacks/bookstack
@echo
@echo "waiting for deployments to be ready... "
@${KUBECTL} wait --namespace=default --for=condition=available deployments/bookstack --timeout=60s
@echo
ssh ${HOST} chmod 777 /zpool/volumes/bookstack/storage-uploads/
ssh ${HOST} chmod 777 /zpool/volumes/bookstack/uploads/

Здесь мы применяем kustomization и затем ждем, пока оба деплоймента будут готовы, что случится, когда их смонтированные тома будут либо привязаны, либо созданы на сервере. Затем мы подключаемся к серверу по SSH, чтобы изменить владельца томов на правильные идентификаторы пользователей и групп. Не идеально, но работает. Образ MySQL при развертывании в этом не нуждается.


Также внимание, как легко преобразовать директиву depends_on из файла docker-compose, поскольку поды схожим образом имеют доступ друг к другу по имени.


Шаг 8 — Все готово!​


Полный код доступен здесь. Приведем весь Makefile для завершения картины:


# set your host IP and name
HOST_IP=192.168.1.60
HOST=k3s
#### don't change anything below this line!
KUBECTL=kubectl --kubeconfig ~/.kube/k3s-vm-config

.PHONY: k3s_install base bookstack portainer samba

k3s_install:
ssh ${HOST} 'export INSTALL_K3S_EXEC=" --no-deploy servicelb --no-deploy traefik"; \
curl -sfL https://get.k3s.io | sh -'
scp ${HOST}:/etc/rancher/k3s/k3s.yaml .
sed -r 's/(\b[0-9]{1,3}\.){3}[0-9]{1,3}\b'/"${HOST_IP}"/ k3s.yaml > ~/.kube/k3s-vm-config && rm k3s.yaml

base:
${KUBECTL} apply -f k8s/ingress-nginx-v1.0.4.yml
${KUBECTL} wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=60s
${KUBECTL} apply -f k8s/cert-manager-v1.0.4.yaml
@echo
@echo "waiting for cert-manager pods to be ready... "
${KUBECTL} wait --namespace=cert-manager --for=condition=ready pod --all --timeout=60s
${KUBECTL} apply -f k8s/lets-encrypt-staging.yml
${KUBECTL} apply -f k8s/lets-encrypt-prod.yml

bookstack:
${KUBECTL} apply -k stacks/bookstack
@echo
@echo "waiting for deployments to be ready... "
@${KUBECTL} wait --namespace=default --for=condition=available deployments/bookstack --timeout=60s
@echo
ssh ${HOST} chmod 777 /zpool/volumes/bookstack/storage-uploads/
ssh ${HOST} chmod 777 /zpool/volumes/bookstack/uploads/

portainer:
${KUBECTL} apply -k stacks/portainer

samba:
${KUBECTL} apply -k stacks/samba

Заключение​


Итак, зачем все это было нужно? Мне потребовалось несколько дней, чтобы все заработало, и я несколько раз бился головой об монитор, однако процесс дал лучшее понимание того, как Kubernetes работает под капотом, как его отлаживать, и теперь с этим Makefile мне требуется всего 4 минуты, чтобы воссоздать конфигурацию NAS для 3 приложений. У меня есть еще дюжина старых docker-compose приложений, которые нужно преобразовать, но с каждым разом это все проще.


 
Сверху