Capistrano 版本 3 发布公告

2013 年 6 月 1 日

经过近乎多年的努力,Capistrano 团队(Tom 和我)很高兴地宣布 Capistrano 的第一个重大版本发布,距上次架构大修已近 5 年。

两次架构大修之间间隔如此之长,原因很多,但可以概括为 Capistrano 是一款广泛使用的工具,在软件部署方面,停机时间至关重要。如果我们在 Capistrano 中进行重大更改,可能会导致许多网站下线,并让许多人感到非常不高兴。直到现在,我们一直认为,升级路径略微不稳定带来的好处,并不值得冒停机风险。

从历史上看,我们才刚刚开始掌握 Ruby 1.9,而 Bundler 的普及意味着现在锁定特定版本的 Gem 变得非常容易。随着 Ruby 生态系统中其他工具的发展,我们更容易对依赖于数百万人使用的工具进行重大更改。

设计目标

我们对这次发布有一些目标,按重要性排序如下:

  • 告别我们自己的 DSL 解决方案。 优秀的 DSL 替代方案(Rake、Sake、Thor 等)已被广泛使用。
  • 更好的模块化。 为了让 Rails 社区以外的人也能从 Capistrano 的最佳实践工作流程中受益,并让 Rails 社区的人可以选择他们使用的组件的支持(数据库迁移、资产管道等)。
  • 更轻松的调试。 Capistrano 的许多问题都源于 PTY 与非 TTY 环境、登录和非登录 Shell 周围的环境问题,更不用说环境管理器(如 rvm、rbenv 和 nvm)了。
  • 速度。 我们知道,在许多环境中,部署速度是一个重要因素,自从 Rails 引入了资产管道之后,以前只需 5 秒的部署现在可能需要 5 分钟。这确实主要不受我们控制,但通过改进对并行性的支持,以及滚动重启,我们相信部署会更快,并且更容易保持快速运行。
  • 适用性。 我们一直认为 Capistrano 是一个糟糕的系统配置工具,而且服务器通常最好用 Chef、Puppet 或类似工具进行设置,虽然我们仍然同意这一点,但 Capistrano 的新功能确实非常适合与这些类型的工具集成。

缺少什么?

在我们过于兴奋之前,值得列出第三版中尚未存在的功能。

  • SSH 网关支持 第三版中尚未实现 SSH 网关支持,我希望这很快就会实现。由于我对此没有直接需求,因此我还没有测试它以实现它的方法。
  • Mecurial、Subversion 和 CVS 支持 这些已被移除,因为我们能够以一种非常简洁的方式实现 Git SCM,这与其他 SCM 不兼容。我们希望打破始终坚持最低公分母的循环,因此我们正在积极寻找对贡献或分享有关从您选择的源代码控制进行快速部署的最佳实践方法的专业知识感兴趣的人。
  • HOSTFILTERROLEFILTER 及其朋友 这些已被移除,因为我们一直认为它们是使用环境变量的糟糕设计决策的体现。这些将作为传递给 cap 的标志重新出现,以及可以在 Capistrano::Application Ruby 类上设置的选项。
  • Shell Shell 已暂时移除,等待更简洁的实现,我们内部正在使用一些东西,但它需要更好的 readline 支持,以及一些关于在某些服务器上出现问题而在其他服务器上没有出现问题时该怎么办的更多控制。
  • 冷部署 cap deploy:cold 是一个非常古老的遗留组件,最初来自 script/spinner 的时代,当时冷部署(启动未运行的 worker)和部署系统是不同的(重启现有的 worker 池,这并不有趣!)。总的来说,这些东西已经消失了,现在是 deploy:cold 消失的时候了。在我们可以找到的每种情况下,调用 setup、seed 和其他 Rake 任务都是安全的,不会出现问题,这应该是我们采取的方法。服务器上的任务应该是幂等的,如果某个任务被调用两次,就让它执行两次。

有什么新变化?

这里每个部分都应该有自己的子标题,因为一些新功能非常棒。

Rake 集成

