Высоко доступный FTP-сервер с хранением данных в AWS S3

от автора

Добрый день, уважаемые читатели.
Снова хочу поделиться с вами приобретенным опытом.На одном из проектов была поставлена цель организовать FTP-сервер повышенной надёжности. Под повышенной надёжностью подразумевалось следующее:

  • Данные хранятся в AWS S3
  • Сам FTP-сервер (выбран был Pure-ftpd) должен быть максимально возможно доступен
  • Организовать балансировку нагрузки (опционально)


Шаг первый: Установка s3fs и монтирование S3 bucket в качестве дискового раздела.
Тут вариантов не много, вернее один (если ошибаюсь — поправьте) — s3fs. Разработчики s3fs на своей странице утверждают, что «s3fs is stable and is being used in number of production environment». Процесс установки s3fs расписывать нет смысла, он есть тут.Остановлюсь только на действительно важных моментах.Во-первых, у последней версии s3fs проблемы с синхронизацией данных. Когда вы заливаете новый файл на S3, он тут же появляется у вас на сервере, но если потом вы вносите изменения в этот файл на S3, то на сервере по-прежнему остается старая версия. Налицо проблема с кешированием. Попытки монтировать S3 bucket с различными опциями включения и выключения кеширования ничего не дали. После тестирования различных релизов s3fs была найдена версия, где данный баг себя не проявил. Скачиваем пакет, распаковываем и устанавливаем как написано в Makefile. Чтобы s3fs заработал корректно, убедитесь что в системе уже установлены следующие пакеты:

  • fuse
  • fuse-devel
  • fuse-libs

Для проверки можно попробовать примонтировать бакет командой:

#/usr/bin/s3fs mybucket /mnt/mybucket/ -o accessKeyId=XXXXXXXXXXXXX -o secretAccessKey=YYYYYYYYYYYYYYYYY -o allow_other,rw -o readwrite_timeout=120; 

Шаг второй: Установка pure-ftpd.
Тут казалось бы ничего интересного. Достаточно просто установить при помощи любого пакетного менеджера. Однако pure-ftpd отличается своей параноидальностью, и перед тем, как удалить файл он сперва копирует его в новый временный файл. А когда размер файла составляет несколько гигабайт, то эта процедура занимает дополнительное время. А в нашем случае, когда данные хранятся не локально, а на S3, то и совсем не малое время.
Чтобы отключить создание временных файлов перед удалением, я пересобрал pure-ftpd с опцией —without-sendfile. Конечно было бы правильнее собрать свой пакет и установить его в систему, но я делал на быструю руку и на это отвлекаться не стал.

Шаг третий: Настройка прав пользователей.
Один из самых интересных нюансов. По требованиям заказчика в каждой домашней папке пользователя должны находиться каталоги, которые пользователь удалить не может или не может в них писать. Если бы мы имели дело с обычными дисковыми разделами, то мы могли просто сменить владельца папки. Но в нашем случае это не сработает так как права будут наследоваться от опции, с которой примонтирован раздел (ro ил rw). То есть пользователь или может всё, или только читать. Но у pure-ftpd есть одно полезное свойство, он умеет «ходить» по ссылкам. Для этого во время сборки добавляем ещё одну опцию —with-virtualchroot. Таким образом, мы можем примонтировать бакет дважды, в read-only и read-write режимах и сделать ссылки на них в домашних директориях пользователей.

#/usr/bin/s3fs mybucket /mnt/mybucketrw/ -o accessKeyId=XXXXXXXXXXXXX -o secretAccessKey=YYYYYYYYYYYYYYYYY -o allow_other,rw -o readwrite_timeout=120; #/usr/bin/s3fs mybucket /mnt/mybucketro/ -o accessKeyId=XXXXXXXXXXXXX -o secretAccessKey=YYYYYYYYYYYYYYYYY -o allow_other,ro -o readwrite_timeout=120;  #mount | grep s3fs s3fs on /mnt/mybucketro type fuse.s3fs (ro,nosuid,nodev,allow_other) s3fs on /mnt/mybucketrw type fuse.s3fs (rw,nosuid,nodev,allow_other)  

Директория пользователя будет выглядеть так:

ls -la /mnt/Users/User1/ . lrwxrwxrwx 1 root root   15 Mar 25 09:10 mybucketro/folder1 -> /mnt/mybucketro/folder1 lrwxrwxrwx 1 root root   15 Mar 25 09:10 mybucketrw/folder2 -> /mnt/mybucketrw/folder2 

