
Но зачем
Знаю, о чем вы думаете — 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.
Итак, вот файлы, необходимые для развертывания 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/

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