Глава 9. Расширенное управление Gem

Теперь у нас имеется среда,целиком работающая на Docker. Тем не менее, имеется ещё одна область которую стоит обмозговать: управление gem.

Вплоть до текущего момента для установки или обновления gem мы просто собирали заново необходимый образ для своего приложения. Это работает по той причине, что bundle install выступает одним из этапов в нашем Dockerfile. Однако, как мы обнаружим через мгновение, при данном подходе существует небольшая тёмная сторона по сравнению с тем когда мы применяем управление gem в соед без Docker (вы могли уже обнаружить это).

В данной главе мы исследуем некий альтернативный подход к управлению своими gem при котором попытаемся избежать тёмных сторон выполнения различных компромиссов (вначале это более сложно).

Будете ли вы пользоваться тем простым подходом, который мы применяли до сих пор. либо вы будете придерживаться данного нового подхода, всецело зависит от ваших потребностей и предпочтений. Вперёд?

Тёмная сторона имеющегося у нас подхода

Наш текущий подход в управлении gem состоит в сборке заново самого образа всякий раз когда нам требуется обновлять свои gem. Однако вы могли заметить, что каждый раз когда мы меняем свой Gemfile, даже если мы просто добавляем некий отдельный gem, все наши gem должны устанавливаться с самого начала. Как результат, обновление наших gem требует времени больше чем обычно.

Почему это происходит?

Образы Bundler и Docker оба пытаются достичь той же самой цели гарантии согласованности среды, но они достигают её различными способами. Процесс сборки образа Docker разбивает некоторые ключевые предположения Bundler, что означает что он работает не совсем так как мы привыкли.

Bundler изначально разрабатывался для использования в системах с длительным временем жизни, в которой устанавливаются gem; основным вариантом применения является обновление самого набора только что установленных gem. Однако сборка некого образа заново похожа на построение некой новой машины с нуля - в этом случае Bundler не применяется в долгоживущей системе. В этом, по существу, и кроется источник проблемы.

Давайте ещё раз взглянем на свой Dockerfile и пройдём по тому что происходит при перестроении нашего образа. Вот самые последние семь строк:


​1: 	COPY Gemfile* /usr/src/app/   
​2: 	WORKDIR /usr/src/app
​3: 	RUN bundle install            
​4: 	
​5: 	COPY . /usr/src/app/
​6: 	
​7: 	CMD [​"bin/rails"​, ​"s"​, ​"-b"​, ​"0.0.0.0"​]
 	   

Когда мы изменяем свой Gemfile, он аннулирует значение кэширования для 1 строки в данном куске кода. Это означает, что все кэшированные промежуточные уровни для последующих шагов отбрасываются, включая те gem, которые ранее устанавливались на шаге bundle install (строка 3). По достижению этого шага сборки вновь, это выглядит так, как будто мы никогда ранее не исполняли bundle install в этой машине ранее. Ничего удивительного, что все gem должны устанавливаться с нуля.

И в чём тут большая проблема?

Это зависит от вашей ситуации. в вашем проекте (ах) может быть больше или меньше gem и вы можете быть более или менее удовлетворены тем что вам придётся ждать установки gem. В основе нашего базового подхода лежит то, что он не является базовым. Никаких дополнительных настроек или сохранений в памяти - просто восстановите свой образ и всё готово. Это может быть вполне приемлемым для большинства.

Тем не менее, если время ожидания сборки gem вас беспокоит, вам стоит рассмотреть иной вариант.

Применение тома кэширования Gem

Как мы уже видели, ключевая проблема состоит в том, что создание образа Docker сродни построению машины с нуля, что противоречит кэшированию gem прм помощи Bundler. А что если вместо того чтобы бороться с процессом сборки образа мы обойдём его? Мы уже видели как тома предоставляют постоянное хранилище файлов, отдельное от файловой системы самого контейнера; мы можем воспользоваться этим чтобы решить свою непростую проблему.

Вот суть решения.

Выполнив монтирование некого тома в том каталоге, в котором Bundler устанавливает наши gem, мы можем исполнять команды Bundler для наполнения и управления своими gem в этом томе, который на самом деле превращается в некий локальный кэш gem. Вспомним, что некий монтируемый том выполняет перекрытие и является отдельным от самой файловой системы контейнера; его файлы остаются в сохранности между всеми жизненными циклами самого данного контейнера.

