Kubing Rails:用Kuby进行无压力的Kubernetes部署

169 阅读12分钟

作者。 Vladimir Dementyev,Evil Martians的首席后端工程师和Travis Turner,Evil Martians的技术编辑

就像古希腊人苦苦挣扎于圆的平方一样,现代的网络开发者也在努力寻找一种方便的方式来在Kubernetes上部署他们的应用。YAML、Helm(以及指南针和直尺)都是久经考验的工具,但仅使用这些工具可能需要太多的步骤来完成任务。有些开发者对此无所谓!另一方面,像我们这样的Ruby主义者,更喜欢把精力集中在编程的创造性方面,而且我们一直在寻找方法来减少常规的工作。因此,今天,为了纪念这种暗示,我想讨论一下在Kubernetes上部署Rails应用程序的最新尝试--Kuby!

多年来,Rails社区一直在呼吁主动部署,这是一种神奇的开箱即用的应用程序部署机制。我甚至不确定是否真的有可能建立一个框架来抽象出所有不同的应用程序部署方式,从裸机到容器。但是,对于部署目标的特定子类,该怎么办呢?虽然Capistrano对真正的服务器仍然很有效,但世界继续向容器化环境发展,特别是Kubernetes。红宝石世界是否有一个一流的解决方案来处理YAML和Pod?这就是导致我仔细研究Kuby的问题,它最近突然出现在我的雷达上,首先。

Kuby声称,通过依靠良好的 "约定俗成的配置原则",使每个人都能使用Kubernetes(而不仅仅是DevOps大师)。作为一个试图避免与k8s打交道的工程师,我发现自己被这个承诺所吸引,我决定试一试(并给Kubernetes第二次机会)。

那么,让我们直接进入:我们如何用Kuby部署一个Rails应用?

Kuby的快速入门指南是最好的起点,由于它非常有效,所以没有必要在这里复制它。我只想指出其中的要点。

  • 选择一个Kubernetes供应商(我选择了Digital Ocean)。
  • 安装Docker、Kuby,并运行rails g kuby

...就这样!有了这些,在理论上,你就可以立即部署你的应用程序了。然而,在实践中,每个Rails应用都是一个独特的软件,很有可能默认的配置不能满足需要。这正是我在处理AnyCable Rails演示时的情况。我想和大家分享我的Kuby之旅,我将重点介绍以下内容。

用于开发的Docker与用于部署的Kuby

我面临的第一个挑战是建立生产用的Docker镜像,同时把所有的开发工作都放在Docker环境中。换句话说,从容器中运行kuby build 是行不通的(失败的原因是error: No such file or directory - build ),因为它需要在系统中运行一个Docker守护程序。我是否应该尝试玩一些Docker Matryoshka Doll的游戏,把Docker放到Docker中?这可不好玩。

相反,我尝试了一种不同的方法:把开发工具和部署工具分开。我想从我的主机系统(已经安装了Docker)运行Kuby,同时,我不想安装所有的应用程序依赖。这是因为这可能需要安装额外的系统包,比如说pg gem的postgresql-dev 。所以,我打算使用Gemfile组件化技术。

首先,我创建了一个单独的Gemfile,其中只有Kuby的依赖。

# gemfiles/kuby.gemfile

# Active Support is required to read credentials.
gem "activesupport", "~> 6.1"
gem "kuby-core", "~> 0.14.0"
gem "kuby-digitalocean", "~> 0.4.3"

现在我们可以通过#eval_gemfile其纳入主Gemfile(因为我们还有config/initializers/kuby.rb ,它依赖于Kuby)。

eval_gemfile "gemfiles/kuby.gemfile"

最后,我再添加一个Gemfile--gemfiles/deploy.gemfile

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '~> 3.0'

eval_gemfile "./kuby.gemfile"

# To debug configuration issues
gem 'pry-byebug'

正如你所看到的,我们已经通过添加调试工具和配置RubyGems扩展了kuby.gemfile

现在,我可以像这样运行Kuby了。

# First, install gems locally
BUNDLE_GEMFILE=gemfiles/deploy.gemfile bundle install

