Python conf in Kharkiv, Nov 16 with Intel, Elastic engineering leaders. Prices go up 21.10

Туториал по развертыванию Rails-приложений на Amazon с помощью Docker. Часть 2

Всем привет! В этой части мы продолжаем наш туториал по развертыванию Rails-приложения на AWS с помощью Docker. Напомню, что в предыдущей части туториала мы:

  • рассмотрели преимущества Docker для развертывания приложений;
  • запустили наше Spree-приложение и все зависимые сервисы на локальной машине.

Какую проблему решаем

После проверки корректности работы приложения в локальном окружении, необходимо развернуть идентичную инфраструктуру в облаке. Этой задаче и будет посвящена вторая часть нашего туториала. Итак, приступим к работе!

Решение: AWS ECS

ECS запускает ваши контейнеры в кластере экземпляров Amazon EC2 с предварительно установленным Docker-ом. ECS управляет установкой контейнеров, масштабированием, мониторингом и управлением ими через API и Консоль управления AWS.

Вы можете разместить и запустить Docker контейнер приложения на EC2 вручную. Но вы лишите себя следующих вещей:

  • Безопасность. Amazon ECS запускает контейнеры в Amazon VPC, что позволяет использовать собственные группы безопасности VPC и списки контроля доступа к сети.
  • Масштабируемость. С помощью ECS вы можете упростить и автоматизировать процесс клонирования ваших сервисов и распределения нагрузки между ними с помощью ELB.
  • Интеграция с сервисами. ECS предоставляет возможность интеграции приложения с такими сервисами AWS, как Amazon ECR, Amazon CloudWatch, AWS CloudFormation, Amazon ELB и так далее.
  • Удобный деплой. ECS помогает запускать приложения в виде микросервисов и обеспечивает непрерывную интеграцию и непрерывное развертывание при помощи API. Также ECS позволяет совершать деплой без времени простоя.
  • Мониторинг состояния. ECS упрощает просмотр использования ресурсов экземпляров EC2, таких как процессор и память.

ECS Cluster

Cluster c двумя EC2 инстансами. Image source

Cluster — это группа EC2 инстансов, на которых запущен один или несколько Docker контейнеров. Если ваш проект состоит из нескольких приложений, вы можете разместить их в одном кластере в виде отдельных сервисов. Такой подход позволяет более эффективно использовать доступные ресурсы и минимизировать время установки.

Task definition

Task definition

Эта инструкция описывает, как и какие Docker контейнеры необходимо запускать. Task Definition можно создать путем определения следующих параметров:

  • какой образ Docker использовать для запуска определенного контейнера;
  • сколько центральных процессоров (ЦП) и памяти использовать для каждой задачи;
  • связи между контейнерами, если нужна коммуникации между ними;
  • команда для запуска контейнера;
  • команда, которую должен запустить контейнер при запуске;
  • способ логирования контейнеров;
  • команда для проверки состояния контейнера.

Из вышеперечисленных пунктов, Task Definition напоминает конфигурацию, которую мы создаем для Docker compose. Так и есть, и благодаря ранее установленному ECS CLI, мы сможем создавать task definition наших сервисов, работая с синтаксисом Docker compose. Но это не значит, что на вашем инстансе все сервисы будут запущены через Docker compose: ECS Agent конвертирует инструкцию из синтаксиса Docker compose в свой нативный синтаксис. Поэтому будьте внимательны, так как не вся конфигурация, которая доступна в Docker compose, будет работать в ECS. По этой причине в туториале мы будем создавать отдельный docker-compose файл, который будет использоваться для AWS.

Task

Создание Task на основе Task Definition. Image source

Если Task Definition — это инструкция по запуску одного или нескольких контейнеров, то Task представляет из себя один или несколько запущенных контейнеров. У задачи есть три состояния:

  • Running — все контейнеры запущены и работают корректно;
  • Pending — контейнеры в процессе запуска;
  • Stopped — контейнеры остановлены.

Service

Группировка Task в один сервис. Image Source

В Service можно вынести одну или несколько задач, где вы определяете, какое минимальное и максимальное количество задач необходимо запустить. Это позволит вам настроить автомасштабирование и балансировку нагрузки для вашего приложения. Так, в случае превышения CPU из-за определенной задачи, которая выполняется, ECS Agent может выделить еще один инстанс и распределить трафик с помощью ELB на время загруженности.
Разумеется, мы можем ограничить максимальное количество задач, которые могут быть запущены, поскольку для запуска дополнительных задач используются дополнительные ресурсы в виде новых инстансов, а это требует финансовых затрат.

Подведем итоги. Если говорить кратко о компонентах ECS, это:

  • Cluster ‒ группа связанных между собой EC2 инстансов;
  • ECR ‒ приватный репозиторий, на котором хранятся Docker-образы нашего приложения;
  • Task definition ‒ инструкция по запуску контейнеров на EC2 кластера;
  • Task ‒ один или несколько запущенных контейнеров;
  • Service ‒ совокупность запущенных Tasks.

