Туторіал по розгортанню Rails-додатків на Amazon за допомогою Docker. Частина 1
Всім привіт! Мене звати Ярослав Безрукавый, я ? Ruby/JavaScript розробник у RubyGarage. Минулого разу я ділився з вами туториалом по налаштуванню Rails-додатки на Amazon EC2 з допомогою Chef. Туторіал викликав жваву дискусію в коментарях, багато хто запитував мене про доцільність Chef, адже вже на той момент з'явилося багато сучасних інструментів начебто Docker і Kubernetes для автоматичного розгортання додатків.
У цьому туториале я хочу повернутися до задачі розгортання Rails-додатки, але вже за допомогою Docker. У туториале ми розглянемо весь цикл: від розгортання програми в локальному оточенні до розгортання staging і production-інфраструктури на AWS з допомогою Docker. Будемо враховувати можливість масштабування і автоматизації процесу деплоя програми.
Яку проблему вирішуємо
Кожен розробник, якому доводилося самостійно розгортати новий додаток локально, а після його ще й підтримувати на віддалених (staging, production) серверах, знає, яким заплутаним і складним може бути цей процес. Як правило, новачки стикаються з наступними проблемами:
- Невдачі при спробі встановити ЗА все залежне, при цьому не «зламавши» нічого локально.
- Нерозуміння, як запустити додаток.
- Брак досвіду розгортання програми на віддалених серверах.
- Підтримка інфраструктури програми на віддалених серверах.
- Масштабування програми на 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 .
План дій
Процес розгортання програми буде відбуватися в кілька етапів. Спочатку ми:
- Розгорнемо наше Web-додаток локально за допомогою Docker і Docker-compose.
- Розгорнемо staging-оточення додатки на AWS.
- Розгорнемо production-оточення додатки на AWS.
- Налаштуємо тестове оточення і continuous integration для staging і production оточення з допомогою CircleCI.
Інструменти і ПО, яке будемо використовувати в циклі туториалов
Туторіал складається з трьох частин. На цій інфографіці ви можете побачити етапи, з яких буде складатися цикл статей туторіал. Також на інфографіці вказаний стек технологій для кожного етапу.
Покрокове опис циклу туториалов і, які ми будемо застосовувати
Дана стаття присвячена першому етапу туториала — Development. Далі у статтях цього циклу ми будемо розглядати Staging і Production.
А тепер перейдемо до найцікавішого — практичній частині :)
Запускаємо Development оточення
Постановка задачі
У цьому розділі ми запустимо наш Spree-додаток і всі залежні сервіси (PostgreSQL, Redis і т. д.) на локальній машині з допомогою Docker і Docker compose.
Стек технологій, який ми будемо використовувати, включає:
- Docker ;
- Docker Compose ;
- Ruby 2.5 ;
- Ruby on Rails 5.2 ;
- Redis 4.0 ;
- PostgreSQL 10.0 ;
- Sidekiq .
Схема інфраструктури, яку ми хочемо розгорнути:
Інфраструктура development-оточення
Рішення завдання
Для цього нам знадобиться:
- Встановити Docker на локальній машині.
- Створити Docker образ Rails-додатки.
- Створити compose-файл для запуску Rails-додатки і залежних сервісів (Redis, PostgreSQL, Sidekiq).
Встановлюємо 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`
Підіб'ємо підсумок
У першій частині туториала ми розглянули:
- Принцип роботи Docker та його компоненти. Переваги та недоліки роботи з Docker в порівнянні з віртуальними машинами.
- Покрокову збірку Rails-додатки в Dockerfile.
- Запуск образу Rails-додатки і залежних сервісів за допомогою Docker compose.
У наступних частинах ми продовжимо розгортання програми за допомогою сервісів AWS. Не пропустіть другу частину туториала!
Опубліковано: 06/06/19 @ 10:00
Розділ Різне
Рекомендуємо:
Олег Рогінський, CEO People.ai: «Простіше підняти $60 млн інвестицій, ніж найняти 10 інженерів»
Як прокачати емоційний інтелект, щоб спілкуватися з колегами результативно
Кейс: Просування з нуля сайту школи психологічного консультування
Рейтинг вишів DOU 2019: біля Могилянки з'єднання явився конкурент за перше місце, а КПІ за межами 10-ки лідерів
Front-end дайджест #34: новий Angular 8 і TypeScript, переходимо на хуки в React