# Now, run Kuby!
BUNDLE_GEMFILE=gemfiles/deploy.gemfile bundle exec kuby <some command>

输入BUNDLE_GEMFILE=... 是很无聊的。让我们创建一个单独的可执行文件--bin/kuby,它将为我们做这些事情。

#!/bin/bash

cd $(dirname $0)/..

export BUNDLE_GEMFILE=./gemfiles/deploy.gemfile
bundle check > /dev/null || bundle install

kuby_env=${KUBY_ENV:-production}

bundle exec kuby -e ${kuby_env} $@

我们在这个脚本中加入了bundle install ,以使事情自动化,所以不需要再担心这个问题。

让我们试一试吧。

$ bin/kuby build

error: No webserver named

🤔 Kuby还是失败了,但现在的错误不同了:没有命名网络服务器。
通过查看源代码,我发现这个异常是在Kuby不能自动检测到网络服务器时引发的。它是如何做到这一点的呢?通过查看加载的gem specs

def default_webserver
  if Gem.loaded_specs.include?("puma")
    :puma
  end
end

puma 我们在deploy.gemfile ,所以Kuby无法找到一个网络服务器。展望未来,我们在Ruby版本和Gemfile路径方面也可能面临类似的问题(两者都是根据当前使用的版本推断出来的)。幸运的是,我们可以在Kuby配置中明确地提供所有的值。

Kuby.define("anycable-rails-demo") do
  environment(:production) do
    # ...
    docker do
      base_image "ruby:3.0.1"
      gemfile "./Gemfile"
      # For multi-part Gemfiles, we also need to specify additional
      # paths to be copied to a Docker container before `bundle install`
      bundler_phase.gemfiles "./gemfiles/kuby.gemfile"

      webserver_phase.webserver = :puma

      # <credentials and image registry settings>
    end
    # ...
  end
end

通过几行额外的代码,我们现在可以通过kuby build ,成功构建我们的生产图像。哦,我说图像?让我们稍微绕一下弯,看看build 命令下面隐藏着什么。

Kuby的Rails插件为你构建了两个镜像:一个带有Rails应用(默认运行puma ... config.ru ),另一个带有NGINX和编译的资产。你可以通过运行专用命令看到dockerfiles的内容,像这样。

$ bin/kuby dockerfiles

# Dockerfile for image registry.digitalocean.com/anycable/anycable-rails-demo with tags 20211119151614, latest
FROM node:12.14.1 AS nodejs
FROM ruby:3.0.1
WORKDIR /usr/src/app
ENV RAILS_ENV=production
ENV KUBY_ENV=production
ARG RAILS_MASTER_KEY
RUN apt-get update -qq && \
  DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends apt-transport-https && \
  DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends apt-utils && \
  DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends \
  ca-certificates
COPY --from=nodejs /usr/local/bin/node /usr/local/bin/node
RUN wget https://github.com/yarnpkg/yarn/releases/download/v1.21.1/yarn-v1.21.1.tar.gz && \
  yarnv=$(basename $(ls yarn-*.tar.gz | cut -d'-' -f 2) .tar.gz) && \
  tar zxvf yarn-$yarnv.tar.gz -C /opt && \
  mv /opt/yarn-$yarnv /opt/yarn && \
  apt-get install -qq -y --no-install-recommends gnupg && \
  wget -qO- https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --import && \
  wget https://github.com/yarnpkg/yarn/releases/download/$yarnv/yarn-$yarnv.tar.gz.asc && \
  gpg --verify yarn-$yarnv.tar.gz.asc
ENV PATH=$PATH:/opt/yarn/bin
RUN gem install bundler -v 2.2.22
COPY ./Gemfile .
COPY ./Gemfile.lock .
COPY ./gemfiles/kuby.gemfile ./gemfiles/kuby.gemfile
ENV BUNDLE_WITHOUT='development test deploy'
RUN bundle install --jobs $(nproc) --retry 3 --gemfile ./Gemfile
RUN bundle binstubs --all
ENV PATH=./bin:$PATH
COPY package.json yarn.loc[k] .npmr[c] .yarnr[c] ./
RUN yarn install
COPY ./ .
RUN bundle exec rake assets:precompile
CMD puma --workers 4 --bind tcp://0.0.0.0 --port 8080 --pidfile ./server.pid ./config.ru
EXPOSE 8080

