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

ARTICLES 10.12.21 10.12.21 50
Бесплатные курсына главную сниппетов

Но зачем

Знаю, о чем вы думаете — 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 есть много концепций, однако я остановлюсь на основных:

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

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

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

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

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

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

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

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

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

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

Прежде чем продолжить, убедитесь, что добавили свой 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 с пропуском нескольких компонентов.

Во второй строке происходит копирование с сервера файла 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

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 приложений, которые нужно преобразовать, но с каждым разом это все проще.

 

 

Осваиваем Kubernetes. Оркестрация контейнерных архитектурОсваиваем Kubernetes. Оркестрация контейнерных архитектур 400 страницы · 2019 · 8.93 MB · русский by Джиджи Сайфан
 
на главную сниппетов
Курсы