Туторіал по розгортанню Rails-додатків на Amazon за допомогою Docker. Частина 1

Всім привіт! Мене звати Ярослав Безрукавый, я ? Ruby/JavaScript розробник у RubyGarage. Минулого разу я ділився з вами туториалом по налаштуванню Rails-додатки на Amazon EC2 з допомогою Chef. Туторіал викликав жваву дискусію в коментарях, багато хто запитував мене про доцільність Chef, адже вже на той момент з'явилося багато сучасних інструментів начебто Docker і Kubernetes для автоматичного розгортання додатків.

У цьому туториале я хочу повернутися до задачі розгортання Rails-додатки, але вже за допомогою Docker. У туториале ми розглянемо весь цикл: від розгортання програми в локальному оточенні до розгортання staging і production-інфраструктури на AWS з допомогою Docker. Будемо враховувати можливість масштабування і автоматизації процесу деплоя програми.

Яку проблему вирішуємо

Кожен розробник, якому доводилося самостійно розгортати новий додаток локально, а після його ще й підтримувати на віддалених (staging, production) серверах, знає, яким заплутаним і складним може бути цей процес. Як правило, новачки стикаються з наступними проблемами:

Рішення: Docker

Docker — це інструмент, створений на основі ідеї упаковування і запуску вашого програмного забезпечення в невеликих ізольованих середовищах, званих контейнерами. Використання контейнерів Linux для розгортання додатків називається контейнеризацией .

Давайте розглянемо, з якими перевагами і недоліками ми можемо зіткнутися під час контейнеризації нашої програми.

Переваги

Незалежність від архітектури сервера

Для сервера контейнер є «чорним ящиком». Задумка контейнера — повна стандартизація. Контейнер з'єднується з сервером певним інтерфейсом, додаток в контейнері не залежить від архітектури або ресурсів сервера. Інтерфейс Docker досить консистентный незалежно від роботи на локальній машині, на continuous integration (CI) сервері або під час деплоя на production-сервері. Створений один і той же образ запускається на кожному етапі Continuous Integration/Continuous Delivery пайплайна, даючи розробнику впевненість у тому, що протестоване додаток буде працювати однаково стабільно в будь-якому оточенні.

Зручне управління версіями і залежностями

Завдяки використанню контейнерів, розробник прив'язує всі компоненти і залежності до додатка, що дозволяє йому працювати як цілісному об'єкту. На сервері не потрібні установки додаткових компонентів або залежностей для запуску програми, яка знаходиться всередині контейнера. Нам достатньо можливості запуску Docker-контейнера.

Простота масштабування

Завдяки ізоляції від зовнішнього сервера і стандартизації розгортання, з'являється можливість швидкого і простого лінійного масштабування. Тобто на одній машині може бути запущено кілька контейнерів, і в той же час вони можуть бути запущені і на декількох серверах.

Оптимальне використання ресурсів

Docker споживає менше ресурсів, і особливо це відчутно на прикладі ізоляції в порівнянні з віртуальними машинами. Через ефективного перевикористання Docker-го шарів файлової системи запуск сотні контейнерів на робочій машині не буде проблемою, на відміну від запуску сотні віртуальних машин.

Інфраструктура як код

Можливість описувати складові інфраструктури у вигляді файлів конфігурацій, які у середовищі Docker мають єдину структуру і стандарт написання. Таким чином, при настроювання середовища ми пишемо код, якої в подальшому можемо зберегти в системі контролю версій.

Недоліки

Продуктивність

Додаткові надбудови на сервер в будь-якому випадку призводять до збільшення навантаження і витраті ресурсів.

Ускладнення архітектури

Контейнеризація — це надбудова над ОС, і, тим самим, ускладнення архітектури сервера.

Непроста налаштування та підтримка

При великих масштабах і навантаженні необхідна чітка і якісна налаштування систем. Для підтримки і супроводу Docker контейнерів необхідні навички системного адміністрування і програмування.

Погана зворотна сумісність

Docker швидко розвивається, і одним з мінусів такого розвитку є обмежена зворотна сумісність. З іншого боку, це дозволяє нової технології розвиватися в рази швидше.

Зацікавлені? Пропоную перейти до наступного розділу і розглянути, як працює Docker і всі його компоненти.

Як працює Docker

Образи і контейнери

Як вже було сказано раніше, ідея роботи Docker будується на контейнеризації додатків в ізольованому середовищі. Контейнер запускається шляхом запуску образу додатки. Образ — це пакет, який включає в себе всі необхідні для запуску програми складові: код програми, операційна система, бібліотеки, змінні середовища, файли конфігурації. Контейнер — це екземпляр образу вашої програми під час виконання.

