Flynn 平台部署 Ruby / Rack / Rails 应用完整指南:从 Gemfile 到进程编排
Flynn 平台部署 Ruby / Rack / Rails 应用完整指南:从 Gemfile 到进程编排
Flynn 通过 slugbuilder 组件内置的 Ruby Buildpack 完成对 Ruby、Rack 与 Rails 应用的检测、编译与发布,支持 MRI、JRuby、Rubinius 多种解释器。本文以官方文档 docs/content/languages/ruby.md 为主线,结合仓库源码(构建脚本、接收端与 CLI 实现)深入讲解 Gemfile 依赖管理、解释器与原生库选择、Procfile 进程类型编排、框架自动检测及 flynn run 一次性任务执行,帮助你从零把 Ruby 应用平滑部署到 Flynn 集群。
Detection:Flynn 如何识别 Ruby 应用
Flynn 对应用的检测遵循 Buildpack 协议。只要应用根目录下存在 Gemfile,即被识别为 Ruby 应用,随后由 Ruby Buildpack 负责后续的依赖解析、编译与发布流程。
从源码结构看,这一检测并非 Flynn 单独实现,而是 slugbuilder 在构建阶段统一驱动的:每个构建包目录下都有 bin/detect 脚本,slugbuilder/builder/build.sh 会按固定顺序遍历所有已安装的 buildpack,逐个执行 bin/detect <build_dir>,第一个成功退出的 buildpack 即被选中;如果显式设置了 BUILDPACK_URL 环境变量,则跳过默认列表、直接使用用户指定的自定义 buildpack。
buildpack 的清单与版本锁定记录在 slugbuilder/builder/buildpacks.txt,其中 Ruby buildpack 被固定到 commit 4ca71a9d,以保证构建环境可复现。安装脚本 slugbuilder/img/packages.sh 会按清单顺序为每个 buildpack 编号(nl -nrz),确保检测时按声明次序执行。
依赖管理
Gemfile 与 Gems
Gemfile 的首要职责是声明 gem 依赖。以下是一个仅依赖 rack 的最小示例:
source "https://rubygems.org"
gem "rack"
本地开发时执行 bundle install,Bundler 会把解析出的全部 gem 与版本快照写入 Gemfile.lock。部署时 Gemfile.lock 必须存在——Flynn 依靠它确定需要安装哪些 gem,缺失会导致部署失败。
环境特定分组
生产环境并不需要所有本地依赖。Flynn 在构建阶段执行 bundle install 时会附加 --without development:test,跳过 Gemfile 中 development 与 test 分组内的 gem。例如本地调试常用的 debugger 与 rspec 应放入分组:
group :development do
gem "debugger"
end
group :test do
gem "rspec"
end
构建阶段实际运行的完整 Bundler 命令如下:
bundle install \
--without development:test \
--path vendor/bundle \
--binstubs vendor/bundle/bin \
-j4 \
--deployment \
--no-clean
要点:--path vendor/bundle 将依赖安装到应用目录内以保证 slug 自包含;--binstubs 生成可执行脚本;-j4 并行安装;--deployment 强制使用 Gemfile.lock 的锁定版本;--no-clean 保留缓存。该命令由 Ruby Buildpack 的 bin/compile 阶段触发,对应构建脚本中的 run_unprivileged ${selected_buildpack}/bin/compile ... 调用(slugbuilder/builder/build.sh)。
指定 Ruby 解释器与版本
如需固定解释器和版本,在 Gemfile 中声明 ruby 指令即可:
- MRI v2.1.2:
ruby "2.1.2"
- JRuby 1.7.16(Ruby 2.0 兼容模式):
ruby "2.0.0", engine: "jruby", engine_version: "1.7.16"
- Rubinius 2.2.10(Ruby 2.1 兼容模式):
ruby "2.1.0", engine: "rbx", engine_version: "2.2.10"
Buildpack 会依据这些声明下载对应的运行时并配置好 PATH,应用在构建和运行两个阶段都能使用指定解释器。
编译原生扩展所需系统库
应用构建与运行所依赖的容器镜像内预装了一批对编译 Ruby 原生扩展有用的系统库,例如:
libssl-devlibmysqlclient-devlibxml2-devlibxslt-dev
这些库随镜像层预置。从仓库看,构建镜像由 builder/manifest.json 中的 slugbuilder-18(基于 heroku-18-build)与 slugbuilder-14(基于 cedar-14)定义,其基础层脚本位于 builder/img/heroku-18.sh 与 builder/img/cedar-14.sh,系统包安装逻辑在 slugbuilder/img/packages.sh 中,可通过这些脚本查看完整的预装库清单。
Process Types:用 Procfile 声明进程
应用支持的进程类型通过根目录下的 Procfile 声明,每行格式为 TYPE: COMMAND。若没有 Procfile,Flynn 会根据框架检测结果分配默认进程类型(见下文「Framework Detection」)。
构建阶段会解析 Procfile 并打印声明的进程类型,见 slugbuilder/builder/build.sh 中的 ruby -r yaml -e "puts YAML.load_file('Procfile')..." 逻辑。
web
web 进程类型会被分配 HTTP 路由和对应的 PORT 环境变量,通常用于启动 HTTP 服务器。常见配置:
- Thin:
web: bundle exec thin start -p $PORT -e $RACK_ENV
- Unicorn:
web: bundle exec -p $PORT -c config/unicorn.rb
- Puma:
web: bundle exec puma -C config/puma.rb
使用 Puma 时,必须在 config/puma.rb 中读取 ENV["PORT"] 作为监听端口。
worker
worker 进程类型通常运行后台任务处理器,消费队列中的任务:
- Resque:
worker: QUEUE=* bundle exec rake resque:work
- Sidekiq:
worker: bundle exec sidekiq
- Delayed::Job:
worker: bundle exec delayed_job start
clock
clock 进程类型用于启动类似 cron 的定时任务进程,可按固定间隔执行代码。Clockwork gem 提供了简洁的 DSL,声明方式如下:
clock: bundle exec clockwork lib/clock.rb
其中 lib/clock.rb 存放 Clockwork 的任务声明。
本地验证进程编排
安装 Foreman gem,在项目根目录的 .env 中加入所需环境变量(如 PORT=5000),即可在本地同时启动全部进程类型:
$ foreman start
14:25:33 web.1 | started with pid 42868
14:25:33 worker.1 | started with pid 42869
14:25:33 clock.1 | started with pid 42870
14:25:34 web.1 | == Sinatra/1.4.5 has taken the stage on 5000 for development with backup from Thin
14:25:34 web.1 | Thin web server (v1.6.3 codename Protein Powder)
14:25:34 web.1 | Maximum connections set to 1024
14:25:34 web.1 | Listening on localhost:5000, CTRL+C to stop
14:25:34 clock.1 | I, [2014-10-24T14:25:34.729860 #42870] INFO -- : Starting clock for 1 events: [ frequent.job ]
14:25:34 clock.1 | I, [2014-10-24T14:25:34.729999 #42870] INFO -- : Triggering 'frequent.job'
14:25:34 clock.1 | Running frequent.job
...
Framework Detection:框架自动识别与默认进程类型
不同 Ruby 框架需要不同配置,Flynn 会根据框架特征自动识别并给出默认进程类型(仅当没有 Procfile 时生效)。规则如下:
Ruby(根目录存在 Gemfile)
rake: bundle exec rake
console: bundle exec irb
Rack(Gemfile.lock 中存在 rack gem)
web: bundle exec rackup config.ru -p $PORT
rake: bundle exec rake
console: bundle exec irb
Rails 2(Gemfile.lock 中 rails gem 版本 >= 2.0.0 且 < 3.0.0)
web: bundle exec ruby script/server -p $PORT
worker: bundle exec rake jobs:work
rake: bundle exec rake
console: bundle exec script/console
Rails 3(rails 版本 >= 3.0.0 且 < 4.0.0)
web: bundle exec rails server -p $PORT
worker: bundle exec rake jobs:work
rake: bundle exec rake
console: bundle exec rails console
Rails 4(rails 版本 >= 4.0.0 且 < 5.0.0)
web: bin/rails server -p $PORT -e $RAILS_ENV
worker: bundle exec rake jobs:work
rake: bundle exec rake
console: bin/rails console
这些默认进程类型由 Buildpack 的 bin/release 阶段写入 .release 文件,slugbuilder 在 slugbuilder/builder/build.sh 中读取并打印 default_process_types,随后由接收端(见下文)将其注册为应用的进程类型,参见 gitreceive/receiver/flynn-receive.go 中读取 slugbuilder.process_types 元数据的逻辑。
Assets:静态资源预编译
如果应用定义了 assets:precompile Rake 任务,它会在应用编译的最后一步被执行。
对于被识别为 Rails 3 或 Rails 4 的应用,若 Gemfile 中没有 rails_12factor gem,Flynn 会安装 rails3_serve_static_assets gem 并设置 config.serve_static_assets = true。这是因为部署后 public 目录中的静态资源没有其他服务途径,必须由应用自身托管。
Run Jobs:运行一次性任务
需要执行 Rake 任务或启动 Rails console 时,使用 flynn run:
$ flynn run rake db:migrate
$ flynn run rails console
从 cli/run.go 的源码看,flynn run 支持以下选项:
-d, --detached:后台运行,不连接输入输出流;-r <release>:指定 release 运行(默认使用当前应用的 release);-l, --enable-log:将输出发送到日志流;--limits <limits>:以逗号分隔的格式为任务设置资源限制;--profiles=<profiles>:为任务指定 job profiles;--mounts-from <proc>:从指定进程类型复制挂载。
实现细节上,flynn run 会先获取当前应用的 release,构造 NewJob 请求;对于 Git 部署产生的 slug 应用,会自动在命令前加上 /runner/init 前缀(cli/run.go),因为这类应用的镜像入口是 slugrunner/runner/init。TTY 模式下还会同步终端尺寸与信号,保证 rails console 这类交互式命令可用。
源码级原理:从 git push 到 slug 的完整链路
将上述各环节串起来,一次 Ruby 应用的 git push 部署在仓库中的完整调用链为:
- Git 服务器收到 push,触发
pre-receive钩子(gitreceive/server.go),把代码打成 tar 流交给/bin/flynn-receiver; - gitreceive/receiver/flynn-receive.go 启动 slugbuilder 任务,入口为
/builder/build.sh,并把BUILDPACK_URL(若通过环境变量设置,或继承上一次 release 的设置)传入任务环境,同时注入构建缓存 URL; - slugbuilder/builder/build.sh 从 stdin 解包应用代码,划分
app_dir、build_dir、cache_root等目录,并在BUILD_CACHE_URL存在时恢复上次构建缓存; - 依次执行 buildpack 的
detect→compile(含bundle install与assets:precompile)→release三阶段,产出.release文件; create-artifact把构建产物打包为 slug 镜像(slugbuilder/artifact),接收端将其注册为应用的 release 与镜像;- 最后接收端启动初始
web任务并等待其就绪(gitreceive/receiver/flynn-receive.go),再依据.release中的进程类型清单创建其余进程的 formation。
整个部署采用零停机策略:新 release 就绪后逐步切换流量,失败时自动回滚,详见 docs/content/apps.md。另外,应用根目录下的 .slugignore 文件可以声明构建产物中要排除的路径(slugbuilder/builder/build.sh),类似 .gitignore 的语法,可用于削减 slug 体积。
小结
部署 Ruby 应用到 Flynn 的要点可以概括为三条:始终提交 Gemfile.lock;在 Procfile 中显式声明进程类型(尤其 web 要使用 $PORT);需要固定解释器时在 Gemfile 中声明 ruby 指令。框架识别、依赖安装与静态资源预编译均由内置 Ruby Buildpack 自动完成,flynn run 则提供了与生产环境完全一致的 Rake 任务与 console 执行通道。进一步了解部署流程可阅读 docs/content/apps.md,构建链路细节可对照 slugbuilder/builder/build.sh 与 gitreceive/receiver/flynn-receive.go。