Apache + FCGI + Ruby + ActiveRecord. Когда RoR много

в 10:42, , рубрики: activerecord, Apache, fcgi, ruby, ruby on rails, rubyonrails, метки:

Истоки стека технологий

В определенный момент пришло моё время написать что-то отдаленно похожее на web сайт. Самый обычный сайт: главная страница с отображением на ней трёх табличек из БД и парой форм для заполнения оных контентом. Такие мной были поставлены начальные требовнаия. Думаю, у каждого при написании первого сайта возникает вопрос: какие инструменты для этого использовать? Для меня были довольно принципиальны критерии:

  • использовать Ruby, так как я с ним довольно хорошо знаком (а с php и python — нет)
  • использовать объектную модель насколько это возможно
  • ограничиться только необходимыми средствами разработки: ведь незачем тащить за собой JQuery, когда нужен всего один POST запрос

Соответственно, взгляд упал на Ruby on Rails: это полноценный framework, довольно популярный и, как ни странно, содержит в себе Ruby.

По поводу 'содержит'
Можно сколько угодно говорить, что там "всё" на Ruby. Но самого языка там нет: только отголоски в виде синтаксиса хешей и т.п…
И тот "подправлен" заботливыми рельсами. Я имею ввиду, что весь встречающийся синтаксис мог внезапно оказаться C++. И никто бы не заметил — всё скрывается framework'ом.

И принялся я его изучать, чего и вам желаю. Хороший гайд: помогает понять, что рельсы — это гигантская сборка gem'ов, которые потом ещё и разворачивать придется на passenger. То есть, кроме мороки с bundler'ом или rvm (чего в итоге не избежать), придется ещё и passenger стыковать с Apache или nginx. Меня эти перспективы напугали и, дочитав таки tutorial, я начал искать чем бы RoR заменить, оставив при этом от него только необходимое. Для меня все ограничилось Ruby и ActiveRecord. Первые поиски пути выполнения Ruby кода на Apache показали, что есть mod_ruby: этот и этот.

Что за mod_ruby

Для тех, кто не понял в чем дело: Apache — довольно модульная система и у него есть API, позволяющий обрабатывать запросы как душе угодно.

Вот на это API и пишутся модули, позвоялющие выполнять скрипты PHP, Python и Ruby. Это не CGI, что сулит повышенную производительность. Вот какие модули я имею ввиду.

Однако, первые же опыты привели меня к такой и вот такой ситуации.

Конечно, не бог весть какие проблемы: всё решаемо, но тут я посмотрел на даты последних коммитов, увидел заветное "пол года назад" и отказался от этой идей.

После этого мой выбор пал на FCGI как довольно перспективное продолжение CGI. Почему не CGI? А потому-что. Т.е. для FCGI у Ruby есть gem, который позволяет обрабатывать запросы не в сыром виде CGI, а посредством интерфейса гема. Смотрится удобно, но об этом позже. Ну вроде всё: есть Apache, есть FCGI, Ruby… И, так как у меня есть небольшой backend в виде БД, а работать с тяжелым mysql или аналогами не хотелось, решил я прихватить ActiveRecord из RoR себе в виде файла sqlite3 БД.

Заинтересовало? Добро пожаловать под кат.

Разбор компонент

Составив такой стек, выделил я себе серверок с CentOS и приступил к воплощению идей в жизнь.

Зачем CentOS?

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

Apache

Всё начинается с Web сервера. В моём случае всё оказалось довольно просто: yum install epel-release; yum install apache mod_fcgid fcgi-devel mod_ssl gnutls-utils.

Такая портянка пакетов обусловлена нашей жаждой оспользовать gem ruby-fcgi… И, конечно тем, что почти все пакеты в EPEL. По старой админской привычке я пропарсил конфиг Apache… И нашел там кучу совершенно бесполезных модулей, чего и вам советую!

Настройка

Настройка Apache не является главной целью статьи, но, для понимания контекста работ, приведу пример виртуального хоста, на котором все это разворачивалось:

<VirtualHost mysite:443>
#common options
    ServerName mysite:443
#loging
    LogLevel info
    ErrorLog logs/mysite-error_log
    CustomLog logs/mysite-access_log common
#main dir
    DocumentRoot /var/www/html/
    <Directory /var/www/html/>
        Options ExecCGI
        DirectoryIndex index.rb.fcgi
        AllowOverride None