Теперь мы дали пользователю доступ на чтение в папку /mnt/mybucketro/folder1 и доступ на запись в папку /mnt/mybucketrw/folder2.На этом этапе можем считать, что пункт первый ТЗ (Данные хранятся в AWS S3) выполнен.

Шаг четвертый: Настройка высокой доступности.
Тут решено было задействовать старый добрый AWS LoadBalancer и его чудесный HealthCheck.
Открываем AWS Console и создаём новый балансер (Уверен, что повторять процесс создания балансера необходимости нет. Если что, вот напоминалочка).
В Ping Protocol выбираем TCP, Ping Port — 21.
Всё, теперь жизнеспособность сервера будет проверяться по доступности 21 порта, то есть нашего FTP-сервера.
Создаем AMI с нашего сервера (на котором уже настроен FTP и примонтированы разделы). Далее всё как всегда, делаем launch-config с созданной AMI и создаём auto-scaling-group.
При создании auto-scaling-group указываем наш новый Load Balancer и опцию —health-check-type ELB.В такой конфигурации, если наш FTP-сервер «упадёт», то Load Balancer удалит его и «поднимет» новый рабочий сервер.Учитывая, что все данные мы храним на S3, то нам такая процедура не навредит.

Шаг пятый(опциональный) Всячески приветствуются ваши наработки: Настройка балансировки нагрузки и автомасштабирования.
Вопрос по балансировке нагрузки на фтп далеко не так просто решается, как, допустим, нагрузка на веб. Я с таким столкнулся впервые и, не найдя, готового бесплатного решения, предложил заказчику балансировать нагрузку с помощью ДНС.
В AWS Route53 есть опция для записей A-типа — weight. Чем выше это значение у записи, тем выше её приоритет в момент ответа клиенту.
То есть, теоретически, мы можем завести 5 записей с одинаковым weight и таким образом равномерно распределять запросы клиентов по 5-ти серверам.Для автоматизации добавления записей в AWS Route53 я сделал два скрипта. Один для добавления записи:

instance_up.sh

#!/bin/bash  zone_id="Z3KU6XBKO52XV4" dns_record="example.com." instance_dns=$(/usr/bin/curl -s http://169.254.169.254/latest/meta-data/public-hostname) instance_ip=$(/usr/bin/curl -s http://169.254.169.254/latest/meta-data/public-ipv4)  let number_nodes=$(route53 get $zone_id | grep $dns_record | wc -l)+1 weight="50" id=$(date "+%Y_%m_%d_%H:%M")  route53 get $zone_id | grep $instance_ip > /dev/null if [ $? -ne 0 ]; then 	route53 get $zone_id | grep $dns_record | awk '{print $4" "$3" "$6" "$7}' | sed 's/id=//' | sed 's/\,//' | sed 's/w=//' | sed 's/)//' | while read i; do  	    route53 del_record $zone_id $dns_record A $i 	    route53 add_record $zone_id $dns_record A $(echo $i | awk '{print $1" "$2" "$3}') $weight 	done 	route53 add_record $zone_id $dns_record A $instance_ip 60 $id $weight fi 

Другой для удаления:

instance_down.sh

#!/bin/bash  zone_id="Z3KU6XBKO52XV4" dns_record="example.com." instance_dns=$(/usr/bin/curl -s http://169.254.169.254/latest/meta-data/public-hostname) instance_ip=$(/usr/bin/curl -s http://169.254.169.254/latest/meta-data/public-ipv4)  let number_nodes=$(route53 get $zone_id | grep $dns_record | wc -l)+1 weight="50" id=$(date "+%Y_%m_%d_%H:%M")  route53 get $zone_id | grep $instance_ip > /dev/null  if [ $? -eq 0 ]; then 	route53 del_record $zone_id $(route53 get $zone_id | grep $instance_ip | awk '{print $1" "$2" "$4" "$3" "$6" "$7}' | sed 's/id=//' | sed 's/\,//' | sed 's/w=//' | sed 's/)//') fi 

Скрипты использует утилиту route53, которая идёт с пакетом python-boto.
Оба скрипта помещаем на сервер, с которого делаем AMI и добавляем их вызов в стартовый скрипт Pure-Ftpd
Теперь при запуске Pure-Ftpd сам добавит в AWS Route53 новую «A» запись со своим IP-адресом, а при выключении удалит её.
Остается только добавить политики для ScaleUP и ScaleDown для нашей auto-scaling-group.

Вот и вся настройка. Такая конфигурация успешно работает на проекте уже полгода.
Если остались вопросы — пишите комментарии, по возможности отвечу. Также буду рад, если кто-то поделится своим опытом в организации подобных систем.

ссылка на оригинал статьи http://habrahabr.ru/post/173699/