在生产环境中闪耀的 Ruby
即使是最漂亮的 Rails 应用程序,如果部署不当,也会失去其优雅。与 Sinatra 等其他 Ruby 框架或语言一样,Rails 也是基于 Rack 接口的。本文将基本介绍 Rack 托管和基于 Rack 的应用程序部署。
当 Rails 于 2005 年首次发布时,开发者们欢欣鼓舞。终于,一个全面的 Web 应用程序开源框架问世了,它包含一系列工具,使 Web 开发变得快速、高效且有趣。Rails 以“开发者天堂”而闻名,但尽管它提供了许多避免典型和重复性任务的便利,但仍然存在一个弱点:部署。部署 Rails 应用程序并非易事。每个人都知道 Rails 应用程序终有一天会被发布到网上,但具体如何发布却不清楚。
平台即服务 (PaaS)开发者通常选择购买平台即服务 (Platform as a Service, PaaS) 托管空间(例如,Heroku、OpenShift 或 EngineYard)。PaaS 非常棒,因为它提供了一个包含完整软件依赖堆栈的即用型环境。一般来说,在 PaaS 平台上发布很容易、很快,而且一切往往(几乎)立即就能工作。但是,至少在两种情况下,PaaS 将无法满足您的需求:当应用程序必须保存在客户的私有基础设施中时,或者当应用程序具有更高的硬件或软件要求时——例如,当您需要 PaaS 提供商不支持的特定软件服务时。
在这种情况下,您必须实施自定义虚拟服务器配置和自定义部署程序。您可以将 Rails 应用程序部署在服务器或虚拟机上。像 Amazon Web Services (AWS) 这样的完整云服务的可用性正在大幅增长,这些服务允许您创建由多个 Web 服务器、数据库服务器和前端负载均衡机器组成的复杂基础设施。这种方法非常灵活,但您必须访问、安装和管理操作系统和发行包,配置网络,激活服务等等。在本文中,我将介绍基于 Rack 的托管软件要求以及一些基本示例配置,以在 GNU/Linux 服务器上实现自动化的 Ruby 托管。
RVM首先,如果您想托管 Ruby 软件,您必须安装 Ruby 平台。您可以使用 apt-get 或 yum 安装 Ruby 和 gems。这很简单,但是当您的应用程序需要特定的 gem 版本或特定的解释器版本时,您将面临一个常见问题。如果您的 GNU/Linux 发行版没有打包这些特定版本,您如何满足这些请求?此外,您如何以干净且可重复的方式维护多个 Ruby 版本?
您可能会认为您可以直接下载 Ruby 平台并手动编译它。可以保证您可以安装所需的解释器版本和 gem 版本。不幸的是,这完全不方便。这种软件管理方式使您的配置难以更新。
有几种解决方案可以克服这些常见问题。我发现对于服务器环境更可靠的一种解决方案名为 Ruby enVironment Manager (RVM)。RVM 包含一组脚本,可帮助您安装和更新 Ruby 生态系统。
以 root 用户身份执行以下命令下载 RVM
# \curl -L https://get.rvm.io | bash -s stable
尽管建议您使用 sudo 等安全工具来使用 RVM,但 rvm
可执行文件必须在您的 root $PATH 环境中可用,因此请以 root 用户身份安装它。对于服务器典型的多用户 RVM 安装,该软件默认保存在 /usr/local/rvm 目录中,因此您可以使用 rm -fr /usr/local/rvm
命令安全地删除整个发行版。
在继续 Ruby 安装之前,请确保您的系统已准备好编译 Ruby。检查您的 PATH 中是否有可用的 rvm
命令(如果没有,请注销并重新登录或使用 bash -s
重新加载您的 shell),然后执行以下命令
$ sudo rvm requirements
RVM 将通过 yum 或 apt-get 安装编译 Ruby 发行版所需的软件包。在本文中,我使用名为 MRI (Matz Ruby Interpreter) 的稳定官方 Ruby 发行版(源自 Ruby 创建者 Yukihiro Matsumoto 的名字)。
现在,您可能需要在您未来的 Ruby 版本中添加一些基本库,这些库通常是某些复杂 gems 或软件所需要的。立即设置这些库将保证 Ruby 软件永远不会抱怨系统库过旧或不兼容,从而产生烦人的错误。以前,您会通过 rvm pkg install <pkg>
命令安装这些额外的软件包,但现在 RVM 弃用了这种做法。相反,只需启用 autolibs,将构建连贯且无错误的发行版的责任委托给 RVM
$ sudo rvm autolibs enable
您终于可以为您的环境提供完整的 Ruby 发行版了。例如,让我们安装官方 MRI 解释器的最新稳定版本,即 2.0.0 版本
$ sudo rvm install 2.0.0
如果一切顺利,该发行版可供 root 用户和系统用户使用。如果不是,通常是 $PATH 问题,因此在 /etc/profile.d 中调整它,并且为了避免部署陷阱,请验证 $GEM_HOME 变量是否已导出到正确的 gem 路径。实际上,如果某些东西工作不正常,请像这样设置以下变量
if [ -d "/usr/local/rvm/bin" ] ; then
PATH="/usr/local/rvm/gems/ruby-2.0.0-p353@global/bin:
↪/usr/local/rvm/bin:$PATH"
GEM_HOME="/usr/local/rvm/gems/ruby-2.0.0-p353@global"
fi
您可以使用以下命令列出可用的 Ruby 版本
$ rvm list known
在运行多个 Ruby 版本的系统上,用户和系统进程可以使用如下命令加载其他环境版本
$ rvm use jruby-1.7.1
并以这种方式设置默认系统发行版
$ rvm --default use 2.0
Web 服务器
Ruby on Rails,像 Sinatra 和许多其他流行的 Ruby 框架或领域特定语言一样,都是基于名为 Rack 的接口。Rack 在支持 Ruby 的 Web 服务器和 Ruby 框架之间提供了最小的抽象。Rack 负责调用在启动文件 config.ru 中指定的应用程序主实例。
因此,托管 Ruby Web 应用程序的 Web 服务器必须了解 Rack 如何通信。有了稳定且干净的 Ruby 环境,您就可以构建能够与 Rack 通信的 Web 服务器了。
使用 Ruby,您可以在许多 Web 服务器之间进行选择。您可能听说过 Mongrel、Unicorn、Thin、Reel 或 Goliath。对于典型的 Rails 部署,Passenger 是最受欢迎的选择之一。它与 Apache 和 Nginx 集成良好,因此在本例中,让我们设置一个 Apache + Passenger 配置。
Passenger 安装Passenger 由 Phusion 开发,以前也称为 mod_rails 或 mod_rack,是一个模块,允许您在流行的 Web 服务器容器 Apache 或 Nginx 中发布 Ruby 应用程序。Passenger 提供“社区”免费版和企业版,企业版包括商业支持和高级功能。
如果您选择通过软件包安装 Ruby,则可以通过 RPM 或 DEB 存储库方便地获得 Passenger,并且 yum 或 apt-get 将安装所有必需的软件。
在 RVM 自定义的系统上,要安装 Passenger 的免费版本,您需要通过 Ruby gems 添加 gem
$ sudo gem install passenger
现在,您可以通过执行 gem 提供的脚本来安装服务器模块(撰写本文时最新版本为 4.0.33)
# passenger-install-apache2-module
让我们仅选择 Ruby,并跳过 Python、Node.js 和 Meteor 支持。如果您的系统缺少软件要求,该脚本将为您提供确切的 yum 或 apt-get 命令行提示,以满足这些依赖项。
经过一些编译时间后,您将看到 Passenger 配置,其中包含有用且不言自明的输出。具体而言,将加载 Passenger 到 Apache 的指令复制到您的主 Apache 配置文件 (apache2.conf 或 httpd.conf) 中
LoadModule passenger_module
/usr/local/rvm/gems/ruby-2.0.0-p353/gems/passenger-4.0.33/
↪buildout/apache2/mod_passenger.so
PassengerRoot /usr/local/rvm/gems/ruby-2.0.0-p353/gems/
↪passenger-4.0.33
PassengerDefaultRuby /usr/local/rvm/wrappers/ruby-2.0.0-p353/ruby
最后,重启 Apache。Et voilà,现在您可以托管 Ruby Web 应用程序了。
虚拟主机如果您的目标是在同一服务器上托管一个或多个 Ruby 应用程序,则应将每个实例激活为虚拟主机。Ruby 托管最重要的指令是 DocumentRoot。它必须指向应用程序根项目目录中的 public/ 目录。public/ 目录是 Rails 应用程序的默认公共路径。假设您有一个用 Rails 制作的 Kolobok 应用程序,并且您必须将其部署到 kolobok.example.com 服务器上的 DNS 区域 kolobok.example.com。这是一个 VirtualHost 示例
<VirtualHost *:80>
ServerName kolobok.example.com
DocumentRoot /srv/www/kolobok/public
<Directory /srv/www/kolobok/public>
# This relaxes Apache security settings.
AllowOverride all
# MultiViews must be turned off.
Options -MultiViews
</Directory>
</VirtualHost>
]]>
</code></pre>
Now, if you have put your application in /srv/www/kolobok, and it's well
configured (configured and binded to the database and so on), enable the
virtual host, reload Apache, and your application is published.
Automating Software Deployments
Ages ago, it was common to deploy Web applications by doing a bulk copy of
files via FTP, from the developer's desktop to the server hosting space, or
by downloading through Subversion or Git. Although this approach still works
for simpler PHP applications, it won't fit more complex projects made
using more complex frameworks, such as Rails.
In fact, a Rails application is not made only of the source code files. To
make a Rails application ready, you have to download and compile its
dependencies as gems (by running bundle), safely manage database access and
other configurations, migrate the database (create the database and the
schema by executing a list of files containing SQL instructions in the Ruby
language), adjust paths for shared content (like images, videos and so on),
precompile the assets (that is, optimizing static content, such as
JavaScript and CSS), and perform many other steps in a large and complex work flow.
You can execute these steps by writing your own scripts, maybe in Ruby or
bash, but this task is tedious and wastes your time. You should instead
invest your time by writing good tests.
The Ruby community provides several ways to accomplish the whole deploy
task, and one very popular method uses Capistrano. Capistrano lets you write a set of
"recipes" that will "cook" your application in the
production environment. Common tasks executed by Capistrano are: 1) pulling
the source code from a git or svn repository; 2) putting it in the right
location; 3) checking if a bundle is needed and, if yes, bundling your
gems; 4)
checking if migrations are required and, if yes, running them; 5) checking if assets
precompile is required and, if yes, precompiling; and 6) checking other Rake tasks
you have defined and running them in order. If the whole recipe fails,
Capistrano will keep the current software release in production; otherwise,
it will substitute the latest release with the one you've just
deployed. Capistrano is a largely tested and very reliable tool. You
definitely can trust it.
Configuring Capistrano
To use Capistrano, you just need to install it through Ruby gems on the
system where the deploy will be done (not on the server):
$ gem install capistrano
When Capistrano is available, you'll have two new binaries in your
PATH: capify and cap. With capify, you build your deploy skeleton.
So, cd
to the
project directory and type:
$ capify .
This command creates a file named Capfile and a config/deploy.rb file.
Capfile tells Capistrano where the right deploy.rb configuration file is.
This is the file that includes your recipes, and typically it's kept in the
project's config/ directory.
Next, verify that Capistrano is installed correctly, and see the many useful
tasks it comes with:
$ cap -T
cap deploy # Deploys your project.
cap deploy:check # Tests deployment dependencies.
cap deploy:cleanup # Cleans up old releases.
cap deploy:cold # Deploys and starts a 'cold' application.
cap deploy:create_symlink # Updates the symlink to the most recently
# deployed...
cap deploy:migrations # Deploys and runs pending migrations.
cap deploy:pending # Displays the commits since your last
# deploy.
cap deploy:pending:diff # Displays the 'diff' since your last
# deploy.
cap deploy:rollback # Rolls back to a previous version and
# restarts.
cap deploy:rollback:code # Rolls back to the previously deployed
# version.
cap deploy:setup # Prepares one or more servers for
# deployment.
cap deploy:symlink # Deprecated API.
cap deploy:update # Copies your project and updates the
# symlink.
cap deploy:update_code # Copies your project to the remote
# servers.
cap deploy:upload # Copies files to the currently deployed
# version.
cap invoke # Invokes a single command on the remote
# servers.
cap link_shared # Link cake, configuration, themes, upload,
# tool
cap shell # Begins an interactive Capistrano session.
The user that will deploy the application will need valid SSH access to
the server (in order to perform remote commands with Capistrano) and
write permissions to the directory where the project will be deployed.
The directory structure created on the server in this directory allows you to
maintain software releases. In the project's document root, Capistrano
keeps two directories, one that contains the released software (releases/,
by default it keeps the latest ten releases), and another that contains
shared or static data (shared/). Moreover, Capistrano manages a symbolic
link named current that always points to the most recent
successfully deployed release.
In practice, each time Capistrano is invoked to deploy an application, it
connects via SSH, creates a temporary release directory named with the
current timestamp (for example, releases/20140115120050), and runs the process
(pull, bundle, migrate and so on). If it finishes with no errors, as final step,
Capistrano links the symlink "current" to
releases/20140115120050.
Otherwise, it keeps "current" symlinked with the latest directory
where the deploy was successful.
So with Capistrano, the system administrator will set the virtual server
DocumentRoot directive to the current directory of the released application
version:
DocumentRoot /srv/www/kolobok/current/public
The Anatomy of a deploy.rb File
A deploy.rb file is virtually made of two parts: one that defines the standard
configurations, like the repository server or the path to
deploy files physically, and another that includes custom tasks defined by the developer
responsible for deploying the application.
Let's deploy the Kolobok application. Open the
kolobok/config/deploy.rb file with your favourite editor, delete the example
configuration and begin to code it from scratch. A deploy.rb file is
programmed in Ruby, so you can use Ruby constructs in your tasks, beyond the
Capistrano "keywords".
Let's start by requiring a library:
require "bundler/capistrano"
This statement orders Capistrano to do the gem bundle each time it's
necessary. Good gem files separate required dependency gems in this way:
group :test do
gem 'rspec-rails'
gem 'capybara'
gem 'factory_girl_rails'
end
group :production do
gem 'execjs'
gem 'therubyracer'
gem 'coffee-rails', '~> 3.1.1'
end
Only the gems common to all environments and included in the :production
group are bundled. Gems belonging to :development and :test environments
are not. And the first time you deploy your application, a bundle install
is executed to bundle all the requirements as specified. The next
time you deploy the software, gems are downloaded, compiled or
removed only if the Gemfile and the Gemfile.lock have changed. The complete
bundle is installed in shared/ and soft-linked into the current instance.
By following this approach, less disk space is required.
Then, from Rails 3.1, it's common to release applications with the
assets pipeline. The pipeline is active if in
config/environments/production.rb the following variable is set to true:
config.assets.compile = true
If your application will use the pipeline, you need to precompile
it. The Rake task to precompile assets is bundle exec rake
assets:precompile
. To insert this task into your work flow and keep the
generated assets pipeline in shared/ and linked into the current release,
load the standard assets functionality:
load "deploy/assets"
After loading the main requirements, specify the application name,
the path on the server where it will be deployed, and the user allowed to
SSH:
set :application, "kolobok"
set :deploy_to, "/srv/www/kolobok"
et :user, "myuser"
With Rails > 3, it's recommended to invoke Rake (it's used to do
the database migrations and to precompile the assets pipeline) with the
correct bundled Rake version in the bundle. So, specify the exact
rake
command:
set :rake, 'bundle exec rake'
Now it's time to configure the repository from which to pull the project
source code:
set :scm, :git
set :branch, "master"
set :repository, "git://github.com/myusername/kolobok.git"
Finally, set the server names:
role :web, "kolobok.example.com"
role :app, "kolobok.example.com"
role :db, "mydb.example.com", :primary => true
web
is the address of the responding Web server, and
app
is where the application will be deployed. These roles are
the same if the application runs on only one host rather than on a cluster.
db
is the address of the database, and
primary => true
means that
migrations will be run there.
Now you have a well-defined deploy.rb and the right server configurations.
Begin by creating the structure tree (releases/ and static/) on the server,
from the desktop host:
$ cap deploy:setup
Releasing Software
After having set up the project directory on the server, run the first
deploy:
$ cap deploy:cold
The actions performed by Capistrano follow the standard pattern: git
checkout, bundle, execute migrations, assets precompile. If everything is
fine, your application is finally published as a reliable versioned
release, with a current symlink.
Normal deploys (skipping the first Rails app configuration, such as
creating the database) will be done in the future by invoking:
$ cap deploy
If you notice that some errors occurred with the current application in
production, you immediately can roll back to the previous release by
calling Capistrano like this:
$ cap deploy:rollback
Easy, reliable and smart, isn't it?
Custom Tasks
When you deploy a more complex application, you'll normally be
handling more complex recipes than the standard Capistrano procedure. For
example, if you want to publish an application on GitHub and release it
open source, you won't put configurations there (like credentials to
access databases or session secret tokens). Rather, it's preferable
to copy them in shared/ on the server and link them on the fly
before modifying the database or performing your tasks.
In Capistrano, you can define hooks to actions to force the tool to execute
required actions before or after other actions. It might be useful, for
instance, to link a directory where users of kolobok have uploaded files.
If you move the current directory to another release path, you might
discover that those files are no longer available to users. So, you can
define a final task that, after having deployed code, links the
shared/uploads into your current release in public/uploads directory.
Notice how this can be managed with ease by exploiting the presence of
the shared_path and release_path paths variables:
desc "Link uploaded directory"
task :link_uploads do
run "ln -nfs #{shared_path}/uploads
↪#{release_path}/public/uploads"
end
Finally, another common task to perform is to restart the application
instance into the server container. In case of Passenger, it's enough
to touch
the tmp/restart.txt file. So, you can write:
desc "Restart Passenger"
task :restart do
run "cd #{current_path} && touch tmp/restart.txt"
end
You execute these two tasks automatically by hooking them at the end of the
deploy flow. So add this extra line just before the tasks definitions:
after "deploy:update_code", :link_uploads, :restart
Performance Issues?
People often complain of Rails' performance in production environments.
This is a tricky topic. Tuning servers and application responsiveness are
rather hard tasks that cannot be discussed briefly, so I don't cover them
here. To make your
application faster, you should involve several technologies and engineering
patterns, like setting intermediate caching services, serving static and
dynamic content with different server containers and monitoring the
application with tools like New Relic to find bottlenecks. After having set
up the right environment to host the application, this is the next
challenge—optimizing. Happy deploys!
Resources
Rack: http://rack.github.com
RVM: https://rvm.ruby-lang.org.cn
Phusion Passenger: https://www.phusionpassenger.com
Capistrano: https://github.com/capistrano/capistrano