#Access
        Allow from all
#LDAP
        AuthLDAPUrl <>
        AuthLDAPBindDN <>
        AuthLDAPBindPassword <>
#Authorization
        AuthType Basic
        AuthBasicProvider ldap
        AuthName "Input your domain login name and password"
        Require ldap-attribute <>
        Require ldap-attribute <>
    </Directory>
#Scripts timeouts
FcgidIOTimeout 300
#SSL
    SSLEngine on
    SSLProtocol TLSv1
    SSLCertificateFile /var/www/html/certs/ca_cert.pem
    SSLCertificateKeyFile /var/www/html/certs/ca_key.pem
</VirtualHost>

<> помечены опущенные параметры

Само собой, имя mysite прописано в DNS сервере, mod_ssl установлен.

mod_fcgid

Как вы должны были заметить, вместе с Apache мы ставим mod_fcgid, который и есть интерфейс для встраивания скриптов для обработки запросов.

При установке этот модуль создал конфиг в /etc/httpd/conf.d/fcgid, содержащий:

LoadModule fcgid_module modules/mod_fcgid.so
AddHandler fcgid-script fcg fcgi fpl
FcgidIPCDir /var/run/mod_fcgid
FcgidProcessTableFile /var/run/mod_fcgid/fcgid_shm

Интерес здесь представляет только строка AddHandler fcgid-script fcg fcgi fpl, которую неплохо бы из соображений безпасности заменить на AddHandler rb.fcgi.

Так мы ограничиваем исполнение FCGID только скриптов с расширением rb.fcgi.

Ruby

Само собой, надо ставить Ruby. Но не в коем случае не из репозитория: многие печали буду. А конкретнее: придется прописывать полные пути до gem'ов при их подключении.

А потом перепрописывать при обновлении. Так что ставим RVM:

sudo su -
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -sSL https://get.rvm.io | bash -s stable --ruby=2.2.1
source /etc/profile.d/rvm.sh 

Почему установка глобальная?

Если проект действительно серъезный, то ни в коем случае так не делайте. Необходимо создать пользователя для сайта и делать всё в его окружении. Но не об этом речь.

После выполнения этого в системе появится rvm и ruby… и ещё кое что.

FCGI gem

Дело дошло до настройки FCGI. Если как обычно, то gem install fcgi. Только в этом случае gem установится в текущий gemset в rvm. А для того, чтобы в скриптах, которые будет выполнять Apache, gem'ы были видны, надо устанавливать их в глобальный gemset. То есть, rvm gemset use global; gem install fcgi. И далее, если я говорю "устанавливаем gem", то имеется в виду нечто подобное.

И снова глобальная установка

Если у вас таки отдельный пользователь и в конфиге VirtualHost сделан Suexec, то можно создать гемсет для него, но не факто что такой фокус удастся: я не представляю как Apache делает Suexec и какой вид окружения пробрасывается.

Буквально это нам даёт право писать require "sqlite3" в FCGI скриптах для Apache. Однако, интерпретатор в скриптах по-прежнему придется указывать #!/usr/local/rvm/rubies/ruby-2.2.1/bin/ruby — мне этого не удалось избежать.

А как же SetEnv?

Он то конечно есть, но от проблемы он не спасает: всеравно статический путь в конфигурациях.

Теперь, потерев руки, можно делать файл /var/www/html/index.rb.fcgi и писать в него чтото типа:

#!/usr/local/rvm/rubies/ruby-2.2.1/bin/ruby
require "fcgi"

FCGI.each { |request|
    case request.env["REQUEST_METHOD"]
        when 'GET'
            request.out.print "Content-Type: text/htmlnStatus: 200 ОКnn<!DOCTYPE html><html><h3 align=center style="color: green"><strong>Success</strong></h3></html>"
        else
            request.out.print "Content-Type: text/htmlnStatus: 403 Forbiddennn<!DOCTYPE html><html><h3 align=center style="color: red"><strong>Access denied</strong></h3></html>"
    end
    request.finish
}

Довольно простой скрипт: выдает страничку с 'Success' на GET запрос и страничку с 'Access denied' на любой другой.

Т.е. fcgi gem предоставляет нам класс FCGI, который, будучи использованный в FCGI скриптах, может принимать запросы самостоятельно.
Мной это изучено по документации проекта.

