Но зачем
Знаю, о чем вы думаете — 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
приложений, которые нужно преобразовать, но с каждым разом это все проще.
Комментарии (7)
Exclipt
09.12.2021 22:47+4Расскажите, почему swarm уже мертв? Про сложные проекты не говорю, интересно с точки зрения применения на малых и средне-малых проектах, до 20-и сервисов и кластерах по 2-5 машин. Я не настоящий сварщик, но как минимум на дев сервере и дома сварма хватает. Что я не учитываю?
vrangel
10.12.2021 03:33+1Присоединяюсь к вопросу. Не первый раз слышу, что swarm мертв, хотелось бы узнать подробности. В swarm порог входа, конечно, заметно ниже. Для мини проектов выглядить более привлекательно.
Ilya_Kuz
10.12.2021 09:40Использовали Swarm в одном из небольших рабочих проектов (кластер из 3 машин) - и удивлялись, насколько он был ненадежен. Обновление сервиса может упасть просто так, несколько раз окружение просто отказывалось подниматься (все сервисы висели со статусом "ожидает деплоя"), сам Swarm часто шедулил почти все сервисы на один хост, пока 2 других простаивали.
slonopotamus
10.12.2021 09:53+3почему swarm уже мертв?
Ну хотя бы потому что его объявили мёртвым: https://www.mirantis.com/blog/mirantis-acquires-docker-enterprise-platform-business/
What About Docker Swarm?
The primary orchestrator going forward is Kubernetes. Mirantis is committed to providing an excellent experience to all Docker Enterprise platform customers and currently expects to support Swarm for at least two years, depending on customer input into the roadmap.
pythoned195
Зачем такие велосипеды , если у вас одна нода ?
Чем minikube не угодил ?
DirectX
Я вижу пару плюсов:
Можно довольно просто развернуть дополнительные виртуальные ноды на этой же машине для тестирования работы в условиях, приближенных к реальному кластеру.
Использование k3s максимально похоже на установку кубернетеса через скрипты k3d на реальном железе.
jreznot
Если дома, то лучше вообще microk8s от Canonical, там не нужна виртуальная машина.