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

Kate

Administrator
Команда форума

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

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

Решение: 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​

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

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

Task definition​

2_nKF2yH0.png
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​

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

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

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

Service​

4_4Fcc8D2.png
Группировка 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:

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

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

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

6_f3Rv4Mh.png
Инфраструктура 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.
 
Сверху