# Dockerfile for image registry.digitalocean.com/anycable/anycable-rails-demo with tags 20211119151614-assets, latest-assets
FROM registry.digitalocean.com/anycable/anycable-rails-demo:20211119150731 AS anycable-rails-demo-20211119150731
RUN mkdir -p /usr/share/assets
RUN bundle exec rake kuby:rails_app:assets:copy
FROM registry.digitalocean.com/anycable/anycable-rails-demo:20211119151614 AS anycable-rails-demo-20211119151614
COPY --from=anycable-rails-demo-20211119150731 /usr/share/assets /usr/share/assets
RUN bundle exec rake kuby:rails_app:assets:copy
FROM nginx:1.9-alpine
COPY --from=anycable-rails-demo-20211119151614 /usr/share/assets /usr/share/nginx/assets

如果你仔细看看-assets 镜像,你会发现我们从两个镜像中复制文件:从刚刚构建的Rails镜像(anycable-rails-demo-20211119151614 )和从以前的版本(anycable-rails-demo-20211119150731 )。这样,我们就可以为用户(浏览器)提供所有文件的两个连续版本(具有不同的摘要),所以部署工作顺利进行,没有任何 "404 Not Found "错误。一些开发者可能没有意识到这个特殊的部署问题,但有了Kuby,我们无论如何都不用担心,因为它已经把我们覆盖了。这不是很酷吗?

在第二个Docker文件中,还有一行引起了我的注意。

RUN bundle exec rake kuby:rails_app:assets:copy

这个Rake任务是我们需要在应用程序中添加Kuby宝石和初始化器的唯一原因。也许我们可以避免这一点,并削减捆绑包的大小?🤔

我们当然可以!让我们从Kuby复制AssetCopyTask 类,并定义我们自己的Rake任务。

# lib/tasks/kuby/assets.rb

class KubyAssetCopyTask
  # class contents from kuby-core
  #
  # The only thing we need to change here is
  # replace `Kuby.logger.info` with `$stdout.puts`
end

namespace :kuby do
  namespace :rails_app do
    namespace :assets do
      task :copy do
        KubyAssetCopyTask.new(
          from: "./public", to: "/usr/share/assets"
        ).run
      end
    end
  end
end

想知道我们通过这个操作到底节省了多少兆字节吗?看看这个。

registry.digitalocean.com/anycable/anycable-rails-demo   latest                       977712dcef08   3 minutes ago   1.32GB
registry.digitalocean.com/anycable/anycable-rails-demo   20211117173245               b85f23c54131   2 days ago      1.53GB

节省了超过200MB的空间!🙀其中一个原因是kuby-core ,这取决于 helm-rbkubectl-rb宝石,它们在发行版中包含相应的二进制文件。这是一个我希望在Ruby社区不存在的模式,但是,可惜了。

尽管这样,图像仍然相当大。Kuby也支持构建Alpine图像,所以如果你觉得自己胆子大,可以自由地踏上那段充满了缺失的依赖和需要其他黑客的旅程。我为AnyCable演示程序做了这个工作(见PR),将图像大小减少到987MB。前端工件占据了大约三分之一的空间。 node_modules/, Yarn缓存,以及预编译的资产。当我们手动制作Rails dockerfiles时,我们通常使用多阶段构建来产生各种资产。我们只在最后一个版本中包含必要的文件。Kuby不这样做。至少,现在还没有😉

所以,我们可以构建并推送我们的应用程序Docker镜像到注册中心。现在是部署它的时候了!

用Action Cable测试飞行

我决定用一个使用Rails和Action Cable(而不是AnyCable)的简单应用程序开始我的Kubernetes之旅。

除了应用程序本身,我们还需要一个数据库(PostgreSQL),以及一个用于Action Cable广播的Redis实例。我决定使用Digital Ocean提供的管理型Postgres数据库(是的,我对将作为应用程序核心的数据库部署到容器化环境中感到犹豫)。那Redis呢?我们这里的用例不需要任何持久性,因为我们只使用PUB/SUB 。所以,启动一个Redis容器对我们来说应该很有效。