У чому відмінність Docker від віртуальних машин

Поки що процес роботи Docker був дуже схожий на роботу з віртуальною машиною. Але робота Docker значно відрізняється від роботи звичних нам віртуальних машин.

Віртуальна машина — це емуляція комп'ютерної системи всередині вашої Host OS (платформа-господар, ваш сервер). Процес віртуалізації забезпечується з допомогою Hypervisor.

Hypervisor — це програмне забезпечення, яке є менеджером віртуальної машини, який створює і запускає віртуальні машини.

Так само кожна віртуальна машина вимагає свою власну операційну систему. Таким чином, процес повної віртуалізації може споживати велику кількість ресурсів вашої машини. В той час, як для віртуальної машини потрібна керуюча програма ОС (hypervisor) та встановлення операційної системи на кожному инстансе, Docker пропонує інше рішення для задачі віртуалізації вашого програмного продукту.

На відміну від віртуальної машини, яка, як правило, надає середовищі більше ресурсів, ніж потрібно більшості додатків, контейнер працює в Linux і розділяє ядро HostOS з іншими контейнерами. Він запускається в окремому процесі, займаючи не більше пам'яті, ніж будь-який інший виконуваний файл.

Образи і шари

Docker образ складається з ряду шарів. Кожен шар являє собою ряд змін від попереднього шару. Кожна команда (RUN, ENTRYPOINT, CMD та інші) в Dockerfile викликає створення нового шару, якому присвоюється унікальний ідентифікатор при складанні образу. Структура зв'язків між шарами Docker — ієрархічна. Є певний базовий шар, на який накладаються інші шари.

Команда Dockerfile як шар Docker способу

Кожен шар описує якусь зміну, яке повинно бути виконано під час складання і запуску контейнера. Коли збірка образу відбувається вдруге, Docker задіює кеш, повторно використовуючи, створені раніше шари. Якщо змін не було, перезбирання шару не буде. Якщо змінити один з шарів, то всі наступні шари будуть створені заново.

Контейнери і шари

Кожен шар образ доступний тільки для читання. Коли запускається контейнер, він створює ще один шар поверх образу, який доступний для запису. Всі зміни, внесені в працюючий контейнер, такі як запис нових файлів, зміна існуючих файлів і видалення файлів записуються в цей шар контейнера.

Окремий шар для кожного контейнера

Таким чином, Docker оптимізує використання пам'яті. Наприклад, вам потрібно запустити 100 инстансов з чином Ubuntu, який важить 1GB. При використанні ПЗ для віртуалізації, наприклад Vagrant, це вимагатиме 100 GB місця. При використанні Docker знадобиться трохи більше 1 GB.

Контейнери і volumes

Коли контейнер видаляється, разом з ним видаляються всі дані, створені під час роботи цього контейнера. Якщо вам потрібно кілька образів, які б мали спільний доступ до одних і тих же даних, або щоб після видалення контейнера його дані зберігалися, для цього використовуйте Docker volumes .

Docker volumes для зберігання перманентних і shared даних

Docker volume — це просто папка хоста, «примонтированная» до файлової системи контейнера. Цей шар даних більше не належить контейнера, відповідно, після перетворення останнього з даними нічого не трапиться. Ми можемо використовувати один volume в декількох контейнерах. Наприклад, ми можемо покласти assets з Rails програми в Nginx і віддавати їх клієнту, обходячи Puma.

Щоб детальніше зануритися у вивчення теорії, я пропоную подивитися офіційні гайди Docker , а також знаходити цікавлять терміни в Docker glossary .

План дій

Процес розгортання програми буде відбуватися в кілька етапів. Спочатку ми:

  1. Розгорнемо наше Web-додаток локально за допомогою Docker і Docker-compose.
  2. Розгорнемо staging-оточення додатки на AWS.
  3. Розгорнемо production-оточення додатки на AWS.
  4. Налаштуємо тестове оточення і continuous integration для staging і production оточення з допомогою CircleCI.

Інструменти і ПО, яке будемо використовувати в циклі туториалов

Туторіал складається з трьох частин. На цій інфографіці ви можете побачити етапи, з яких буде складатися цикл статей туторіал. Також на інфографіці вказаний стек технологій для кожного етапу.

Покрокове опис циклу туториалов і, які ми будемо застосовувати

Дана стаття присвячена першому етапу туториала — Development. Далі у статтях цього циклу ми будемо розглядати Staging і Production.

А тепер перейдемо до найцікавішого — практичній частині :)

Запускаємо Development оточення