Мы получаем при запросе объект request, который имеет:

  • env поле: содержит всю информацию о запросе: тип запроса, параметы запроса, тип брузера, пользователь и многое другое. Все это можно (и нужно) использовать при обработке запроса;
  • in поле представляет собой тело запроса. То есть, насколько мне стало ясно, env — это представление заголовка, а in — буквально тело HTTP запроса;
  • out поле служит для записи в него всего, что надо отдать в ответ на запрос: полная FCGI-свобода действий, что видно по отправке статуса 200 и 403 выше.

in и out — обычные объекты типа IO, что какбы намекает.

Одна из задач выполнена: мы получаем чистый Ruby для разработки сайтов. Можно использовать все, что только есть в интернетах, для генерации html.

ActiveRecord gem

Вот тут, внезапно, пронадобилось использовать БД как backend. И, раз уж мы на Ruby, воспользуемся Rails фишкой в виде ActiveRecord.
Не скрою, я пользовался этой статьёй, но у меня есть что добавить. А, дабы не путать вас в ссылках на неё, опишу всё по порядку.

Устанавливаем gem: gem install activerecord sqlite3 — для работы с sqlite3 адаптером БД.

Подключение БД

Для работы с БД необходимо 'создать соединение' с БД. Это выглядит както так:

require 'active_record'
require 'sqlite3'
ActiveRecord::Base.establish_connection(
  :adapter => 'sqlite3',
  :database => "/var/www/html/db/mysite.db"
)

Ничего сверхъестественного и ничего нового они не превносят — просто указание на расположение БД и её формат. Эти строки надо добавить при инициализации любого fcgi скрипта для подключения в его окружение БД.

Миграции

Миграции (в терминологии Rails) — это метод автоматизаци разворачивания схемы БД при разработке, тестировании, внедрении,… По сути, это скрипты, которые запускаются для создания другими людьми БД, используемой сайтом.

Пример такого скрипта:

#!/usr/local/rvm/rubies/ruby-2.2.1/bin/ruby
require 'active_record'
require 'sqlite3'
ActiveRecord::Base.establish_connection(
  :adapter => 'sqlite3',
  :database => "/var/www/html/db/mysite.db"
)
class CreateUsers < ActiveRecord::Migration
  def up
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :group
      t.timestamps null: false
    end
  end
end
CreateUsers.migrate(:up)

Этот скрипт создает таблицу users, содержащую 5 полей — id, name, email, group и timestamp.

Существует довольно много действий, осуществимых с БД путем миграций, но не будем заострять на этом внимание. Все умеют читать мануалы. А для нас главное понять, что для создания и модификации БД необходимо написать ряд скриптов, а не носить везде с собой файл sqlite со схемой. Эти файлы никуда не включаются — это отдельная часть проекта и при работе сайта она не используется.

Валидации

Казалось бы, БД есть — почему бы не использовать её. Но не все так просто: неплохо бы определиться с тем, что наша БД может содержать, а что категорически нет.

Это подразуммевает, что у нас на руках есть схема таблиц БД с обозначенными полями, их типами, допустимыми значениями и связями между ними.

Что-то код не работает

Я привожу только часть кода как пример: опускаю некоторый функционал, так как реализация чего-либо не является целью статьи.
Цель — вывести ActiveRecord на чистую воду.

Здесь и начинается то, что упускает большинство руководств по использванию ActiveRecord. Не буду томить, скрипт валидаций:

class Host < ActiveRecord::Base
    belongs_to :user, :inverse_of => :hosts, :validate => true

    validates :address, :presence => true, :uniqueness => true
    validates :user_id, :presence => true
    validate :address_and_user_should_exists,

    def address_and_user_should_exists
        if Address.find_by_id(address_id) == nil
            errors.add(:address_id, "should points at exist address")
        end
        if User.find_by_id(user_id) == nil
            errors.add(:user_id, "should points at exist user")
        end
    end

    validates :name, length: { minimum: 2, maximum: 20 }, presence: true, :format => { :with => /S(S|-)*S[^z]/i }, :uniqueness => true
    validates :purpose, length: { minimum: 4, maximum: 100 }, presence: true, :format => { :with => /[^$^&`]+/i }
    validates :description, length: { maximum: 700 }, presence: false, :format => { :with => /[^$^&`]*/i }

