cover

在 Fly.io 部署 Rails 应用

这几天我把 GeekNote 的部署环境迁移到了 Fly.io,我觉得这是一个值得推荐的平台,所以写这篇日志介绍一下。

为什么选择 Fly.io

Fly.io 是一个基于容器的部署平台,致力于在全球各个区域提供贴近最终用户的部署环境。

跟传统的基于 VM 的云服务相比,基于容器的的部署平台不需要管理底层操作系统,能确保应用在开发和生产环境运行在同一环境,并且可以轻松缩放。

跟其他容器部署平台相比(例如 Heroku),Fly.io 提供了更多的部署区域(例如中国香港),提供数据持久层(Volume),自带虚拟私有网络(Heroku 企业版内容),更慷慨的免费政策,可以说是在很多层面是一个更好的 Heroku。

由于 Fly.io 的容器的扩展非常灵活,按秒计价,所以它在价格上也有不少优势。GeekNote 之前部署在某云的香港机房,每月花费 400 元。由于某云是以月租方式购买服务器,即使资源有很多闲置我也没有动力调整配置。在迁移到 Fly.io 后,根据情况降低了服务器配置,再扣去 Fly.io 的免费额度,每个月预计花费 50 元左右。

如果香港机房可以满足你的应用需求,那么 Fly.io 会是一个不错的选择。

Rails 部署教程

Fly.io 自身的文档已经提供了 Ruby 应用部署Docker 应用部署。但官方文档没有完整介绍部署一个 Rails 应用的过程,在此记录和分享一下我的经验。

容器化

为了部署在 fly.io,需要先把 Rails 应用容器化。虽然 Fly.io 也提供像 Heroku buildpack 那样基于代码的部署方式,但是 buildpack 不一定满足你的需求,使用容器才是最好的利用容器部署平台的方式。

关于如何将 Rails 应用容器化,可以看我之前写的系列文章《Rails on Docker》。

💡 实际上,Fly.io 并不用 docker 运行应用,而是用 Firecracker VM,以实现更好的环境隔离。Docker 镜像只是作为一个标准化的镜像压缩包。详情请看Docker without Docker

这里假设我们有一个 Rails 应用,项目目录下已经有 Dockerfile 文件,用 docker 编译该文件会得到这个应用的容器镜像。

/myapp
  | ...
  \ Dockerfile

这个应用需要四个进程或服务:Web,Postgres,Redis,Worker,其中 Web 和 Worker 都使用 Rails 应用的镜像,只是运行的命令不同。

在 Fly.io 中,进程或服务都以“app”为单位进行管理,我们需要创建 4 个 app。

安装 flyctl

Fly.io 的大多数操作需要使用命令行,所以先要安装命令行工具 flyctl。

安装方式可以参考官方文档:https://fly.io/docs/getting-started/installing-flyctl/

对于 Mac 来说,可以通过 brew 安装:

$ brew install superfly/tap/flyctl

注册 fly.io

在 Fly.io 创建应用之前先要注册,如果之前没有注册可以通过命令行执行:

$ flyctl auth signup

它会打开浏览器进入注册流程,注册完毕后会自己跳回到命令行通过验证。

如果之前已经注册过了,可以执行:

$ flyctl auth login

要注意的是为了避免免费资源被滥用,fly.io 要求注册之后填写信用卡完成验证。当然,这样也便于转化付费用户。

注册完成后,我们就可以正式开始部署应用。

Web

创建应用

首先来创建 web app,在项目目录下执行命令:

$ flyctl launch

命令行会提示几个问题:

Creating app in /path/to/project
Scanning source code
Detected a Dockerfile app
? App Name (leave blank to use an auto-generated name): 
? Select organization: Name (personal)
? Select region: hkg (Hong Kong)
Created app app-slug in organization personal
Wrote config file fly.toml
? Would you like to deploy now? No

其中 App Name 需要是全局唯一的,而且之后不能更改,同时会作为 app 的子域名,推荐用 app-name 这样的格式。如果只是尝试一下,也可以直接回车自动生成。其他选项按照需要选择即可。最后一步问是否现在部署,选择 No,因为在启动前还需要修改配置,设置环境变量,创建数据库等。

运行 launch 之后会生成一个 fly.toml 的配置文件。由于我们后面还要创建 app,为了不影响之后的操作,将它改名为 fly/web.toml

💡 对于 flyctl,如果当前目录下有 fly.toml,它会读取里面的 app-name 作为默认 app-name;如果没有,则需要用 --app APP_NAME 参数指定要操作的 app。

fly/web.toml 里面需要修改一些内容以符合 Rails 应用的需求。修改的部分如下:

[experimental]
  # 在镜像内要执行的命令
  cmd = ["bin/rails", "server"]

[[services]]
  # Rails 进程监听的端口
  internal_port = 3000

[deploy]
  # 每次部署后执行数据迁移
  release_command = "bin/rails db:migrate"

其他配置的含义可以看官方文档:https://fly.io/docs/reference/configuration/