我们已经放弃了自己的 DSL 实现,转而将 Capistrano 作为Rake 应用程序来实现。

Rake 一直支持作为子应用程序进行子类化,但文档很少。通过子类化 Rake::Application,可以指定Rakefile 的外观、搜索位置以及如何加载其他Rakefile

Rake DSL 被广泛使用,众所周知,功能非常强大。由于 Rake 本质上是一个依赖关系解析系统,它提供了很多不错的方法,例如,可以将构建 tarball 作为上传和部署的依赖关系。

这使我们能够完全放弃复制策略,因为它现在可以用不到十行代码从头开始实现。

指导原则是依赖关系解析和与其他工具的互操作性,例如

    # Capistrano 3.0.x
    task :notify do
      this_release_tag = sh("git describe --abbrev=0 --tags")
      last_ten_commits = sh("git log #{this_release_tag}~10..#{this_release_tag}")
      Mail.deliver do
        to      "[email protected]"
        subject "Releasing #{this_release_tag} Now!"
        body    last_ten_commits
      end
    end

    namespace :deploy
      task default: :notify
    end

最后三行依赖于 Rake 的增量任务声明,通过重新定义 deploy:default 任务并添加另一个依赖关系来实现。Rake 会在运行时自动解析此依赖关系,将最新的变更日志发送给您的团队,假设一切设置正确。

内置阶段支持

在以前版本的 Capistrano 中,阶段支持是事后才想到的,通过 capistrano-ext Gem 提供,后来合并到主代码库中,人们坚持使用 capistrano-ext 版本,无论如何。

在 Capistrano 3.0.x 中,阶段支持是内置的,在安装时,默认情况下会创建两个阶段,stagingproduction;添加更多阶段很容易,只需在 config/deploy/______.rb 中添加一个文件,该文件遵循我们在为您创建的示例中建立的约定。

要在安装时创建不同的阶段,只需将 STAGES 环境变量设置为用逗号分隔的阶段列表即可

    $ cap install STAGES=staging,production,ci,qa

并行

在旧版本的 Capistrano 中,有一个名为 parallel 的选项,用于在不同服务器组上以不同的方式运行不同的任务,它看起来像这样

    # Capistrano 2.0.x
    task :restart do
      parallel do |session|
        session.when "in?(:app)", "/u/apps/social/script/restart-mongrel"
        session.when "in?(:web)", "/u/apps/social/script/restart-apache"
        session.else "echo nothing to do"
      end
    end

这总让人感觉有点不干净,事实上,这只是一个黑客行为,最初是为了方便一家大型德国公司进行滚动部署而实现的,当时是由两位自由职业者进行咨询的。(提示,其中一位后来创办了 Travis-CI!)

在 Capistrano v3 中,等效的代码看起来像这样

    # Capistrano 3.0.x
    task :restart do
      on :all, in: :parallel do |host|
        if host.roles.include?(:app)
          execute "/u/apps/social/script/restart-mongrel"
        elsif host.roles.include?(:web)
          execute "/u/apps/social/script/restart-web"
        else
          info sprintf("Nothing to do for %s with roles %s", host,
          host.properties.roles)
        end
      end
    end

第二段代码,代表新的 Rake 派生的 DSL 并演示如何使用并行执行模式,代码稍微长一些,但我认为它更清晰,更符合 Ruby 代码的习惯用法,它对 Capistrano DSL 的工作原理的依赖更少。它还暗示了内置的日志记录子系统,请继续阅读以了解更多信息。

并行的其他模式包括

    # Capistrano 3.0.x
    on :all, in: :groups, limit: 3, wait: 5 do
      # Take all servers, in groups of three which execute in parallel
      # wait five seconds between groups of servers.
      # This is perfect for rolling restarts
    end

    on :all, in: :sequence, wait: 15 do
      # This takes all servers, in sequence and waits 15 seconds between
      # each server, this might be perfect if you are afraid about
      # overloading a shared resource, or want to defer the asset compilation
      # over your cluster owing to worries about load
    end

    on :all, in: :parallel do
      # This will simply try and execute the commands contained within
      # the block in parallel on all servers. This might be perfect for kicking
      # off something like a Git checkout or similar.
    end