end

class User < ActiveRecord::Base
    has_many :hosts, :inverse_of => :user, :dependent => :destroy

    def self.valid_groups
        ["test", "probe" "etc"]
    end
    validates :name, length: { minimum: 2, maximum: 30 }, presence: true, :format => { :with => /S(S| )*S[^z]/i }, :uniqueness => true
    validates :email, length: { minimum: 5, maximum: 40 }, presence: true, :format => { :with => /A[w+-.]+@[a-zd-]+(.[a-z]+)*.[a-z]+z/i }
    validates :group, length: { minimum: 2, maximum: 30 }, presence: true, :format => { :with => /(w|-)+/i },
        :inclusion => { :in => valid_groups, :message => "%{value} is not a valid group. Select one from drop-down hint."}
end

require и подключение БД опущены

Он предназначен для определения допустимых значений, записываемых в БД. В приведенном скрипте:

  • описаны две таблицы: users и hosts (согласно нотации Activerecord, классу User соответствует таблица users и т.п.). Они представлены классами, наследующими ActiveRecord:Base.
  • описаны отношения между полями этих таблиц в виде директив has_many и belongs_to
  • описаны сами валидаторы значений полей — это все методы класса (self.) и все вызовы `validate`

Начнем разбор всего, что написано в валидаторе. Строка class User < ActiveRecord::Base означает, что в окружении у нас теперь есть класс User, который представляет собой (связан с) таблицей users БД, которую мы где-то выше подключили. Так, вызов User.find_by_id позволяет производить поиск записей по id в таблице. И ничего больше. То есть, никакой магии нет — просто возвращается запись, если она есть.

Далее в обоих классах идёт строка, описывающая связи между таблицами.

Например, has_many :hosts, :inverse_of => :user, :dependent => :destroy означает, что:

  • для записи этой таблицы можно ожидать, что она имеет ноль или несколько связанных записей в таблице hosts
  • является логической парой связи user, опиcанной в User
  • при удалении записи из этой таблицы будут удалены связанные с ней записи

Здесь и кроется основная тайна ActiveRecord: связи AR не являются связями в БД. В БД их попросту нет.

То есть, при использовании связи has_one не следует ожидать, что AR будет отслеживать соблюдение отношения 1:1 при добавлении записей: это на вашей совести.

А ActiveRecord только гарантирует, что объявление связи позволит находить связанные записи.

И, наконец, идёт список валидаторов полей:

    validates :user_id, :presence => true
    validate :address_and_user_should_exists,
    def address_and_user_should_exists
        if Address.find_by_id(address_id) == nil
            errors.add(:address_id, "should points at exist address")
        end
        if User.find_by_id(user_id) == nil
            errors.add(:user_id, "should points at exist user")
        end
    end
    validates :name, length: { minimum: 2, maximum: 20 }, presence: true, :format => { :with => /S(S|-)*S[^z]/i }, :uniqueness => true

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

Там можно узнать как пользоваться валидаторами, в том числе и в своих коварных целях.

К чему я веду: ActiveRecord упрощает жизнь тем, что не приходится иметь дело с sql синтаксисом или чем-то подобным. Однако, вам придется реализовать всю логику работы схемы БД самостоятельно.

Это неплохо: можно проверить что угодно на соотвестсвие чему угодно при добавлении, изменении и удалении записей БД. Но вся эта реализация на совести разработчика.

Сухой остаток

После написания всех упомянутых выше вещей мы получаем:

  • Доступ к записям БД посредством вызова User.find_by(:name => "Vasya"), User.first.name, User.all.each {...}
  • Возможность создания и удаления записей (с выполнением наших валидаций): User.create(:name => "Oleg", ...), User.first.destroy
  • Возможность получения связанных записей: User.first.hosts (причем, если связь 1:1, то было бы User.first.host). Поконкретнее про связи здесь.

Вместо заключения

На выходе мы получили исполнение Ruby скриптов по запросам Apache с вкраплениями ActiveRecord. В такой стек хорошо вписывается MVC из RubyOnRails путем использования валидаций как модели, Ruby + FCGI как контроллера и Ruby генераторов html как представлений. Надо ли приводить гайд на структуру проектов на основе вышеописанного покажет время.

Спасибо за внимание.

Автор: deman_killer

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js