Теперь, когда мы рассмотрели, из чего состоит ECS, приступим к практической реализации.

План действий

  • Настроить инструменты для работы с AWS.
  • Реализовать возможность безопасного хранения таких чувствительных данных нашего приложения, как пароли от внешних сервисов, ключей доступа и т. д.
  • Создать образ Docker для веб-сервера Nginx.
  • Подготовить staging-инфраструктуру на AWS.
  • Запустить staging-приложение на AWS.

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

Пошаговое описание цикла туториалов и ПО, которые мы будем применять

Инфраструктура staging-приложения практически идентична той, что мы разворачивали локально. Впрочем, есть несколько отличий:

  • Сборка образов. В отличие от локального окружения, образ основного приложения мы будем хранить не на машине, где запускается приложение (Host OS), а в приватном хранилище образов на AWS.
  • Веб-сервер. В облачной инфраструктуре важно иметь в наличии веб-сервер, который бы контролировал все входящие запросы к основному серверному приложению.

Схема инфраструктуры, которую мы хотим развернуть:

Инфраструктура staging-окружения

Решение

Инструменты для настройки сервисов на AWS

Мы будем использовать AWS CLI для установки и настройки веб-сервисов Amazon.

Есть много удобных инструментов развертывания инфраструктуры, например Terraform и CloudFormation, которые позволяют автоматизировать деплой приложения. Работу с такими инструментами лучше рассматривать отдельно. Наша основная задача в этой главе ‒ разобраться, какие Amazon сервисы используются для развертывания приложения на AWS с помощью Docker.

Конфигурация инструментов

Устанавливаем AWS CLI

Устанавливаем AWS CLI при помощи следующей команды:

pip install -Iv awscli==1.16.28

Для конфигурации AWS вводим следующие значения:

aws configure
# AWS Access Key ID [None]: YOUR_AWS_ACCESS_KEY
# AWS Secret Access Key [None]: YOUR_AWS_SECRET_KEY
# Default region name [None]: us-east-1
# Default output format [None]: json

Чтобы получить YOUR_AWS_ACCESS_KEY и YOUR_AWS_SECRET_KEY, нужно создать аккаунт пользователя AWS.

При создании аккаунта AWS, вы по умолчанию являетесь root-пользователем. Настоятельно не рекомендую настраивать инфраструктуру от имени root-пользователя. Поэтому, в целях безопасности, все команды AWS CLI в этом разделе будут совершаться от имени отдельно созданного AWS-пользователя с AdministratorAccess правами. Подробнее о создании Administrator-пользователя в веб-версии AWS можно прочитать в разделе Creating an Administrator IAM User and Group (Console).

Устанавливаем ECS CLI

После, необходимо установить ECS CLI, следуя инструкции.

Реализация возможности хранения sensitive data приложения

После успешной конфигурации вы получите AWS credentials, которые понадобятся нам в дальнейшем. Поскольку эти данные должны быть скрыты от посторонних лиц, мы будем хранить их в зашифрованном виде в репозитории приложения. Эта возможность появилась совсем недавно, в версии Rails 5.2.

Дополнение: если у вас версия Rails меньше 5.2, то можно использовать гем sekrets.

Чтобы начать работать с зашифрованными данными, нужно проинициализировать YAML-файл, в который в дальнейшем мы добавим AWS-ключи и другие sensitive данные. Переменные в нем будут сгруппированы по имени текущего окружения. Поскольку директория config примонтирована к контейнеру, как volume, мы можем ее изменить через bash самого контейнера.

docker-compose -f docker-compose.development.yml -p spreeproject exec server_app bash

В bash-контейнере вызовем следующую команду:

EDITOR=nano rails credentials:edit # or EDITOR=vim

И добавим в него необходимые нам ключи:

staging:
  AWS_ACCESS_KEY_ID: 'YOUR_AWS_ACCESS_KEY_ID'
  AWS_SECRET_ACCESS_KEY: 'YOUR_AWS_SECRET_ACCESS_KEY'
  DEVISE_SECRET_KEY: 'YOUR_DEVISE_SECRET_KEY'
  SECRET_KEY_BASE: 'YOUR_SECRET_KEY_BASE'

Узнать свои AWS credentials можно с помощью следующей команды:

cat ~/.aws/credentials

После того, как мы добавили ключи, сохраняем файл. В результате, вы можете увидеть созданный файл config/credentials.yml.enc в директории приложения. Теперь версионирование доступно и для секретных ключей приложения.

Также был добавлен файл config/master.key, который содержит ключ RAILS_MASTER_KEY. Он необходим для расшифровывания данных. Этот ключ обязательно должен быть скрыт от посторонних лиц. В дальнейшем он будет задействован для запуска приложения на AWS.