内部任务,用于标准部署配方,会根据正常情况使用所有这些模式,无需再担心可怕的缓慢部署了!

流式 IO

这种 IO 流式模型意味着来自命令的结果、命令本身以及任何其他任意输出都将作为对象发送到具有 IO 式接口的类,该类知道如何处理这些东西。有一个 progress 格式化程序,它为调用的每个命令打印点,还有一个 pretty 格式化程序,它打印完整的命令、其在标准输出和标准错误上的输出,以及最终的返回状态。实现 HTML 格式化程序或向您的 IRC 房间或电子邮件报告的格式化程序将是微不足道的。我期待在社区中看到更多这样的格式化程序。

主机定义访问

如果您没有略过上面的 Parallism 部分,您可能已经注意到我们做了一些在 Capistrano v2 中不可能做到的巧妙的事情;我们在执行块中访问了 host

由于 Capistrano v2 中的许多原因,这在以前是不可能的,该块本质上只评估一次,并在每个主机上逐字调用。这导致了一些令人失望的缺失功能,例如无法从 Capistrano 中提取主机列表并检查角色以执行诸如控制 Chef solo 或类似操作之类的操作。

在 Capistrano v3 中,host 对象与定义服务器时创建的相同对象,并在内部使用,例如传递给 ERB 模板以渲染成功部署后转储到每个服务器上的最后部署消息。最后部署日志包含 Capistrano 在部署期间了解到的有关该服务器的所有信息。

Capistrano v2 的用户可能熟悉永恒的 cap deploy:cleanup 问题,当服务器在旧版本列表中存在差异时,这个问题就会出现,想象一下一个场景,有两个服务器,一个服务器自您启动以来一直是您的主力,它拥有数百个来自您过去几个月或几年中所有精彩部署的旧版本。第二个服务器已经加入集群大约一个月了,它没有完全干净地插入,因此旧版本的列表看起来有点奇怪,您手动删除了一些,无论如何那里可能只有十个左右的版本。

现在想象一下,你调用了 cap deploy:cleanup,旧的 capture() 实现只会在第一个匹配定义属性的服务器上静默运行,所以服务器一返回了一个包含大约 95 个旧时间戳的发布目录的列表。接下来,Capistrano v2 会在 **两个** 服务器上调用 rm -rf release1..release95,导致服务器二出错,并在服务器一上留下一个未定义的状态,因为 Capistrano 会简单地挂断两个连接。

现在可以更好地实现这个清理例程(这实际上或多或少是新 Gem 中的实际实现)

    # Capistrano 3.0.x
    desc "Cleanup all old releases (keeps #{fetch(:releases_to_keep_on_cleanup)}
    old releases"
    task :cleanup do
      keep_releases     = fetch(:releases_to_keep_on_cleanup)
      releases          = capture(:ls, fetch(:releases_directory))
      releases_to_delete = releases.sort_by { |r| r.to_i }.slice(1..-(keep_releases + 1))
      releases_to_delete.each do |r|
        execute :rm, fetch(:releases_directory).join(r)
      end
    end

这里需要注意的是,我们这个虚构的例子中的服务器一和服务器二都会独立地进行评估,当两个服务器都完成删除旧发布后,task :cleanup 块就会完成。

此外,在 Capistrano v3 中,大多数路径变量都是 [Pathname] 对象,因此它们可以原生响应诸如 #basename#expand_path#join 等操作。

警告: #expand_path 可能不会像你预期的那样工作,它会在你的 工作站 机器上执行,而不是在远程主机上执行,因此它可能会在远程存在但本地不存在的路径情况下返回错误。

主机属性

由于 host 对象现在可用于任务块,因此有必要使其能够针对它们存储任意值。

输入 host.properties。这是一个简单的 OpenStruct,可用于存储对你的应用程序很重要的任何其他属性。