Постановка задачі

У цьому розділі ми запустимо наш Spree-додаток і всі залежні сервіси (PostgreSQL, Redis і т. д.) на локальній машині з допомогою Docker і Docker compose.

Стек технологій, який ми будемо використовувати, включає:

Схема інфраструктури, яку ми хочемо розгорнути:

Інфраструктура development-оточення

Рішення завдання

Для цього нам знадобиться:

Встановлюємо Docker

Посилання для установки Docker для Linux, Mac or Windows.

Пакуємо Rails-додатки в Docker образ

Що таке Dockerfile і як він працює

Ми з вами вже згадали поняття образу, який є шаблоном для кожного запущеного контейнера. Dockerfile представляє з себе інструкцією для складання образ вашого ПЗ.

Покроковий принцип роботи Dockerfile, Image, Container

Тепер розглянемо Dockerfile нашого додатка і з яких шарів воно складатиметься.

Кожна команда (RUN, ENTRYPOINT, CMD та інші) в Dockerfile викликає створення нового шару при складанні образу. Структура зв'язків між шарами Docker — ієрархічна. Є певний базовий шар, на який накладаються інші шари.

Структура Dockerfile для нашого додатка

Безпека способу

За замовчуванням, всі команди по збірці образу і процеси всередині контейнера виконуються від імені root користувача. Такий підхід не безпечний. Тому для запуску програми ми використовуємо www-data користувача. Робимо ми це за допомогою команди USER, яка задає користувача, від імені якого будуть виконуватися всі перераховані нижче команди, включаючи RUN, ENTRYPOINT CMD.

Корисні посилання

Повний список інструкцій для Dockerfile знайдете тут . Будь ласка, ознайомтесь з ними перед тим, як рухатися далі туториалу.

Описуємо спосіб з допомогою Dockerfile

Тепер приступимо до опису образу Dockerfile для нашого основного додатка. В якості демо-додатки в туториале ми будемо використовувати Spree-додаток.

1. Копіюємо готове демо програми з GitHub:

git clone [email protected]:bezrukavyi/spree-docker-demo.git

2. І переходимо в директорію з додатком:

cd spree-docker-demo

3. Ініціалізуємо Dockerfile і додаємо в нього раніше розглянуту структуру, в яких більш детально описана кожна конфігурація:

touch Dockerfile

Dockerfile

# Layer 0. Качаємо образ Debian OS з встановленим ruby версії 2.5 і менеджером для управління gem'ами bundle з DockerHub. Використовуємо його в якості батьківського образу.
FROM ruby:2.5.1-slim

# Layer 1. Задаємо користувача, від чийого імені будуть виконуватися наступні команди RUN, ENTRYPOINT, CMD і т. д.
USER root

# Layer 2. Оновлюємо і встановлюємо потрібне для Веб-сервера
RUN apt-get update -qq && apt-get install -y \
 build-essential libpq-dev libxml2-dev libxslt1-dev nodejs imagemagick apt-transport-https curl nano

# Layer 3. Створюємо змінні оточення які буду далі використовувати в Dockerfile
ENV APP_USER app
ENV APP_USER_HOME /home/$APP_USER
ENV APP_HOME /home/www/spreedemo

# Layer 4. Оскільки за замовчуванням Docker запускаємо контейнер від імені користувача root, то настійно рекомендується створити окремого користувача з певними UID і GID і запустити процес від імені цього користувача.
RUN useradd -m -d $APP_USER_HOME $APP_USER

# Layer 5. Даємо root користувачем користувачеві app права owner'а на необхідні директорії
RUN mkdir /var/www && \
 chown -R $APP_USER:$APP_USER /var/www && \
 chown -R $APP_USER $APP_USER_HOME

# Layer 6. Створюємо і вказуємо папку, у яку буде вміщено додаток. Так само тепер команди RUN, ENTRYPOINT, CMD будуть запускатися з цієї директорії.
WORKDIR $APP_HOME

# Layer 7. Вказуємо всі команди, які будуть виконуватися від імені app користувача
USER $APP_USER

# Layer 8. Додаємо файли Gemfile і Gemfile.lock з директорії, де лежить Dockerfile (коренева директорія програми на HostOS) в root директорію WORKDIR
COPY Gemfile Gemfile.lock ./

# Layer 9. Викликаємо команду по установці gem-залежностей. Рекомендується запускати цю команду від імені користувача від якого буде запускатися програма
RUN bundle check || bundle install

# Layer 10. Копіюємо вміст директорії програми в root-директорію WORKDIR
COPY . .

