Контейнеры для самых маленьких

40 0

Чтобы лучше понять, как нынешнее ИТ-сообщество пришло к контейнерам, и что это вообще такое, необходимо немного окунуться в историю, а именно в 1979 год!

Историческая часть

В 1979 году Unix V7 (не путать с Linux) представила системный вызов chroot, который стал началом того, что мы сегодня понимаем как “виртуализация процессов”. Сhroot позволял ядру изменить видимый корневой каталог процесса и его “детей”. Т.е. процесс думает, что он работает на машине отдельно, потому что его файловая система отделена от других процессов. Позже это появилось в BSD, где оно приобрело концепт тюрьмы (jail), что стало толчком к разработке одноименной утилиты в FreeBSD. Она ограничивала доступ не только к файловой системе, но также к сети, общей памяти и видимым переменным ядра. Таким образом, ПО в “тюрьме” никак не могло навредить основной системе или другим программам в аналогичных клетках. Позднее, концепт “тюрьмы” перекочевал в Linux, и в последствии появилась реализация Linux Namespaces, а уже потом появились привычные сегодня контейнеры. Проще говоря, какие-то «патлатые гики в свитерах» для своего удобства изобрели контейнеризацию для упрощения тестирования нового ПО, ОС и так далее.

Теоретическая часть

Как же контейнеры помогают в обкатке ПО? Тут все просто! Давайте подумаем, как мы можем быстро запустить несколько копий одного приложения на одном сервере? Первое что приходит в голову - создать виртуальную машину (ВМ), подготовить всю среду для запуска приложения, запустить там один экземпляр, и повторить действия для необходимого количества приложений... Скажем честно, это не просто сложно, это крайне трудоемко... Да, соглашусь, можно это немного оптимизировать, используя Ansible, клонирование ВМ и так далее, но все-же, не кажется ли это вам громоздким и неприятным для исполнения?

Вот как раз ответ на вышестоящий вопрос - контейнер! Время на билд контейнера и его запуск минимально, а в совокупности с простотой создания и управления контейнеризация не оставляет никаких шансов привычным ранее виртуальным машинам! И это мы еще не берем в расчет затраченные впустую серверные мощности, ведь на виртуализацию тратится во много раз больше ресурсов. Для примера: минимальная Ubuntu 20.04 в простое потребляет ~300 мегабайт оперативной памяти. В случае виртуализации windows, то там пропорционально от выданных мощностей резервируется оперативная память (чем больше дадите ВМ оперативной памяти, тем больше в процессе работы она будет РЕЗЕРВИРОВАТЬ под только свои нужды).

Что мы видим в сухом остатке? Контейнер - некая сущность, которая должна все делать хорошо, а плохо не делать. Но у не знакомых с контейнеризацией пользователей, возникает много вопросов, и самый частый из них: «А как мне сделать свой контейнер, что мне для этого нужно?». Для ответа на этот вопрос я решил подготовить краткий ликбез с примером создания вашего собственного первого контейнера. В данном примере мы будем создавать контейнер с Elasticsearch 7.4.2 со всеми необходимыми плагинами для работы полнотекстового поиска системы DirectumRX.

Для начала работы нам необходим образ контейнера, на основании которого мы будем создавать свой контейнер с приложением. В нашем случае будет использоваться платформа контейнеризации docker. Для начала мы немного окунемся в матчасть, а именно узнаем немного про Dockerfile и что он умеет. Dockerfile – это сценарий, который состоит из последовательности команд и аргументов, необходимых для создания образа. Такие сценарии упрощают развёртывание и процесс подготовки приложения к запуску. Сначала Dockerfile определяет образ, на основе которого будет происходить сборка. Затем идёт ряд методов, команд и аргументов, которые создадут новый образ. Содержимое Dockerfile передаётся демону Docker для сборки образа. Из крайне немногочисленных инструкций который могут содержаться в Dockerfile нам необходимы:

  • FROM - обозначение родительского образа, на основании чего будет собираться наш образ.
  • RUN - запускает команды, создаёт слой образа, используется для установки пакетов и библиотек внутри контейнера.
  • COPY - копирует файлы в директорию контейнера
  • CMD - указывает команду и аргументы для выполнения внутри контейнера. Использоваться может только одна CMD инструкция.
  • EXPOSE - объявление, что необходимый нам порт будет ВНУТРИ контейнера открыт.