我遇到了kuby-redisgem,它听起来正是我在寻找的东西。我们只需要在我们的配置中添加这一行代码。

add_plugin(:redis) { instance(:cable) }

我本来以为这里会有一些魔法,但不幸的是,这并没有完全实现。首先,我发现我们还需要安装KubeDB。当然,有kuby-kubedb来做这个!问题是,它只支持v1alpha1的API规范。此外,它与最近版本的KubeDB不兼容。此外,老版本的KubeDB与现代Kubernetes API不兼容。⛔️一个死胡同。

所以,我们不得不退回到DO的管理Redis实例。下面是这一步的最终Kuby配置。

# We need to require some Rails stuff to read encrypted credentials
require "active_support/core_ext/hash/indifferent_access"
require "active_support/encrypted_configuration"

require "kuby"
require "kuby/digitalocean"

Kuby.define("anycable-rails-demo") do
  environment(:production) do
    app_creds = ActiveSupport::EncryptedConfiguration.new(
      config_path: "./config/credentials/production.yml.enc",
      key_path: "./config/credentials/production.key",
      env_key: "RAILS_MASTER_KEY"
    )

    docker do
      base_image "ruby:3.0.1"
      gemfile "./Gemfile"

      webserver_phase.webserver = :puma

      credentials do
        username app_creds[:do_token]
        password app_creds[:do_token]
      end

      image_url "registry.digitalocean.com/anycable/anycable-rails-demo"
    end

    kubernetes do
      add_plugin :rails_app do
        hostname "kuby-demo.anycable.io"
        manage_database false

        env do
          data do
            add "RAILS_LOG_TO_STDOUT", "enabled"
            add "DATABASE_URL", app_creds[:database_url]
            add "REDIS_URL", app_creds[:redis_url]
            add "ACTION_CABLE_ADAPTER", "redis"
          end
        end
      end

      provider :digitalocean do
        access_token app_creds[:do_token]
        cluster_id app_creds[:do_cluster_id]
      end
    end
  end
end

从顶部开始,我们需要运行kuby setup :这将为我们的集群添加一些系统级资源(Ingress、CertManager等)。之后,我们可以用一个命令部署(和重新部署,或重新配置)我们的生产应用程序。

用Kuby部署Action Cable应用程序

用Kuby部署Action Cable应用程序

似乎我们的部署被卡在了 "Still waiting"(等待这个世界停止仇恨? )状态。发生了什么?如果你再等一会儿,你会看到一个错误信息。

[FATAL][2021-11-22 20:08:20 +0300] ActionController::RoutingError (No route matches [GET] "/healthz"):

我们没有通过健康检查。那么,"/healthz 从哪里来的,为什么没有找到?还记得我们是如何从生产包中删除所有的Kuby宝石的吗?事实证明,(惊喜,惊喜)Kuby也添加了一个健康检查的中间件。所以,让我们把它带回来吧!为此,我们可以使用rack-healthgem并在我们的config.ru 中添加一行代码。

require_relative "config/environment"

use Rack::Health, path: "/healthz"
run Rails.application

在添加了这一改动并重建镜像后,我们终于能够成功地部署我们的应用程序了!

为了使用我们提供的主机名访问该应用程序(kuby-demo.anycable.io),我们必须配置我们的DNS记录。要做到这一点,我们需要获得我们集群的Ingress的公共IP地址。让我们使用kubectl (通过kuby )来实现。

$ bin/kuby kubectl -N -- get ing

NAME                          CLASS    HOSTS                   ADDRESS         PORTS
anycable-rails-demo-ingress   <none>   kuby-demo.anycable.io   164.90.241.97   80, 443

在添加了指向164.90.241.97 的DNS A记录后,我们终于可以访问我们的演示应用程序了,它被部署在Kubernetes的kuby-demo.anycable.io。🎉 🎉 我们成功了!

通过kuby-anycable真正使用AnyCable

