Сейчас очень часто в вакансиях на ДевОпс ищут людей которые, понимают и могут в Helm чарт.
Как правило, собеседование это не просто экзамен знаю/не знаю - это еще и проверка на понимание принципов работы какой-то технологии, умение решать задачи.
Я решил поделиться с вами своим опытом и понял что тут как никогда пригодится метод понимания процессов через визуализацию. О принципах работы метода «Дворца памяти» можно прочитать тут же на Хабр «https://habr.com/ru/articles/437580/»
Я предполагаю что вы пришли сюда уже с пониманием работы в Kubernetes. Так как Helm чарты нужны именно тут. Если вы еще не знакомы с Kubernetes - то очень сложно будет понять практическую пользу данной технологии. Смотря на отклики на эту статью я подумаю о том как составить «Дворец памяти» и для Kubernetes.
Суть метода «Дворца памяти» заключается в том, чтобы составить для какой-либо сложной темы, визуальные образы в своей голове и гулять по ним. Тогда нам будет легче что-то не только запомнить, но и самое главное понять суть! А это как раз и нужно в любых технологических решениях, которые сегодня есть на рынке. Благодаря этому методу мы не только запомним основные команды, но и подходы для решения тех или иных вопросов. Это и хотят слышать интервьюеры.
Для начала я своими словами дам краткую информацию, что такое Helm и для чего он используется.
Helm - это пакетный менеджер. Это как apt для Linux, yum для CentOS, pip для Python или npm для Node.js.
Только это вот для Kubernetes.
Зачем Kubernetes пакетный менеджер? Это же вроде что-то про серверы? Как вы знаете в kubernetes есть файлики с расширением YAML. Их называют манифесты. В них идет описание, что и как нужно запускать. PostgreSQL, Redis, Kafka, Prometheus, Grafana, NGINX ingress и так далее. Когда их 5 - 10 ок, настроил и забыл. Но когда их переваливает за десятки, становится не просто за всем этим следить. Это может произойти не только из-за наличия «зоопарка» всяких сервисов, но и из-за желания развернуть несколько изолированных контуров. Например, dev prod. То есть приходится дублировать сервисы.
Но как же это все работает?
И вот тут уже перейдем в "дворец памяти". Добро пожаловать в мир Helm (чуть не написал Hell)!
Представьте себе типографию. Потому что сфера работы Helm похожа на сферу работы типографии. Ведь Helm чарты также что-то делают по шаблонам, оформляют информацию по определенным лекалам и выпускают что-то в жизнь.
Если Helm-чарты это типографии. То сам Helm это законодатель для типографий. Как они должны работать и на каком языке общаться.
Как же появляются эти самые типографии? Они появляются, если выполнить команду helm create. Команда helm create используется для создания нового Helm чарта (типографии). Она генерирует стандартную структуру директорий и набор файлов-шаблонов, которые служат основой для вашего будущего приложения в Kubernetes. Это базовая команда, которая просто создает файлы на вашем локальном диске и пока никак не взаимодействует с кластером Kubernetes.
И что же она создаст?
Итак, переносимся в основное здание типографии (Helm чарт).
В любой компании есть устав. Тут тоже. Устав типографии файл Chart.yaml - Метаданные. Название типографии. Номер телефона типографии, связана ли эта типография с другими типографиями (Читай зависимости и да да они тут тоже есть).
Самый главный отдел - это отдел Макетирования (Папка templates). Это сердце типографии. Здесь лежат матрицы и формы ( как несложно догадаться по названию папки - шаблоны YAML для Kubernetes). Это не сами страницы (которые ждут на выходе) а формы для их отливки. Например, есть форма с названием deployment.yaml. В ней есть места, куда нужно вписать переменные данные «Название газеты» (name), «Номер выпуска» и например «для кого предназначается эта газета». В общем суть мы уловили.
Так - а где же брать эти данные для вставки в шаблон? Для этого есть склад с подписанными полочками (файл - values.yaml). Тут будут находиться запчасти (сведения) которые будут подставляться в шаблон. Здесь лежат все динамические данные которые впоследствии можно менять и использовать для шаблона. Каким цветом печатать, сколько копий, какой заголовок подставить.
Так же у нас есть тут советский ОТК - Отдел контроля качества и контроля (файл values.schema.json). Если мы их попросим (а это необязательно), то они могут проверять на ввод данные которые приходят со склада (из файла - values.yaml). Происходит валидация, проверка на адекватность передаваемых данных. Чуть ниже подробнее будет.
Есть отдел заготовок для склада (файл _helpers.tpl, но можно любой файл который начинается с нижнего подчеркивания) — это вспомогательные template-функции.
Так же есть комната для хранения продукции с других типографий (папка charts/) — тут будут зависимости, если есть.
Если вы хотите в конце каждой газеты приложить некое послание, то для этого есть отдел где дают информацию в конце газеты (Файл NOTES.txt)— вывод информации после установки. Типа как md файлы в гите - которые ра��скажут что-нибудь вам по-человечески.
Смотрите всего 6 отделов и 1 журнал. Отдел Макетирования, Склад для отдела макетирования, Отдел контроля, Отдел заготовок, отдел для хранения продукции с других типографий и отдел где дают информацию в конце газеты. Всего 6 комнат - не так уж и сложно запомнить - правда?
Итог типографии газета - это называется ревизия.
Так разобрались что у нас есть. А как же запустить процесс сборки? Через команду Install или template (или helm install --dry-run). Когда мы запускаем эти команды, курьер бежит на склад (values.yaml) и берет все краски и настройки. Он идет в отдел макетирования (папка templates). И начинает прогонять каждый макет через печатный станок. В те места где в макете написано что-то в фигурных скобках {{ .Values.replicaCount (о правилах чуть позже) }} станок подставляет данные со склада.
На выходе получается готовая отпечатанная страница (готовый YAML файл). И далее по этой «газете» будет работать Kubernetes. Можно ли выпускать несколько газет для разных тем (контуров дев или прод) - да конечно. Kubernetes будет читать все.
Процесс создания как я говорил ранее запускается командой Install.
Примерhelm install prod-nginx nginx-chart
Сделать газету с названием «prod-nginx» используя шаблон «nginx-chart».
Вот мы и узнали буквально за пару минут о Helm и чем он там занимается. Мне кажется это было совсем не сложно.
Хотите продолжить знакомится с миром Helm и о его особенностях дальше? Тогда продолжим.
Одна из практических задач - развернуть контур для дева и прода. Представим что главный редактор (разработчик) хочет выпустить спецвыпуск (только для тестовой среды) и он говорит: «Возьми шаблон со склада, но для этого тиража замени заголовок». Для этого используем Install с флагом set.
helm install nginx-chart ./chart --set image.pathenv=env
(Установить ревизию «nginx-chart» используя шаблон «./chart» определив env для дева). Но чаще set используется в CI/CD, при подстановке версии Docker-образа.
Для своего окружения (спец выпуски) можно использовать заготовки для set в виде файлов. Для этого создают отдельные values файлы для окружений. К примеру values-dev.yaml, values-stage.yaml, values-prod.yaml.А потом
helm install nginx-chart ./chart -f values-prod.yaml
При желании можно даже передавать несколько файлов
helm install nginx-chart ./chart -f values.yaml -f values-prod.yaml
В каждой типографии есть некоторые особые станки которые включаются до или после основных этапов печати. Опять же при необходимости.
Pre-install hook - станок, который проверяет качество бумаги до того как запустят главный станок, например в нашем случае проверить доступна ли база данных, создать какой-то env, сделать бэкап базы чтобы ничего не сломать.
Post-install hook - станок который делает отметку об успешной печати или отправляют уведомление, что газета вышла. Например, нам нужно сделать какие-то тесты или отправить сообщение в телегу - что «все ок парни, я справился».
Хорошо вот выпустили мы газету и оказалась что газетенка то так себе и может взорвать общество Kubernetes и вывести всех на митинг. А нам такое надо, трогать цеховиков? Нет! Тогда надо как то откатывать. А как откатывать если ты этих газет на выпускал и непонятно где там все эти экземпляры?
Для этого есть архив куда можно глянуть - команда history
helm history prod-nginx
Тут prod-nginx общее название тиража.
Эта команда покажет тебе номера всех запущенных ревизий. И вот по ним можно узнать конкретный выпуск и ты поймешь по номеру, что нужно откатить или удалить (uninstall, rollback)
Конечно же в Helm есть что-то и для отзыва газеты - это механизм rollback.
helm rollback prod-nginx 1
А что это за единица в конце? Это номер ревизии. А где узнать этот номер ревизии? Для этого и есть команда history которую обсуждали выше.
Представьте газета была хороша - но вы узнали что там есть опечатки или забыли что-то допечатать. В Helm это не проблема. Вы хотите заменить эту газету. Для этого есть upgrade. Типография не создаёт новую газету с нуля, а перевыпускает тот же номер с изменениями.
helm upgrade prod-nginx nginx-chart
Что происходит? Helm берёт тот же chart, берёт values, снова рендерит templates, сравнивает с тем, что уже работает в Kubernetes, применяет изменения, то есть upgrade это обновление существующего релиза (газеты).
Есть еще такое
helm upgrade --install nginx-chart ./chart -f values.yaml
Это значит если такого нет для апгрейда, то установи. Типа если газеты такой нет, то тогда напечатай.
На каком языке пишутся макеты?
Ответ — Go Template.
Этот язык точно так же работает, как и другие языки типа js и python. Он умеет не только тупо подставлять значения, но и делать условия, делать циклы, вызывать функции (ну а как же) и даже инклюдить заготовки (помните файл _helpers.tpl).
В шаблоне как мы говорили выше есть специальные метки {{ }} Это места, куда подставляются значения.
Примерreplicas: {{ .Values.replicaCount }}
Что происходит?
Типография идет на склад (Helm читает values.yaml) там на полочке видит replicaCount: 3 и он подставляет в шаблон. На выходе в газете мы читаем
replicas: 3
Далее про умные (ладно пусть будут smart) формы (if). Есть реклама для газеты - о, тогда давай напечатаем. (Если ... То...)
В Go Template это делается так к примеру
{{if .Values.ingress.enabled}}
apiVersion: networking.k8s.io/v1
kind: Ingress
{{ end }}
А на складе (в файле values.yaml) у нас стоит
ingress:
enabled: true
А если false — то Ingress вообще не создаётся.
Циклы (тут это называется range. «Диапазон»)
Представьте что нам нужно напечатать список всех спонсоров. Тогда мы использовали бы цикл.
Как пример:env:
{{ range .Values.env }}
- name: {{ .name }}
value: {{ .value }}
{{ end }}
А на складе (в файле values.yaml) так написано:env:
- name: APP_ENV
value: prod
- name: DEBUG
value: false
Функции (очень любят на интервью поспрашивать)
Helm добавляет много функций.
Основные
quote - это кавычки
upper - преобразования строки в верхний регистр
default - значение по дефолту
toYaml - запихать сразу несколько строк в отформатированном виде
nindent - количество пробелов (знаете как yaml любит эти пробелы?)
printf "%s-%s" - даем два аргумента и будет красиво написано. Шаблон в шаблоне.
replace и два аргумента - будет поменян первый аргумент на второй
trunc и число - обрежет слово до нужного количество символов
trimSuffix и один аргумент, строка - проверяет, заканчивается ли строка указанным суффиксом, и если да — удаляет его.
Пример:
image: {{ .Values.image.repository | quote }}
Получится:image: "nginx"
Пример:
replicas: {{ .Values.replicaCount | default 1 }}
Если значение не передали → будет 1.
Используемые функции. пишем в _helpers.tpl
Пример
{{- define "nginx.fullname" -}}
{{ .Release.Name }}-nginx
{{- end }}
Использование уже в шаблоне:
name: {{ include "nginx.fullname" . }}
Обратили внимание на эти черточки перед define и в конце. Это тут не просто так. Когда в макетах типографии используются конструкции {{ }}, они просто вставляют данные в форму печати, как есть. Но иногда вокруг этих вставок могут оставаться лишние пробелы или пустые строки, которые портят yaml. Если использовать конструкцию {{- }}, то печатный станок сначала уберёт лишние пробелы перед местом вставки, а уже потом подставит нужное значение.
Как пример:
{{- include "msvc-chart.someFragment" . | nindent 4 }}
В нашей типографии это выглядит так: станок берёт готовый фрагмент макета (include), очищает место для вставки от лишних пробелов, а затем аккуратно вставляет его, добавляя ровно 4 пробела отступа. В итоге вся страница получается ровной и аккуратно отформатированной.
Так же можно брать информацию не только со склада (Values). Но также из .Release, .Chart.
.Release - это информация о релизе
Доступные поля:
Name — имя релиза
Namespace — неймспейс
Revision — номер ревизии (int)
IsUpgrade — true если upgrade
IsInstall — true если install
.Chart - это информация о чарте
Доступные поля:
Name — имя чарта
Version — версия чарта, это поле должно меняться при каждой его модификации
AppVersion — версия приложения внутри чарта
Description — описание
Home — URL домашней страницы
Maintainers — список мейнтейнеров (не пользовался)
Sources — список исходников
Как пример
metadata:
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
Последний пример очень хорош потому что в Kubernetes у каждого объекта должно быть уникальное имя. Чтобы не было путаницы, хорошей практикой считается делать так: дать пользователю возможность задать имена компонентов вручную, если ему это нужно. Но если он ничего не указал, типография должна сама сгенерировать корректные имена. Чаще всего для этого используют имя релиза — Release.Name (пример выше), которое Helm гарантирует как уникальное в пределах неймспейса. Тогда все создаваемые ресурсы получают имена, привязанные к конкретному выпуску газеты, и можно без конфликтов развернуть один и тот же чарт несколько раз в одном и том же неймспейсе. Надеюсь не запутал, но фишку думаю вы поняли. В общем если повторите, то не ошибетесь.
Ну давайте еще для закрепления пример где можно использовать .Chart.Name. Вдруг есть желание дать информацию в NOTES.txt (помните это приветственное письмо после установки?)
То так.
Спасибо за установку {{ .Chart.Name }} версии {{ .Chart.Version }}!
Приложение: {{ .Chart.AppVersion }}
Документация: {{ .Chart.Home }}
Поддержка: {{ .Chart.Maintainers }}
Итак с языком немного разобрались.
Следующая задача:
Представь, что ты хочешь выпустить газету, но сначала как то бы проверить ее не мешало бы? Как посмотреть YAML, который Helm реально сгенерирует, не устанавливая его. Ну вот просто для проверки. Как быть?
Это очень полезно для отладки. Это делает команда helm template (или можно еще helm install --dry-run, но как то много писать). Она рендерит твой чарт в привычный YAML, но при этом никуда не устанавливает.
Пример
helm template nginx-chart ./chart -f values-prod.yaml
А как проверить газету на ошибки - вдруг все сломается в Kubernetes когда мы такую газету им отправим и все таки пойдут на митинг? Прежде чем коммитить, как проверить чарт на ошибки?Есть похожая команда например в nginx (nginx - t), здесь же есть lint.
Пример
helm lint ./mychart
Поэтому в реальной практике DevOps часто делают следующий флоу (последовательность)
helm lint - проверили
helm template - посмотрели все ли ок
helm upgrade --install - установили
А что там в отделе качества и контроля (файл values.schema.json)? Для чего он?
Когда-то люди любили типизацию переменных, потом не любили, потом сново полюбили. Вот число, вот строка. Тут эту опцию решили прикрутить как раз через этот файл values.schema.json. Еще раз это необязательно - но если очень хочется порядка, то можно. Простыми словами, тут в файле контракт, который говорит пользователю чарта: "Ты можешь передать мне только вот такие значения, иначе я не буду работать" . Тут есть свои преимущества.
В первую очередь защита от ошибок. Если пользователь ошибется в values.yaml (например, напишет replicas: "два" вместо 2), Helm не пойдет деплоить сломанный манифест и не выпустит вашу газету. Он сразу скажет: "Ошибка! Поле replicas должно быть числом".
Так же это же готовая документация не хуже Swarm. JSON Schema служит машиночитаемой документацией. IDE (например, VS Code с плагинами) могут подсказывать пользователю, какие поля доступны и какие у них типы, прямо во время редактирования values.yaml.
Так же тут есть контроль допустимых значений. Можно ограничить значения только разрешенным списком (например, pullPolicy может быть только IfNotPresent, Always или Never).
А еще обязательные поля. Можно указать, какие поля обязательны, а какие — опциональны.
Есть еще одна очень интересная ситуация. Есть такой файл для Kubernetes - deploy.yaml. А правильнее сказать значение kind = "Deployment" в файле. И вот например там вы использовали для env (знаете наверное что это такое) значение из склада values.yaml - например пароль для базы данных. Вот вы поменяли пароль. Но что интересно ваш «pod» не запустится, потому что сам Deployment - не изменился. Ну Kubernetes так думает - что все так же. Попросите перевыпустить газету через upgrade, а ничего не произойдет. Потому что для Kubernetes файл деплоймент (тот кто следит за подами) такой же. В итоге Helm обновляет объект Secret в Kubernetes. Но поды не перезапускаются. А значит контейнер продолжает работать со старым значением.
Почему? Потому что Deployment не изменился. Kubernetes делает rollout только если изменился Pod template.То есть этот кусок:
spec:
template:
Если там ничего не поменялось, то pod не будет перезапущен.
Решение:
Искусственно изменить Pod template Для этого нужно сделать так, чтобы Deployment видел изменение.
Добавим аннотацию checksum/config.
Как пример
metadata:
annotations:
checksum/config: {{ include "msvc-chart.propertiesHash" . }}
А внутри _helpers.tpl{{- define "msvc-chart.propertiesHash" -}}
{{- $secrets := include (print $.Template.BasePath "/secrets.yaml") . | sha256sum -}}
{{- $urlConfig := include (print $.Template.BasePath "/urls-config.yaml") . | sha256sum -}}
{{ print $secrets $urlConfig | sha256sum }}
{{- end -}}
Если checksum поменяется то Kubernetes, считает что Pod template изменился и тогда он уже точно выполнит rolling update.
Сам код объяснять смысла нет, при желании можно разобраться, но главное, чтобы уловить суть.
В нашу типографию при желании можно создать еще один интересный отдел. Отдел тестирования. Не нужно путать это с командой Lint который просто проверяет yaml на ошибки. Тут смотрим - как все заработает внутри. Например, нужно сделать некий Ping на какой-то адрес?
Это можно сравнить с тем что перед тем как разослать газету по всей стране, отдел качества делает тестовый тираж и проверяет реакцию. Делается это командой
helm test
Helm ищет тестовые ресурсы в chart, если находит то, запускает Kubernetes Job или Pod
После проверяет результат выполнения и если все океюшки то показывает статус Succeeded (Успех) ну или если все плохо то Failed.
Где лежат тесты?
В той же папке templates/.
Но у них есть специальная аннотация.
Пример:
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "app.fullname" . }}-test"
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": hook-succeeded,hook-failed
spec:
containers:
- name: test
image: busybox
command: ['wget']
args: ['http://app:80']
restartPolicy: Never
С помощью тестов можно проверять, например
взаимодействие сервисов между собой
доступность APIсоединение с базой данных
корректность работы внешних зависимостей
Проще говоря тест в Helm — это обычный Kubernetes-объект, описанный в YAML-шаблоне. Этот объект выполняет какое-то разовое действие, например HTTP-запрос к сервису. Если команда внутри контейнера завершается без ошибки, Helm считает тест пройденным.
К примеру нам нужно сделать запрос. Если запрос не удаётся выполнить, Job (если это Джоб) перезапустит контейнер и попробует ещё раз. Количество повторов ограничивается параметром:
У Job еще можно указать backoffLimit: 3
Это означает, что Kubernetes попробует выполнить задачу максимум три раза.
Как вы наверное заметили из примера выше в объекте используются специальные аннотации (я их выделил):
helm.sh/hook
helm.sh/hook-delete-policy
Они сообщают Helm, что данный YAML — это тестовый объект, а не обычный ресурс приложения.
Аннотация helm.sh/hook: test говорит Helm, что этот объект должен запускаться при выполнении команды helm test. Аннотация helm.sh/hook-delete-policy: hook-succeeded,hook-failed указывает Helm удалить Job после завершения теста, независимо от результата.
Ну не круто ли?
Важно! При использовании Helm все изменения в кластере должны (ну ладно, желательно что бы не париться с helm upgrade --force) выполняться через Helm, а не напрямую через kubectl.
Почему? Потому что Helm ведёт архив всех выпусков газеты. Помните о history. В этом архиве хранится информация о том, какие макеты использовались и какие изменения были внесены. Если кто-то начнёт самовольно менять страницы газеты прямо в типографии (а почему бы и нет ведь я так привык к моей kubectl), минуя редакцию, в архиве эти изменения не появятся. Когда редакция попытается выпустить новый номер или откатить старый, система может запутаться и допустить ошибки. Ахтунг! Все изменения должны проходить только через редакцию — то есть через Helm. В общем либо Helm, либо kubectl. Тут уж как с девушками - либо, либо.
Ну и в конце пробежимся по еще некоторым полезным командам которые часто используются.
helm list
показывает список всех газет (установленных приложений) которые сейчас читают. Не путать с history - которая показывает историю версий одной газеты (приложения)
helm status
Это как карточка конкретного тиража.
Тут можно увидеть Название выпуска, Версию макета, Когда напечатан, Сколько станков работает. То есть подробная информация о выпуске газеты. Думаю аналогия понятна
helm get values
Показывает значения параметров (values), которые были использованы при установке.
Например:
replicaCount: 3
image:
tag: 1.2.0
service:
port: 80
Можно также посмотреть старую ревизию:
helm get values prod-nginx --revision 2
А где посмотреть номер ревизии? В history конечно же.
helm show values ./chart
Не путать с «get values». Эта команда показывает все возможные настройки чарта — фактически выводит файл values.yaml. Это по сути дефолтные параметры, которые можно переопределить.
А еще есть
helm get manifest prod-nginx
Показывает все Kubernetes-объекты, которые Helm создал для этой инсталляции.Например:
Deployment
Service
ConfigMap
Ingress
Secret
Ну и последнее.
Как нам нашу настроенную типографию куда-нибудь сохранить? Ведь мы так долго старались и все настраивали, приводили порядок. Не напрасно же?
Для этого можно настроить Helm-репозиторий. По сути это место, откуда другие пользователи или системы могут скачивать готовые шаблоны для установки приложений. Технически таким репозиторием может быть любой HTTP-сервер или даже S3-хранилище. Как устроен Helm-репозиторий? Helm-репозиторий должен содержать архивы чартов (.tgz). Помните обсуждать вверху что там в архиве будет лежать? И специальный файл index.yaml. Файл index.yaml играет тут роль каталога, в котором хранится информация о чартах: их версиях, описании и ссылках на архивы.
Самое простое создать вашу репу там где мы и привыкли создавать репы - в GitHub.
В нашем GitHub-репозитории создадим папку:
docs
В ней будут храниться:
архив чарта - файл index.yaml
Далее выполним две команды.
Упаковка чарта
helm package msvc-chart/ -d docs/
Эта команда упакует чарт в архив .tgz и поместит его в папку docs.
Например:
docs/msvc-chart-0.1.0.tgz
Создание индекса репозито��ия
helm repo index docs/ --url https://.github.io//
Эта команда создаст файл:
docs/index.yaml
В этом файле будет указано:
какие чарты доступныих версии, ссылки на архивы.
Параметр --url задаёт будущий адрес, по которому будут доступны архивы чарта.
Важный момент
Каждый раз, когда чарт изменяется или выпускается новая версия, необходимо снова выполнить helm repo index, чтобы обновить файл index.yaml. Иначе Helm-репозиторий не будет знать о новых версиях чарта.
После генерации файлов нужно запушить их в репозиторий.
Затем в настройках GitHub:
Settings - Pages
нужно активировать GitHub Pages и выбрать источник:
main branch /docs folder
После этого папка docs станет доступна по HTTP-адресу, и её можно будет использовать как Helm-репозиторий.
На этом основная суть Helm изложена. За несколько минут пройдясь по этой типографии можно многое понять. Никто нам не мешает добавить сюда же еще комнат или обьектов.
Ну как все таки прост оказывается весь этот Helm? Если дать ему что-то осязаемое в нашем представлении, то оперировать данными будет легче. В общем, если вы разберетесь во всем этом, то можете сказать работодателю, что в Helm вы можете и самолет построить.
Успехов на собесе.
А что ты там говорил про Kubernetes? Его то как запихать к нам в дворец памяти?
Наверное для Kubernetes лучше отправится в Космос...
Всем лучиков добра и хорошего настроения. Спасибо, что прочитали.