По умолчанию в Rails 5.2, credentials не позволяет определять переменные текущего окружения (environment variables) через ENV-константу. Для этого в нашем приложении написан инициалайзер SecretsEnvLoader(config/secrets_env_loader.rb). Переменные development окружения хранятся в config/credentials.local.yml.

Создание Docker образа для веб-сервера Nginx

Зачем мы добавили веб-сервер?

Конечно, можно использовать только сервер приложения (Puma или Unicorn), но тогда вы лишитесь следующих преимуществ, которые предоставляют веб-серверы, такие как Nginx:

  • Статический редирект. Вы можете настроить Nginx на редирект всего HTTP-траффика на тот же URL с HTTPS. Таким образом, можно настроить более безопасную коммуникацию между сервером и клиентом.
  • Multipart upload. Nginx лучше подходит для обработки multipart uploads. Nginx объединит все запросы и отправит один файл в Puma.
  • Работа со статическими файлами. Используя Nginx, вы можете отдавать статические файлы (которые лежат в public директории Rails-приложения) без обращения к Puma. Этот способ в разы быстрее.
  • Защита от DDoS. В Nginx встроены некоторые базовые средства защиты от DDoS-атак.

Прежде чем мы перейдем к созданию образа для Nginx, создадим отдельную директорию deploy, в которой будем хранить все конфигурации внешних сервисов, связанных с деплоем приложения и AWS.

mkdir ./deploy && mkdir ./deploy/configs && cd $_

Проинициализируем Dockerfile для Nginx:

mkdir nginx && touch nginx/Dockerfile

И опишем в нем следующую инструкцию:

# В качестве родительского образа будем использовать готовый образ:
FROM nginx:1.16.0

# Все последующие команды будут выполняться от имени root-пользователя:
USER root

# Устанавливаем программное обеспечение, необходимое для корректной работы приложения:
ENV BUILD_PACKAGES curl
RUN apt-get update -qq && apt-get install -y $BUILD_PACKAGES