Только эти инструкции необходимы конкретно в нашем случае, более подробно по инструкциям можно почитать в документации: https://docs.docker.com/engine/reference/builder/.

На этом матчасть заканчивается, приступаем к написанию Dockerfile. Для удобства создайте пустую папку и поместите туда все необходимые плагины из комплекта поставки, для полнотекстового поиска:

  • tika-core-1.22.jar,
  • synonyms.txt,
  • analysis-morphology-7.4.2.zip.

Практическая часть

Переходим в директорию с плагинами и создаем там Dockerfile:

>vi Dockerfile

Выбираем на основании чего мы будем собирать наш образ, в нашем случае советую ubuntu:20.04, все ниже написанное будет подходить только под нее!

FROM ubuntu:20.04

Создадим группу и пользователя, чтоб потом в случае чего нам не доставило проблем:

RUN groupadd -g 1000 elasticsearch && useradd elasticsearch -u 1000 -g 1000

Теперь нам необходимо, добавить в контейнер все необходимое для скачивания и работы elasticsearch:

RUN apt-get update && \
apt-get install -y --no-install-recommends \
apt-transport-https \ wget -y

Сохраняем файл. НЕ ТОРОПИТЕСЬ ЕГО БИЛДИТЬ! Мы только в начале пути!

Теперь нам нужен самый минимальный конфиг для elasticsearch, чтоб он удовлетворял нашим требованиям (одна нода, а не кластер). Создаем файл:

>vi elasticsearch.yml

И записываем туда:

path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
cluster.name: "rx-elk-docker"
network.host: 0.0.0.0
http.port: 9200
discovery.type: single-node

Сохраняем файл и выходим. Также создадим минимальный файлик с настройками jvm:

>vi jvm.options

Запишем туда:

## JVM configuration

# То, сколько необходимы выдать джава машине оперативной памяти
# В нашем случае 4 гига оперативы хватит с головой
-Xms4g
-Xmx4g

-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-Des.networkaddress.cache.ttl=60
-Des.networkaddress.cache.negative.ttl=10
-XX:+AlwaysPreTouch
-Xss1m

Сохраняемся и выходим. Возвращаемся в Dockerfile.

>vi Dockerfile

Добавляем копирование созданного ранее файла:

COPY elasticsearch.yml /etc/elasticsearch/elasticsearch.yml

Также добавляем jvm.options:

COPY jvm.options etc/elasticsearch/jvm.options

Далее необходимо скачать и установить необходимую нам версию ELK:

RUN wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.4.2-amd64.deb --no-check-certificate
RUN dpkg -i --force-confdef ./elasticsearch-7.4.2-amd64.deb

Создаем директорию куда скинем плагины для elasticsearch:

RUN mkdir /usr/plugins/

И копируем туда:

COPY analysis-morphology-7.4.2.zip /usr/plugins/

Устанавливаем плагин ingest-attachment:

RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch ingest-attachment

Копируем tika-core-1.22.jar в папку с ранее установленным плагином:

COPY tika-core-1.22.jar usr/share/elasticsearch/plugins/ingest-attachment

Устанавливаем analysts-morphology-7.4.2:

RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:/usr/plugins/analysis-morphology-7.4.2.zip

Копируем синонимы:

COPY synonyms.txt /etc/elasticsearch

Удаляем установочный файл:

RUN rm elasticsearch-7.4.2-amd64.deb

Казалось бы все сделано, и если мы соберем контейнер и запустим, то все будет работать, но это не так. По умолчанию Elasticsearch отключен как служба, и его необходимо включить навсегда, но systemctl в docker-е нет, приходится искать обходной путь, в нашем случае он будет крайне простой.

Создаем скрипт для запуска elasticsearch, который в последствии у нас будет выполняться каждый раз при старте контейнера.

RUN echo "service elasticsearch start" > start.sh

Делаем его исполняемым:

RUN chmod +x start.sh

Теперь осталось лишь добавить команду, которая будет выполняться при запуске контейнера:

CMD bash start.sh ; /bin/sh

И напоследок объявляем порты, которые будут открыты для контейнера:

EXPOSE 9200

Хочу внести немного ясности. Инструкцией выше, мы не объявляем то, что при билде и запуске контейнера по умолчанию будет прокидываться порт напрямую в контейнер через хост систему. Это лишь объявление того, что сам контейнер для себя откроет порт 9200. Теперь посмотрим что у нас в итоге получилось в Dockerfile:

FROM ubuntu:20.04
RUN groupadd -g 1000 elasticsearch && useradd elasticsearch -u 1000 -g 1000
RUN apt-get update && \
apt-get install -y --no-install-recommends \
apt-transport-https \
wget -y
COPY elasticsearch.yml /etc/elasticsearch/elasticsearch.yml
COPY jvm.options etc/elasticsearch/jvm.options
RUN wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.4.2-amd64.deb --no-check-certificate
RUN dpkg -i --force-confdef ./elasticsearch-7.4.2-amd64.deb
RUN mkdir /usr/plugins/
COPY analysis-morphology-7.4.2.zip /usr/plugins/
RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch ingest-attachment
COPY tika-core-1.22.jar usr/share/elasticsearch/plugins/ingest-attachment
RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:/usr/plugins/analysis-morphology-7.4.2.zip
COPY synonyms.txt /etc/elasticsearch
RUN rm elasticsearch-7.4.2-amd64.deb
RUN echo "service elasticsearch start" > start.sh
RUN chmod +x start.sh
CMD bash start.sh ; /bin/sh
EXPOSE 9200

Итого, 19 слоев образа... Много, конечно, но это же первый контейнер, знать все тонкости в начале пути невозможно. Теперь мы можем начать собирать контейнер.

Но подождите, а что такое СЛОЙ? Давайте вновь окунемся немного в матчасть: ”Каждый Docker-образ состоит из слоёв, каждый из которых описывает какую-то инструкцию. Далее – Docker объединяет информацию из каждого слоя, и создает шаблон-образ, из которого запускается контейнер, в котором выполняются инструкции из каждого слоя, который был включен в данный образ. Каждый слой описывает какое-то изменение, которое должно быть выполнено с данными на запущенном контейнере.” Вроде бы все понятно, но если у вас еще возникнут вопросы, постараюсь своими словами объяснить ситуацию, примерно так-же как объясняют почти на любой конференции: «Чем меньше слоев, тем лучше, потому-что при внесении изменений в нижние слои не будет происходить пересборка вышестоящих слоев». Т.е если мы добавим после EXPOSE 9200 любой слой, например:

RUN mkdir /root/test

то все верхние слои у нас будут в кэше, и новый слой просто создастся и закэшируется. Но если мы добавим это же в начало, перед строчкой

RUN groupadd -g 1000 elasticsearch && useradd elasticsearch -u 1000 -g 1000

то все слои ниже будут пересобриться и кэшироваться вновь, а это не круто, поэтому давайте приведем наш Dockerfile к более-менее приемлемому виду. Вновь открываем наш Dockerfile:

>vi Dockerfile
FROM ubuntu:20.04
RUN groupadd -g 1000 elasticsearch && useradd elasticsearch -u 1000 -g 1000 && apt-get update && \
apt-get install -y --no-install-recommends \
apt-transport-https \
wget -y
COPY elasticsearch.yml /etc/elasticsearch/elasticsearch.yml
COPY jvm.options etc/elasticsearch/jvm.options
RUN wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.4.2-amd64.deb --no-check-certificate && dpkg -i --force-confdef ./elasticsearch-7.4.2-amd64.deb && rm elasticsearch-7.4.2-amd64.deb
RUN mkdir /usr/plugins/
COPY analysis-morphology-7.4.2.zip /usr/plugins/
RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch ingest-attachment
COPY tika-core-1.22.jar usr/share/elasticsearch/plugins/ingest-attachment
RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:/usr/plugins/analysis-morphology-7.4.2.zip
COPY synonyms.txt /etc/elasticsearch
RUN echo "service elasticsearch start" > start.sh && chmod +x start.sh
CMD bash start.sh ; /bin/sh
EXPOSE 9200