到目前为止,我们只成功地部署了一个纯Rails应用程序,没有额外的服务(只有数据库)。不过,我们的目标是部署一个全功能的AnyCable应用。那么,我们接下来应该怎么做呢?

AnyCable需要与应用程序一起部署两个额外的服务:一个gRPC服务器(里面有Rails应用程序)和一个WebSocket服务器(anycable-go)。从Kubernetes的角度来看,我们需要服务、部署、配置图等等。听起来有很多东西需要手工编写,不是吗?

幸运的是,Kuby提供了一个建立自定义插件的API,将它们注入到构建和部署的生命周期中。我们可以所有必要的资源隐藏在一个插件中。这意味着用户不需要担心这些问题,另外我们可以提供一个规范的方式来部署AnyCable到Kubernetes。

实际上,这正是我们介绍新的kuby-anycable插件的最佳时机用它从Action Cable切换到AnyCable,就这么简单。

# kuby.rb

Kuby.define("anycable-rails-demo") do
  environment(:production) do
    #...

    kubernetes do
      # ...

      add_plugin :anycable_rpc
      add_plugin :anycable_go
    end
  end
end

使用kuby-anycable ,将AnyCable Rails应用程序的部署简化为仅仅两行代码!

就是这样!使用默认配置的部署只需要两行代码。多亏了Kuby和Ruby,我们可以自动推断出所有需要的配置参数。让我们看一下插件中的一些片段来证明这一点。

推断配置

AnyCable RPC服务器是同一个Rails应用,只是它连接到了一个gRPC服务器。因此,我们需要使其配置与Web服务器保持同步。这可以通过内省Kuby资源和为:rpc 容器创建一个EnvFrom 声明来实现。为此,我们使用#after_configuration 钩子。

def after_configuration
  return unless rails_spec

  deployment.spec.template.spec.container(:rpc).merge!(
    rails_spec.deployment.spec.template.spec.container(:web), fields: [:env_from]
  )
end

AnyCable-Go需要知道在哪里可以找到RPC服务以及如何连接到Redis实例。没问题!我们也可以找到这些信息。

def after_configuration
  configure_rpc_host
  configure_redis_url

  # ...
end

def configure_rpc_host
  return if config_map.data.get("ANYCABLE_RPC_HOST")

  config_map.data.add("ANYCABLE_RPC_HOST", "dns:///#{rpc_spec.service.metadata.name}:50051")
end

def configure_redis_url
  return if config_map.data.get("REDIS_URL") || config_map.data.get("ANYCABLE_REDIS_URL")

  # Try to lookup Redis URL from the RPC and Web app specs
  [rpc_spec, rails_spec].compact.detect do |spec|
    %w[ANYCABLE_REDIS_URL REDIS_URL].detect do |env_key|
      url = spec.config_map.data.get(env_key)
      next unless url

      config_map.data.add("ANYCABLE_REDIS_URL", url)
      true
    end
  end
end

智能并发性设置

目前,AnyCable需要在两端设置并发限制:对于RPC服务器,我们定义工作者池大小(处理请求的线程数);对于AnyCable-Go,我们定义并发限制(同时调用的最大数量)。我们要让它们保持同步,以防止超时和重试。当我们在默认情况下有一台RPC服务器和一台Go服务器时,默认设置可以完美地工作。但如果我想独立地扩展它们,并允许所有Go应用与所有RPC应用进行通信(通过负载均衡),该怎么办?我们需要仔细计算AnyCable-Go的限制,以避免RPC过载(或欠载)。

通过kuby-anycable ,我们为你做所有的计算

def configure_concurrency
  return if config_map.data.get("ANYCABLE_RPC_CONCURRENCY")
  return unless rpc_spec

  rpc_pool_size = rpc_spec.config_map.data.get("ANYCABLE_RPC_POOL_SIZE")
  return unless rpc_pool_size

  rpc_replicas = rpc_spec.replicas

  concurrency = ((rpc_pool_size.to_i * rpc_replicas) * 0.95 / replicas).round

  config_map.data.add("ANYCABLE_RPC_CONCURRENCY", concurrency.to_s)
end

Alpine与gRPC

