管理 Rails 应用程序

Rails 是一个非常受欢迎的 Web 应用程序框架(从某种意义上说,是由于它被广泛应用而不是人们真正喜欢它)。 因此,在某些时候,你可能会被要求管理它。 本节要介绍的处方包含了安装一台运行 Rails 应用程序服务器所要做的绝大部分工作。 本处方假定你会使用 Nginx 和 Passenger 作为 Web 服务器, 然而你也可以轻松地修改本处方,使用 Apache 替换它。

操作步骤

  1. 创建 rails 模块的目录结构:

    # mkdir /etc/puppet/modules/rails
    # mkdir /etc/puppet/modules/rails/manifests
    # mkdir /etc/puppet/modules/rails/templates
    # mkdir /etc/puppet/modules/rails/files
    
  2. 使用如下内容创建 /etc/puppet/modules/rails/manifests/init.pp 文件:

    class rails {
        include rails::passenger
    
        package { "bundler":
            provider => gem,
            ensure   => installed,
        }
    
        define app( $sitedomain ) {
            include rails
    
            file { "/opt/nginx/sites-available/${name}.conf":
              content => template("rails/app.conf.erb"),
              require => File["/opt/nginx/sites-available"],
            }
    
            file { "/opt/nginx/sites-enabled/${name}.conf":
              ensure  => link,
              target  => "/opt/nginx/sites-available/${name}.conf",
              require => File["/opt/nginx/sites-enabled"],
              notify  => Exec["reload-nginx"],
            }
    
            file { "/opt/nginx/conf/includes/${name}.conf":
              source => [ "puppet:///modules/rails/${name}.conf",
                          "puppet:///modules/rails/empty.conf" ],
              notify => Exec["reload-nginx"],
            }
    
            file { [ "/var/www",
                     "/var/www/${name}",
                     "/var/www/${name}/releases",
                     "/var/www/${name}/shared",
                     "/var/www/${name}/shared/config",
                     "/var/www/${name}/shared/log",
                     "/var/www/${name}/shared/system" ]:
                ensure => directory,
                mode   => 775,
                owner  => "www-data",
                group  => "www-data",
            }
        }
    }
    
  3. 使用如下内容创建 /etc/puppet/modules/rails/manifests/passenger.pp 文件:

    class rails::passenger {
        $passenger_version = "3.0.7"
        $passenger_dependencies = [ "build-essential",
                                    "libcurl4-openssl-dev",
                                    "libssl-dev",
                                    "ruby",
                                    "rubygems" ]
    
        package { $passenger_dependencies: ensure => installed }
            exec { "install-passenger":
            command => "/usr/bin/gem install passenger
     --version=${passenger_version}",
             unless => "/usr/bin/gem list | /bin/grep passenger |/bin/
     grep ${passenger_version}",
            require => [ Package["rubygems"], Package[$passenger_
             dependencies] ],
            timeout => "-1",
        }
    
        exec { "install-passenger-nginx-module":
            command => "/usr/lib/ruby/gems/1.8/gems/passenger-
      ${passenger_version}/bin/passenger-install-nginx-module
     --auto --auto-download --prefix=/opt/nginx",
            creates => "/opt/nginx/sbin/nginx",
            require => Exec["install-passenger"],
            timeout => "-1",
        }
    
        file { [ "/opt/nginx",
                 "/opt/nginx/conf",
                 "/opt/nginx/conf/includes",
                 "/opt/nginx/sites-enabled",
                 "/opt/nginx/sites-available",
                 "/var/log/nginx" ]:
            ensure => directory,
            owner => "www-data",
            group => "www-data",
        }
    
        file { "/opt/nginx/sites-enabled/default":
            ensure => absent,
            require => Exec["install-passenger-nginx-module"],
        }
    
        file { "/opt/nginx/conf/nginx.conf":
            content => template("rails/nginx.conf.erb"),
            notify  => Exec["reload-nginx"],
            require => Exec["install-passenger-nginx-module"],
        }
    
        file { "/etc/init.d/nginx":
            source  => "puppet:///modules/rails/nginx.init",
            mode    => "700",
            require => Exec["install-passenger-nginx-module"],
        }
    
        service { "nginx":
            enable => true,
            ensure => running,
            require => File["/etc/init.d/nginx"],
        }
    
        exec { "reload-nginx":
            command     => "/opt/nginx/sbin/nginx -t && /etc/init.d/nginx reload",
            refreshonly => true,
            require     => Exec["install-passenger-nginx-module"],
        }
    }
    
  4. 使用如下内容创建 /etc/puppet/modules/rails/templates/app.conf.erb 文件:

    server {
        listen 80;
        root /var/www/<%= name %>/current/public;
        server_name <%= sitedomain %>;
        access_log /var/log/nginx/<%= name %>.access.log;
        error_log /var/log/nginx/<%= name %>.error.log;
    
        passenger_enabled on;
        passenger_min_instances 1;
    }
    
    passenger_pre_start http://<%= sitedomain %>;
    
  5. 使用如下内容创建 /etc/puppet/modules/rails/templates/nginx.conf.erb 文件:

    events {
      worker_connections 1024;
      use epoll;
    }
    
    http {
      passenger_root /usr/lib/ruby/gems/1.8/gems/passenger-<%=
      passenger_version %>;
    
      server_names_hash_bucket_size 64;
    
      sendfile on;
      tcp_nopush on;
      tcp_nodelay off;
      client_body_temp_path /var/spool/nginx-client-body 1 2;
    
      client_max_body_size 100m;
    
      include /opt/nginx/conf/mime.types;
      default_type application/octet-stream;
    
      log_format main '$remote_addr - $remote_user [$time_local] '
              $request" $status $body_bytes_sent "$http_referer" '
              '"$http_user_agent" "$http_x_forwarded_for"' ;
    
      access_log /var/log/nginx/access.log main;
    
      gzip on;
      gzip_http_version 1.0;
      gzip_comp_level 2;
      gzip_proxied any;
      gzip_min_length 1100;
      gzip_buffers 16 8k;
      gzip_types text/plain text/html text/css application/x-javascript
       text/xml application/xml application/xml+rss text/javascript;
      gzip_disable "MSIE [1-6].(?!.*SV1)";
      gzip_vary on;
    
      include /opt/nginx/sites-enabled/*;
    }
    
  6. 使用如下内容创建 /etc/puppet/modules/rails/files/nginx.init 文件:

    #!/bin/sh
    
    ### BEGIN INIT INFO
    # Provides: nginx
    # Required-Start: $all
    # Required-Stop: $all
    # Default-Start: 2 3 4 5
    # Default-Stop: 0 1 6
    # Short-Description: starts the nginx web server
    # Description: starts nginx using start-stop-daemon
    ### END INIT INFO
    
    PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
    DAEMON=/opt/nginx/sbin/nginx
    NAME=nginx
    DESC=nginx
    
    test -x $DAEMON || exit 0
    
    # Include nginx defaults if available
    if [ -f /etc/default/nginx ] ; then
       . /etc/default/nginx
    fi
    
    set -e
    
    # Return LSB status, grabbed from a newer lsb-base
    status_of_proc () {
        local pidfile daemon name status
    
        pidfile=
        OPTIND=1
        while getopts p: opt ; do
     case "$opt" in
                p) pidfile="$OPTARG";;
            esac
     done
    
      shift $(($OPTIND - 1))
    
        if [ -n "$pidfile" ]; then
      pidfile="-p $pidfile"
        fi
    
      daemon="$1"
        name="$2"
    
        status="0"
        pidofproc $pidfile $daemon >/dev/null || status="$?"
        if [ "$status" = 0 ]; then
      log_success_msg "$name is running"
            return 0
        else
      log_failure_msg "$name is not running"
            return $status
        fi
    }
    
    . /lib/lsb/init-functions
    
    case "$1" in
      start)
        echo -n "Starting $DESC: "
        start-stop-daemon --start --quiet --pidfile /var/run/$NAME.pid \
         --exec $DAEMON -- $DAEMON_OPTS || true
     echo "$NAME."
        ;;
      stop)
        echo -n "Stopping $DESC: "
        start-stop-daemon --stop --quiet --pidfile /var/run/$NAME.pid \
         --exec $DAEMON || true
     echo "$NAME."
        ;;
      restart|force-reload)
        echo -n "Restarting $DESC: "
        start-stop-daemon --stop --quiet --pidfile \
         /var/run/$NAME.pid --exec $DAEMON || true
      sleep 1
        start-stop-daemon --start --quiet --pidfile \
         /var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS || true
     echo "$NAME."
        ;;
      reload)
        echo -n "Reloading $DESC configuration: "
        start-stop-daemon --stop --signal HUP --quiet --pidfile \
         /var/run/$NAME.pid --exec $DAEMON || true
     echo "$NAME."
        ;;
      status)
        status_of_proc -p /var/run/$NAME.pid "$DAEMON" nginx \
         && exit 0 || exit $?
        ;;
      *)
        N=/etc/init.d/$NAME
        echo "Usage: $N {start|stop|restart|reload|forcereload|
     status}" >&2
        exit 1
        ;;
    esac
    exit 0
    
  7. 添加如下代码到一个节点:

    rails::app { "furiouspigs":
        sitedomain => "furiouspigs.com",
    }
    
  8. 运行 Puppet:

    # puppet agent --test
    info: Retrieving plugin
    info: Caching catalog for cookbook.bitfieldconsulting.com
    info: Applying configuration version '1309960678'
    
    notice: /Stage[main]/Rails::Passenger/File[/opt/nginx]/ensure:
    created
    
    notice: /Stage[main]/Rails::Passenger/File[/opt/nginx/sitesenabled]/
    ensure: created
    
    notice: /Stage[main]//Node[cookbook]/Rails::App[furiouspigs]/
    File[/opt/nginx/sites-enabled/furiouspigs.conf]/ensure: created
    
    notice: /Stage[main]/Rails::Passenger/File[/opt/nginx/conf]/
    ensure: created
    
    notice: /Stage[main]/Rails::Passenger/File[/opt/nginx/conf/
    includes]/ensure: created
    
    notice: /Stage[main]//Node[cookbook]/Rails::App[furiouspigs]/
    File[/opt/nginx/conf/includes/furiouspigs.conf]/ensure: defined
    content as '{md5}d41d8cd98f00b204e9800998ecf8427e'
    
    notice: /Stage[main]/Rails::Passenger/File[/opt/nginx/sitesavailable]/
    ensure: created
    
    notice: /Stage[main]//Node[cookbook]/Rails::App[furiouspigs]/
    File[/opt/nginx/sites-available/furiouspigs.conf]/ensure: defined
    content as '{md5}c1a4c2bc4e7381b1c2f88dfee004a594'
    
    notice: /Stage[main]/Rails::Passenger/Exec[install-passenger]/
    returns: executed successfully
    
    notice: /Stage[main]/Rails::Passenger/Exec[install-passengernginx-
    module]/returns: executed successfully
    --- /opt/nginx/conf/nginx.conf 2011-07-06 14:04:33.231999538
    +0000
    +++ /tmp/puppet-file20110706-5343-k8ouds-0 2011-07-06
    14:04:34.246867124 +0000
    ...
    
    info: /Stage[main]/Rails::Passenger/File[/opt/nginx/conf/nginx.
    conf]: Filebucketed /opt/nginx/conf/nginx.conf to puppet with sum
    34d60856b6570e9d59cd6eecde5da000
    
    notice: /Stage[main]/Rails::Passenger/File[/opt/nginx/conf/nginx.
    conf]/content: content changed '{md5}34d60856b6570e9d59cd6eecde5
    da000' to '{md5}72132deeb45e6ee5b83cd246dffefc5f'
    info: /Stage[main]/Rails::Passenger/File[/opt/nginx/conf/nginx.
    conf]: Scheduling refresh of Exec[reload-nginx]
    notice: /Stage[main]/Rails::Passenger/Exec[reload-nginx]:
    Triggered 'refresh' from 1 events
    notice: Finished catalog run in 398.73 seconds
    

工作原理

这个处方比本书中其他的处方更长、更复杂,因此需要更详细的解释。 如果你觉得这有些烦人,尽管去使用这个配方吧,不必担心它是如何工作的。 稍后当你想学习更多详细工作过程时,可以再回过头来看这些解释。

上面所有代码的目的就是可以让你写出如下的实例化代码:

rails::app { "furiouspigs":
    sitedomain => "furiouspigs.com",
}

这需要不少幕后工作。我们需要安装配置包含 Passenger 模块的 Nginx, 配置应用程序的虚拟主机,包括所有应用程序特定的配置(比如重定向以及其他服务配置), 安装 Ruby 和 Rubygems,安装 Bundler,并创建要部署的应用程序所需的所有目录。

Nginx 和 Passenger

此处分离出的 passenger.pp 文件用于安装 Nginx 和 Passenger 所需的一切。 之前曾经提到过,Nginx 没有像 Apache 一样的动态模块概念, 因此,你不能仅仅通过安装发行版中的 Nginx 并安装提供 Passenger 功能的软件包来实现。 Nginx 必须与你想要的任何模块一起编译。

幸好,Phusion 社区里的好心人已经为我们提供了一个编译脚本(passengerinstall-nginx-module)。 一旦你已经安装了 Passenger 的 gem 包,这个脚本就会帮你完成编译工作。 所以首先需要做的事就是安装 Passenger 的 gem

class rails::passenger {
    $passenger_version = "3.0.7"
    $passenger_dependencies = [ "build-essential",
                                "libcurl4-openssl-dev",
                                "libssl-dev",
                                "ruby",
                                "rubygems" ]
    package { $passenger_dependencies: ensure => installed }

    exec { "install-passenger":
        command => "/usr/bin/gem install passenger
 --version=${passenger_version}",
        unless => "/usr/bin/gem list | /bin/grep passenger \
 |/bin/grep ${passenger_version}",
        require => [ Package["rubygems"], Package[$passenger_
         dependencies] ],
        timeout => "-1",
    }

我们把要安装的 Passenger 版本号设置在变量 $passenger_version 中, 因为 Nginx 需要知道 Passenger 的安装路径(路径中包括版本号)。 所以我们会在 nginx.conf 模板中引用 $passenger_version 变量。

下一步是运行 passenger-install-nginx-module 脚本:

    exec { "install-passenger-nginx-module":
        command => "/usr/lib/ruby/gems/1.8/gems/passenger-${passenger_
 version}/bin/passenger-install-nginx-module --auto --autodownload
 --prefix=/opt/nginx",

        creates => "/opt/nginx/sbin/nginx",
        require => Exec["install-passenger"],
        timeout => "-1",
    }

注记

你应该注意到了,此处 gem 的路径是固定的 /usr/lib/ruby/gems/1.8/gems。 这有些脆弱?—?在大部分生产基础设施中,我使用 RVM 管理 Ruby 版本和不同版本的 gemsets, 这就解决了由于使用固定路径带来的脆弱性。 然而,添加 RVM 会使本处方变得难于理解,所以我将使用 RVM 的部分放在 更多用法 小节中。 一旦你熟悉了这个处方,就可以修改它,使之适合你自己的需求,包括整合 RVM。

这也就意味着,如果你使用 Ruby 1.9,这个处方不会工作,当本书出版后你读到此处时很可能会发生这种情况。 如果是这样,或者你遇到其他问题,请手工运行 gem contents passenger 命令之后查找 passenger-installnginx-module 脚本的路径。

下一步,我们创建了 Nginx 配置文件所需的目录结构:

file { [ "/opt/nginx",
         "/opt/nginx/conf",
         "/opt/nginx/conf/includes",
         "/opt/nginx/sites-enabled",
         "/opt/nginx/sites-available",
         "/var/log/nginx" ]:
    ensure => directory,
    owner  => "www-data",
    group  => "www-data",
}

我们要删除默认的 Nginx 虚拟主机配置,否则可能会干扰我们要创建的虚拟主机。 这可以通过如下代码实现:

file { "/opt/nginx/sites-enabled/default":
    ensure  => absent,
    require => Exec["install-passenger-nginx-module"],
}

实际上,如果你从源代码构建 Nginx 或是通过 Passenger 构建(本例中的方法),这是不需要的, 但是如果要想使本处方也适用于发行版的 Nginx 软件包,这会是有益的。

下面是 Nginx 的主配置文件:

file { "/opt/nginx/conf/nginx.conf":
    content => template("rails/nginx.conf.erb"),
    notify  => Exec["reload-nginx"],
    require => Exec["install-passenger-nginx-module"],
}

主配置文件使用 nginx.conf.erb 模板生成,因为我们需要插入之前定义的 Passenger 版本号:

passenger_root /usr/lib/ruby/gems/1.8/gems/passenger-<%= passenger_
version %>;

否则,它就是一个标准的 Nginx 配置,你也可以在模板中添加任何你的服务器所需的特殊参数。

因为我们没有使用发行版提供的软件包,所以我们需要为节点应用一个 init 脚本 (改编自 Ubuntu 版本,仅做了轻微的修改):

file { "/etc/init.d/nginx":
    source  => "puppet:///modules/rails/nginx.init",
    mode    => "700",
    require => Exec["install-passenger-nginx-module"],
}

为了运行 Nginx 服务需要如下代码:

service { "nginx":
    enable  => true,
    ensure  => running,
    require => File["/etc/init.d/nginx"],
}

要确保错误的配置不会使服务器宕机,配置文件的改变会通知如下的 “配置检查和重载” 资源:

exec { "reload-nginx":
    command     => "/opt/nginx/sbin/nginx -t \
 && /etc/init.d/nginx reload",
    refreshonly => true,
    require     => Exec["install-passenger-nginx-module"],
}
Rails

你已经设置好了 Passenger 和 Nginx,接下来配置 Rails 需求的类:

class rails {
    include rails::passenger

    package { "bundler":
        provider => gem,
        ensure => installed,
    }
}

Bundler 是一个管理应用程序或解决 gem 依赖关系的工具。 你可以使用手工方式(或通过 Puppet)指定要安装的所有 gem 包以及依赖的包, 取代这种方式的更好办法是使用 Bundler 来实现,它是 Rails 部署的一部分。 例如,你应该注意到了,我们没有安装 rails 的 gem;这通常是通过 Bundler 安装的, 或者在应用程序的 vendor 目录中已经提供了一份特定版本的 rails。 如果你没有使用 Bundler,或者你需要为你的 Rails 应用程序设置一些额外的依赖, 请在这个类中使用 Puppet 的 package 资源做相应的配置来安装它们。

rails 类的的主要部分是 define 函数 app, 对于你要管理的每个应用程序,它都会被实例化一次:

define app( $sitedomain ) {
    include rails

为应用程序做的第一件事是安装 Nginx 虚拟主机配置文件,它由 app.conf.erb 模板生成:

    file { "/opt/nginx/sites-available/${name}.conf":
        content => template("rails/app.conf.erb"),
        require => File["/opt/nginx/sites-available"],
    }

    file { "/opt/nginx/sites-enabled/${name}.conf":
        ensure => link,
        target => "/opt/nginx/sites-available/${name}.conf",
        require => File["/opt/nginx/sites-enabled"],
        notify => Exec["reload-nginx"],
    }

虚拟主机配置文件的模板相当小:

server {
    listen 80;
    root /var/www/<%= name %>/current/public;
    server_name <%= sitedomain %>;
    access_log /var/log/nginx/<%= name %>.access.log;
    error_log /var/log/nginx/<%= name %>.error.log;

    passenger_enabled on;
    passenger_min_instances 1;
}

passenger_pre_start http://<%= sitedomain %>;

通常一个应用程序需要特定的 Nginx 配置指令,比如 redirects。 你可以在 Rails 模块中添加一个名为 files/furiouspigs.conf 的文件来包含这些配置指令。 Puppet 会从如下的这段代码找到这个文件,并分发给节点:

    file { "/opt/nginx/conf/includes/${name}.conf":
        source => [ "puppet:///modules/rails/${name}.conf",
                    "puppet:///modules/rails/empty.conf" ],
        notify => Exec["reload-nginx"],
    }

注意这个文件使用了多个源,第二个源 empty.conf 确保 Puppet 不会因为应用程序指定的配置文件不存在而抱怨。

最后我们要确保为准备部署的标准 Rails 目录结构配置 www-data 用户属主及适当的权限(775)。 如果你的 Nginx 以及部署的应用程序以不同的用户身份执行,请使用你的用户名替换所有的 www-data。

       file { [ "/var/www",
                "/var/www/${name}",
                "/var/www/${name}/releases",
                "/var/www/${name}/shared",
                "/var/www/${name}/shared/config",
                "/var/www/${name}/shared/log",
                "/var/www/${name}/shared/system" ]:
            ensure => directory,
            mode   => 775,
            owner  => "www-data",
            group  => "www-data",
        }
    }
}

更多用法

使用 Puppet 管理 Rails 应用程序,你可能还需要考虑一些其他的事情。

RVM

正如我之前所提到的,使用 RVM 管理多版本 Ruby 和多版本的 gemsets 是一种强大的解决方案。 当然,使用 RVM 也带来了自身的有趣问题?—?RVM 的开发活跃,其主体经常变化。 无论如何,RVM 为我们带来的好处还是比其带来的麻烦要多的多。 所以,我建议你对生产线上的 Rails 站点,或许可以通过类似这样的代码应用 RVM:

class rails::rvm {
  package { [ "autoconf",
              "bison",
              "curl",
              "libreadline-dev",
              "subversion",
              "zlib1g-dev" ]:
      ensure => installed
  }

  file { "/usr/local/bin/rvm-install-system-wide":
      source => "puppet:///modules/rails/rvm-install-system-wide",
      mode   => "700",
  }

  exec { "install-rvm":
      command => "/usr/local/bin/rvm-install-system-wide",
      creates => "/usr/local/bin/rvm",
      require => [ Package["curl"], Package["subversion"],
                   File["/usr/local/bin/rvm-install-system-wide"] ],
      logoutput => on_failure,
  }

  append_if_no_such_line { "setup-rvm-shell-environment":
      file => "/etc/bash.bashrc",
      line => "[[ -s /usr/local/rvm/scripts/rvm ]] \
 && . /usr/local/rvm/scripts/rvm",
  }
}

rvm-install-system-wide 脚本来自 RVM 网站: https://rvm.beginrescueend.com/install/rvm

日志滚动

在生产环境中你可能会需要为 Nginx 和 Rails 生成的日志文件添加 logrotate 配置片段, 以确保这些日志不会逐步占满你的磁盘。因为篇幅限制和保持简单的原因,本例省略了对日志滚动的配置。

数据库

本处方中,未对 Rails 应用程序创建任何数据库及访问数据库的用户; 配置何种数据库取决于你团队的开发者正在使用何种数据库(MySQL、Postgres、MongoDB 等), 你需要自行添加管理数据库的 Puppet 代码。如果使用的是 MySQL, 你可以参考 创建 MySQL 数据库及用户 一节的内容并作适当的改写。

SSL 证书

一些应用程序会需要 SSL 证书并为 vhost 配置安全的 URLs,例如:处理网上支付。 这已超出了本处方的讲解范围,但你应该可以发现添加所需的代码并不困难。 你可以在 rails::app 函数的定义中添加一个可选参数,例如:

define app( $sitedomain, $ssl = false ) {

并添加如下代码处理这个参数:

if $ssl {
   file { "/etc/ssl/certs/${name}.crt":
       source => "puppet:///modules/rails/${name}.crt",
   }
}

然后,只要使用如下的代码对你的应用程序进行实例化即可:

rails::app { "irritatedbadgers":
    sitedomain => "irritatedbadgers.com",
    ssl        => true,
}