Так сказать немного причесали файлик, но это далеко не предел совершенства, в самом лучшем случае все COPY из файла необходимо убрать, оставить только один, а всю установку плагинов сделать в 1 слой, это уже если вам необходимо сделайте сами, ученье - свет, а не ученье - невозможность выйти из vim. Теперь когда мы привели в порядок наш файлик, настал наконец момент билда!

>docker build -t %nickname%/%reponame%:%tag% .

Под переменными выше можете подставить любые значения, а лучше зарегистрироваться на hub.docker.com и создать свой репозиторий.
После сборки образа, хочется уже запустить наше приложение. Но подождите, обратимся к документации elasticsearch для решения одной проблемы, которая не даст нам полноценно запустить контейнер. Идем в поисковик и гуглим "docker elasticsearch".
Переходим по первой ссылке до раздела

Using the Docker images in production
Там первый подраздел:

>set vm.max_map_count to at least 262144

На ХОСТОВОЙ МАШИНЕ открываем файлик /etc/sysctl.conf и пишем в самый низ новую строчку:

vm.max_map_count=262144

Далее, чтобы изменения применились без перезапуска или перечитки конфига, используем команду

>sudo sysctl -w vm.max_map_count=262144

Также создадим отдельную docker-сеть для elasticsearch:

>sudo docker network create -d bridge elastic
>docker run -d --name elasticsearch -v elasticsearch:/var/lib/elasticsearch --net elastic -p 9200:9200 -it %nickname%/%reponame%:%tag%

Поясню, что делает каждый флаг:

  • -d это демонизация контейнера, т.е он будет работать в фоне.
  • --name - название контейнера,
  • -v это volume, т.е том, который мы будем прокидывать С хостовой системы в контейнер.

Для понимания: в нашем случае используется том на хостой машине elasticsearch который является прокинутым по пути /var/lib/elasticsearch. Т.е. ВСЕ, что создаст контейнер по пути /var/lib/elasticsearch будет доступно на хостовой машине. И также если вы создадите что-то в этом томе, будет доступно в контейнере. У любого админа сразу возникает вопрос, а как мне на хостовой машине посмотреть эти файлики? Как потрогать? Тут все просто:
Смотрим, какие тома у нас есть:

>sudo docker volume ls
<
DRIVER VOLUME NAME
local elasticsearch

Видим, что у нас появился наш том. Смотрим на него физически:

>sudo ls /var/lib/docker/volumes/
<
elasticsearch metadata.db

Все, что внутри этой папки, это прямой путь в контейнере. В нашем случае это elasticsearch. Если вы «старовер» и не доверяете абстрактным томам docker-а, то пожалуйста, можете создать на хостовой системе путь /var/lib/elasticsearch и прокидывать его напрямую в контейнер, тогда у вас строка запуска будет иметь вид:

>docker run -d --name elasticsearch -v /var/lib/elasticsearch:/var/lib/elasticsearch --net elastic -p 9200:9200 -it %nickname%/%reponame%:%tag%
  • -p говорит, какие порты мы прокинем с ХОСТОВОЙ машины в контейнер,
  • -it говорит о том, что после выполнения команды, оставить контейнер, а не выключать его сразу.

Ну и все, теперь смотрим какие контейнеры у нас запущены

>sudo docker ps
<
9582606c8fa3 paperdad/elasticsearch-rx:full "/bin/sh -c 'bash st…" 4 seconds ago Up 1 second 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp elasticsearch

И все. Мы создали с нуля свой первый образ, а из него запустили контейнер, который можно использовать в продуктивной среде без опаски. Но чтоб скачать еще где-то контейнер, его необходимо добавить в репозиторий, а для этого либо создаем локальный и выкидываем на внешку, либо добро пожаловать на hub.docker.com.

Заключение

Данный материал рассчитан, в первую очередь, на новичков. Тертый админ сможет упрекнуть статью в том, что все описанное выше делится на 0 просто потому, что есть заранее готовый контейнер с эластиком, который понимает входящие переменные. ​Соглашусь что это так, и за основу можно было брать его. Но, по моему мнению, лучше научить человека основам, и принципам, а потом он уже сам доработает все под себя.

Спасибо за уделенное время на прочтение статейки! Комментарии и пожелания приветствуются.

Пока комментариев нет.

Авторизуйтесь, чтобы написать комментарий