AnyCable的用户如果决定尽可能地精简他们的Docker镜像,通常会遇到安装grpcgoogle-protobuf gems的问题。要成功做到这一点,首先,你需要一些额外的系统包。其次,你需要从源代码中为这些宝石构建C语言扩展。

我们决定在使用Kuby时,让开发者从Debian迁移到Alpine的过程变得透明。我们的插件带有一个虚拟包(anycable-build),它执行了所有必要的步骤,使Bundler在没有任何黑客的情况下工作。

Kuby.define("my-app") do
  environment(:production) do
    docker do
      # ...
      distro :alpine
      package_phase.add("anycable-build")

      # ..
    end

    # ...
  end
end

添加这个包后,你的Docker文件中会出现以下几行。

RUN apk add --no-cache --update libc6-compat && \
    ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2

# Pre-install grpc-related gems to build extensions without hacking Bundler
# or using BUNDLE_FORCE_RUBY_PLATFORM during `bundle install`.
# Uses gem versions from your Gemfile.lock.
RUN gem install --platform ruby google-protobuf -v '3.17.1' -N
RUN gem install --platform ruby grpc -v '1.38.0' -N --ignore-dependencies && \
    rm -rf /usr/local/bundle/gems/grpc-1.38.0/src/ruby/ext

使用Kuby和GitHub Actions持续部署

到目前为止,我们一直在使用我们的本地机器来部署应用程序。这对于实验和设置东西来说是很好的,但对于真正的生产使用来说,我们更喜欢使用自动化(或用git push origin production )。

将Kuby操作转移到CI是小菜一碟,它只包括三个步骤。kuby build,kuby push, 和kuby deploy

这种直接的方法有一个明显的缺点:通过CI构建Docker镜像的速度很慢。对于AnyCable演示应用程序,构建生产镜像需要15分钟左右。我将向你展示,通过简单地改变几行代码,我们可以将构建时间减少10倍。

Building a Docker image on the CI before and after adding a cache

添加缓存前后在CI上构建Docker镜像

我们最近发表了一篇文章,涉及如何使用BuildKit在GitHub Actions上加速Docker构建。由于我们没有直接调用Docker,而是通过Kuby来调用,所以我们不能在这里完全应用所描述的技术。此外,从那时起,BuildKit中加入了内置的GitHub Actions Cache支持

说得够多了,下面是有注释的Deploy动作配置(请随意使用)。

name: Deploy

on:
  workflow_dispatch:
  push:
    branches:
    - production

jobs:
  kuby:
    runs-on: ubuntu-latest
    env:
      BUNDLE_GEMFILE: gemfiles/deploy.gemfile
      BUNDLE_JOBS: 4
      BUNDLE_RETRY: 3
      BUNDLE_FROZEN: "true"
      KUBY_ENV: production
      CI: true
      RAILS_MASTER_KEY: ${{ secrets.master_key }}
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.0.1
          bundler-cache: true
      # This action setups environment variables required
      # to use GitHub Actions Cache with BuildKit
      - uses: crazy-max/ghaction-github-runtime@v1
      # Installing Buildx plugin
      - uses: docker/setup-buildx-action@v1
        with:
          # This option is important:
          # it makes `docker build` use `buildx` by default
          install: true
      # We're going to push images directly via BuildKit,
      # not with Kuby; that's why we need to authenticate ourselves.
      # NOTE: Don't forget to add `DO_TOKEN` secret to GitHub Actions.
      - uses: docker/login-action@v1
        with:
          registry: registry.digitalocean.com
          username: ${{ secrets.do_token }}
          password: ${{ secrets.do_token }}
      # We add `--cache-from` and `--cache-to` to the build command to enable caching.
      # We also build images manually one by one (via the `--only` option): we have to use
      # different cache scopes to distinguish caches for different images.
      # Finally, the `--push` flag tells Docker to push images to the registry right away.
      - name: Build and push Docker images
        run: |
            bundle exec kuby build --only app -- \
              --cache-from=type=gha,scope=app --cache-to=type=gha,scope=app,mode=max \
              --push
            bundle exec kuby build --only assets -- \
              --cache-from=type=gha,scope=assets --cache-to=type=gha,scope=assets,mode=max \
              --push
      - name: Deploy Rails app
        # Consider adding a timeout to deploy action to avoid waiting
        # too long in case of deployment misconfiguration
        timeout-minutes: 5
        run: |
          bundle exec kuby deploy