# Layer 11. Вказуємо всі команди, які будуть виконуватися від імені користувача root
USER root

# Layer 12. Даємо root користувачем користувачеві app права owner'а на WORKDIR
RUN chown -R $APP_USER:$APP_USER "$APP_HOME/."

# Layer 13. Вказуємо всі команди, які будуть виконуватися від імені app користувача
USER $APP_USER

# Layer 14. Запускаємо команду для компіляції статичних (JS і CSS файлів
RUN bin/rails assets:precompile

# Layer 15. Вказуємо команду за замовчуванням для запуску майбутнього контейнера. По скільки в `Layer 13` ми переопределили користувача, puma сервер буде запущений від імені www-data користувача.
CMD bundle exec puma -C config/puma.rb

Команди, які повинні бути запущені перед запуском контейнера (entrypoint) ми виносимо в docker-entrypoint.sh . Створимо файл за допомогою наступної команди:

touch docker-entrypoint.sh
chmod +x docker-entrypoint.sh

І додамо в нього команди для створення бази даних і прогону міграцій Rails-додатки.

#!/bin/bash
# Identifier Interpreter

# Exit on fail
set -e

rm -f $APP_HOME/tmp/pid/server.pid
rm -f $APP_HOME/tmp/pid/sidekiq.pid

bundle exec rake db:create
bundle exec rake db:migrate

exec "$@"

Виключити файли, що не належать до складання

Іноді потрібно уникнути додавання певних файлів в образ додатка, наприклад secrets files або файлів, які відносяться тільки до локального оточення. Для цього є .dockerignore. Принцип роботи .dockerignore такий же, як з .gitignore.

4. Створюємо файл .dockerignore.

touch .dockerignore

І додаємо в нього наступне:

.git
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
!/tmp/pid/.keep
!/tmp/cache/.keep
/виробника/bundle
/public/assets
/config/master.key
/config/credentials.local.yml
/.bundle

Отже, ми розглянули схему запуску нашого Spree-додатки. У наступному розділі ми навчимося запускати програму і всі залежні сервіси (PostgreSQL, Redis, API, client) за допомогою однієї команди.

Запуск образу Rails-додатки і залежних сервісів

Docker compose

Робота майбутнього додатки залежить від роботи сторонніх сервісів, таких як PostgreSQL, Redis, а також ідентичне основного додатком Sidekiq-додаток. Дотримуючись ідеології Docker, всі ці сервіси повинні бути ізольовані від локального оточення і запущені в окремих контейнерах, які «спілкуються» один з одним. Якщо структура проекту складається з великої кількості сервісів, то піднімати кожен окремий Docker-сервіс вручну незручно.

Тому для автоматизації процесу запуску всіх сервісів будемо використовувати Docker-compose .

Docker Compose — це інструмент для визначення і запуску многоконтейнерных додатків Docker. З Compose ви використовуєте файл YAML для налаштування сервісів(контейнерів) вашого додатка. Потім за допомогою однієї команди ви створюєте та виконуєте всі сервіси зі своєї конфігурації.

Docker compose як схема інфраструктури

Корисні посилання

Повну структуру файлу для Docker compose знайдете тут . Будь ласка, ознайомтеся з нею перед тим, як рухатися далі туториалу.

Структура для Docker compose

Тепер розглянемо структуру нашого додатки з боку реалізації конфігурації для Docker compose.

Для цього створимо файл docker-compose.development.yml

touch docker-compose.development.yml

У нього ми додамо розглянуту раніше конфігурацію, де більш детально коментарями описана кожна конфігурація:

# Version - версія синтаксису compose-файлу. Файл Compose завжди починається з номера версії, який вказує використовуваний формат файлу. Це допомагає гарантувати, що додатка буде працювати як очікується, так як нові функції або критичні зміни постійно додаються в Compose.
версія: '3.1'

# Volume – дисковий простір між HostOS і ContainerOS. Простіше – це папка на вашій локальній машині примонтированная всередину контейнера.
volumes: # Оголосимо volumes, які будуть доступні в сервісах
redis:
postgres:

# Service - запущений контейнер
services: # Оголошуємо сервіси(контейнери), які будуть запущені за допомогою compose
db:
 image: postgres:10 # В якості способу сервісу використовується офіційний образ Postgresql з Docker Hub
expose:
 - 5432 # Виділяємо для postgres 5432-ий порт контейнера
 environment: # Вказуємо список глобальних ENV-змінних всередині поточного контейнера
 POSTGRES_USER: postgres
 POSTGRES_PASSWORD: postgres
 POSTGRES_DB: spreedemo_development