# Удалим дефолтную welcome-страницу nginx
RUN rm /usr/share/nginx/html/*

# Скопируем custom и default nginx конфигурации
COPY configs/nginx.conf /etc/nginx/nginx.conf
COPY configs/default.conf /etc/nginx/conf.d/default.conf

# Даем права www-data пользователю на системные директории для корректной работы nginx
RUN touch /var/run/nginx.pid && \
    chown -R www-data:www-data /var/run/nginx.pid && \
    chown -R www-data:www-data /var/cache/nginx && \
    chown -R www-data:www-data /etc/nginx && \
    chown -R www-data:www-data /var/log

# Все последующие команды будут выполняться от имени www-data пользователя:
USER www-data

# Команды, которые будут выполнены только перед запуском контейнера:
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["./docker-entrypoint.sh"]

# Стандартная команда по запуску образа:
CMD ["nginx", "-g", "daemon off;"]

Конфигурация nginx будет храниться в директории nginx/configs:

mkdir nginx/configs && touch nginx/configs/nginx.conf

Определим следующие надстройки для nginx.conf:

# Настройки безопасности взяты из https://gist.github.com/plentz/6737338

# Указываем количество workers для запуска (обычно равно количеству CPU ядер)
worker_processes auto;

# Указываем максимальное количество открытых файлов за процесс
worker_rlimit_nofile 4096;

events {
  # Указываем максимальное количество одновременных соединений, которые могут быть открыты worker процессом
  worker_connections 1024;
}

http {
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  # ---------------------------------------------------------------------------

  # Отключаем отображение версии Nginx в случае ошибок: 
  server_tokens off;

  # https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
  add_header X-Frame-Options SAMEORIGIN;

  # При обслуживании пользовательского контента включайте заголовок X-Content-Type-Options: nosniff вместе с заголовком Content-Type:
  add_header X-Content-Type-Options nosniff;

  # Этот заголовок включает фильтр Cross-site scripting (XSS), который встроен в самые последние веб-браузеры.
  add_header X-XSS-Protection "1; mode=block";
  # ---------------------------------------------------------------------------

  # Избегайте ситуаций, когда имя хоста слишком длинное при работе с vhosts
  server_names_hash_bucket_size 64;
  server_names_hash_max_size 512;

  # Оптимизация производительности.
  sendfile on;
  tcp_nopush on;

  # http://nginx.org/en/docs/hash.html
  types_hash_max_size 2048;

  # Включаем gzip для всего, кроме IE6.
  gzip on;
  gzip_disable "msie6";

  # Конфигурация по умолчанию для бэкэнда приложения.
  include /etc/nginx/conf.d/default.conf;
}

Также создадим файл default.conf

touch nginx/configs/default.conf

В файл default.conf добавим следующие конфигурации:

# Объявляем хост и порт upstream сервер. В данном случае APP_NAME заменится на наше server_app, а APP_PORT на 3000, на котором будем запущен сервер приложения Puma.
upstream app {
  server APP_NAME:APP_PORT;
}

# Перенаправить адреса www на версию без www, а также позаботиться о перенаправлениях на HTTPS одновременно
server {
  # Указываем что nginx будет слушать порт 8080 на текущем хосту. APP_VHOST заменится на хост EC2 инстанса на котором будет запущен nginx.
  listen 8080;
  server_name www.APP_VHOST;
  return 301 http://APP_VHOST$request_uri;
}

server {
  # Указываем что nginx будет слушать порт 8080. 'deferred' уменьшает количество формальностей между сервером и клиентом.
  listen 8080 default deferred;
  server_name APP_VHOST;

  # Указываем директории для записи логов
  access_log /var/log/nginx.access.log;
  error_log /var/log/nginx.error.log info;

  # Указываем редирект в случае ошибок 405 и 503
  error_page 405 /405.html;
  error_page 503 /503.html;

  # Устанавливает максимально допустимый размер тела запроса клиента, указанного в поле заголовка запроса «Content-Length»
  client_max_body_size 64M;

  # Указываем время ожидания в сек, в течение которого клиентское соединение keep-alive будет оставаться открытым на стороне сервера.
  keepalive_timeout 10;

  # Путь к статическим ресурсам, который считывается из VOLUME текущего контейнера по маршруту STATIC_PATH.
  root STATIC_PATH;

  # Указываем маршрут для обслуживания статических ресурсов
  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  # Указываем доступные методы запросов
  if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){
    return 405;
  }

  # Указываем локации для обсуживания статических файлов ошибки. Internal означет, что данное местоположение может использоваться только для внутренних запросов

  location = /503.html {
    internal;
  }

  location = /405.html {
    internal;
  }


  # Все запросы буду обрабатываться блоком app_proxy объявленным ниже
  location / {
    try_files $uri @app_proxy;
  }

  # Объявляем блок который будет проксировать запросы на созданный вначале документа upstream server с нужныеми заголовками
  location @app_proxy {
    proxy_redirect off;
    proxy_set_header Client-Ip $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    gzip_static on;
    proxy_pass http://app;
  }
}

Создадим файл docker-entrypoint.sh, который будет выполняться перед запуском контейнера:

touch nginx/docker-entrypoint.sh && chmod +x nginx/docker-entrypoint.sh

И опишем в нем команды для замены APP_NAME, APP_PORT и APP_VHOST в конфигах nginx в docker-entrypoint.sh:

#!/usr/bin/env bash

# Завершаем выполнение скрипта, в случае ошибки:
set -e

APP_NAME=${CUSTOM_APP_NAME:="server_app"} # имя контейнера с запущенным приложением Spree
APP_PORT=${CUSTOM_APP_PORT:="3000"} # порт, по которому доступно приложение Spree
APP_VHOST=${CUSTOM_APP_VHOST:="$(curl http://169.254.169.254/latest/meta-data/public-hostname)"} # Хост виртуального сервера на AWS по умолчанию ссылается на общедоступный DNS-адрес AWS EC2, "подтянув" эту информацию из метаданных EC2. Это позволяет нам динамически настраивать Nginx во время запуска контейнера

DEFAULT_CONFIG_PATH="/etc/nginx/conf.d/default.conf"

# Заменяем все инстансы плейсхолдеров на значения, указанные выше: 
sed -i "s+APP_NAME+${APP_NAME}+g"     "${DEFAULT_CONFIG_PATH}"
sed -i "s+APP_PORT+${APP_PORT}+g"     "${DEFAULT_CONFIG_PATH}"
sed -i "s+APP_VHOST+${APP_VHOST}+g"   "${DEFAULT_CONFIG_PATH}"
sed -i "s+STATIC_PATH+${STATIC_PATH}+g"   "${DEFAULT_CONFIG_PATH}"

# Выполнение CMD из Dockerfile с передачей всех аргументов
exec "$@"

Вернемся в root-директорию приложения и добавим docker-compose.staging.yml, в котором будет присутствовать сервис для веб-сервера Nginx:

version: '3.1'

volumes:
  redis:
  postgres:
  assets:

services:
  db:
    image: postgres:10
    expose:
      - 5432
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: spreedemo_staging
    volumes:
      - postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]

  in_memory_store:
    image: redis:4-alpine
    expose:
      - 6379
    volumes:
      - redis:/var/lib/redis/data
    healthcheck:
      test: ["CMD", "redis-cli", "-h", "localhost", "ping"]

  server_app: &server_app
    build: .
    command: bundle exec puma -C config/puma.rb
    entrypoint: "./docker-entrypoint.sh"
    volumes:
      - assets:/home/www/spreedemo/public/assets
      - ./config/master.key:/home/www/spreedemo/config/master.key
    environment:
      RAILS_ENV: staging
      DB_HOST: db
      DB_PORT: 5432
      DB_NAME: spreedemo_staging
      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
    expose:
      - 3000
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000"]

  server_worker_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"]

  web_server:
    build: ./deploy/configs/nginx
    volumes:
      - assets:/home/www/spreedemo/public/assets
    environment:
      CUSTOM_APP_VHOST: server_app
      STATIC_PATH: /home/www/spreedemo/public
    ports:
      - 80:8080
    depends_on:
      - server_app
    healthcheck:
      test: ["CMD-SHELL", "service nginx status || exit 1"]

Убедимся, что все development контейнеры, созданные и запущенные ранее, отключены:

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

И запустим staging-сервисы с помощью команды:

docker-compose -p spreeproject -f docker-compose.staging.yml up --build

После этого, Rails-приложение будет доступно через Nginx на 80-м порте машины, то есть localhost.

Cервер на AWS

В качестве платформы облачных сервисов мы будем использовать AWS, который позволяет запросто создать сервер EC2 для своих задач.

AWS предоставляет вычислительные мощности в облаке. С помощью веб-сервиса EC2 вы можете создать для своих задач виртуальную машину с нужными характеристиками и разместить там ваше программное обеспечение. Именно на EC2 мы разместим наше приложение.

Firewall

Интернет-серверы важно обезопасить от несанкционированного доступа со стороны злоумышленников с помощью firewall policy. AWS предоставляет виртуальный firewall Security groups, позволяющий ограничить доступ к определенному сервису или группе сервисов. Проще говоря, с помощью Security Groups вы можете явно указать, какие порты сервера и каким клиентам будут открыты.

Таким образом, к инстансу, на котором мы разместим наше приложение, нужно подвязать ряд ограничений доступа. Для этого создадим security-группу.

# GroupId группы серверного приложения обозначим, как `$STAGING_SERVER_APP_SG`

aws ec2 create-security-group \
  --group-name staging-spreeproject-server-app \
  --description "Staging Spree project Server App"

Все обращения к нашему приложению будут происходить через веб-сервер, который запущен на 80-м порту. Следовательно, мы должны открыть доступ любому клиенту (0.0.0.0/0) только на 80-й порт EC2 инстанса, на котором будет запущен Nginx.

aws ec2 authorize-security-group-ingress \
  --group-id $STAGING_SERVER_APP_SG \
  --protocol tcp \
  --port 80 \
  --cidr 0.0.0.0/0

Хранение изображений и быстрый доступ к ним

AWS предоставляет сервис хранения объектов S3. Этот сервис мы будем использовать для хранения изображений нашего приложения.

Создадим его с помощью следующей команды:

aws s3api create-bucket --bucket spreeproject-staging

После, обновим переменные credentials, добавив туда имя созданного нами bucket:

RAILS_MASTER_KEY=YOUR_RAILS_MASTER_KEY EDITOR=nano rails credentials:edit
staging:
  # ...
  S3_BUCKET_NAME: 'spreeproject-staging'
  S3_REGION: 'us-east-1'
  S3_HOST_NAME: 's3.amazonaws.com'

Настраиваем Staging окружение

План действий

  1. Создаем Cluster с одним EC2 инстансом.
  2. Импортируем актуальные образы Rails-приложения и веб-сервера Nginx на ECR.
  3. Описываем и регистрируем Task Definition для запуска Rails-приложения и веб-сервера Nginx с помощью compose-файла.
  4. Создаем и запускаем Service с двумя Tasks, Rails-приложения и веб-сервера Nginx.

Решение

Создадим конфигурацию для будущего кластера spreeproject-staging, введя следующую команду в консоли:

CLUSTER_NAME=spreeproject-staging # сохраним в глобальную переменную имя будущего кластера для удобного использования в дальнейшем.
ecs-cli configure --region us-east-1 --cluster $CLUSTER_NAME --config-name $CLUSTER_NAME

Для создания инстанса необходимо получить Subnets, VPС и Keypair для будущего инстанса:

aws ec2 describe-subnets

В результате команды выбираем SubnetId, у которых одинаковый VpcId и DefaultForAz параметр имеет значение true. И записываем их в переменную.

# Пример
AWS_SUBNETS=subnet-e49c19b8,subnet-20ae1647,subnet-319d1a1f

Также необходимо получить список доступных VPC:

aws ec2 describe-vpcs

Дальше VpcId этих subnets записываем в переменную $AWS_VPC. Например:

AWS_VPC=vpc-0e934a76

Теперь создадим keypair. Это ключ, по которому будет происходить вход на инстанс по SSH соединению. Это необходимо из-за соображений безопасности.

Создадим его с помощью следующей команды:

aws ec2 create-key-pair \
  --key-name spreeproject_keypair \
  --query 'KeyMaterial' \
  --output text > ~/.ssh/spreeproject_keypair.pem

Разрешаем чтение этого файла:

chmod 400 ~/.ssh/spreeproject_keypair.pem

Если в дальнейшем нужно будет осуществить вход по SSH-ключу, просто обновите security group:

aws ec2 authorize-security-group-ingress \
  --group-id $STAGING_SERVER_APP_SG \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0

Команда для подключения к определенному инстансу по ssh-ключу:

ssh -i ~/.ssh/spreeproject_keypair.pem ec2-user@$EC2_PUBLIC_DOMAIN

AWS предоставляет ряд готовых images для инстансов для каждого региона. Мы воспользуемся образом ami-0a6be20ed8ce1f055 для региона us-east-1.

После создадим кластер spreeproject-staging, к которому будет привязан один инстанс EC2 типа t2.micro. Для этого вводим в терминале следующую команду:

ecs-cli up \
  --keypair spreeproject_keypair \
  --capability-iam \
  --size 1 \
  --instance-type t2.micro \
  --vpc $AWS_VPC \
  --subnets $AWS_SUBNETS \
  --image-id ami-0a6be20ed8ce1f055 \
  --security-group $STAGING_SERVER_APP_SG \
  --cluster-config $CLUSTER_NAME \
  --verbose

ECR

Актуализируем образ нашего Rails-приложения, вызвав команду:

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

Теперь необходимо импортировать эти образы на AWS, для этого AWS предоставляет ECR. Проходим аутентификацию с помощью следующей команды:

$(aws ecr get-login --region us-east-1 --no-include-email)

После, создаем ECR репозиторий server_app для нашего Spree-приложения:

aws ecr create-repository --repository-name spreeproject/server_app

Далее, загрузим локальный образ в репозиторий YOUR_ECR_ID.dkr.ecr.us-east-1.amazonaws.com. YOUR_ECR_ID — registryId созданного репозитория:

docker tag spreeproject_server_app:latest $YOUR_ECR_ID.dkr.ecr.us-east-1.amazonaws.com/spreeproject/server_app:staging
docker push $YOUR_ECR_ID.dkr.ecr.us-east-1.amazonaws.com/spreeproject/server_app:staging

Сделаем тоже самое для web_server, в котором будет образ Nginx:

aws ecr create-repository --repository-name spreeproject/web_server

И загрузим локальный образ в репозиторий:

docker tag spreeproject_web_server:latest $YOUR_ECR_ID.dkr.ecr.us-east-1.amazonaws.com/spreeproject/web_server:staging
docker push $YOUR_ECR_ID.dkr.ecr.us-east-1.amazonaws.com/spreeproject/web_server:staging

AWS Logs

Все логи контейнеров будут храниться в AWS Logs. Для это создадим группу log-group
aws logs create-log-group --log-group-name $CLUSTER_NAME.

ECS Tasks

После, создадим docker-compose.staging.yml как compose staging версии приложения для Task Definition

mkdir deploy/configs/ecs && touch deploy/configs/ecs/docker-compose.staging.yml

С помощью docker-compose.staging.yml мы указываем, какие сервисы и как необходимо будет запустить на EC2 инстансе.

Замените YOUR_ECR_ID, CLUSTER_NAME и YOUR_RAILS_MASTER_KEY на собственные значения:

version: '3'

volumes:
  assets:

services:
  web_server:
    image: YOUR_ECR_ID.dkr.ecr.us-east-1.amazonaws.com/spreeproject/web_server:staging
    volumes:
      - assets:/home/www/spreedemo/public/assets
    environment:
      STATIC_PATH: /home/www/spreedemo/public
    ports:
      - 80:8080
    links:
      - server_app
    logging:
      driver: awslogs
      options:
        awslogs-group: CLUSTER_NAME
        awslogs-region: us-east-1
        awslogs-stream-prefix: web_server
    healthcheck:
      test: ["CMD-SHELL", "service nginx status || exit 1"]

  server_app: &server_app
    image: YOUR_ECR_ID.dkr.ecr.us-east-1.amazonaws.com/spreeproject/server_app:staging
    command: bundle exec puma -C config/puma.rb
    entrypoint: "./docker-entrypoint.sh"
    ports:
      - 3000
    environment:
      RAILS_ENV: staging
      RAILS_MASTER_KEY: YOUR_RAILS_MASTER_KEY
      DB_HOST: db
      DB_PORT: 5432
      DB_NAME: spreeproject_staging
      DB_USERNAME: postgres
      DB_PASSWORD: postgres
      REDIS_DB: "redis://in_memory_store:6379"
    volumes:
      - assets:/home/www/spreedemo/public/assets
    links:
      - db
      - in_memory_store
    logging:
      driver: awslogs
      options:
        awslogs-group: CLUSTER_NAME
        awslogs-region: us-east-1
        awslogs-stream-prefix: server_app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000"]

  worker_app:
    <<: *server_app
    command: bundle exec sidekiq -C config/sidekiq.yml
    entrypoint: ''
    logging:
      driver: awslogs
      options:
        awslogs-group: CLUSTER_NAME
        awslogs-region: us-east-1
        awslogs-stream-prefix: worker_app
    healthcheck:
      test: ["CMD-SHELL", "ps ax | grep -v grep | grep sidekiq || exit 1"]

  db:
    image: postgres:10
    environment:
      POSTGRES_DB: spreeproject_staging
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432
    volumes:
      - /postgres:/var/lib/postgresql/data
    logging:
      driver: awslogs
      options:
        awslogs-group: CLUSTER_NAME
        awslogs-region: us-east-1
        awslogs-stream-prefix: db
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]

  in_memory_store:
    image: redis:4-alpine
    ports:
      - 6379
    volumes:
      - /redis:/var/lib/redis/data
    logging:
      driver: awslogs
      options:
        awslogs-group: CLUSTER_NAME
        awslogs-region: us-east-1
        awslogs-stream-prefix: in_memory_store
    healthcheck:
      test: ["CMD", "redis-cli", "-h", "localhost", "ping"]

После заменяем переменные на ваши собственные значения:

Untitled 
sed -i -e "s/YOUR_ECR_ID/$YOUR_ECR_ID/g" deploy/configs/ecs/docker-compose.staging.yml
sed -i -e "s/CLUSTER_NAME/$CLUSTER_NAME/g" deploy/configs/ecs/docker-compose.staging.yml
sed -i -e "s/YOUR_RAILS_MASTER_KEY/$YOUR_RAILS_MASTER_KEY/g" deploy/configs/ecs/docker-compose.staging.yml

ECS Task — это конфигурационный файл, в котором мы определяем, какие контейнеры необходимо запускать и каким образом.

Хорошая практика безопасности в Docker ‒ ограничивать потребление ресурсов, которые может задействовать контейнер. Нашему контейнеру мы укажем такой лимит, который необходим для его корректной работы, но не больше. В ECS есть возможность определить task size, то есть сколько CPU и памяти необходимо использовать задаче или контейнеру, запущенному этой задачей. Именно с помощью ecs-params.staging.yml мы указываем эти параметры.

Подробнее о структуре конфигурации задачи с помощью ecs-params.
touch deploy/configs/ecs/ecs-params.staging.yml

version: 1
task_definition:
  ecs_network_mode: bridge

  task_size:
    cpu_limit: 768 # 896
    mem_limit: 0.5GB # 900

  services:
    web_server:
      essential: true
    server_app:
      essential: true
    worker_app:
      essential: true
    db:
      essential: true
    in_memory_store:
      essential: true

Регистрируем задачу для будущего сервиса ECS:

# create task definition for a docker container
ecs-cli compose \
  --file deploy/configs/ecs/docker-compose.staging.yml \
  --project-name $CLUSTER_NAME \
  --ecs-params deploy/configs/ecs/ecs-params.staging.yml \
  --cluster-config $CLUSTER_NAME \
  create

После создания задачи, ей будет присвоенный определенный номер. Запишем этот номер в переменную TASK_NUMBER.

Запускаем Staging-приложение

ECS Services

Теперь создадим и запустим сервис по этой задаче.

aws ecs create-service \
  --service-name "spreeproject" \
  --cluster $CLUSTER_NAME \
  --task-definition "spreeproject-staging:$TASK_NUMBER" \
  --desired-count 1 \
  --deployment-configuration "maximumPercent=200,minimumHealthyPercent=50"

После того, как у всех задач Health Status станет HEALTHY, мы сможем получить доступ к нашему staging-приложению по публичному DNS инстанса.

Важно! После завершения работы с ECS, удалите RAILS_MASTER_KEY с файлов конфигураций. Повторюсь, что этот ключ не должен храниться в репозитории приложения.

Подведем итог

В этой части туториала мы развернули инфраструктуру staging-приложения:

  • реализовали возможность хранения чувствительных данных приложения;
  • создали Docker образ для веб-сервера Nginx;
  • подготовили конфигурацию для развертывания staging инфраструктуры на AWS;
  • запустили staging-приложение на AWS.

В следующей части мы развернем готовое к масштабированию production-приложение. Stay tuned!


Предыдущая часть туториала

LinkedIn

19 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Я возможно упустил это...
А как данные из postgresql будут переносится при redeploy-e если в ECS кластере больше одной ноды? Есть какой то affinity?

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

Почти. Допустим у вас в ECS кластере более одной ноды. Вы задеплоили приложение, где postgresql сохраняет свои данные в примонтированый docker volume на ECS node #1. Спустя время вы зарелизили новую версию приложения и передеплоили его, но ECS выбрал node #2 в этот раз. Как результат, приложение запустится с пустой базой, так как node #2 ничего не знает про файлы на node № 1. Если нет node affinity, то эта конфигурация не будет работать на ECS cluster, где кол-во node > 1, не важно сколько тасков вы запускаете.

Все верно. Поскольку AWS не умеет шарить volumes для разных инстансов (пытался всеми возможными способами это сделать), то конфигурация в этой главе предназначена только для dev, staging окружений. Поэтому для production лучше использовать отдельные инстансы/сервисы с postgres, redis и т.д. Если же встал вопрос о перезде с одной архитектуры на другую, то да, придется делать импорт. Если все же не хотите подключать внешние сервисы, то вы сетапите отдельную задачу для postgres и отдельно сервис с одной или несколькими задачами для application server (которые уже коннектите к инстансу с базой данных)

Хмм получается если невозможно указать EBS/EFS как Docker volume, то использовать ECS для statefull container-ов вообще не имеет смысла, не важно запущены они как отдельный сервис или нет, так как рано или поздно их придется рестартовать/update-ить и т.д., и опять придется думать как мигрировать данные с одной ноды на другую. Я думаю, это стоит упомянуть в статье, а то это немного вводит в заблуждение.

Нет. После рестарта сервисов данные не пропадают. Это было бы глупо.
Прошу прощения, если высказался непонятно. Я имел ввиду сложности масштабирования в случае когда у нас на одном инстансе запущенны все сервисы, как и statefull, так и stateless

Вы уверены? (no sarcasm)
Потому что без возможности «pin tasks to a container instance» (что по сути называется node affinity в терминах k8s) я не вижу способа как это сделать. BTW вот на это открытое issue: github.com/...​ainers-roadmap/issues/127

Для dev/staging инфраструктуры используется один инстанс для postgres, redis, application и т.д., (в целях экономии).

Этот инстанс не удаляется и не заменяется новым (как в случае с auto scalling) Это нам позволяет все данные из директории контейнера postgres (`/var/lib/postgresql/data`) складывать в директорию инстанса (`/postgres`). Когда контейнер postgres заново запустится, ему вмонтируется директория `/postgres`, которая хранится непостредственно в инстансе, который, повторю, не удаляется.

volumes:
— /postgres:/var/lib/postgresql/data

Тут misunderstanding небольшой.
Я не говорю что инстанс удаляется. Я говорю, что таск может запустится на другом инстансе, где /postgres директория пустая. Если истанса 2 (два), как вы котролируете на каком истансе запускается таск? (это вопрос, просто это не очевидно из статьи) docs.aws.amazon.com/...​guide/task-placement.html

Я понял. Прошу прощение за недопонимание.
Да, вы все верно говорите. В таком случае мы не можем использовать volumes. Поэтому решение в этой часте только для кейса когда у нас один инстанс на котором все сервисы.

Случай когда нам нужно запускать задачи на нескольких инстансах, я буду рассматривать в следующей часте, напримере масштабирования и какие сложности с ним могут возникнуть (stateless особенности контейнера). Как предложенное решение — использовать RDS, ElastiCache.

Поймал себя на мысли что перевожу на Английский чтобы понимать что образ это Image и тд. чтобы вкурить что написано

Бірмана почитай, пацан:

ilyabirman.ru/...​hile/all/itunes-to-music
Айтюнс
Мьюзик
конвертера мп3 в аак

ilyabirman.ru/...​-v-egee-na-baze-editorjs
ХТМЛ
ЭдиторЖС
Гугль-док
Альт-Комманд-1

Ну і там ще несуться всілякі ПХП, энжинкс, апач і так далі.

Так шо тут не норм.

А теперь автоматизуйте все це ))))

Я деколи думаю шо напевне простіше буде вивчити cfn/тераформ, підняти дженкінс, підняти там пайплайни з баш-скріптами для того всього (ніж робити отаке як ви все вручну) і мати деплой по кожному коміту.

тільки замість Jenkins — AWS Code Deploy, та замість terraform- CloudFormation і буде дуже красиве рішення :)

AWS Code Deploy
замість terraform- CloudFormation
дуже красиве

топ кек

Больше спасибо за комментарий. Несомненно эти инструменты более элегантны для развертывания инфраструктуры. Я об этом упоминал 🙂

Есть много удобных инструментов развертывания инфраструктуры, например Terraform и CloudFormation,

Так как рассматривая тема непроста, как и работа с этими инструментами, их лучше подавать в отдельном материале.

Наша основная задача в этой главе ‒ разобраться, какие Amazon сервисы используются для развертывания приложения на AWS с помощью Docker.

Терраформ выучить проще, тк у него вменяемая документация. Хуже доки, чем у АВС я не видел: очевидные вещи разжеваны до мелочей, важные вещи разбросаны по тысяче мелких статей, базовые инфраструктурные вещи не объясняются вообще.

Поэтому этот туториал и вышел. Разобраться с документацией Amazon ECS не так-то просто

Для меня самое сложное было это настроить сетевую инфраструктуру, контейнер запустился относительно легко.

Подписаться на комментарии