Давайте рассмотрим как это работает на практике.

Прежде всего нам потребуется настроить Bundler для применения некого определённого в явном виде известного каталога для установки gem, скажем, в ... /gems. Мы делаем это настраивая свою переменную среды BUNDLE_PATH. Давайте обновим наш Dockerfile следующим образом:


​​ 	version: ​'​​3'​
​ 	
​ 	services:
​ 	  web:
​ 	    build: ​.​
​ 	    ports:
​ 	      - ​"​​3000:3000"​
​ 	      - ​"​​4000:4000"​
​ 	    volumes:
​ 	      - ​.:/usr/src/app​
»	      - ​gem_cache:/gems​
​ 	    env_file:
​ 	      - ​.env/development/web​
​ 	      - ​.env/development/database​
​ 	    environment:
​ 	      - ​WEBPACKER_DEV_SERVER_HOST=webpack_dev_server​
​ 	
​ 	  webpack_dev_server:
​ 	    build: ​.​
​ 	    command: ​./bin/webpack-dev-server​
​ 	    ports:
​ 	      - ​3035:3035​
​ 	    volumes:
​ 	      - ​.:/usr/src/app​
»	      - ​gem_cache:/gems​
​ 	    env_file:
​ 	      - ​.env/development/web​
​ 	      - ​.env/development/database​
​ 	    environment:
​ 	      - ​WEBPACKER_DEV_SERVER_HOST=0.0.0.0​
​ 	
​ 	  redis:
​ 	    image: ​redis​
​ 	
​ 	  database:
​ 	    image: ​postgres​
​ 	    env_file:
​ 	      - ​.env/development/database​
​ 	    volumes:
​ 	      - ​db_data:/var/lib/postgresql/data​
​ 	
​ 	  selenium_chrome:
​ 	    image: ​selenium/standalone-chrome-debug​
​ 	    logging:
​ 	      driver: ​none​
​ 	    ports:
​ 	      - ​"​​5900:5900"​
​ 	
​ 	volumes:
​ 	  db_data:
»	  gem_cache:
 	   

В точности так же как мы поступали в разделе Отделение данных от контейнера, мы создаём некий новый именованный том - на этот раз он называется gem_cache - добавляя его в свой перечень volumes. Compose обработает все подробности того где хранится данный том.

Далее, в определении для своей службы web мы сообщаем Compose о необходимости монтирования нашего тома gem_cache в /gems в данном контейнере, и именно там теперь Bundler настраивает установку gem.

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


​ 	​$ ​​docker-compose​​ ​​build​​ ​​web​
		

Наша служба web должна быть уже остановлена и мы ранее удалили её контейнер, так что для создания некого нового контейнера web, помимо самого тома gem_cache, нам просто надо сделать:


​ 	​$ ​​docker-compose​​ ​​up​​ ​​-d​​ ​​web​
​ 	Creating volume "myapp_gem_cache" with default driver
​ 	Recreating myapp_web_1 ... done
		

Получив запущенным данное приложение, а наш том gem_cache созданным, мы теперь способны исполнять команды Bundler напрямую для своего запущенного контейнера web:


​ 	​$ ​​docker-compose​​ ​​exec​​ ​​web​​ ​​bundle​​ ​​install​
		

Благодаря нашему BUNDLE_PATH, данные установки всех gem проекта выполняются в самом каталоге /gems контейнера. Как мы знаем, именно здесь смонтирован наш том gem_cache, благодаря отображению нашего тома. Вследствие этого, все наши gem теперь устанавливаются в нашем томе gem_cache.

По мере наполнения кэша наших gem, давайте попробуем установить некий новый gem. Допустим, мы желаем для аутентификации добавить Devise. Давайте добавим его в свой Gemfile:


​ 	...
​ 	gem ​'redis'​, ​'~> 4.0'​
​ 	​# Authentication​
​ 	gem ​'devise'​, ​'~> 4.4'​, ​'>= 4.4.1'​
 	   

Хорошо. Теперь, как мы это обычно делаем в некоторой среде без Docker, мы установим необходимый gem исполнив команду bundle install :


​ 	​$ ​​docker-compose​​ ​​exec​​ ​​web​​ ​​bundle​​ ​​install​
		

Вы должны получить вывод аналогичный следующему:


​ 	The dependency tzinfo-data (>= 0) will be unused by any of the platforms
​ 	Bundler is installing for. Bundler is installing for ruby but the dependency
​ 	is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those plat-
​ 	forms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32
​ 	x64-mingw32 java`.
​ 	Fetching gem metadata from https://rubygems.org/.........
​ 	Fetching gem metadata from https://rubygems.org/.
​ 	Resolving dependencies.....
​ 	Using rake 12.3.2
​ 	Using concurrent-ruby 1.1.4
​ 	...
​ 	Fetching warden 1.2.8
​ 	Installing warden 1.2.8
​ 	Fetching devise 4.5.0
​ 	Installing devise 4.5.0
​ 	...
​ 	Using turbolinks-source 5.2.0
​ 	Using turbolinks 5.2.0
​ 	Using uglifier 4.1.20
​ 	Using web-console 3.7.0
​ 	Using webpacker 3.5.5
​ 	Bundle complete! 21 Gemfile dependencies, 90 gems now installed.
​ 	Bundled gems are installed into `/gems`
		

Этот вывыод показывает, что Devise является единственным устанавливаемым gem- все прочие повторно применяются из нашего кэша gem.

Существует одна окончательная сложность: наша служба webpack_dev_server. так как она применяет тот же самый Dockerfile, что и наща служба web, нам также требуется выполнить повторную сборку и её образа:


​ 	​$ ​​docker-compose​​ ​​build​​ ​​webpack_dev_server​
		

Аналогично, мы далее повторно строим свой контейнер webpack_dev_server из этого нового образа:


​ 	​$ ​​docker-compose​​ ​​up​​ ​​-d​​ ​​webpack_dev_server​
​ 	Recreating myapp_webpack_dev_server_1 ... done
		

Так как мы применяем тот же самый том gem_cache и для web, и для webpack_dev_server, добавляемый обновлением соответствующей службы web gem будет автоматически доступен для имеющейся слуюжбы webpack_dev_server и наоборот.

Давайте рассмотрим все преимущества и недостатки такого подхода:

За:

  • Получаем ускорение управлением gem для всех действий Bundler: добавления, удаления или обновления gem

  • Применяем знакомый нам рабочий поток bundle install, в особенности, в процессе разработки

Против:

  • Команды Bundle обновляют только наш локальный том; нам всё ещё в конечном счёте приходится собирать заново свой образ

  • Возможна путаница относительно того какие именно gem подлежат загрузке и применению

  • Дополнительная сложность изменения как Dockerfile, так и docker-compose.yml, плюс потребность в понимании всех нюансов gem, которые осуществляют перекрытие в локальном томе

Многие разработчики Rails найдут этот подход привлекательным, так как мы в явном виде управляем своими gem при помощи Bundler, как мы и привыкли это делать, вместо того чтобы полагаться на перестройку своего образа запуском bundle install. Если вы сможете разобраться со сложностью, данная стратегия несомненно даст возможность вашей разработке ощущать себя намного более быстрой, в особенности когда вы вносите много изменений gem.

Беглый обзор

Кто мог предвидеть что управление gem может быть настолько горячей темой? Имея в своём распоряжении такую новую возможность, вы должны ощащуть себя готовым к чему угодно.

Давайте оглянемся назад на то что мы рассмотрели в этой главе:

  1. Мы обсудили те негативные моменты, которые имеются в наших предыдущих подходах к управлению gem: каждое изменение gem требует изменения всех gem для повторной сборки с нуля.

  2. Мы изучили некий способ ускорения изменений gem, который вовлекает создание некого тома для кэширования наших gem. Монтируя этот том в своём контейнере (и устанавливая BUNDLE_PATH), мы можем управлять своими gem вручную и получать преимущества от более быстрых сборок.

Надеюсь, вы также начинаете ценить здесь такой мета пункт: как могут быть выявлены, продуманы и в конечном итоге решены проблемы с нашей настройкой Docker. Чем больше вы применяете Docker и осознаёте как он работает, тем больше вы начинаете обнаруживать самостоятельно возможности для улучшений.

Как бы не было всё замечательно, Compose не обходится без сбоев. Прежде чем мы завершим свой раздел разработки, в духе полной открытости, мы намерены изучит пару распространённых болевых моментов с которыми вы можете столкнуться при применении Compose. В нашей следующей главе вы узнаете об этих раздражителях и том, что мы можем сделать для минимизации их воздействия.