volumes:
 - postgres:/var/lib/postgresql/data # Усі дані з директорії data буду лягати в volume `postgres`
healthcheck:
 test: ["CMD", "pg_isready", "U", "postgres"] # Команда для перевірки стану сервісу

in_memory_store:
 image: redis:4-alpine # В якості способу сервісу використовується офіційний образ Redis з Docker Hub
expose:
 - 6379 # Виділяємо для redis 6379-ий порт контейнера
volumes:
 - redis:/var/lib/redis/data
healthcheck:
 test: ["CMD", "redis-cli", "h", "localhost", "ping"]

 server_app: &server_app
 build: . # Як образу буде використовуватися Dockerfile в поточній директорії
 command: bundle exec rails server -b 0.0.0.0 # перевизначаємо команду запуску контейнера
 entrypoint: "./docker-entrypoint.sh" # вказуємо яку команду потрібно запустити перед тим як контейнер запуститься
volumes:
 - .:/home/www/spreedemo # Вказуємо, що директорія програми в контейнері буде посилатися на директорію програми на Host OS (локальна нода). Таким чином, зміна файлів з app або інших директорій на вашій локальній машині, всі зміни будуть застосовані і на контейнер з цим сервісом.
 - /home/www/spreedemo/виробника/bundle # Виключаємо монтування встановлених гемов в контейнер
 - /home/www/spreedemo/public/assets # Виключаємо монтування згенерованих assets в контейнер
 tty: true # Відкриваємо доступ для деббагинга контейнера
 stdin_open: true # Відкриваємо доступ для деббагинга контейнера
 restart: on-failure # Перезапустити контейнер у разі помилки
environment:
 RAILS_ENV: development
 DB_HOST: db
 DB_PORT: 5432
 DB_NAME: spreedemo_development
 DB_USERNAME: postgres
 DB_PASSWORD: postgres
 REDIS_DB: "redis://in_memory_store:6379"
 SECRET_KEY_BASE: STUB
 DEVISE_SECRET_KEY: STUB
 depends_on: # Вказуємо список сервісів від яких залежить поточний сервіс. Поточний сервіс буде запущений тільки після того, як запрацюють залежні сервіси
 - db
 - in_memory_store
ports:
 - 3000:3000 # Вказуємо що порт з контейнера буде проксироваться на порт HostOS (HostPort:ContainerPort)
healthcheck:
 test: ["CMD", "curl", "f", "http://localhost:3000"]

server_worker_app:
 <<: *server_app # Наследуемся від сервісу server_app
 command: bundle exec sidekiq -C config/sidekiq.yml
 entrypoint: "
 ports: []
depends_on:
 - db
 - server_app
 - in_memory_store
healthcheck:
 test: ["CMD-SHELL", "ps ax | grep -v grep | grep sidekiq || exit 1"]

Контейнери і volumes

Незважаючи на те, що всі програма упаковано в образ і запущено в ізольованому контейнері, нам як і раніше доступний hot rails reloader. Все тому, що ми скористалися Volumes. Ми вказали, що директорія app і директорія vendor/assets з запущеного контейнера будуть посилатися на локальну теку HostOS.

Тепер можна запустити всю інфраструктуру програми, виконавши команду:

docker-compose -f docker-compose.development.yml -p spreeproject up

`-p` вказує, який префікс додати контейнерів. Бажано використовувати подібний контекст, щоб коли проектів на Docker стане більше, вам було простіше орієнтуватися по контексту;
`-f` вказує, якою docker-compose файл використовувати.

Тут ви знайдете інші корисні команди для взаємодії з compose.

Перевірити стан запущених сервісів ми можемо за допомогою наступної команди:

docker-compose -f docker-compose.development.yml -p spreeproject ps

Коли всі сервіси буду в статусі healthy,

Додаток буде доступно за адресою `localhost:3000`

Підіб'ємо підсумок

У першій частині туториала ми розглянули:

У наступних частинах ми продовжимо розгортання програми за допомогою сервісів AWS. Не пропустіть другу частину туториала!

Опубліковано: 06/06/19 @ 10:00
Розділ Різне

Рекомендуємо:

Олег Рогінський, CEO People.ai: «Простіше підняти $60 млн інвестицій, ніж найняти 10 інженерів»
Як прокачати емоційний інтелект, щоб спілкуватися з колегами результативно
Кейс: Просування з нуля сайту школи психологічного консультування
Рейтинг вишів DOU 2019: біля Могилянки з'єднання явився конкурент за перше місце, а КПІ за межами 10-ки лідерів
Front-end дайджест #34: новий Angular 8 і TypeScript, переходимо на хуки в React