设置环境变量和 secrets

Rails 运行过程需要设置一些环境变量,例如 SECRET_KEY_BASE 等。这时候可以使用 Fly.io 的 secrets 机制,设置的 secrets 变量会自动转换为容器内的环境变量。用以下命令设置 secrets:

$ flyctl secrets set NAME=VALUE NAME2=VALUE2 --app WEB_APP_NAME

每次修改 secrets 后会触发一次部署,为了节省时间可以一次设置多个变量。

我们也可以用一行一个变量的方式将变量写在文件里,然后用从文件读取的方式导入变量:

$ flyctl secrets import  --app WEB_APP_NAME < .env.production

设置完后先放着,接下来创建数据库。

Postgres

现在来创建 Postgres app。命令行输入:

$ flyctl pg create

跟创建 web app 时一样,flyctl 会询问几个问题:

flyctl pg create
? App Name: 
? Select organization: Name (personal)
? Select region: hkg (Hong Kong)
For pricing information visit: https://fly.io/docs/about/pricing/#postgresql-clusters
? Select configuration: Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
Creating postgres cluster  in organization personal
……

跟创建 web app 不同的是,询问完后会立即执行部署,毕竟也没什么需要设置的。

部署完毕后命令行会输出 postgres 的管理员账号密码,可以就找个安全的地方记录一下。

下一步是把数据库挂载到 web app,输入以下命令:

$ flyctl pg attach --postgres-app PG_APP_NAME --app WEB_APP_NAME

执行操作后,fly 会在 postgres 里面创建一个 database 和有对应读写权限的 dbuser,然后自动把连接信息设置到 web app 的环境变量 DATABASE_URL 内,所以这个变量不需要自己设置。

更多 postgres 的管理信息,可以查看文档 https://fly.io/docs/reference/postgres/

Redis

现在来创建 Redis app。对于 Redis,fly 没有提供像 Postgres 那么自动化的管理脚本,需要自己创建应用和设置持久化。

运行以下命令:

$ flyctl launch --image flyio/redis:6.2.6

这里指定了镜像 flyio/redis:6.2.6 作为部署镜像,而不是从 Dockerfile 构建。

创建应用后,flyctl 会创建一个新的 fly.toml 文件,为了不影响之后的操作,将它改名为 fly/redis.toml

里面的 [[services]] 相关的内容是不需要的,将它删除,因为这个应用不需要对外提供服务。

为了让 redis 数据持久化,需要给它挂载一个 volume,用以下命令创建 volume:

$ flyctl volumes create redis_data --size 1 --app REDIS_APP_NAME

然后在 fly.redis.toml 内添加以下内容:

[[mounts]]
  destination = "/data"
  source = "redis_data"

加上可选的系统检测端口:

[metrics]
  port = 9091
  path = "/metrics"

设置 Redis 密码:

$ flyctl secrets set REDIS_PASSWORD=MY_REDIS_PASSWORD --app REDIS_APP_NAME

部署 Redis 应用:

$ flyctl deploy -c fly/redis.toml

将连接参数设置到 web app 的环境变量:

$ flyctl secrets set REDIS_URL=redis://:MY_REDIS_PASSWORD@REDIS_APP_NAME.internal:6379/0

现在部署 Redis 应用:

$ flyctl deploy -c fly/redis.toml

部署 Web 应用

准备工作已经完成,现在可以部署 Web 应用,输入以下命令

$ flyctl deploy -c fly/web.toml

如果部署顺利,之后就可以用以下命令打开应用:

$ flyctl open -a WEB_APP_NAME

你也可以直接在浏览器打开 https://WEB_APP_NAME.fly.dev

Worker

不要忘了还有一个 Worker app,它跟 Web app 使用同一个镜像,只是启动方式不同。

再新建一个 app:

$ flyctl launch

把新生成的 fly.toml 改名为 fly/worker.toml,修改内容,加上这一行:

[experimental]
  cmd = ["bundle", "exec", "sidekiq"]

同时删掉 [[services]] 的内容,因为不需要对外服务。

接着设置环境变量:

$ flyctl secrets set SECRET_KEY_BASE=... DATABASE_URL=... REDIS_URL=.. --app WORKER_APP_NAME

💡 由于重复输入 ENV 很麻烦,我建议用前面导入 .env 文件的方式设置。但要注意不要把 .env 提交到 git 里。

然后部署应用:

$ flyctl deploy -c fly/worker.toml

其他

现在已经完成了部署一个 Rails 应用的过程。还有一些问题可能要处理,例如:

这些可以通过官方文档再深入了解,由于文章篇幅在此不再叙述。

Fly.io 还是个年轻的公司,团队很小CEO 都还在做客服。不过这个平台的体验很接近我理想中的容器部署平台,别的容器部署服务依然很烂,Fly.io 让我眼前一亮。我把网站迁移到 Fly.io,也是想见证它的成长。如果这让你也产生了兴趣,不妨自己尝试一下。

5
5
3