它的用法示例可能如下

    h = SSHKit::Host.new 'example.com'
    h.properties.roles ||= %i{wep app}

更具表现力的命令语言

在 Capistrano v2 中,经常会发现诸如以下命令

    # Capistrano 2.0.x
    task :precompile, :roles => lambda { assets_role }, :except => { :no_release => true } do
      run <<-CMD.compact
        cd -- #{latest_release} &&
        RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} #{rake} assets:precompile
      CMD
    end

在 Capistrano v3 中,它看起来更像这样

    # Capistrano 3.0.x
    task :precompile do
      on :sprockets_asset_host, reject: lambda { |h| h.properties.no_release } do
        within fetch(:latest_release_directory) do
          with rails_env: fetch(:rails_env) do
            execute :rake, 'assets:precompile'
          end
        end
      end
    end

同样,在其他示例中,这种格式稍微长一些,但更具表现力,并且所有 shell 转义的噩梦都由内部为你处理,环境变量被大写并应用在正确的位置(即在本例中,在 cdrake 调用之间)。

这里还有其他选项,包括 as :a_user

更好的 magic 变量支持

在 Capistrano v2 中,存在一些魔法,如果调用一个变量并且会引发 NoMethodError(例如 latest_release_directory 变量),这个变量在全局命名空间中并不存在,作为回退,会查询 set() 变量列表。

这种魔法导致人们有时没有意识到正在使用魔法变量。Capistrano v2 的魔法变量系统也包含一种方法,可以在变量可能尚未设置的情况下 fetch(:some_variable, 'with a default value'),但它并没有被广泛使用,人们更经常地使用类似 latest_release_directory 的东西,而不知道在幕后会引发异常,然后被捕获,并且 :latest_release_directory 在变量映射中实际上是一个延续,它在第一次使用时被评估,然后该值被缓存直到脚本结束。

该系统现在 100% 不再有魔法。如果你使用 set() 设置一个变量,可以使用 fetch() 获取它,如果你设置到变量中的值响应 #call,那么它将在每次使用时在当前上下文中执行,这些值不会被缓存,除非你的延续执行一些显式的缓存。再次强调,我们优先考虑清晰度而不是微优化

SSHKit

Capistrano 中许多与日志记录、格式化、SSH、连接管理和池化、并行性、批处理执行等相关的新功能来自一个从 Capistrano v3 开发过程中分离出来的库。

SSHKit 是一个更低级的工具包,比 Net::SSH 更高级,但缺少 Capistrano 的角色、环境、回滚和其他更高级的功能。

如果你只需要连接到一台机器并运行一些任意命令,SSHkit 是理想的选择,例如

    # Rakefile (even without Capistrano loaded)
    require 'sshkit'
    desc "Check the uptime of example.com"
    task :uptime do |h|
      execute :uptime
    end

SSHKit 可以做的事情远不止这些,我们有一个非常广泛的 示例列表。在 Capistrano v3 中,大部分在 on() 块内发生的事情都在 SSHkit 中发生,该库的文档是查找更多信息的最佳去处。

命令映射

这是 SSHKit 的另一个功能,旨在消除先前的一些歧义,它有一个所谓的命令映射。

当执行类似以下内容时

    # Capistrano 2.0.x
    execute "git clone ........ ......."

命令将完全不变地传递到远程服务器。这包括可能设置的选项,例如用户、目录和环境变量。**这是设计使然。**此功能旨在让人们在需要时使用heredocs编写非平凡的命令,例如

    # Capistrano 3.0.x
    execute <<-EOBLOCK
      # All of this block is interpreted as Bash script
      if ! [ -e /tmp/somefile ]
        then touch /tmp/somefile
        chmod 0644 /tmp/somefile
      fi
    EOBLOCK

注意:SSHKit 多行命令清理逻辑将删除换行符并在每行后添加一个; 来分隔命令。因此,请确保您不在then 和以下命令之间添加换行符。

在 Capistrano v3 中,编写该命令的惯用方式是使用分隔的变参数方法来指定命令

    # Capistrano 3.0.x
    execute :git, :clone, "........", "......."

… 或者对于更大的示例

    # Capistrano 3.0.x
    file = '/tmp/somefile'
    unless test("-e #{file}")
      execute :touch, file
    end

通过这种方式,会查询命令映射,命令映射会将所有未知命令(在本例中为git,该行其余部分是git参数)映射到/usr/bin/env ...。这意味着此命令将扩展为/usr/bin/env git clone ...... ......,这与在没有完整路径的情况下调用git 时发生的情况相同,env 程序(可能间接)被用来确定要运行哪个git

诸如rakerails 之类的命令通常最好以bundle exec 为前缀,在这种情况下,可以将其映射到

    SSHKit.config.command_map[:rake]  = "bundle exec rake"
    SSHKit.config.command_map[:rails] = "bundle exec rails"

也可以像这样在映射位置应用一个lambdaProc

    SSHKit.config.command_map = Hash.new do |hash, key|
      if %i{rails rake bundle clockwork heroku}.include?(key.to_sym)
        hash[key] = "/usr/bin/env bundle exec #{key}"
      else
        hash[key] = "/usr/bin/env #{key}"
      end
    end

在这两种选择之间,应该有相当强大的选项来映射环境中的命令,而无需仅仅因为路径不同或二进制文件名称不同而覆盖 Capistrano 的内部任务。

这也可以在使用shim 可执行文件的环境中略微滥用,例如rbenv 包装器

    SSHKit.config.command_map = Hash.new do |hash, key|
      if %i{rails rake bundle clockwork heroku}.include?(key.to_sym)
        hash[key] = "/usr/bin/env myproject_bundle exec myproject_#{key}"
      else
        hash[key] = "/usr/bin/env #{key}"
      end
    end

以上假设您已经执行类似 rbenv wrapper default myproject 的操作,该操作会创建包装器二进制文件,这些文件会正确设置 Ruby 环境,而无需交互式登录 shell。

测试

Capistrano 的旧测试套件纯粹是单元测试,并没有涵盖各种问题情况,特别是 deploy.rb(即实际的部署代码)中没有任何内容经过测试;由于我们有自己的 DSL 实现和其他一些奇怪的设计点,因此测试实际的配方非常痛苦。

测试一直是 Capistrano v3 的重点。集成测试套件使用 Vagrant 启动一台机器,使用可移植的 shell 脚本配置某些场景,然后对它们执行命令,将常见配置部署到典型的 Linux 系统。这执行起来很慢,但提供了比以前更强的保证,确保没有出现任何问题。

Capistrano v3 还提供了一种可能性,可以替换后端实现。这很有趣,因为为了测试您自己的配方,您可以使用打印机后端,并验证输出是否与您的预期相符,或者使用一个存根后端,您可以在其上验证是否按预期进行了调用或未进行调用。

任意日志记录

Capistrano 在 on() 块中公开了方法 debug()info()warn()error()fatal(),这些方法可用于使用现有的日志记录基础设施和流式 IO 格式化程序进行日志记录。

    # Capistrano 3.0.x
    on hosts do |host|
      f = '/some/file'
      if test("[ -d #{f} ]")
        execute :touch, f
      else
        info "#{f} already exists on #{host}!"
      end
    end

### 升级

要深入了解具体细节,请访问 升级文档

简单地说,没有直接升级路径,版本 2 和 3 不兼容。

这在一定程度上是出于设计考虑,旧的 DSL 在某些地方不够精确,这会使在大多数情况下做正确的事情变得很困难,我们选择投资更多功能和更好的可靠性,而不是投资保持向后兼容的 API。

下面列出了许多注意事项,但主要要点是内置角色的新名称,以及 Capistrano v3 默认情况下与平台无关,如果您需要 Rails 支持(用于迁移、资产管道等),则需要 require 支持文件。

注意事项

Rake DSL 是累加的

在 Capistrano v2 中,如果你重新定义一个任务,它会替换原始实现。人们利用这一点,用自己的实现逐段替换内部任务。

sudo 行为

Fork me on GitHub