没有Rails的Kuby怎么办?

最后,我想谈一谈无Rails(或无Rails?

Kuby是为部署Rails应用而设计的:核心gem (kuby-core)包含所有必要的Rails-gluing代码(Rake任务、中间件),并且默认包含rails_app 插件(并且没有API可以移除它🤷🏻♂️)。这是一个比较常见的情况,对于一个年轻的项目来说完全正常。不过,社区应该介入,并对使用Kuby来部署例如Hanami应用程序表现出兴趣。或者,我们可以让Kuby成为一个通用的基础设施即代码解决方案?我相信我们可以。

你能想象一个没有附加监控工具的生产应用吗?毋庸置疑。在Evil Martians,我们总是将Prometheus/Grafana/Loki添加到Kubernetes安装中。安装仪器堆栈本身可能超出了Kuby的范围(例如,Digital Ocean为此提供了一键式应用);相反,用Prometheus连接一个应用程序看起来很适合试验无Rails的Kuby。

对于我们的演示安装,我通过Yabeda添加了Ruby应用指标。AnyCable-Go提供了开箱即用的Prometheus支持。为了在Grafana中看到实际数据,我们需要指示Prometheus从我们的部署中收集度量。

Kubernetes监控栈使用Prometheus操作员。因此,为了设置指标收集,我们需要创建一个ServiceMonitor Kubernetes资源。而且与应用程序资源不同,我们必须将其部署在kube-prometheus-stack 命名空间下。

为了做到这一点,我不得不对Kuby进行了一些扩展和修补。这段代码仍然是实验性的,因此,它还没有被提取到一个宝石中。不过,你可以在演示PR中找到它。那么,废话不多说,让我向你展示最终的Kuby配置来管理监控资源。

Kuby.define("anycable-rails-demo") do
  environment(:production) do
    app_creds = ActiveSupport::EncryptedConfiguration.new(
      config_path: "./config/credentials/production.yml.enc",
      key_path: "./config/credentials/production.key",
      env_key: "RAILS_MASTER_KEY",
      raise_if_missing_key: true
    )

    # This is a custom method to define an empty Kubernetes after_configuration
    # (without anything related to Rails)
    kubernetes_appless do
      namespace do
        metadata do
          name "kube-prometheus-stack"
        end
      end

      add_plugin :prometheus_service_monitor do
        monitor do
          spec do
            selector do
              match_labels do
                add :app, "anycable-rails-demo"
              end
            end

            namespace_selector do
              match_names "anycable-rails-demo-production"
            end

            endpoint do
              port "metrics"
            end
          end
        end
      end

      provider :digitalocean do
        access_token app_creds[:do_token]
        cluster_id app_creds[:do_cluster_id]
      end
    end
  end
end

我们将把这段代码放到一个单独的文件中(deploy/kuby_monitoring.rb),并按如下方式运行。

bin/kuby deploy -c ./deploy/kuby_monitoring.rb

很好!现在我们可以用Kuby来管理所有与应用程序相关的基础设施。


这不是很神奇吗?Kuby降低了Rails应用采用Kubernetes的门槛,利用了惯例优于配置原则的力量。就像Rails以其 "在15分钟内建立一个博客 "的想法征服了世界一样,Kuby也可以在部署方面称王称霸--"在15分钟内将Rails部署到Kubernetes上"。

时间会证明一切。该项目仍在成长,新的插件正在出现(如kuby-anycable),我认为现在是一个很好的时机,可以尝试一下,并贡献你自己的想法。


顺便说一下,在Evil Martians,我们提供关于设置和优化Kubernetes部署的专业服务。如果这是你正在挣扎的事情,或者你只是需要一些专家建议,请随时给我们打电话。


P.S. 我要感谢Cameron Dutro为Kubernetes世界带来了Ruby的快乐,他做了一件了不起的工作。