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

от автора

Но зачем

Знаю, о чем вы думаете — 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, чтобы избежать «проблемы с релизами», но хорошего понемножку. После освоения настройки виртуальной машины, переход на физический сервер не должен представлять проблемы.

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

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

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

Установка 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) Мне очень нравится, с ним легко работать, идеально подходит для удаленных систем.

k9s

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

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

Здесь следует отметить одну вещь: я использую Kustomize для управления файлами YAML при развертывании. Это связано с тем, что другой инструмент, Kompose, выводит множество различных YAML файлов при преобразовании docker-compose файлов в файлы K8s. 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 приложений, которые нужно преобразовать, но с каждым разом это все проще.


ссылка на оригинал статьи https://habr.com/ru/articles/594533/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *